多线程速成

##多线程速成要点

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

除了了解进程间的通信方式,还要知道线程的通信方式,通信主要指线程之间的协作机制,例如Wait、Notify

另外需要知道Java为多线程提供的一些机制,例如Threadlocal用来保存线程独享的数据,Fork/Foin机制用于大任务的分割与汇总,Volatile对多线程数据可见性的保证以及线程的中断机制。

其他还有: Threadlocal的实现机制。Fork/Join的工作窃取算法等内容。

1、线程的状态转换

线程是Jvm执行任务的最小单元,理解线程的状态转换是理解后续多线程问题的基础。

在Jvm运行中,线程一共有New、Runnable、Blocked、Waiting、Timed_waiting、Terminated六种状态,这些状态对应Thread.State枚举类中的状态。

当创建一个线程的时候,线程处在New状态,运行Thread的Start方法后,线程进入Runnable可运行状态。

这个时候,所有可运行状态的线程并不能马上运行,而是需要先进入就绪状态等待线程调度,如图中间的Ready状态。在获取到Cpu后才能进入运行状态,如图中的Running。运行状态可以随着不同条件转换成除New以外的其他状态。

先看左边,在运行态中的线程进入Synchronized同步块或者同步方法时,如果获取锁失败,则会进入到Blocked状态。当获取到锁后,会从Blocked状态恢复到就绪状态。

再看右边,运行中的线程还会进入等待状态,这两个等待一个是有超时时间的等待,例如调用Object.wait、Thread.join等。另外一个时无超时的等待,例如调用Thread.join或者Locksupport.park。

这两种等待都可以通过Notify或Unpark结束等待状态恢复到就绪状态。

最后是线程运行完成结束时,如图下方,线程状态变成Terminated

2、CAS与ABA问题

解决线程同步与互斥的主要方式是Cas、Synchronized、和Lock。

Cas是属于乐观锁的一种实现,是一种轻量级锁,Juc中很多工具类的实现就是基于Cas。

Cas操作是线程在读取数据时不进行加锁,在准备写回数据时,比较原值是否修改,若未被其他线程修改则写回,若已被修改,则重新执行读取流程。这是一种乐观策略,认为并发操作并不总会发生。

比较并写回的操作是通过操作系统原语实现的,保证执行过程中不会被中断。

Cas容易出现Aba问题,如果线程T1读取值A之后,发生过两次写入,先由线程T2写回了b,又由T3写回了A,此时T1在写回比较时,值还是A,就无法判断是否发生过修改。

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

3、Synchronized

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

我们知道Synchronized是对对象进行加锁,在Jvm中,对象在内存中分为三块区域:对象头、实例数据和对齐填充。在对象头中保存了锁标志位和指向Monitor对象的起始地址。当Monitor被某个线程持有后,就会处于锁定状态,Owner部分会指向持有Monitor对象的线程。另外Monitor中还有两个队列,用来存放进入及等待获取锁的线程。

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

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

4、Aqs与Lock

在介绍Lock前,先介绍Aqs,也就是队列同步器,这是实现Lock的基础。

Aqs有一个State标记位,值为1时表示有线程占用,其他线程需要进入到同步队列等待。同步队列是一个双向链表。

当获得锁的线程需要等待某个条件时,会进入Condition的等待队列,等待队列可以有多个。

当Condition条件满足时,线程会从等待队列重新进入到同步队列进行获取锁的竞争。

Reentrantlock就是基于Aqs实现的,Reentrantlock内部有公平锁和非公平锁两种实现,差别就在于新来的线程会不会比已经在同步队列中的等待线程更早获得锁。

和Reentrantlock实现方式类似,Semaphore也是基于aqs,差别在于Reentrantlock是独占锁,Semaphore是共享锁。


5、线程池

线程池通过复用线程,避免线程频繁创建和销毁。

Java的Executors工具类中,提供了5种类型线程池的创建方法,它们的特点和适用场景如下:

第1种是:固定大小线程池,特点是线程数固定,使用无界队列,适用于任务数量不均匀的场景、对内存压力不敏感,但系统负载比较敏感的场景;

第2种是:Cached线程池,特点是不限制线程数,适用于要求低延迟的短期任务场景;

第3种是:单线程线程池,也就是一个线程的固定线程池,适用于需要异步执行但需要保证任务顺序的场景;

第4种是:Scheduled线程池,适用于定期执行任务场景,支持按固定频率定期执行和按固定延时定期执行两种方式;

第5种是:工作窃取线程池,使用的ForkJoinPool,是固定并行度的多任务队列,适合任务执行时长不均匀的场景。

6、线程池参数介绍

前面提到的线程池,除了工作窃取线程池外,都是通过ThreadPoolExecutor的不同初始化参数来创建的。

第1个参数:设置核心线程数。默认情况下核心线程会一直存活。

第2个参数:设置最大线程数。决定线程池最多可以创建的多少线程。

第3个参数和第4个参数:用来设置线程空闲时间,和空闲时间的单位,当线程闲置超过空闲时间就会被销毁。可以通过AllowCoreThreadTimeOut方法来允许核心线程被回收。

第5个参数:设置缓冲队列,图中左下方的三个队列是设置线程池时常使用的缓冲队列。其中Array Blocking Queue是一个有界队列,就是指队列有最大容量限制。Linked Blocking Queue是无界队列,就是队列不限制容量。最后一个是Synchronous Queue,是一个同步队列,内部没有缓冲区。

第6个参数:设置线程池工厂方法,线程工厂用来创建新线程,可以用来对线程的一些属性进行定制,例如线程的Group、线程名、优先级等。一般使用默认工厂类即可。

第7个参数:设置线程池满时的拒绝策略。如右下角所示有四种策略,abort策略在线程池满后,提交新任务时会抛出Rejected Execution Exception,这个也是默认的拒绝策略。

Discard策略会在提交失败时对任务直接进行丢弃。CallerRuns策略会在提交失败时,由提交任务的线程直接执行提交的任务。Discard Oldest策略会丢弃最早提交的任务。

■前面的5种线程池都是使用怎样的参数来创建的呢?

固定大小线程池创建时核心和最大线程数都设置成指定的线程数,这样线程池中就只会使用固定大小的线程数。队列使用无界队列Linked Blocking Queue。

Single线程池就是线程数设置为1的固定线程池。Cached线程池的核心线程数设置为0,最大线程数是Integer.Max_Value,主要是通过把缓冲队列设置成SynchronousQueue,这样只要没有空闲线程就会新建。scheduled线程池与前几种不同的是使用了Delayed Work Queue,这是一种按延迟时间获取任务的优先级队列。


  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值