文章目录
一 常见java 锁名词
锁大概有以下名词:
阻塞锁,可重入锁,读写锁,互斥锁,悲观锁,乐观锁,公平锁,偏向锁,对象锁,线程锁,锁粗化,锁消除,轻量级锁,重量级锁,信号量,独享锁,共享锁,分段锁
1. 常见的锁
Synchronized 和 Lock
Synchronized,它就是一个:非公平,悲观,独享,互斥,可重入的重量级锁。原生语义上实现的锁。
以下两个锁都在JUC包下,是API层面上的实现:
ReentrantLock,它是一个:默认非公平但可实现公平的,悲观,独享,互斥,可重入,重量级锁。
ReentrantReadWriteLocK,它是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入,重量级锁。
1.1 公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获得锁。有可能会造成优先级反转或者饥饿现象。对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
1.2 乐观锁/悲观锁
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。比如Java里面的同步原语synchronized关键字的实现也是悲观锁。
乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。
1.3 独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReentrantReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。
1.4 互斥锁/读写锁
独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock,读写锁在Java中的具体实现就是ReentrantReadWriteLock
1.5 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
二、sychronized
2.1 简介
Synchronized是Java中解决并发问题的一种最常用的方法,Synchronized的作用主要有三个:
(1)确保线程互斥的访问同步代码
(2)保证共享变量的修改能够及时可见
(3)有效解决重排序问题。
2.2 应用方式
synchronized关键字最主要有以下3种应用方式,下面分别介绍
1.修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
2.修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
3.修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
public class Test {
String ss="aaa";
public synchronized void add1(){}
public static synchronized void add2(){}
public void add3(){
synchronized (Test.class){
}
synchronized (ss){
}
synchronized (this){
}
}
}
2.3 实现原理
对象锁(monitor)机制
编译用sychronized修饰的同步代码块,再用javap -v查看字节码文件:执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。
[外链图片转存失败(img-tZ5mOznY-1567232183822)(./images/1566287434266.png)]
2.4 Java虚拟机对synchronized的优化
2.4.1 Java对象头
在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
- 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
- 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐
对象头,它实现synchronized的锁对象的基础,其主要结构是由Mark Word 和、Klass Pointer(类型指针)组成,Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,而Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等
[外链图片转存失败(img-CsQynr48-1567232183835)(./images/1566288192581.png)]
2.4.2 锁优化
锁一共有4种状态,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
- 偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程
①偏向锁获取过程:
1.访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
2.如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
3.如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
4.如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)(在这个时间点上没有字节码正在执行)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
5.执行同步代码。
②偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
- 轻量级锁
倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;
轻量级锁的加锁过程:
1.在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
2.拷贝对象头中的Mark Word复制到锁记录中;
3.拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
4.如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态
5.如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁解锁:
轻量级解锁时,会使用原子的 CAS 操作来将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
-
自旋锁
轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般设置10次,可能是50个循环或100循环,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。
在JDK 1.6中引人了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上, 自旋等待刚刚成功获得过锁,井且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环,另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚报机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。 -
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,append方法用了synchronized关键词,它是线程安全的。但我们可能仅在线程内部把StringBuffer当作局部变量使用,不同线程在执行这个方法时,会在当前线程虚拟机栈中创建一个自己的栈帧,且它是线程私有的,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。
+锁粗化
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是大某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的讲求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。
另一种需要锁粗化的极端的情况是:
for(int i=0;i<size;i++){
synchronized(lock){
}
}
上面代码每次循环都会进行锁的请求、同步与释放,看起来貌似没什么问题,且在jdk内部会对这类代码锁的请求做一些优化,但是还不如把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求,除非有特殊的需要:循环需要花很长时间,但其它线程等不起,要给它们执行的机会。
锁粗化后的代码如下:
synchronized(lock){
for(int i=0;i<size;i++){
}
}
整个synchronized锁流程如下:
检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
如果自旋成功则依然处于轻量级状态。
如果自旋失败,则升级为重量级锁。
三、JUC常用同步器框架AQS
3.1 简介
JUC当中的大多数同步器(ReentrantLock/Semaphore/CountDownLatch…各种锁机制)功能实现都是围绕共同的基础行为,比如等待队列、条件队列、独占获取,共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。
3.2 具体实现
核心思想使用标志状态位state(volatile int state)和 一个双向队列来实现。
3.2.1 state变量
state这个状态变量是用volatile来修饰的
·getState():获取当前同步状态。
·setState(int newState):设置当前同步状态。
·compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
3.2.2 AQS同步队列
同步器AQS内部的实现是依赖同步队列(CLH队列其实就是双向链表,java中的CLH队列是原CLH队列的一个变种,队列里的线程由原自旋机制改为阻塞机制)来完成同步状态的管理。
同步队列主要包含节点的引用:一个指向头结点的引用(head),一个指向尾节点的引用(tail)。
同步队列的基本结构如下:
[外链图片转存失败(img-HvlZGoWR-1567232183836)(./images/1566288228774.png)]## 3.3 AQS锁的类别
分为独占锁和共享锁两种。
独占锁:锁在一个时间点只能被一个线程占有。根据锁的获取机制,又分为“公平锁”和“非公平锁”。等待队列中按照FIFO的原则获取锁,等待时间越长的线程越先获取到锁,这就是公平的获取锁,即公平锁。而非公平锁,线程获取的锁的时候,无视等待队列直接获取锁。ReentrantLock和ReentrantReadWriteLock.Writelock是独占锁。
共享锁:同一个时候能够被多个线程获取的锁,能被共享的锁。JUC包中ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch和Semaphore都是共享锁。
四、LOCK
Lock和synchronized有以下几点不同:
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现, lock是通过代码实现的.
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
4.1 Lock锁主要的两个子类:
4.1.1 ReentrantLock
这个类中有3个内部类,分别是Sync抽象同步器、NonfairSync非公平锁同步器、FairSync公平锁同步器。
Sync是NonfairSync和FairSync的抽象父类。
static abstract class Sync extends AbstractQueuedSynchronizer
final static class NonfairSync extends Sync
final static class FairSync extends Sync
1. ReentrantLock 中的公平锁的加锁实现:
(1)首先公平锁对应的逻辑是 ReentrantLock 内部静态类 FairSync
(2)加锁时会调用lock()方法去获取锁,lock()会调用用acquire()(AQS中),而这个acquire方法内部又去调用了tryAcquire的方法
-
tryAcquire方法通过getState(获取当前同步状态,发现state为0,则通过CAS设置该状态值,state初始调用值为1,设置锁的拥有者为当前线程,tryAcquire返回true,反之,返回false(如果同一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加同一个线程,重入一次锁状态就加1)
-
如果第一步获取锁失败,也就是tryAcquire返回false,则调用的 addWaiter(Node mode)方法把该线程包装成一个node节点入同步队列(FIFO)法,(加入队列时,先去判断这个队列是不是已经初始化了,没有初始化,则先初始化,生成一个空的头节点,然后才是线程节点),即尝试通过CAS把该节点追加到队尾,修改为节点为最新的节点,如果修改失败,意味着有并发,同步器通过进入enq死循环的方式来保证节点的正确添加,只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法中返回,否则当前线程不断的尝试设置。
3.加入了同步队列的线程,通过acquireQueued方法把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞)。
lock源码:
lock:
final void lock() {
// 调用 AQS acquire 获取锁
acquire(1);
}
acquire(AQS):
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire:如果获取到了锁,tryAcquire返回true,反之,返回false。
hasQueuedPredecessors(),判断是否有其他线程比当前线程在同步队列中等待的时间更长。有的话,返回 true,否则返回 false,进入队列中会有一个队列可能会有多个正在等待的获取锁的线程节点,可能有Head(头结点)、Node1、Node2、Node3、Tail(尾节点),如果此时Node2节点想要去获取锁,在公平锁中他就会先去判断整个队列中是不是还有比我等待时间更长的节点,如果有,就让他先获取锁,如果没有,那我就获取锁(这里就体会到了公平性)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
- acquireQueued方法当前线程在死循环中获取同步状态,而只有前驱节点是头节点才能尝试获取同步状态(锁)( p == head && tryAcquire(arg))
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
addWaiter方法的源码:
如果尝试获取同步状态失败的话,则构造同步节点(独占式的Node.EXCLUSIVE),通过addWaiter(Node node,int args)方法将该节点加入到同步队列的队尾。
尝试通过CAS把当前现在追加到队尾,修改为节点为最新的节点,如果修改失败,意味着有并发,这个时候进入enq中的死循环,进行“自旋”的方式修改
compareAndSetTail(pred, node)) enq(node);
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
enq方法的源码:
该方法就是循环调用CAS,即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)。同步器通过死循环的方式来保证节点的正确添加,在“死循环” 中通过CAS将节点设置成为尾节点之后,当前线程才能从该方法中返回,否则当前线程不断的尝试设置。
enq方法将并发添加节点的请求通过CAS变得“串行化”了。
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
Node h = new Node(); // Dummy header
h.next = node;
node.prev = h;
if (compareAndSetHead(h)) {
tail = node;
return h;
}
}
else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
2. ReentrantLock 中的公平锁的释放锁实现:
1.头节点在释放同步状态的时候,会调用unlock(),而unlock会调用release(),release()会调用tryRelease方法尝试释放当前线程持有的锁,先判断当前线程是否为持有锁的线程,如果是,则执行减减操作,否则抛出异常(同步状态)
2.成功的话调用unparkSuccessor() 唤醒后继线程,并返回true,否则直接返回false,
注意:
- 只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁,当A把锁完全释放后,state恢复为0
- 队列中的节点在被唤醒之前都处于阻塞状态。当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除
unlock源码:
unlock:
public void unlock() {
sync.release(1);
}
release(AQS):
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放锁成功后,找到AQS的头结点,并唤醒它即可:unparkSuccessor --> Node s = node.next
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
tryRelease:
3. ReentrantLock 中的非公平锁的实现:
(1)首先非公平锁对应的逻辑是 ReentrantLock 内部静态类 NoFairSync
(2)加锁时会先从lock方法中去获取锁,不同的是,它的lock方法是先直接 CAS 设置 state 变量,如果设置成功,表明加锁成功。抢占失败,再调用 acquire 方法将线程置于队列尾部排队。也是去获取锁调用acquire()方法,acquire方法内部同样调用了tryAcquire的方法,它的tryAcquire方法的if判断少了一个 !hasQueuedPredecessors()。
hasQueuedPredecessors():判断是否有其他线程比当前线程在同步队列中等待的时间更长。有的话,返回 true,否则返回 false,进入队列中会有一个队列可能会有多个正在等待的获取锁的线程节点,可能有Head(头结点)、Node1、Node2、Node3、Tail(尾节点),如果此时Node2节点想要去获取锁,在公平锁中他就会先去判断整个队列中是不是还有比我等待时间更长的节点,如果有,就让他先获取锁,如果没有,那我就获取锁(这里就体会到了公平性)
非公平锁的机制:如果新来了一个线程,试图访问一个同步资源,只需要确认当前没有其他线程持有这个同步状态,即可获取到。
公平锁的机制:既需要确认当前没有其他线程持有这个同步状态,而且要确认同步器的FIFO队列为空,或者队列不为空但是自己是队列中头结点指向的下一个节点。
这个区别很重要,因为线程在阻塞和非阻塞之间切换时需要比较长的时间,如果刚好线程A释放了资源,A会去唤醒下一个排着队的Node节点,当这个唤醒操作还没完成的时候,这时又来了一个线程B,线程B发现当前没人持有这个资源,于是自己就迅速拿到了这个资源,充分利用了线程A去唤醒B的这一段时间,这就是公平锁和非公平锁之间的差异,这里也体现了非公平锁性能较高的地方。
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
进入到nonfairTryAcquire()方法这里并没有调用hasQueuedPredecessors()这个方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
4.1.2 ReentrantReadWriteLock
ReentrantReadWriteLock是 Lock 的另一种实现方式,我们已经知道了 ReentrantLock 是一个排他锁,同一时间只允许一个线程访问,而 ReentrantReadWriteLock 允许多个读线程同时访问,但不允许写线程和读线程、写线程和写线程同时访问。相对于排他锁,提高了并发性。在实际应用中,大部分情况下对共享数据(如缓存)的访问都是读操作远多于写操作,这时ReentrantReadWriteLock能够提供比排他锁更好的并发性和吞吐量。
另外:
- ReentrantReadWriteLock支持锁的降级,即先获取写锁,再获取读锁,再释放写锁。
- 读锁不支持Condition,会抛出UnsupportedOperationException异常,写锁支持Condition。
- ReadLock 和 WriteLock 方法部分是通过调用 Sync 的方法实现的,Sync 源码分析:
AQS 的状态 state 是 32 位(int 类型)的,高16位表示持有读锁的线程数(sharedCount),低16位表示写锁的重入次数 (exclusiveCount)。
状态值为 0 表示锁空闲,sharedCount 不为 0 表示分配了读锁,exclusiveCount 不为 0 表示分配了写锁,sharedCount和exclusiveCount 一般不会同时不为 0,
只有当线程占用了写锁,该线程可以重入获取读锁,反之不成立。
ReentrantReadWriteLock 包含五个内部类:
abstract static class Sync extends AbstractQueuedSynchronizer{...}
static final class NonfairSync extends Sync{...}
static final class FairSync extends Sync {...}
public static class ReadLock implements Lock, java.io.Serializable{...}
public static class WriteLock implements Lock, java.io.Serializable{...}
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
// 由于读锁用高位部分,所以读锁个数加1,其实是状态值加 2^16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 写锁的可重入的最大次数、读锁允许的最大数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 写锁的掩码,用于状态的低16位有效值
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 读锁计数,当前持有读锁的线程数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁的计数,也就是它的重入次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
1.读锁进行加锁流程:
调用 lock() 方法进行加锁,会调用acquireShared(),而acquireShared会调用tryAcquireShared(),
-
==这个方法先判断是否写锁被占用,如果写锁被占用,并且占用写锁的线程不是当前线程,直接返回; 再通过得到state值,进而得到读锁的sharedCount的值,判断此时状态后是否超出读锁最大保存状态个数,没有超出,去修改state:
1.如果此时得到的值为0,说明当前线程是第一个获取读锁的线程,则保存第一个获取读锁的线程的重入的次数(将0置为1,如果之后当前线程还 要再次获得锁,则进行次数++)
2.如果此时得到的值不为0,且判断当前线程不是目前持有读锁的线程,则用一个threadLocal修饰的holdCount类,保存当前线程的的id,以及之后重入锁的个数lock源码:
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
//体现锁降级的思想,如果写锁被占用,并且占用写锁的线程不是当前线程,返回。
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
//保存第一个获取到读锁的线程
firstReader = current;
//保存第一个获取读锁的线程的重入的次数
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//保存最近获取读锁的线程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
private void doAcquireShared(int arg){
final Node node = addWaiter(Node.SHARED); // 1. 将当前的线程封装成 Node 加入到 Sync Queue 里面
boolean failed = true;
try {
boolean interrupted = false;
for(;;){
final Node p = node.predecessor(); // 2. 获取当前节点的前继节点 (当一个n在 Sync Queue 里面, 并且没有获取 lock 的 node 的前继节点不可能是 null)
if(p == head){
int r = tryAcquireShared(arg); // 3. 判断前继节点是否是head节点(前继节点是head, 存在两种情况 (1) 前继节点现在占用 lock (2)前继节点是个空节点, 已经释放 lock, node 现在有机会获取 lock); 则再次调用 tryAcquireShared 尝试获取一下
if(r >= 0){
setHeadAndPropagate(node, r); // 4. 获取 lock 成功, 设置新的 head, 并唤醒后继获取 readLock 的节点
p.next = null; // help GC
if(interrupted){ // 5. 在获取 lock 时, 被中断过, 则自己再自我中断一下(外面的函数可能需要这个参数)
selfInterrupt();
}
failed = false;
return;
}
}
if(shouldParkAfterFailedAcquire(p, node) && // 6. 调用 shouldParkAfterFailedAcquire 判断是否需要中断(这里可能会一开始 返回 false, 但在此进去后直接返回 true(主要和前继节点的状态是否是 signal))
parkAndCheckInterrupt()){ // 7. 现在lock还是被其他线程占用 那就睡一会, 返回值判断是否这次线程的唤醒是被中断唤醒
interrupted = true;
}
}
}finally {
if(failed){ // 8. 在整个获取中出错(比如线程中断/超时)
cancelAcquire(node); // 9. 清除 node 节点(清除的过程是先给 node 打上 CANCELLED标志, 然后再删除)
}
}
}
2. 读锁进行释放锁流程:
调用 unlock() 方法进行解锁,会调用release(),而release会调用tryRelease(),
这个方法先判断是占用读锁的线程不是当前线程,
- 是当前线程:先得到当前线程已有的读锁重入个数(firstReaderHoldCount值),如果此时得到的值为1,进行减一操作,再将读锁置为null(如果当前线程多次获得过再读锁,则进行Count–)
- 如果当前线程不是目前持有读锁的线程,则将对应该线程的holdCount类(用threadLocal修饰的)里count属性值进行减减操作。
unlock源码:
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
3. 写锁进行加锁流程:
1.调用 lock() 方法进行加锁,会调用acquire(),而acquire会调用tryAcquire()
2.先获取state状态值,如果不为0
-
当前存在读锁, 时直接返回false
-
再判断此时状态后是否超出写锁最大保存状态个数,没有超出,则进行加锁修改状态值(同一个线程多次加锁,count值++,锁重入),否则抛异常
lock源码:
lock:
public void lock() {
sync.acquire(1);
}
acquire:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
4. 写锁进行释放锁流程:
调用 unlock() 方法进行解锁,会调用release(),而releaseShared会调用tryRelease(),
这个方法先判断是占用读锁的线程不是当前线程,
- 是当前线程:先得到当前线程已有的读锁重入个数(firstReaderHoldCount值),如果此时得到的值为1,进行减一操作,再将读锁置为null(如果当前线程多次获得过再读锁,则进行Count–)
unlock源码:
unlock:
public void unlock() {
sync.release(1);
}
release(AQS):
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease:
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
4.1.3 锁降级
1.定义:写锁降为读锁:在写锁未释放时,获取到读锁,再释放读锁
2.意义:写锁操作的数据结果,对同一个方法里的读锁读取的时候不可见。假设线程A修改了数据,释放了写锁,这个时候线程T获得了写锁,修改了数据,然后也释放了写锁,线程A读取数据的时候,读到的是线程T修改的,并不是线程A自己修改的,那么在使用修改后的数据时,就会出现错误的结果。书上说的【当前线程无法感知线程T的数据更新】,是说线程A使用数据时,并不知道别的线程已经更改了数据,所以使用的是错误的结果。
public void readWrite() {
r.lock(); // 为了保证isUpdate能够拿到最新的值
if (!isUpdate) {
r.unlock();
w.lock();
map.put("xxx", "xxx");//1
r.lock();//2
w.unlock();//3
}
Object obj = map.get("xxx");//4
System.out.println(obj);//5
r.unlock();//6
}
A线程这里加了读锁,所以后来在get值的时候拿到的是自己修改的值,否则如果另来一个B线程后续再对它进行修改,A线程get拿的就是B修改的值,这就出大问题了!!
4.2 CountDownLatch
(共享锁)
1.定义:CountDownLatch是一个计数器闭锁,通过它可以完成类似于阻塞当前线程的功能,即:一个线程或多个线程一直等待,直到其他线程执行的操作完成。CountDownLatch用一个给定的计数器来初始化,该计数器的操作是原子操作,即同时只能有一个线程去操作该计数器。调用该类await方法的线程会一直处于阻塞状态,直到其他线程调用countDown方法使当前计数器的值变为零,每次调用countDown计数器的值减1。当计数器值减至零时,所有因调用await()方法而处于等待状态的线程就会继续往下执行。这种现象只会出现一次,因为计数器不能被重置,如果业务上需要一个可以重置计数次数的版本,可以考虑使用CycliBarrier。
在某些业务场景中,程序执行需要等待某个条件完成后才能继续执行后续的操作;典型的应用如并行计算,当某个处理的运算量很大时,可以将该运算任务拆分成多个子任务,等待所有的子任务都完成之后,父任务再拿到所有子任务的运算结果进行汇总。
await():
await()---->acquireSharedInterruptibly---->doAcquireSharedInterruptibly(AQS) —>tryAcquireShared
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
doAcquireSharedInterruptibly(AQS) —>tryAcquireShared
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
countDown -->releaseShared
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
4.3 CyclicBarrier
CyclicBarrier也是一个同步辅助类,它允许一组线程相互等待,直到到达某个公共屏障点(common barrier point)。通过它可以完成多个线程之间相互等待,只有当每个线程都准备就绪后,才能各自继续往下执行后面的操作。类似于CountDownLatch,它也是通过计数器来实现的。当某个线程调用await方法时,该线程进入等待状态,且计数器加1,当计数器的值达到设置的初始值时,所有因调用await进入等待状态的线程被唤醒,继续执行后续操作。因为CycliBarrier在释放等待线程后可以重用,所以称为循环barrier。CycliBarrier支持一个可选的Runnable,在计数器的值到达设定值后(但在释放所有线程之前),该Runnable运行一次,注,Runnable在每个屏障点只运行一个。
使用场景类似于CountDownLatch与CountDownLatch的区别
CountDownLatch主要是实现了1个或N个线程需要等待其他线程完成某项操作之后才能继续往下执行操作,描述的是1个线程或N个线程等待其他线程的关系。CyclicBarrier主要是实现了多个线程之间相互等待,直到所有的线程都满足了条件之后各自才能继续执行后续的操作,描述的多个线程内部相互等待的关系。
CountDownLatch是一次性的,而CyclicBarrier则可以被重置而重复使用。
await:
await:–>dowait–> ReentrantLock lock.lock()
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
dowait: ReentrantLock lock.lock()
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
if (g.broken)
throw new BrokenBarrierException();
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
if (index == 0) { // tripped
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException();
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
4.4 Semaphore
[ˈseməfɔː®]
Semaphore与CountDownLatch相似,不同的地方在于Semaphore的值被获取到后是可以释放的,并不像CountDownLatch那样一直减到底。它也被更多地用来限制流量,类似阀门的 功能。如果限定某些资源最多有N个线程可以访问,那么超过N个主不允许再有线程来访问,同时当现有线程结束后,就会释放,然后允许新的线程进来。有点类似于锁的lock与 unlock过程。相对来说他也有两个主要的方法:
用于获取权限的acquire(),其底层实现与CountDownLatch.countdown()类似;
用于释放权限的release(),其底层实现与acquire()是一个互逆的过程。
acquire:---->acquireSharedInterruptibly—>tryAcquireShared
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
acquireSharedInterruptibly
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
tryAcquire
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0;
}
nonfairTryAcquireShared
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
release:—>releaseShared
public void release() {
sync.releaseShared(1);
}
五 、 CAS
5.1 简介
CAS(Compare and Swap)有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。java.util.concurrent(J.U.C)种提供的atomic包中的类,使用的是乐观锁,用到的机制就是CAS,当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程可能更新变量的值,而其他线程都失败,失败的线程不会被挂起,而是被告知这次竞争失败,并可以再次尝试。
5.2 AtomicInteger
以AtomicInteger为例,研究在没有锁的情况下是如何做到数据正确性的。
例如 AtomicInteger 中有一个原子方式 i++ 操作,即
1.调用incrementAndGet(),而incrementAndGet() 调用 unsafe下的方法getAndAddInt()
2.getAndAddInt()中有一个 valueOffset 参数,这个值是 value 值在 AtomicInteger 类型中内存的偏移地址。传入的 valueOffset 参数会在后续方法中,直接从内存位置读取这个字段的值。
3.得到最新值后,调用 compareAndSwapInt 来更新最新值,如果对象 o 中 offset 偏移位置的值等于期望值(expected),就将该 offset 处的值更新为 x,当更新成功时,返回 true。结合前面调用来看,如果当前值是 v,就设置为 v+1。否则重试直到成功为止。
不仅仅是 AtomicInteger 用到了 CAS,整个 java.util.concurrent 中所有无阻塞共享内存和锁的实现都是基于 CAS 实现的。
代码如下:
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
在 unsafe 中,getAndAddInt 如下:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。
1.ABA问题。
因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2.循环时间长开销大。
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
3.只能保证一个共享变量的原子操作。
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。
六、 ThreadLocal
1.ThreadLocal 提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程
2.通常,如果我不去看源代码的话,我猜 ThreadLocal 是这样子设计的:每个 ThreadLocal类都创建一个 Map,然后用线程的 ID threadID 作为 Map 的 key,要存储的局部变量作为 Map 的 value,这样就能达到各个线程的值隔离的效果。这是最简单的设计方法,JDK 最早期的 ThreadLocal 就是这样设计的。
但是,JDK 后面优化了设计方案,现时 JDK8 ThreadLocal 的设计是:每个 Thread 维护一个 ThreadLocalMap 哈希表,这个哈希表的 key 是 ThreadLocal 实例本身,value才是真正要存储的值 Object。
这个设计与我们一开始说的设计刚好相反,这样设计有如下几点优势:
1) 这样设计之后每个 Map 存储的 Entry 数量就会变小,因为之前的存储数量由Thread 的数量决定,现在是由 ThreadLocal 的数量决定。
2) 当 Thread 销毁之后,对应的 ThreadLocalMap 也会随之销毁,生命周期与线程相同,能减少内存的使用。
注:ThreadLocalMap其实是线程自身的一个成员属性threadLocals的类型。也就是线程本地数据都存在这个threadLocals应用的ThreadLocalMap中。
6.1 ThreadLocal 常用操作的底层实现原理吗?如存储 set(Tvalue),获取 get(),删除 remove()等操作。
6.1.1 获取 get()
1 .获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象。
2 .判断当前的 ThreadLocalMap 是否存在:
- 如果存在,则以当前的 ThreadLocal 为 key,调用 ThreadLocalMap 中的 getEntry 方法获取对应的存储实体 e。找到对应的存储实体 e,获取存储实体 e 对应的 value 值,即为我们想要的当前线程对应此 ThreadLocal 的值,返回结果值。
- 如果不存在,则证明此线程没有维护的 ThreadLocalMap 对象,调用 setInitialValue 方法进行初始化。返回 setInitialValue 初始化的值。
3.setInitialValue 方法的操作如下:
1 ) 调用 initialValue 获取初始化的值。
2 ) 获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象,并判断当前的 ThreadLocalMap 是否存在: - 如果存在,则调用 map.set 设置此实体 entry。
- 如果不存在,则调用 createMap 进行 ThreadLocalMap 对象的初始化,并将此实体 entry 作为第一个值存放至 ThreadLocalMap 中。
get()对应源码:
get:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
setInitialValue:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
createMap:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
6.1.2 set(Tvalue)
1 ) 获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象。
2 ) 判断当前的 ThreadLocalMap 是否存在:
如果存在,则调用 map.set 设置此实体 entry。
如果不存在,则调用 createMap 进行 ThreadLocalMap 对象的初始化,并将此实体 entry 作为第一个值存放至 ThreadLocalMap 中。
set()对应源码:
set()与setInitialValue一样的,只不过setInitialValue是 调用initialValue()方法获得初始值,而set()是直接将要设置的值传入方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
6.1.2 删除 remove(T value)
1 ) 获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象。
2 ) 判断当前的 ThreadLocalMap 是否存在:
如果存在,则调用 map.remove 方法,进行移除。
remove()对应源码:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
6.2 ThreadLocalMap 的内部底层实现(了解)
ThreadLocalMap 的底层实现是一个定制的自定义 HashMap 哈希表,核心组成元素有:
1 ) Entry[] table;:底层哈希表 table, 必要时需要进行扩容,底层哈希表 table.length 长度必须是 2 的 n 次方。其中 Entry[] table;哈希表存储的核心元素是 Entry,Entry 包含:
- ThreadLocal<?> k;:当前存储的 ThreadLocal 实例对象
- Object value;:当前 ThreadLocal 对应储存的值 value
需要注意的是,此 Entry 继承了弱引用 WeakReference,所以在使用 ThreadLocalMap 时,发现 key ==null,则意味着此 key ThreadLocal 不在被引用,需要将其从 ThreadLocalMap 哈希表中移除
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
2) int size;:实际存储键值对元素个数 entries
private int size = 0;
3 ) int threshold;:下一次扩容时的阈值,阈值 threshold = 底层哈希表 table 的长度 len * 2 /3。当 size >= threshold 时,遍历 table 并删除 key 为 null 的元素,如果删除后 size >=threshold* 3/4 时,需要对 table 进行扩容
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
ThreadLocalMap 的构造方法是延迟加载的,也就是说,只有当线程需要存储对应的 ThreadLocal 的值时,才初始化创建一次(仅初始化一次)。
6.2.1 ThreadLocalMap 初始化步骤如下:
1) 初始化底层数组 table 的初始容量为 16。
private static final int INITIAL_CAPACITY = 16;
2) 获取 ThreadLocal 中的 threadLocalHashCode,通过 threadLocalHashCode &(INITIAL_CAPACITY - 1),即 ThreadLocal 的 hash 值 threadLocalHashCode & 哈希表的长度
length 的方式计算该实体的存储位置。
3) 存储当前的实体Entry, key 为 : 当前 ThreadLocal value:真正要存储的值
4)设置当前实际存储元素个数 size 为 1
5)设置阈值 setThreshold(INITIAL_CAPACITY),为初始化容量 16 的 2/3。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
6.2.2 ThreadLocalMap 的 getEntry()
ThreadLocal 的 get()操作实际是调用 ThreadLocalMap 的 getEntry(ThreadLocal<?> key)方法,此方法快速适用于获取某一存在 key 的实体 entry
- 计算要获取的 entry 的存储位置,存储位置计算等价于:ThreadLocal 的 hash 值threadLocalHashCode % 哈希表的长度 length。
2.根据计算的存储位置,获取到对应的实体 Entry。判断对应实体 Entry 是否存在 并且 key 是否相等:
- 存在对应实体 Entry 并且对应 key 相等,即同一 ThreadLocal,返回对应的实体 Entry。
- 不存在对应实体 Entry 或者 key 不相等,则通过调用 getEntryAfterMiss(ThreadLocal<?> key, inti, Entry e)方法继续查找。
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
3.getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)方法操作如下:
1 ) 获取底层哈希表数组 table,循环遍历对应要查找的实体 Entry 所关联的位置。
2 ) 获取当前遍历的 entry 的 key ThreadLocal,比较 key 是否一致,一致则返回。
3 ) 如果 key 不一致 并且 key 为 null,则证明引用已经不存在,这是因为 Entry 继承的是WeakReference,这是弱引用带来的坑。调用 expungeStaleEntry(int staleSlot)方法删除过期的实体 Entry。
4 ) key 不一致 ,key 也不为空,则遍历下一个位置,继续查找。
5 ) 遍历完毕,仍然找不到则返回 null。
?>
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
}
6.2.2 ThreadLocalMap 的 set()
ThreadLocal 的 set(T value)操作实际是调用 ThreadLocalMap 的 set(ThreadLocal<?> key, Objectvalue)方法,该方法进行了如下操作:
1 ) 获取对应的底层哈希表 table,计算对应 threalocal 的存储位置。
2 ) 循环遍历 table 对应该位置的实体,查找对应的 threadLocal。
3 ) 获取当前位置的 threadLocal,如果 key threadLocal 一致,则证明找到对应的 threadLocal,将新值赋值给找到的当前实体 Entry 的 value 中,结束。
4 ) 如果当前位置的 key threadLocal 不一致,并且 key threadLocal 为 null,则调用replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot)方法,替换该位置 key ==null 的实体为当前要设置的实体,结束。
5 ) 如果当前位置的 key threadLocal 不一致,并且 key threadLocal 不为 null,则创建新的实体,并存放至当前位置 i tab[i] = new Entry(key, value);,实际存储键值对元素个数 size + 1,由
于弱引用带来了这个问题,所以要调用 cleanSomeSlots(int i, int n)方法清除无用数据,才能判断现在的 size 有没有达到阀值 threshhold,如果没有要清除的数据,存储元素个数仍然 大于 阈值
则调用 rehash 方法进行扩容
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
七、阻塞队列(BlockingQueue)
7.1 简介
1.阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
2.阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
7.2 BlockingQueue的实现。
BlockingQueue 是个接口,你需要使用它的实现之一来使用BlockingQueue,java.util.concurrent包下具有以下 BlockingQueue 接口的实现类:
- ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,其内部实现是将对象放到一个数组里。有界也就意味着,它不能够存储无限多数量的元素。它有一个同一时间能够存储元素数量的上限。你可以在对其初始化的时候设定这个上限,但之后就无法对这个上限进行修改了(译者注:因为它是基于数组实现的,也就具有数组的特性:一旦初始化,大小就无法修改)。
- LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。如果需要的话,这一链式结构可以选择一个上限。如果没有定义上限,将使用 Integer.MAX_VALUE 作为上限。
- DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
- PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。你无法向这个队列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
- SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
7.2 ArrayBlockingQueue
ArrayBlockingQueue内部是通过一个Object数组和一个ReentrantLock实现的。因为数组是有界的,所以在数组为空和数组已满两种情况下需要阻塞线程,所以使用了Condition来实现线程的阻塞。
7.2.1 put方法:
put方法是一个阻塞的方法,它的执行首先要判断队列是否已满:
- 如果队列元素已满,那么当前线程将会被notFull条件对象来阻塞当前调用put方法的线程,直到线程又再次被唤醒执行。
- 但如果队列没有满,那么就直接调用enqueue(e)方法将元素加入到队列中。
- enqueue方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(items[putIndex] = x),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(notEmpty.signal())。
private final Condition notFull;
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
7.2.2 take方法
take方法其实很简单,有就删除没有就阻塞,注意这个阻塞是可以中断的,
- 如果队列为空,那么就调用notEmpty条件对象来阻塞当前调用take方法的线程
- 若队列不为空则获取数据,即完成出队操作dequeue
- dequeue方法也主要做了两件事情:1. 获取队列中的数据,即获取数组中的数据元素((E) items[takeIndex]);2. 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。
private final Condition notEmpty;
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
八、 线程池
8.1 线程池的定义
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源,提高了代码执行效率。
线程池和工厂模式、观察者模式有关(自己不要提,设计模式是个坑)
8.2 使用线程池的好处
1.降低资源消耗。通过重复利用已创建的线程,从而降低创建和销毁的消耗。
2.提高响应速度。当任务到达时,不需要等待线程创建,直接可以执行任务。
3.提高线程的可管理性。使用线程池可以实现对线程统一分配、调优和监控。
8.3 线程池的使用
在JDK的java.util.concurrent.Executors中提供了多种生成多种线程池的静态方法,然后调用他们的execute方法即可
(这几个方法都是 newThreadPoolExecutor(),只是调用构造方法以及传参有些区别)
- ExecutorService newCachedThreadPool=Executors.newCachedThreadPool();
- ExecutorService newFixedThreadPool= Executors.newFixedThreadPool(int nThreads);
- ScheduledExecutorService newScheduledThreadPool=Executors.newScheduledThreadPool(int corePoolSize);
- ExecutorService newSingleThreadExecutor=Executors.newSingleThreadExecutor();
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
8.4 常用的线程池有哪些
- newCachedThreadPool:创建一个可缓存的线程池,该线程池不会对线程池大小做限制,它的大小取决于操作系统(或JVM)能够创建的最大线程的大小。
- newFixedThreadPool:创建一个大小固定的线程池,每提交一个任务,就创建一个线程,直到达到线程池的最大线程大小。
- newScheduledThreadPool:创建一个支持定时和周期性执行任务的线程池。
- newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
8.5 线程池的所有参数
-
corePoolSize: (该线程池中核心线程数的最大值)线程池新建线程的时候,如果当前线程总数小于核心池线程数量,则新建的是核心线程,如果超过核心池线程数量,则新建的是非核心线程。核心池程默认情况下会一直存活在线程池中。
-
maximumPoolSize(线程池最大数量): 线程总数 = 核心线程数 + 非核心线程数。
-
BlockingQueue(阻塞队列): 用于保存等待执行的任务的阻塞队列。可选择以下几个阻塞队列
- ArrayBlockingQueue:基于数组 FIFO,这个队列接受到任务之后,如果当前线程小于核心线程数,则新建核心线程处理任务;如果当前线程等于核心线程数,则进入队列等待。如果这个队列也满了,则新建非核心线程执行任务,如果线程总数大于 线程池最大数量,则发生错误。
- LinkedBlockingQueue:基于链表 FIFO,这个队列接受到任务之后,如果当前线程小于核心线程数,则新建核心线程处理任务;如果当前线程等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即超过核心线程数的任务都将添加到队列中,这就导致最大线程池的设置失效。吞吐量高于上面的
- SynchronousQueue:PriorityBlockingQueue:具有自定义排序优先级
- RejectedExecutionHandler(饱和策略):当队列和线程池满了,采取策略,默认是AbortPolicy (抛出异常)。JDK5 有四种策略:
- AbortPolicy :直接抛出异常
- CallerRunsPolicy:只用调用者所在的线程来运行任务。
- DiscardOldestPolicy:丢弃队列里最近一个任务,并执行当前任务
- DiscardPolicy:不处理,丢弃掉
- keepAliveTime:线程活动保持的时间
- TimeUtil:活动保持时间单位 如:时分秒
- ThreadFactory:线程工厂,用于创建线程,一般用默认的Runnable接口来创建线程
public interface ThreadFactory {
Thread newThread(Runnable r);
}
8.6.线程池的启动策略
当调用execute()去添加一个任务时,线程会做以下判断:
- 如果正在运行的线程数小于corePoolSize(核心线程数),那么马上会创建线程执行这个任务
- 如果正在运行的线程数大于或等于corePoolSize,那么会将这个这个任务放入队列。
- 如果此时队列满了,而且正在运行的线程数小于maximumPoolSize,那么还是要创建线程执行这个任务
- 如果此时队列满了,而且正在运行的线程数大于或等于maximumPoolSize,那么线程池会抛出异常,告诉调用者无法再接受任务了。
注意:
(1)在这个过程中,当一个线程完成任务,它会从队列里取出下一个任务来执行
(2)当一个线程无事可做,超过一定的时间(KeepAliveTime:线程被回收前的空闲时间)时,线程池会判断;如果当前运行的线程数大于corePoolSize,那么这个线程就会被停掉。
(3)当线程池所有任务完成后,他最终会收缩到corePoolSize的大小。