Solo  当前访客:0 开始使用

Carson

记录精彩的程序人生

04多线程与并发 有更新!

2019-04-28 11:43:31 dulinanaaa
0  评论    171  浏览

重点

  1. 线程的状态转换
  2. 线程的同步与互斥
  3. 线程池的运作机制详解
  4. JUC中常用的工具类

多线程归纳

  • 死锁

    多线程协作时,因为对资源的锁定与等待,会产生死锁,要了解产生死锁的4个基本条件
    要明白竞争条件与临界区的概念,知道通过破坏造成死锁的4个条件来防止死锁

  • 线程通信
    前面了解了进程的通信方式,这里也要了解线程的通信方式。
    通信主要指线程之间的协作机制,例如:wait/notify
  • 机制

    知道Java为多线程提供的机制,例如:
    * ThreadLocal用来保存线程独享的数据,
    * Fork/Join机制用于大任务的分割与汇总,
    * Volatile对多线程数据可见性的保障,
    * 以及线程的中断机制
    * ThreadLocal的实现机制,Fork/Join的工作窃取算法

线程的状态转换

线程是JVM执行任务的最小单元

041.jpg

线程有6种状态
new –初始
runnable –运行
blocked –阴塞
waiting –等待
time_waiting –超时等待
terminated –终止
对应Thread中枚举类的状态

  • 当创建一个线程的时候,线程处在new状态
  • 运行Thread.start方法后,线程进行runnable可运行状态,这时候所有可运行状态的线程,并不能马上运行,而是先要进入到就绪状态等待线程调度,就是图中的ready状态。在获取到CPU后,才能进入运行状态,就是图中的running。运行状态,可随着不同条件转换成除new以外的其它状态
  • 先看左边,处运行状态的线程,进入Synchronized同步块或者同步方法时,如果获取锁失败,就会进入到blocked状态,当获取到锁时,会在blocked状态恢复到就绪状态
  • 再看右边,运行中的线程,还会进入到等待状态。这两个等待,一个是有超时时间的等待,例如:进入Object类的wait方法,Thread类的join方法等。另外一个是无超时的等待,例如调用Thread类的join或者LockSupport类的park方法,这两种等待都可以通过notify或unpark结束等待状态,恢复到就绪状态
  • 最后是下方,线程运行结束时,线程状态就变成了terminated。

线程的同步与互斥

解决线程同步与互斥的主要方式是CAS、Sychronized和Lock

  • CAS

    • 是一种乐观锁的实现
    • 是一种轻量级锁
    • JUC中很多工具类的实现就是基于CAS

    操作流程:

    042.jpg

    线程在读取数据时不进行加锁
    在准备写回数据时比较原值是否修改,若未被其它线程修改,则写回;若已被修改,则重新执行读取流程。
    这是一种乐观策略,认为并发操作并不总会发生,比较并写回的操作是通过操作系统的原语实现的,保证执行过程中不会被中断

    CAS容易出现ABA问题

    043.jpg

    线程T1在执行完读取完值A后,发生过两次写入:先由线程T2写回了B,又由线程T3写回了A。此时,T1再写回时进行比较,发现值还是A,就无法判断是否发生过修改。

    ABA问题并不一定会影响结果,但还是需要防范。解决的办法可以增加额外的标志位或者时间戳。JUC工具包中提供了这样的类。

  • Sychronized

    Sychronized是最常用的线程同步手段之一,它是如何保证同一时刻只有一个线程可以进入到临界区呢?

    我们知道,Sychronized是对对象进行加锁,在JVM中,对象在内存中分为三块区域:对象头,实例数据和对齐填充。

    044.jpg

    在对象头中,保存了锁标志位和指向Monitor对象的起始地址。(图中右面的就是一个对象所对应的Monitor对象)
    当Monitor被某个线程占用后,就会处于锁定状态,如图中的Owner部分,会指向持有Monitor对象的线程。
    Monitor还有两个队列,用来存放进入以及等待获取锁的线程

    Synchronized应用在方法上时,在字节码中是通过方法的acc_synchronized标志来实现的。
    Synchronized应用在同步块上时,在字节码中是通过monitorenter和monitorexit来实现的。

    针对Synchronized获取锁的方式,JVM使用了锁升级的优化方式:就是先使用偏向锁,优先同一线程再次获取锁。如果失败,就升级为CAS轻量级锁。如果再失败就进行短暂的自旋,防止线程被系统挂起。最后如果以上都失败,就会升级为重量级锁。

  • AQS与Lock

    045.jpg

    介绍Lock前先介绍AQS,也就是队列同步器。这是实现Lock的基础,左图就是AQS的结构图

    AQS的结构图:图中可以看出AQS有一个state的标志位:值为1时,表示有线程占用,其它线程需要进入到同步队列等待(同步队列是一个双向链表),当获得锁的线程需要等待某个条件时,会进入到condition的等待队列,等待对列可以有多个,当condition条件满足时,线程会从等待队列重新进入到同步队列进行获取锁的竞争。ReentrantLock就是基于AQS实现的。

    ReentrantLock结构图:内部有公平锁和非公平锁两种锁实现。差别就在于新来的线程有没有可能比已经在同步队列中等待的线程更早获得锁。ReentrantLock的实现方式类似,Semaphore也是基于AQS。差别在于ReentrantLock是独占锁,Semaphore是共享锁

