java多线程/并发学习笔记

Posted guangdeshishe

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java多线程/并发学习笔记相关的知识,希望对你有一定的参考价值。

java多线程/并发学习笔记

线程

  • Runnable\\Callable(FeatureTask可以返回结果)\\继承Thread类;实现接口比继承人更好,java不支持多继承,但是可以实现多个接口
  • Daemon守护线程:所有非守护线程结束时,程序结束,会同时杀死所有守护线程
  • Thread.sleep()\\InterruptException
  • thread.interrupt():中断线程,如果线程处于阻塞(除了I/O阻塞和synchronized锁阻塞)或者等待状态则会抛出InterruptException,从而提前结束线程
  • Thread.interrupted():判断是否调用了thread.interrupt()方法
  • Thread.yield():当前线程已经完成主要功能,可以给其他相同优先级的线程运行机会
  • thread.join():在A线程中join另一个线程B,A会挂起等待B执行完成后再继续
  • Object.wait/notify/notifyAll: 用于线程之间的协调同步(等待/通知);wait会释放锁,而sleep不会;wait是Object的方法,sleep是Thread方法;
    • kotlin中与Object对应的是Any,但是Any中没有这几个方法,可以先创建java.lang.Object()对象,再调用该对象的相应方法(kotlin只能在synchronized代码块中)
  • Condition.await/signal/signalAll:Lock.newCondition生成,相比Object的wait/notify/notifyAll,可以同时创建多个等待的条件(https://zhuanlan.zhihu.com/p/38011904)

线程池:

管理多个线程执行,不用显示的管理线程生命周期;避免过多重复创建线程

  • ExecutorService
  • Exectors.newCachedThreadPool:核心线程数为0;最大线程数为Integer.MAX_VALUE;任务执行完线程等待60秒;SynchronousQueue(容量为0,一边生产一边消费,没有消费时线程阻塞)
  • Exectors.newScheduledThreadPool:通过schedule等方法可以定时执行任务,比Timer.schedule相对更准确
  • Exectors.newFixedThreadPool:核心线程数和最大线程数相同;任务执行完立即结束线程;LinkedBlockingQueue(单向链表阻塞队列;先进先出;默认容量为Integer.MAX_VALUE)
  • Exectors.newSingleThreadExecutor:核心线程数为1的FixedThreadPool
  • executorService.shutdown():等待所有线程执行完成后关闭
  • executorService.shutdownNow():调用每个线程的interrupt()方法
  • executorService.submit():返回一个Feature,通过它的cancel(true)方法中断单个线程,get方法阻塞线程并获取结果
    • exectorService.execute()不能获取执行结果
  • Exectors.callable(Runnable task, Object result):可以将runnable转换成callable
  • ThreadPoolExecutor:推荐适用该方法创建线程池
    • Executors自带那几个创建线程池的方法有弊端
      • FixedThreadPool和SingleThreadExecutor的等待队列长度最大是整数的最大值,可能会导致OOM
      • CachedThreadPool和ScheduledTheadPoolExecutor允许创建的线程数最大为整数的最大值,可能会因为创建大量线程而OOM
    • int corePoolSize:核心线程数,总是运行在线程池中,即使任务执行完也不会停止,除非设置了allowCoreThreadTimeOut为true
    • int maximumPoolSize:线程池中最多允许存在的线程数量(包括核心线程在内),值不能小于核心线程数
    • long keepAliveTime:非核心线程任务执行完之后,等待下一个任务的最长时间,超过这个时间线程就停止了
    • TimeUnit unit:非核心线程超时等待的时间单位
    • BlockingQueue workQueue:等待任务执行的阻塞队列

互斥同步

  • synchronized:JVM实现;不能中断等待;非公平(公平是指按照申请顺序依次获得锁);不会导致死锁问题;可用于同步代码块、同步方法、同步类
    • kotlin中使用@Synchronized注解代替(https://www.jianshu.com/p/3963e64e7fe7)
  • ReentrantLock:重入锁,JDK实现;可中断放弃等待;可公平(默认非公平);兼容性不够好(不是所有jdk版本支持);仅针对对象;可以绑定多个Condition
    • 先通过CAS将state从0改成1,如果成功则将独占访问的线程设置为当前线程
    • 调用一次lock方法state值+1
    • 获得资源的线程可以lock多次,state值为0时其他线程才有机会获取资源
  • Synchronized和ReentrantLock区别:
    • 它们都是可重入锁:同一个线程获取到某个锁时,在没有释放该锁之前,可以再次获取该锁,同一个线程每获取一次锁计数器会+1,只有当计数器为0时,锁才会释放
    • synchronized是jvm实现,而ReentrantLock是JDK实现
    • ReentrantLock增加了一些高级功能:
      • 等待可中断:lock.lockInterruptibly(),调用该方法可以放弃等待,改为处理其他事情
      • 可实现公平锁:公平是指安全请求的先后顺序,ReentranntLock(boolean fair)
      • 可实现选择性通知(锁可以绑定多个条件),可以唤醒指定线程
  • volatile:保证变量的可见性以及防止指令重排序
    • kotlin中使用@Volatile注解代替
  • synchronized和volatile区别:
    • 都可以用于线程同步
    • volatile属于轻量级,不会阻塞线程,但synchronized可能会
    • volatile不能保证数据的原子性和线程安全,但synchronized都能保证
    • synchronized可以修饰变量、方法和代码块,但是volatile则只能修饰变量

线程状态

  • 新建(New):创建后未启动
  • 可运行(Runnable):正在java虚拟机中运行,但是在操作系统层面,也可能正在等待资源调度/CUP时间片;包含操作系统线程状态:Running和Ready
  • 阻塞(Blocked):等待获取monitor lock进入Synchronized方法或者代码块,要重新回到Runnable状态,需要其他线程释放monitor lock;等待获取一个排他锁
  • 无限等待(Waiting):等待其他线程显示唤醒,与阻塞的区别是,阻塞是被动的, 等待是主动的,比如通过Object.wait或者Thread.sleep方法进入
  • 期限等待(Timed_Waiting):一定时间后会自动唤醒
  • 死亡(Terminated): 线程结束

JUC-AQS

  • JUC: java.util.concurrent大大提高了并发性能,AQS又是JUC的核心
  • AQS:AbstractQueuedSynchronizer,抽象队列同步器,是很多锁(ReentrantLock)、并发类(CountDownLatch)的核心,内部主要维护一个state变量(通过CAS来改变值)和同步等待队列(FIFO双向链表)
    • AQS持有头部和尾部Node节点来控制队列的进和出,获取资源成功则出队列,失败则入队列
    • https://www.jianshu.com/p/cc308d82cc71
    • 独占式锁:acquire:独占式获取同步状态,如果获取失败则进入等待队列
      • release:释放同步状态,同时会唤醒队列中下一个线程
    • 共享式锁:acquireShared:共享式获取同步状态,同一时刻可以有多个线程获取状态
      • releaseShared:共享式释放同步锁
    • CAS:Compare and Swap,当前值与期望值比较,如果相等才替换成新的的值并返回true;实现类是sun.misc.Unsafe;提供硬件级别原子操作;
      • 高并发下如果尝试反复更新值,但总是更新不成功,会自旋不断重试,会给cpu带来比较大压力;解决办法:限制自旋次数或者时间
      • CAS只能对单个变量进行原子操作,无法对多个变量,解决办法:1、使用互斥锁;2、将多个变量合并成一个变量,比如自定义View中的MeasureSpec= mode + size
      • ABA问题:如果一个变量值从A变成B又变回A,无法判断该变量是否修改过,再CAS操作还是会成功;解决办法:1、通过引入版本号,每次修改值+1,例如:AtomicStampedReference或者AtomicMarkableReference解决;2、改用互斥同步
      • AtomicStampedReference内部是通过Pair保存变量值和一个int类型的标识,每当值改变一次时,该标识也一起改变,从而解决ABA问题,AtomicMarkableReference和它类似,只不过Pair里的标识是bool类型
    • 需要子类实现的方法:
      • isHeldExclusively:该线程是否在独占资源,只有用到Condition才需要实现(ReentranntLock)
      • tryAcquire(独占式,成功返回true)
      • tryAcquireShared(共享式,失败返回负数,0表示成功且刚还没有更多资源,大于0表示成功且还剩资源)
      • tryRelease(独占式,成功返回true)
      • tryReleaseShared(共享式,成功返回true)
    • ReentranntLock:state刚开始为0,当调用lock()方法后,内部会调用tryAcquire方法,将state通过CAS操作加1并将当前线程标记为独占,当其他线程调用lock()方法时,会检查state值是否为0,不为0则阻塞等待;如果该线程已经占有了该锁,则可以多次调用lock(),每次调用state都会加1,调用了多少次,就要release多少次,以保证sate值变回0;
      • ReentranntReadAndWriteLock:同时支持独占式和共享式两种方式
  • CountDownLatch(倒计时器):只有当每个线程任务都执行到调用countDown地方后,调用await的地方才能继续执行,AQS实现
    • state的初始值是N(表示有N个线程);每当有一个线程执行完成调用countDown()方法后,state值会通过CAS操作减1,直到state值为0时,调用了await的线程就会继续执行
  • CyclicBarrier(循环栅栏): 只有当每个线程都到达调用await的地方,这些线程才会继续执行
    • 与CountDownLatch区别:CyclicBarrier支持rest方法重复使用
  • Semaphore(信号量): 用于控制对互斥资源的访问线程数,例如限制客户端同时发起的请求数,acquire获取资源,release释放资源
  • FutureTask: 可用于异步获取执行结果或者取消任务
  • BlockingQueue: 阻塞队列,如果队列为空take()将阻塞,直到队列中有内容;如果队列已满put()将阻塞,直到队列有空闲为止;
    • FIFO:LinkedBlockingQueue(默认Integer.MAX_VALUE)、ArrayBlockingQueue(固定长度)
    • 优先级:PriorityBlockingQueue(优先级队列)
  • ForkJoin:主要用于并行计算,与MapReduce原理类似,都是把大的计算任务拆分成多个小任务并行计算
    • ForkJoinTask\\RecursiveTask(有返回值)\\RecursiveAction(没有返回值):要执行的任务,负责将任务提交给ForkJoinPool,fork()提交任务给ForkJoinPool,join获取结果
    • ForkJoinPool:线程池,默认线程数为cpu核心数,会将任务保存在工作线程的双端队列中,当前一个工作线程中没有任务时,会从另一个工作线程队列中的尾部获取一个任务来执行
    • ForkJoinWorkerThread:负责执行任务,有个双端队列保存任务

java内存模型:

JDK1.2之前各个线程都是在主内存中存取数据的,效率比较低,cpu的速度又比内存速度快很多,所以加入了高速缓存,每个线程都有一块工作内存映射到主内存,他们之间通过read\\load\\use\\assign\\store\\write等操作实现交互

  • 主内存:所有变量存储在主内存中
  • 高速缓存:用于解决处理器的寄存器速度比内存快很多问题,如果多个缓存共享一块主内存,需要加入一些协议来解决缓存一致性问题
  • 工作内存:每个线程都有自己的工作内存,拥有主内存的拷贝,存储在高速缓存或者寄存器中
  • 主内存和工作内存之间的交互:变量值->主内存-read(传输)->工作内存(load)-use->线程-assign->工作内存-store(传输)->主内存(write);lock/unlock
  • 三大特性:
    • 原子性:内存模型保证了上面的8个操作都具有原子性(64位数据会被当做两次32位操作进行,例如:long\\double),像int等原子类型在多线程中会有线程安全问题,因为在修改值的时候会有assign\\store\\write操作,整体上是不具有原子性的,可以使用AtomicInteger代替
      • AtomicInteger:主要利用CAS(Unsafe类实现) + volatile + native方法来保证原子操作,避免synchronized的高开销
    • 可见性:当线程中修改了某个变量,其他线程能立刻知道这个修改,修改时立刻同步到主内存,读取时立刻从主内存更新,主要有三种实现方式:
      • volatile:被valatile修饰的变量,当值发生改变时会立刻同步到主内存,其他线程获取到该变量的值会立即失效,并重新从主内存中获取最新数据;
        • 在多线程中它不能保证操作的原子性,以变量i++为例,当对变量自增操作时,会分为三个步骤:1、获取值,2、对值+1操作3、更新到主内存;volatile只能保证第一步获取到最新的值,但是如果其他线程改变了这个值,会导致2、3操作失效,值重新变成主内存中最新的值
      • synchronized:在对一个变量执行unlock操作之前,会把变量值同步到主内存
      • final:被final修饰的字段,一旦初始化,并且没有引用到初始化一半的对象,其他线程就能看到final字段的值
    • 有序性:JVM和CUP可能会对指令重排序,也就是在单线程中保证结果不变的情况下待变代码的执行顺序;但是在多线程中可能会出现问题,以单例模式中双重检查判断为例,通常我们要在单例变量前加volatile修饰,这是因为对象的初始化大致分为三个步骤:1、分配内存2、对象初始化3、变量引用该对象的内存地址;由于jvm和cpu对指令的重排序,第2、3步骤都有可能先执行,如果先执行3,再执行2,其他线程就可能获取到不为null但是还未初始化的对象,加上volatile修饰,则可以保证执行顺序执行
      • volatile:通过在指令之间添加内存屏障方式来避免指令重排序问题
      • synchronized:保证某一时刻只能有一个线程执行同步代码
  • 先行发生原则:jvm让一个操作无需控制就能先于另一个操作完成
    • 单一线程原则:在单个线程中,程序的前面操作优先于后面的操作
    • 管理锁定规则:多线程中unlock操作要优先于lock操作
    • volatile:写操作优先于读操作
    • 线程启动规则:Thread的start方法优先于此线程中的其他操作
    • 线程加入规则:Thread对象的结束优先于join()方法返回
    • 线程中断规则:interrupt方法的调用优先发生于interruptted(用于检测是否发生中断)
    • 对象终结规则:对象初始化完成优先于finalize方法
    • 传递性:如果操作A先行发生与B,B先行发生于C,那么A先行发生于C
  • 线程安全:
    • 不可变类型:
      • final、String、枚举、Number(Long、Double、BigInteger和BigDecimal等,AtomicInteger和AtomicLong是可变的)
      • 集合可以使用Collections.unmodifiableMap()返回一个不可变的集合(例如UnmodifiableMap),内部会对持有原有对象,这个类修改了put等修改数据的方法实现,在执行这类修改操作时抛出异常UnsupportedOperationException
    • 互斥同步:也叫阻塞同步,线程阻塞和唤醒所带来的的性能开销;悲观锁;synchronized和ReentrantLock
    • 非阻塞同步:乐观锁;只有当需要竞争资源时才会通过自旋的方式阻塞线程,通过CAS更新值;AtomicInteger
    • 无同步方案:不涉及共享数据则不需要进行同步
      • 栈封闭:多个线程同步访问同一个方法里的局部变量,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有
      • 线程本地存储(ThreadLocal):在当前线程范围内的全局变量,在每个线程中都有一个ThreadLocalMap对象(非真正的map,而是Entry数组),它以ThreadLocal对象作为key,值作为value封装成Entry对象,当调用get/set方法时,操作的实际时当前线程的LocalThreadMap里的Entry数组
        • 不是用来解决线程同步问题
        • 为了避免内存泄漏,使用完之后需要手动调用remove()方法,出现内存泄漏是因为它的生命周期跟当前线程一样长,在Entry中,ThreadLocal作为key被弱引用,当ThreadLocal被虚拟机回收后,key就变成null,但是value却还一直被引用着。
        • ReferenceQueue,配合WeakReference/SoftReference,当引用被jvm回收后,会进入该队列,可以通过该队列来判断引用是否被回收
      • 可重入代码(Reentrant Code):也叫纯代码(Pure Code),当多个线程访问时不会带来线程安全问题,比如:定义某个方法,方法只使用传递进来的参数,不使用任何全局变量,不使用外部数据
  • 锁优化:主要是指JVM对synchronized的优化
    • 自旋锁:线程互斥同步进入阻塞状态的开销比较大,如果资源锁定时间比较短,则可以通过循环的方式不断请求资源,如果在这段时间内能够请求成功则可以避免进入阻塞状态
      • 占用CUP时间,控制自旋时间或者次数
    • 自适应自旋锁:自旋的时间不固定,而是由上一次自旋时间和锁拥有者的状态来决定
      - 如果同一个锁对象上,最近刚获取过锁,并且线程正在运行,虚拟机会认为它自旋成功的概率很大,就会允许它自旋的时间更久一些
      - 如果某个锁自旋很少成功过,那么自旋的时间会更少,避免浪费cpu资源
    • 锁消除:对于检测(逃逸分析)出来不可能存在竞争的共享数据的锁进行消除,例如:String字符串的拼接,在JDK1.5之前是通过StringBuffer实现,append方法是通过synchronized同步方法实现,但是拼接字符串时该方法总是连续调用的,不可能再被其他地方调用,所以会对该方法的锁进行消除
    • 锁粗化:当检测到对同一个对象反复加锁/解锁操作时,例如上面的StringBuffer多次append,就会将锁的范围扩大到第一个append调用之前,以及最后一次append操作之后,这样只需要加锁一次就行了
    • 轻量级锁:无竞争或者短时间竞争的情况,如果竞争不激烈则可以使用自旋锁优化,竞争激烈则膨胀为重量级锁(阻塞线程),目的是减少线程阻塞的性能开销(用户态转为内核态)
      • JDK1.6引入偏向锁和轻量级锁,锁的状态:无锁状态(unlock)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)、重量级锁状态(inflated)
    • 偏向锁:使用锁的线程只有一个,一旦获取该锁之后,后续都不在需要同步操作,一旦有其他线程竞争则膨胀为轻量级锁

Synchronized

jvm实现,用于多线程中同步数据,可作用于方法、类、代码块、对象

  • java早期属于重量级锁,线程的挂起或者唤醒都需要等待系统从用户态转为内核态,开销大,JDK1.6开始,jvm对它做了大量优化,例如:自旋锁、适应性自旋锁、锁消除、锁粗化、轻量级锁、偏向锁等来减少锁操作的开销
  • 单例模式中双重检验版:volatile修饰、synchronized对xxx.class锁定;创建对象分为三步:1、分配内存2、初始化对象3、变量执行该对象内存地址;由于jvm指令重排可能导致先执行第3步,这会导致拿到的对象是还没初始化的,volatile可以禁止jvm指令重排
  • jvm实现:字节码中同步代码块中,所以java对象可以作为锁使用)使用的是monitorenter(锁计数器变为1)和monitorexit(锁计数器变为0);同步方法使用的是ACC_SYNCHRONIZED标记;
    • monitor锁对象存在于每个java对象的头【Mark Word】 记录获得锁的线程id],获取到锁的线程id会记录到【Mark Word】中,不用map全局保存是因为map也需要处理线程安全问题,数据太多的时候也占内存
  • kotlin中同步方法使用的是Synchronized注解;同步代码块是kotlin中的定义的一个方法,内部实现也是monitorenter和monitorexit
  • 锁优化:自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁、偏向锁;随着竞争激烈而最终升级到重量级锁,锁只能升级不能降级,这种策略是为了提高获得锁和释放锁的效率
    • 锁降级:是指读写锁获得【写锁】后,还可以获取【读锁】,当【写锁】释放后,就只剩下【写锁】,这就出现了锁降级现象,锁降级一般应用在写入数据后,还需要继续其他操作的情况