线程池

线程池通过复用线程避免线程频繁的创建与销毁。Java的Executors工具类,提供了5种类型的线程池的创建方法。

046.jpg

  1. 固定大小线程池:newFixedThreadPool
    • 线程数固定
    • 使用无界缓冲队列
    • 适用于任务数据不均匀的场景及对内存压力不敏感,但对系统负载比较敏感的场景
  2. Cached线程池:newCachedThreadPool
    • 不限制创建的线程数
    • 适用于要求低延迟的短期任务的场景
  3. 单线程线程池:newSingleThreadExecutor
    • 一个线程的固定线程池
    • 适用于需要异步执行,但需要保证任务执行顺序的场景
  4. Scheduled线程池:newScheduledThreadPool
    • 适用于定期执行任务的场景
    • 支持按固定的频率定期执行和按固定的延时定期执行两种方式
  5. 工作窃取线程池:newWorkStealingPool
    • 使用ForkJoinPool是固定并行度的多任务队列
    • 适合多任务执行时长不均匀的场景

前面5种线程池的创建,除了工作窃取线程池,都是按照ThreadPoolExecutor的不同初始化参数来创建的。

构造函数的参数列表:(按顺序)

  1. 第一个参数:核心线程数,默认情况下,核心线程会一直存活
    1. 第二个参数:设置最大线程数,决定线程池最多可以创建多少线程
    2. 第三、四个参数:设置线程的空闲时间和空闲时间的单位,当线程闲置超过空闲时间时就会被销毁。可以通过allowCoreThreadTimeOut方法来允许核心线程被回收
    3. 第五个参数:设置缓冲队列,图中左下方的三个队列是设置线程池时最常使用的缓冲队列。
      • ArrayBlockingQueue 是一个有界队列。就是指队列有最大容量限制
      • LinkedBlockingQueue是一个无蜀队列。就是队列不限制容量
      • SynchronousQueue 是一个同步队列。内部没有缓冲区
    4. 第六个参数:设置线程池工厂方法。线程工厂用于创建新的线程。可以用来对线程的一些属性进行定制。例如:线程的group、线程名、优先级等。一般使用默认工厂类即可
    5. 第七个参数:设置线程池满时的拒绝策略。如右下角所示有4种策略:
      • Abort 在线程池满后,提交新任务时会抛出RejectedExecutionException,这个也是默认的拒绝策略
      • Discard 会在提交失败时对任务直接进行丢弃
      • CallerRuns 会在提交失败时由提交任务的线程直接执行的提交任务
      • DiscardOldest 会丢弃最早提交的任务

前面几种线程池使用怎样的方式创建?

  • 固定大小线程池:
    • 核心和最大线程数都设置成指定的线程数,这样线程池中就只会使用固定大小的线程数
    • 缓冲队列使用的是无界队列LinkedBlockingQueue
  • 单线程线程池:
    • 就是线池数设置成1的固定线程池
  • Cached线程池:
    • 核心线程数:设置为0
    • 最大线程数:设置为Integer整数的最大值
    • 缓冲队列:设置成SynchronousQueue,这样只要没有空闲的线程就会新建
  • Scheduled线程池:
    • 与前几种不同,使用了DelayedWorkQueue,这是一种按延迟时间获取任务的优先级队列

我们向线程池提交任务时,可以使用execute和submit。区别就是submit可以返回一个Future对象,通过Future对象可以了解任务的执行情况,可以取消任务的执行,还可以获取执行结果或者执行异常。submit最终也是通过execute执行的

向线程池提交任务的执行顺序

047.jpg

向线程池提交任务时,会首先判断线程池中的线程数是否大于设置的核心线程数。

如果不大于,就创建一个核心线程来执行任务。

如果大于核心线程数,就会判断缓冲队列是否满了

  如果没满则放入队列,等待线程空闲时再执行

  如果队列已经满了,就判断是否达到了线程池设置的最大线程数

    如果没达到,就创建新的线程来执行任务

    如果已经达到了最大线程数,就会执行指定的拒绝策略

注意:
队列的判断与最大线程数的判断他们之间的顺序,不要弄反。

JUC常用工具

JUC是Java提供的用于多线程处理的工具类库。
看下常用工具类的作用。

类名 特点
AtomicLong 都是基本数据类型的原子类
AtomicLong是通过unsafe类实现的,基于CAS(unsafe类是底层工具类,JUC中很多类的底层都使用到了unsafe包中的功能;unsafe类中提供了类似c中的指针操作,提供了cas的功能;unsafe类中的所有方法都是由native修饰的)
LongAdder的4个类是jdk1.8中提供的更高效的类
LongAdder基于cell实现,使用分段锁思想。是一种以空间换时间的策略,更适合高并发场景
LongAccumulator提供了比LongAdder更强大的功能,能够指定对数据的操作规则,例如,可以把对数据的相加操作改成相乘操作
AtomicInteger
AtomicBoolean
LongAdder
DoubleAdder
LongAccumulator
DoubleAccumulator
AtomicReference 提供了对对象的原子读写功能
AtomicStampedReference和AtomicMarkableReference用来解决ABA问题,分别基于时间戳和标记位来解决
AtomicStampedReference
AtomicMarkableReference
ReentrantLock 提供了锁相关的类
例如之前说的Reentrant重入锁,与ReentrantLock的独占锁不同。Stamphore是共享锁,允许多个线程共享资源,适用于限制使用共享资源线程数量的场景。例如100个车辆要使用20个停车位,那么最多允许20个车占用停车位
StampedLock是1.8中改进的读写锁,是一种使用CLH的乐观锁,能够有效防止写饥饿(所谓写饥饿,就是在多线程读写时,读线程访问非常频繁,导致总是有读线程占用资源,写线程很难加上写锁)
ReentrantReadWriteLock
StampedLock
LockSupport
Executors 异步执行相关的类
主要了解jdk1.8中提供的CompletableFuture,可以支持流式调用,可以方便的通过多Future组合使用,例如可以同时执行两个异步任务,然后对执行结果进行合并处理。还可以很方便的设置完成时间
jdk1.7中提供的ForkJoinPool,采用分治思想。将大任务分解成多个小任务来处理,然后再合并处理结果。ForkJoinPool的特点是使用工作窃取算法,可以有效平衡工作任务时间长短不一的场景
ForkJoinPool
FutureTask
CompletableFuture
LinkedBlockingQueue 常用的阻塞队列
LinkedBlockingQueue是双端队列,也就是可以分别从队头和队尾操作入队与出队。
ArrayBlockingQueue是单端队列,只能从队尾入队,从队头出队
ArrayBlockingQueue
CountDownLatch 控制多线程协作时使用的类
CountDownLatch实现计数器功能,可以用来等待多个线程执行任务后进行汇总
CyclicBarrier可以让一组线程等待至某个状态后再全部同时执行,一般在测试时使用,可以让多线程更好的并发执行
Semaphore前面已经介绍过,用来控制对共享资源的并发访问度
CyclicBarrier
Semaphore
ConcurrentHashMap 比较常用的两个集合类
ConcurrentHashMap前面课程介绍过了
CopyOnWriteArrayList:COW通过写入数据时进行copy修改,然后再更新引用的方式来消除并行写入中的锁使用,比罗适合读多写少,数据量比较小,但是并发非常高的场景
CopyOnWriteArrayList