死锁:

线程之间互相持有对方所申请的资源,死锁所需要的四个条件:

  • 一个资源同时只能被一个线程使用
  • 线程在请求另一个资源被阻塞期间,已获得的资源不释放
  • 线程已经持有的资源,在未使用完之前,不能被强行剥夺
  • 线程之间存在逻辑上的循环,也就是同时请求对方所持有的资源
  • 避免死锁:消除逻辑上的循环,也就是避免互相同时请求对方所持有资源这种情况

多线程开发建议:

  • 给线程起个有意义的名字,方便找bug
  • 缩小同步范围,减少锁竞争,例如对于synchronized,应尽量使用同步代码块而不是同步方法
  • 多用jdk自带的同步相关类,少用wait()、notify()等,例如CountDownLatch|CyclicBarrier|Semaphore|Exchanger等
  • 使用BlockingQueue实现生产消费问题
  • 多用并发集合少用同步集合,例如多用ConcurrentHashMap,少用HashTable
  • 使用本地变量和不可变类来保证线程安全
  • 使用线程池而不是直接创建线程,便于管理和优化线程的使用

以上是关于java多线程/并发学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

Java学习笔记—多线程(同步容器和并发容器)

Java多线程编程(学习笔记)

Java学习笔记—多线程(并发工具类,java.util.concurrent.atomic包)

JAVA 多线程和并发学习笔记(四)

Java学习笔记之三十四超详解Java多线程基础

通俗易懂两种常用的多线程实现方式——Java并发系列学习笔记