考查点

  • 理解线程的同步与互斥的原理
    • 临界资源、临界区的概念
    • 重量级锁、轻量级锁、自旋锁、偏量锁、重入锁锁、读写锁的概念
  • 掌握线程安全的相关机制
    • CAS、Synchronized、Lock三种同步方式的实现原理
    • 明白ThreadLocal是每个线程独享的局部变量
    • 了解ThreadLocal使用弱引用的ThreadLocalMap保存不同的ThreadLocal变量
  • 了解JUC工具类的使用场景与几种主要工具类的实现原理

    • ReentrantLock、ConcurrentHashMap、LongAdder的实现方式
  • 熟悉线程池的原理、使用场景、常用配置
    • 大量短期任务的场景适合使用Cached线程池
    • 系统资源比较紧张时可以使用固定线程池
    • 注意慎用无界队列,可能会有OOM的风险
  • 深刻理解线程的同步与异步、阻塞与非阻塞
    • 同步与异步的区别是任务是否是在同一个线程中执行的
    • 阻塞与非阻塞的区别是异步执行任务时线程会不会阻塞等待结果,还是会执行后面的逻辑

加分项

  • 结合实际项目经验或实际案例介绍原理
    • 介绍线程池设置时,可以提到自己的项目中有一个高吞吐量的场景,使用了Cached线程池
  • 解决多线程问题的排查思路与经验
  • 熟悉常用的线程分析工具与方法

    • 会使用Jstack分析线程的运行状态
    • 查找锁对象的持有状况
  • 了解Java8对JUC工具类做了哪些增强
    • 用LongAdder替换AtomicLong,更适合并发度高的场景
  • 了解Reactive异步编程思想,了解背压的概念与应用场景

真题汇总

  1. 如何实现一个生产者与消费者模型?
    • 可利用锁,信号量,线程通信,阻塞队列等不同方式来实现
  2. 如何理解线程的同步与异步,阻塞与非阻塞?
  3. 线程池处理任务的流程?
  4. wait与sleep有什么不同?
    1. wait属于Object类,sleep属于Thread类
    2. wait会释放对象锁,sleep不会
    3. 出现的位置:wait要在同步块中使用,sleep可以在任何地方使用
    4. sleep需要捕获异常,而wait不需要
  5. Synchronized与ReentrantLock有什么不同,各适用什么场景?
  6. 读写锁适用于什么场景?ReentrantReadWriteLock是如何实现的?
    • 读写锁比较适合读并发多、写并发少的场景
    • 另外一个解决该场景的方法是CopyOnWrite
  7. 线程之间如何通信?

    • 介绍下wait/notify机制
    • 共享变量的synchronize或者lock同步机制
  8. 保证线程安全的方法有哪些?
    • 可以提到CAS,synchronized,lock,ThreadLocal等机制
  9. 如何尽可能提高多线程并发性能?
    • 减少临界区范围
    • 使用ThreadLocal
    • 减少线程切换
    • 使用读写锁或CopyOnWrite等机制
  10. ThreadLocal用来解决什么问题?ThreadLocal是如何实现的?
    • ThreadLocal不是用来解决多线程共享变量问题,而是用来解决线程数据隔离问题
  11. 死锁的产生条件,如何分析线程是否有死锁?
  12. 在实际工作中遇到过什么样的并发问题,如何发现(排查)并解决的?

,
TOP