1、synchronized底层原理
实现原理:
synchronrized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入临界区,同时它还可以保证共享变量的内存可见性。
Java中每一个对象都可以作为锁,这是synchronzied实现同步的基础:
1、普通同步方法,锁是当前实例对象。
2、静态同步方法,锁的是当前类的class对象。
3、同步方法块,锁的是括号里的对象。
当一个线程访问同步代码块时,它首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须释放锁。
public class SynchronizedTest {
public synchronized void test1() {
}
public void test2() {
synchronized(this) {
}
}
}
利用Javap工具查看生成的class文件信息来分析synchronized的实现。
从图中可以看出
1)、同步代码块时使用monitorenter和monitorExit指令来实现的。
2)、同步方法依靠的是方法修饰符上的 ACC_SYNCHRONIZED实现的。
同步代码块: monitorenter指令插入到同步代码块开始的位置,momitorexit指令插在同步代码块结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之对应。任何对象都有一个Monitor与之相关联,当且一个Monitor被持有后,它将处于锁定阶段。线程执行到monitorenter指令时,将会尝试获取对象锁对应的Monitor所有权,即尝试获得该对象的锁。
同步方法: synchronized方法则会被翻译成普通的方法和返回指令 如:invokevirtual、areturn指令,在JVM字节码层面并没有任何特别指令来实现被synchronized修饰的方法,而在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置设置为1,表示该方法为同步方法,并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass作为锁对象。
同步代码块实现原理:
1、在同步代码块中,JVM通过monitorenter和monitorexist指令实现同步锁的获取和释放功能
2、monitorenter指令是在编译后插入到同步代码块的起始位置
3、monitorexit指令是插入到方法结束处和异常处。(两个monitorexit,防止异常时出现死锁问题)。
4、JVM要保证每个monitorenter必须有对应的monitorexit与之配对
5、任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态
6、线程执行monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁线程
7、执行monitorexit指令时,将会将进入次数-1直到变成0时释放监视器
8、同一时刻只有一个线程能够成功,其它失败的线程会被阻塞,并放入到同步队列中,进入BLOCKED状态
同步方法实现原理:
1、同步方法时通过使用ACC_SYNCHRONIZED标记符隐式地实现。
2、通过方法调用指令检查该方法在常量池中是否包含ACC_SYNCHRONIZED标记符,如果有,JVM要求线程在调用该方法前需要获取到锁。
OOP-Klass模型:
Klass是在class文件在加载过程中创建的,OOP则是在Java程序运行过程中new对象时创建的。
一个OOP对象包含以下几个部分:
1、instanceOopDesc,也叫对象头
Mark Word,主要存储对象运行时记录信息,如hashcode, GC分代年龄,锁状态标志,线程ID,时间戳等
元数据指针,即指向方法区的instanceKlass实例
2、实例数据 instanceData:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组长度,这部分内存按4字节对齐
3、填充padding:由于JVM要求对象起始地址必须是8字节的整数倍,当不满足8字节时会自动填充(因此填充数据并不是必须的,仅仅是为了字节对齐)
Java对象头
synchronized用的锁是存在Java对象头里的。那么什么是对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Markword和Klass pointor(类型指针)。
- Klass ponitor 是对象指向它的类元数据的指针,虚拟机根据这个Klass pointor来确定这个对象是哪个类的实例。
- Mark Word 是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID、偏向时间戳等。
Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码代表4个字节,也就是32bits,所以对象头占用8个字节,64bits)。但是如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,无法从数组的元数据来确认数组的大小,所以需要用一块来记录数组的长度。
Java对象头的存储结构(32位虚拟机):
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word 会随着程序的运行发生变化,变化状态如下:
32为虚拟机:
64位虚拟机: Mark Word的默认存储结构(对于32位无锁状态,有25bit没有使用)
Monitor
什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。
特点:
- **互斥:**一个Monitor锁在同一个时刻只能被一个线程占用,其他线程无法占用。
- **信号机制(signal):**占用Monitor锁失败的线程会暂时放弃竞争并等待某个条件成立,当该条件成立后,当前线程会通过释放锁通知正在等待这个条件变量的其他线程,让其可以重新竞争锁。
monitor监视器:
- 每个对象都有一个监视器,在同步代码块中,JVM通过monitorenter和monitorexit指令实现同步锁的获取和释放功能。
- 当一个线程获取到同步锁时,即是通过获取monitor监视器进而等价获取到锁。
- monitor的实现类似于操作系统中的管程。
monitorenter指令:
每个对象都有一个监视器。当该监视器被占用时即是锁定状态(或者说获取监视器即是获得同步锁)。线程执行monitorenter指令时会尝试获取监视器的所有权,过程如下:
1、若该监视器的进入次数为0,则该线程进入监视器并将进入次数设置为1,此时该线程即为该监视器的所有者。
2、若线程已经占有该监视器并重入,则进入次数+1。
3、若其他线程已经占有该监视器,则线程会被阻塞直到监视器的进入次数为0,之后线程间会竞争获取该监视器的所有权。
4、只有首先获得锁的线程才能允许继续获取多个锁。
monitorexit指令:
1、执行monitorexit指令的线程必须是该对象实例所对应的监视器的所有者。
2、指令执行时,线程会先将进入次数-1,若-1后进入次数变成0,则线程退出监视器(释放锁)。
3、其他阻塞在该监视器的线程可以重新竞争该监视器的所有权。
siganl机制:
1、占有该monitor发送释放锁通知时,并未立即失去锁,而是让其他等待线程等待在队列中,重新竞争锁。
2、这种机制里,等待者拿到锁后不能确定在这个时间差里是否有别的等待者进入过Monitor,因此不能保证谓词一定为真,所以对条件的判断必须使用while
Monitor Record
- Monitor Record(统一简称MR)是线程私有的数据结构,每一个线程都有一个可用的MR列表,同时还有一个全局的可用列表。
- 一个被锁住的对象都会和一个MR关联(对象头的Markword中的Lockword指向MR的起始地址)。
- MR中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被该线程占用。
Monitor Record结构
Owner: 1)初始时为 NULL 表示当前没有任何线程拥有该 Monitor Record;
2)当线程成功拥有该锁后保存线程唯一标识;
3)当锁被释放时又设置为 NULL 。
EntryQ:关联一个系统互斥锁( semaphore ),阻塞所有试图锁住 Monitor Record失败的线程 。
RcThis:表示 blocked 或 waiting 在该 Monitor Record 上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的 HashCode 值(可能还包含 GC age )。
Candidate:用来避免不必要的阻塞或等待线程唤醒。因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate 只有两种可能的值 :1)0 表示没有需要唤醒的线程;2)1 表示要唤醒一个继任线程来竞争锁。
Monitor Record工作原理
- 线程如果获得监视锁成功,将成为该监视锁对象的拥有者。
- 在任一时刻,监视器对象只属于一个活动线程(Owner)。
- 拥有者可以调用wait方法自动释放监视锁,进入等待状态
锁优化
简单来说,在 JVM 中 monitorenter 和 monitorexit 字节码依赖于底层的操作系统的Mutex Lock 来实现的,但是由于使用 Mutex Lock 需要将当前线程挂起并从用户态切换到内核态来执行,这种切换的代价是非常昂贵的。然而,在现实中的大部分情况下,同步方法是运行在单线程环境(无锁竞争环境),如果每次都调用 Mutex Lock 那么将严重的影响程序的性能。
因此,JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁的操作的开销。
自旋锁
由来:
线程的阻塞和唤醒,需要 CPU 从用户态转为核心态。频繁的阻塞和唤醒对 CPU 来说是一件负担很重的工作,势必会给系统的并发性能带来很大的压力。同时,我们发现在许多应用上面,对象锁的锁状态只会持续很短一段时间。为了这一段很短的时间,频繁地阻塞和唤醒线程是非常不值得的。所以引入自旋锁。
定义:
所谓自旋锁,就是让该线程等待一段时间,不会被立即挂起,看持有锁的线程是否会很快释放锁。
自旋等待不能替代阻塞,先不说对处理器数量的要求(多核,貌似现在没有单核的处理器了),虽然它可以避免线程切换带来的开销,但是它占用了处理器的时间。如果持有锁的线程很快就释放了锁,那么自旋的效率就非常好,反之,自旋的线程就会白白消耗掉处理的资源,它不会做任何有意义的工作,典型的占着茅坑不拉屎,这样反而会带来性能上的浪费。
所以说,自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。
自旋锁在 JDK 1.4.2 中引入,默认关闭,但是可以使用 -XX:+UseSpinning 开开启。
在 JDK1.6 中默认开启。同时自旋的默认次数为 10 次,可以通过参数 -XX:PreBlockSpin 来调整。
如果通过参数 -XX:PreBlockSpin 来调整自旋锁的自旋次数,会带来诸多不便。假如我将参数调整为 10 ,但是系统很多线程都是等你刚刚退出的时候,就释放了锁(假如你多自旋一两次就可以获取锁),你是不是很尴尬。于是 JDK 1.6 引入自适应的自旋锁,让虚拟机会变得越来越聪明。
自适应自旋锁
JDK1.6引入了自适应自旋锁。
所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?
线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。
反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。
锁消除
由来:
为了保证数据的完整性,我们在进行操作时需要对这部分操作进行同步控制。但是,在某些情况下,JVM检测到不可能存在共享数据竞争,这时JVM会对这些同步锁进行锁消除,如果不存在竞争,为什么还需要加锁呢?所以锁消除可以节省毫无意义的请求锁时间。
定义:
锁消除的依据是逃逸分析的数据支持。变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是对于我们程序员来说这还不清楚么?我们会在明明知道不存在数据竞争的代码块前加上同步吗?但是有时候程序并不是我们所想的那样?我们虽然没有显示使用锁,但是我们在使用一些 JDK 的内置 API 时,如 StringBuffer、Vector、HashTable 等,这个时候会存在隐性的加锁操作。比如 StringBuffer 的 #append(…)方法,Vector 的 add(…) 方法:
public void vectorTest(){
Vector<String> vector = new Vector<String>();
for (int i = 0 ; i < 10 ; i++){
vector.add(i + "");
}
System.out.println(vector);
}
在运行这段代码时,JVM 可以明显检测到变量 vector 没有逃逸出方法 #vectorTest() 之外,所以 JVM 可以大胆地将 vector 内部的加锁操作消除。
锁粗化
由来:
我们知道在使用同步锁的时候,需要让同步块的作用范围尽可能小:仅在共享数据的实际作用域中才进行同步。这样做的目的,是为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。
在大多数的情况下,上述观点是正确的,但是如果一系列的连续加锁解锁操作,可能会导致不必要的性能损耗,所以引入锁粗话的概念。
定义:
锁粗话概念比较好理解,就是将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。
如上面实例:vector 每次 add 的时候都需要加锁操作,JVM 检测到对同一个对象(vector)连续加锁、解锁操作,会合并一个更大范围的加锁、解锁操作,即加锁解锁操作会移到 for 循环之外。
锁升级
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。它们会随着竞争的激烈而逐渐升级。注意,锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
重量级锁
重量级锁通过对象内部的monitor监视器实现的。
其中,Monitor的本质是,依赖于底层操作系统的Mutex Lock实现。操作系统实现线程之间的切换,需要从用户态切换成内核态,切换成本非常高。
轻量级锁
引入轻量级锁的主要目的是,在没有多个线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
当关闭偏向锁功能或者多个线程竞争偏向锁,导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁,其步骤如下:
获取轻量级锁:
1、判断当前对象是否处于无锁状态?若是,则JVM首先将在当前线程的栈帧中,建立一个名为锁记录(Lock Record)的空间,用于存储锁对象的Mark word的拷贝(官方把这份拷贝加了一个 Displaced 前缀,即 Displaced Mark Word);否则,执行步骤3;
2、JVM通过CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果成功,表示竞争到锁,则标志位变成00,(表示此对象处于轻量级锁),执行同步操作;如果失败,则执行步骤3;
3、判断当前对象的Mark Word是否指向线程的栈帧?如果是,则表示当前线程已经持有该对象的锁,则直接执行同步代码;否则,只能说明该锁对象被其他线程占有了,当前线程便尝试使用自旋来获取锁。若自旋后没有获得锁,此时轻量级锁会升级为重量级锁。锁标志位变成10,当前线程会阻塞。
释放轻量级锁:
轻量级锁的释放也是通过CAS操作来进行的,主要步骤是:
1、取出在获取轻量级锁保存在Displaced Mark Word中的数据。
2、使用CAS操作将取出的数据替换当前对象的Mark Word中。如果成功,则说明释放锁成功;否则执行步骤3;
3、如果CAS操作失败,说明有其他线程尝试获取该锁,则需要在释放锁的同时需要唤醒被挂起的线程。
锁膨胀流程图:
注意事项
对于轻量级锁,其性能提升的依据是:“对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”。如果打破这个依据则除了互斥的开销外,还有额外的 CAS 操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。
偏向锁
引入偏向锁的主要目的是:为了在无多线程竞争的情况下,尽量减少不必要的轻量级锁执行路径。
轻量级锁的加锁解锁操作,是需要依赖多次CAS原子指令的。
而偏向锁只需要检查是否为偏向锁、锁标识以及ThreadId即可。
获取偏向锁:
1、检查Mark Word是否为可偏向状态,即是否为偏向锁的标识位为1,锁标识位为01.
2、若为可偏向状态,则检查线程ID是否是当前线程ID?如果是,则加锁成功,执行同步代码,如果不是,执行步骤3
3、如果线程ID不是当前线程ID,则通过CAS操作竞争锁。竞争成功,则将MarkWord的线程ID替换成当前线程ID,执行同步代码。
4、如果通过CAS竞争锁失败,证明当前存在多个线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级成为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
撤销偏向锁:
偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。其他线程来竞争时,才判断拥有偏向锁的线程是否存活。
偏向锁的撤销需要等待全局安全点(这个时间点是线程上没有正在执行的代码)。其步骤如下:
1、暂停拥有偏向锁的线程,判断线程是否还存活,如果线程非活动状态,则将对象头设置为无锁状态(01,是否偏向锁标志位为0)。其他线程会重新获取该偏向锁。
2、如果线程是活动状态,拥有偏向锁的栈会被执行,遍历偏向锁对象的锁记录,并将对栈中的锁记录(Lock Record)和对象头的Mark Word进行重置。
- 要么重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程"被"释放了锁)
- 要么恢复到无锁或者标记锁对象不适合作为偏向锁(此时锁会被升级为轻量级锁)
最后唤醒暂停的线程,被阻塞在安全点的线程继续往下执行同步代码块。
偏向锁的获取和释放流程:
关闭偏向锁:
偏向锁在 JDK 1.6 以上,默认开启。开启后程序启动几秒后才会被激活,可使用 JVM 参数 -XX:BiasedLockingStartupDelay = 0 来关闭延迟。
如果确定锁通常处于竞争状态,则可通过JVM参数 -XX:-UseBiasedLocking=false 关闭偏向锁,那么默认会进入轻量级锁。
**优势:**偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令,其余时刻不需要 CAS 指令(相比其他锁)。
**隐患:**由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的 CAS 原子指令的性能消耗(这个通常只能通过大量压测才可知)。
**对比:**轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。
三种锁的转换图:
2、ReentrantLock底层原理
简介
ReentrantLock,可重入锁,是一种递归无阻塞的同步机制,它可以等同于synchronized的使用,但是ReentrantLock提供了比synchronized更加强大、灵活的锁机制,可以减少死锁发生的概率。
ReentrantLock将由最近成功获得锁定,并且还没有释放该锁定的线程所拥有。当锁定没有被另一线程所拥有时,调用lock方法的线程将成功获得锁定并返回。如果当前线程已经拥有该锁定,此方法将立即返回。可以使用#isHeldByCurrentThread() 和 #getHoldCount() 方法来检查此情况是否发生。
ReentrantLock还提供公平锁和非公平锁的选择,通过构造方法接收一个可选的fair参数(默认是非公平锁);当设置为true时,表示公平锁,否则为非公平锁。
公平锁和非公平锁的区别在于,公平锁的锁获取是有顺序的。但公平锁的效率往往没有非公平锁的效率高,在多线程访问的情况下,公平锁表现出较低的吞吐量。
ReentrantLock整体架构如下:
ReentrantLock实现了Lock接口,基于内部的Sync实现。
Sync实现AQS,提供了FailSync和NonFairSync两种实现。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
Sync抽象类
Sync是ReenTrantLock的内部静态类,实现AbstractQueuedSynchronizer抽象类,同步抽象类。它使用AQS的state字段,来表示当前锁的持有数量,从而实现了可重入的特性。
lock
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
执行锁。抽象了该方法的原因是,允许子类实现快速获得非公平锁的逻辑
nonfairTryAcquire
nofairTryAcquire(int acquires)方法,非公平锁的方式获得锁,代码如下:
final boolean nonfairTryAcquire(int acquires) {
//当前线程
final Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//state == 0,表示没有该锁处于空闲状态
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;
}
方法逻辑:
1、判断同步状态 state是否为0 。
2、如果为0,表示该锁还未被其他线程持有,直接通过CAS获得同步状态,如果成功,返回true,失败,则返回false。
3、如果不为0,表明线程已经被线程持有,判断当前线程是否是持有锁的线程,如果是,则获得锁,成功返回true。成功获取锁的线程,再次获取锁,增加了同步状态state。否则返回false。
理论上这个方法应该在子类FairSync中实现的,但是为什么会在这里呢?在下文 **ReentrantLock.tryLock()**中详细介绍。
tryRelease
tryRelease(int releases)实现方法,释放锁,代码如下:
protected final boolean tryRelease(int releases) {
// 减掉releases
int c = getState() - releases;
// 如果释放的不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// state == 0 表示已经释放完全了,其他线程可以获取同步状态了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
代码逻辑:
1、判断当前线程是否为拥有锁的线程,保证该方法的线程的安全性。
2、只有同步状态彻底释放后,该方法才会返回true,当state==0时,则将锁持有的线程设置为null,free= true,表示锁释放成功。
其他实现方法
// 是否当前线程独占
@Override
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
// 新生成条件
final ConditionObject newCondition() {
return new ConditionObject();
}
// Methods relayed from outer class
// 获得占用同步状态的线程
final Thread getOwner() {
return getState() == 0 ? null : getExclusiveOwnerThread();
}
// 获得当前线程持有锁的数量
final int getHoldCount() {
return isHeldExclusively() ? getState() : 0;
}
// 是否被锁定
final boolean isLocked() {
return getState() != 0;
}
/**
* Reconstitutes the instance from a stream (that is, deserializes it).
* 自定义反序列化逻辑
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
Sync实现类
NonfairSync
NonfairSync是ReentrantLock的内部静态类,实现Sync抽象类,非公平锁实现类。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
lock方法:
首先基于AQS state进行CAS操作,将0-》1,若成功,则获取锁成功。若失败,则执行AQS的正常同步状态获取逻辑。此时可能有N+1个线程正在获取锁,其中1个线程已经获得锁,释放的瞬间,恰好被新的线程抢夺到,而不是排队的N个线程。体现了非公平锁的特点。
tryAcquire方法:
直接调用 #nonfairTryAcquire(int acquires) 方法,非公平锁的方式获得锁。
FairSync
FairSync是ReentrantLock的内部静态类,实现了Sync抽象,公平锁实现类。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && //<1>
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;
}
}
lock方法: 直接执行 AQS 的正常的同步状态获取逻辑。
tryAquire方法:
比较非公平锁和公平锁获取同步状态的过程,会发现两者唯一的区别就在于,公平锁在获取同步状态时多了一个限制条件 <1> 处的 #hasQueuedPredecessors() 方法,是否有前序节点,即自己不是首个等待获取同步状态的节点。代码如下:
// AbstractQueuedSynchronizer.java
public final boolean hasQueuedPredecessors() {
Node t = tail; //尾节点
Node h = head; //头节点
Node s;
//头节点 != 尾节点
//同步队列第一个节点不为null
//当前线程是同步队列第一个节点
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
主要是判断当前线程是否位于 CLH 同步队列中的第一个。如果是则返回 true ,否则返回 false 。
Lock接口
定义方法如下:
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
ReentrantLock的实现基本都是调用Sync的方法。
Condition
在没有lock之前,我们使用synchronized来控制同步,配合Object的wait()和notify()等一系列方法可以实现等待/通知模式。
在Java SE5之后加入了Lock接口,相对于sunchronized而言,Lock提供了Condition,对线程的等待、唤醒操作更加灵活和详细。
Condition接口
java.util.concurrent.locks.Condition ,条件 Condition 接口,定义了一系列的方法,来对阻塞和唤醒线程:
// ========== 阻塞 ==========
void await() throws InterruptedException; // 造成当前线程在接到信号或被中断之前一直处于等待状态。
void awaitUninterruptibly(); // 造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
long awaitNanos(long nanosTimeout) throws InterruptedException; // 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在`nanosTimeout` 之前唤醒,那么返回值 `= nanosTimeout - 消耗时间` ,如果返回值 `<= 0` ,则可以认定它已经超时了。
boolean await(long time, TimeUnit unit) throws InterruptedException; // 造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
boolean awaitUntil(Date deadline) throws InterruptedException; // 造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回 true ,否则表示到了指定时间,返回返回 false 。
// ========== 唤醒 ==========
void signal(); // 唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
void signalAll(); // 唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。
Condition 是一种广义上的条件队列。他为线程提供了一种更为灵活的等待 / 通知模式,线程在调用 await 方法后执行挂起操作,直到线程等待的某个条件为真时才会被唤醒。Condition 必须要配合 Lock 一起使用,因为对共享状态变量的访问发生在多线程环境下。一个 Condition 的实例必须与一个 Lock 绑定,因此 Condition 一般都是作为 Lock 的内部实现。
ConditionObject对象
获取一个 Condition 必须要通过 Lock 的 #newCondition() 方法。该方法定义在接口 Lock 下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。Condition 为一个接口,其下仅有一个实现类 ConditionObject ,由于 Condition 的操作需要获取相关的锁,而 AQS则是同步锁的实现基础,所以 ConditionObject 则定义为 AQS 的内部类。代码如下:
public class ConditionObject implements Condition, java.io.Serializable {
/** First node of condition queue. */
private transient Node firstWaiter; // 头节点
/** Last node of condition queue. */
private transient Node lastWaiter; // 尾节点
public ConditionObject() {
}
// ... 省略内部代码
}
从上面代码可以看出,ConditionObject 拥有首节点(firstWaiter),尾节点(lastWaiter)。当前线程调用 #await()方法时,将会以当前线程构造成一个节点(Node),并将节点加入到该队列的尾部。结构如下:
Node 里面包含了当前线程的引用。Node 定义与 AQS 的 CLH 同步队列的节点使用的都是同一个类(AbstractQueuedSynchronized 的 Node 静态内部类)。
ConditionObject 的队列结构比 CLH 同步队列的结构简单些,新增过程较为简单,只需要将原尾节点的 Node.next 指向新增节点,然后更新 ConditionObject.lastWaiter 即可。
大体实现流程
AQS 等待队列与 Condition 队列是两个相互独立的队列。
- #await() 就是在当前线程持有锁的基础上释放锁资源,并新建 Condition 节点加入到 Condition 的队列尾部,阻塞当前线程 。
- #signal() 就是将 Condition 的头节点移动到 AQS 等待节点尾部,让其等待再次获取锁。
1、初始化阶段: AQS等待队列有 3 个Node,Condition 队列有 1 个Node(也有可能 1 个都没有)。
2、节点1执行 Condition.await()
(1)将head后移
(2)释放节点1的锁并从AQS等待队列中移除。
(3)将节点1加入到Condition的等待队列中。
(4)更新lastWaiter为节点1
3、节点2执行Condition.signal()操作
(5)将firstWaiter后移
(6)将节点4移出Condition队列
(7)将节点4加入到AQS的等待队列中。
(8)更新AQS的等待队列的tail。
等待
await
调用 Condition 的 #await() 方法,会使当前线程进入等待状态,同时会加入到 Condition 等待队列,并且同时释放锁。当从 #await() 方法结束时,当前线程一定是获取了Condition 相关联的锁。
public final void await() throws InterruptedException {
// 当前线程中断
if (Thread.interrupted())
throw new InterruptedException();
//当前线程加入等待队列
Node node = addConditionWaiter();
//释放锁
long savedState = fullyRelease(node);
int interruptMode = 0;
/**
* 检测此节点的线程是否在同步队上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待
* 直到检测到此节点在同步队列上
*/
while (!isOnSyncQueue(node)) {
//线程挂起
LockSupport.park(this);
//如果已经中断了,则退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//竞争同步状态
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清理下条件队列中的不是在等待条件的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
1、首先,将当前线程新建一个节点同时加入到条件队列中。
2、然后,释放当前线程持有的同步状态。
3、之后,则是不断检测该节点代表的线程,出现在 CLH 同步队列中(收到 signal 信号之后,就会在 AQS 队列中检测到),如果不存在则一直挂起。
4、最后,重新参与竞争,获取到同步状态。
addConditionWaiter
#addConditionWaiter() 方法,加入条件队列,代码如下:
private Node addConditionWaiter() {
Node t = lastWaiter; //尾节点
//Node的节点状态如果不为CONDITION,则表示该节点不处于等待状态,需要清除节点
if (t != null && t.waitStatus != Node.CONDITION) {
//清除条件队列中所有状态不为Condition的节点
unlinkCancelledWaiters();
t = lastWaiter;
}
//当前线程新建节点,状态 CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
/**
* 将该节点加入到条件队列中最后一个位置
*/
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
该方法主要是将当前线程加入到 Condition 条件队列中。当然,在加入到尾节点之前,会调用 #unlinkCancelledWaiters() 方法,清除所有状态不为 Condition 的节点。
fullyRelease
#fullyRelease(Node node) 方法,负责完全释放该线程持有的锁,因为例如 ReentrantLock 是可以重入的。代码如下:
final long fullyRelease(Node node) {
boolean failed = true;
try {
// 节点状态--其实就是持有锁的数量
long savedState = getState();
// 释放锁
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
1、正常情况下,释放锁都能成功,因为是先调用 Lock#lock() 方法,再调用 Condition#await() 方法。
2、那么什么情况下会失败,抛出 IllegalMonitorStateException 异常呢?例如,当前线程未持有锁,未调用 Lock#lock() 方法,而直接调用 Condition#await() 方法,此时就会抛出该异常。
3、另外,释放失败的情况下,会设置 Node 的等待状态为 Node.CANCELED 。
isOnSyncQueue
#isOnSyncQueue(Node node) 方法,如果一个节点刚开始在条件队列上,现在在同步队列上获取锁则返回 true 。代码如下:
final boolean isOnSyncQueue(Node node) {
// 状态为 Condition,获取前驱节点为 null ,返回 false
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 后继节点不为 null,肯定在 CLH 同步队列中
if (node.next != null)
return true;
return findNodeFromTail(node);
}
unlinkCancelledWaiters
#unlinkCancelledWaiters() 方法,负责将条件队列中状态不为 Condition 的节点删除。代码如下:
// 等待队列是一个单向链表,遍历链表将已经取消等待的节点清除出去
// 纯属链表操作,很好理解,看不懂多看几遍就可以了
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null; // 用于中间不需要跳过时,记录上一个 Node 节点
while (t != null) {
Node next = t.nextWaiter;
// 如果节点的状态不是 Node.CONDITION 的话,这个节点就是被取消的
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
通知
signal方法
调用 ConditionObject的 #signal() 方法,将会唤醒在等待队列中等待最长时间的节点(条件队列里的首节点),在唤醒节点前,会将节点移到CLH同步队列中。
public final void signal() {
//检测当前线程是否为拥有锁的独
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//头节点,唤醒条件队列中的第一个节点
Node first = firstWaiter;
if (first != null)
doSignal(first); //唤醒
}
该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后调用 #doSignal(Node first) 方法,唤醒条件队列中的头节点。代码如下:
private void doSignal(Node first) {
do {
//修改头结点,完成旧头结点的移出工作
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
主要是做两件事:1)修改头节点;2)调用 #transferForSignal(Node first) 方法将节点移动到 CLH 同步队列中。代码如下:
final boolean transferForSignal(Node node) {
//将该节点从状态CONDITION改变为初始状态0,
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点
Node p = enq(node);
int ws = p.waitStatus;
//如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
整个通知的流程如下:
1、判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
2、如果线程已经获取了锁,则将唤醒条件队列的首节点
3、唤醒首节点是先将条件队列中的头节点移出,然后调用 AQS 的 #enq(Node node) 方法将其安全地移到 CLH 同步队列中
4、最后判断如果该节点的同步状态是否为 Node.CANCEL ,或者修改状态为 Node.SIGNAL 失败时,则直接调用 LockSupport 唤醒该节点的线程。
总结
一个线程获取锁后,通过调用 Condition 的 #await() 方法,会将当前线程先加入到条件队列中,然后释放锁,最后通过 #isOnSyncQueue(Node node) 方法,不断自检看节点是否已经在 CLH 同步队列了,如果是则尝试获取锁,否则一直挂起。
当线程调用 #signal() 方法后,程序首先检查当前线程是否获取了锁,然后通过#doSignal(Node first) 方法唤醒CLH同步队列的首节点。被唤醒的线程,将从 #await() 方法中的 while 循环中退出来,然后调用 #acquireQueued(Node node, int arg) 方法竞争同步状态。
Condition 的应用
public class ConditionTest {
private LinkedList<String> buffer; //容器
private int maxSize ; //容器最大
private Lock lock;
private Condition fullCondition;
private Condition notFullCondition;
ConditionTest(int maxSize){
this.maxSize = maxSize;
buffer = new LinkedList<String>();
lock = new ReentrantLock();
fullCondition = lock.newCondition();
notFullCondition = lock.newCondition();
}
public void set(String string) throws InterruptedException {
lock.lock(); //获取锁
try {
while (maxSize == buffer.size()){
notFullCondition.await(); //满了,添加的线程进入等待状态
}
buffer.add(string);
fullCondition.signal();
} finally {
lock.unlock(); //记得释放锁
}
}
public String get() throws InterruptedException {
String string;
lock.lock();
try {
while (buffer.size() == 0){
fullCondition.await();
}
string = buffer.poll();
notFullCondition.signal();
} finally {
lock.unlock();
}
return string;
}
}
ReentrantLock和synchronized的区别
1、与 synchronized 相比,ReentrantLock提供了更多,更加全面的功能,具备更强的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
2、ReentrantLock 还提供了条件 Condition ,对线程的等待、唤醒操作更加详细和灵活,所以在多个条件变量和高度竞争锁的地方,ReentrantLock 更加适合。
3、ReentrantLock 提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继续,否则可以等到下次运行时处理,而 synchronized 则一旦进入锁请求要么成功要么阻塞,所以相比 synchronized 而言,ReentrantLock会不容易产生死锁些。
4、ReentrantLock 支持更加灵活的同步代码块,但是使用 synchronized 时,只能在同一个 synchronized 块结构中获取和释放。注意,ReentrantLock 的锁释放一定要在 finally 中处理,否则可能会产生严重的后果。
5、ReentrantLock 支持中断处理,且性能较 synchronized 会好些。
6、ReenttrantLock可以实现非公平锁和公平锁,而synchronized只能实现非公共锁。
3、ReentrantReadWriteLock底层实现原理
简介
重入锁ReentrantLock是排他锁,排他锁在同一时刻仅有一个线程可以进行访问。但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较少。然而,读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会导致性能降低。所以就提供了读写锁。
读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升。
- 在同一时间,可以允许多个读线程同时访问。
- 但是,在写线程访问时,所有读线程和写线程都会被阻塞。
读写锁的主要特性:
- 公平性:支持公平性和非公平性。
- 重入性:支持重入。读写锁最多支持65535个递归写入锁和65535个递归读取锁。
- 锁降级:遵循获取写锁,再获取读锁,最后释放写锁的次序,如此写锁能够降级成为读锁。
ReadWriteLock读写锁接口
java.util.concurrent.locks.ReadWriteLock ,读写锁接口。定义方法如下:
Lock readLock();
Lock writeLock();
一对方法,分别获得读锁和写锁 Lock 对象。
ReentrantReadWriteLock
java.util.concurrent.locks.ReentrantReadWriteLock ,实现 ReadWriteLock 接口,可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 Writer 线程,读取锁可以由多个 Reader 线程同时保持。也就说说,写锁是独占的,读锁是共享的。
ReentrantReadWriteLock 类的大体结构如下:
/** 内部类 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 内部类 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 返回用于写入操作的锁 */
@Override
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
/** 返回用于读取操作的锁 */
@Override
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {
/**
* 省略其余源代码
*/
}
public static class WriteLock implements Lock, java.io.Serializable {
/**
* 省略其余源代码
*/
}
public static class ReadLock implements Lock, java.io.Serializable {
/**
* 省略其余源代码
*/
}
1、ReentrantReadWriteLock 与 ReentrantLock一样,其锁主体也是 Sync,它的读锁、写锁都是通过 Sync 来实现的。所以 ReentrantReadWriteLock 实际上只有一个锁,只是在获取读取锁和写入锁的方式上不一样。
2、它的读写锁对应两个类:ReadLock 和 WriteLock 。这两个类都是 Lock 的子类实现。
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。
分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么:
写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)
读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。
代码如下:
// Sync.java
static final int SHARED_SHIFT = 16; // 位数
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 每个锁的最大重入次数,65535
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; }
#exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数。
#sharedCount(int c) 静态方法,获得持有读状态的锁的线程数量。不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器。详细解析,见 「6. HoldCounter」 。
在读锁获取锁和释放锁的过程中,我们一直都可以看到一个变量 rh (HoldCounter ),该变量在读锁中扮演着非常重要的作用。
我们了解读锁的内在机制其实就是一个共享锁,为了更好理解 HoldCounter ,我们暂且认为它不是一个锁的概率,而相当于一个计数器。一次共享锁的操作就相当于在该计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。所以 HoldCounter 的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。
HoldCounter 是 Sync 的内部静态类。
static final class HoldCounter {
int count = 0; // 计数器
final long tid = getThreadId(Thread.currentThread()); // 线程编号
}
HoldCounter 定义非常简单,就是一个计数器 count和线程编号 tid 两个变量。按照这个意思我们看到 HoldCounter 是需要和某给线程进行绑定了。我们知道如果要将一个对象和线程绑定仅仅有 tid 是不够的,而且从上面的代码我们可以看到 HoldCounter 仅仅只是记录了 tid ,根本起不到绑定线程的作用。那么怎么实现呢?答案是实现 ThreadLocal 的 ThreadLocalHoldCounter 类,代码如下:
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
@Override
public HoldCounter initialValue() {
return new HoldCounter();
}
}
通过 ThreadLocalHoldCounter 类,HoldCounter 就可以与线程进行绑定了。故而,HoldCounter 应该就是绑定线程上的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。从上面我们可以看到 ThreadLocal 将 HoldCounter 绑定到当前线程上,同时 HoldCounter 也持有线程编号,这样在释放锁的时候才能知道 ReadWriteLock 里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get() 方法的次调用数,因为这也是一个耗时操作。需要说明的是这样HoldCounter 绑定线程编号而不绑定线程对象的原因是,避免 HoldCounter 和 ThreadLocal 互相绑定而导致 GC 难以释放它们(尽管 GC 能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助 GC 快速回收对象而已。
看到这里我们明白了 HoldCounter 作用了,我们在看一个获取读锁的代码段:
//如果获取读锁的线程为第一次获取读锁的线程,则firstReaderHoldCount重入数 + 1
else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//非firstReader计数
if (rh == null)
rh = cachedHoldCounter;
//rh == null 或者 rh.tid != current.getId(),需要获取rh
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
//加入到readHolds中
else if (rh.count == 0)
readHolds.set(rh);
//计数+1
rh.count++;
cachedHoldCounter = rh; // cache for release
}
【写锁】tryAcquire
@Override
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//当前锁个数
int c = getState();
//写锁
int w = exclusiveCount(c);
if (c != 0) {
//c != 0 && w == 0 表示存在读锁
//当前线程不是已经获取写锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//超出最大范围
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
// 是否需要阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置获取锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
1、该方法和 ReentrantLock 的 #tryAcquire(int arg) 大致一样,差别在判断重入时,增加了一项条件:读锁是否存在。因为要确保写锁的操作对读锁是可见的。如果在存在读锁的情况下允许获取写锁,那么那些已经获取读锁的其他线程可能就无法感知当前写线程的操作。因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写锁获取了,所有其他读、写线程均会被阻塞。
2、调用 #writerShouldBlock() 抽象方法,若返回 true ,则获取写锁失败。
【读锁】tryAcquireShared
#tryAcqurireShared(int arg) 方法,尝试获取读同步状态,获取成功返回 >= 0 的结果,否则返回 < 0 的结果。代码如下:
protected final int tryAcquireShared(int unused) {
//当前线程
Thread current = Thread.currentThread();
int c = getState();
//exclusiveCount(c)计算写锁
//如果存在写锁,且锁的持有者不是当前线程,直接返回-1
//存在锁降级问题,后续阐述
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//读锁
int r = sharedCount(c);
/*
* readerShouldBlock():读锁是否需要等待(公平锁原则)
* r < MAX_COUNT:持有线程小于最大数(65535)
* compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { //修改高16位的状态,所以要加上2^16
/*
* holdCount部分后面讲解
*/
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);
}
读锁获取的过程相对于独占锁而言会稍微复杂下,整个过程如下:
1、因为存在锁降级情况,如果存在写锁且锁的持有者不是当前线程,则直接返回失败,否则继续。
2、依据公平性原则,调用 #readerShouldBlock() 方法来判断读锁是否不需要阻塞,读锁持有线程数小于最大值(65535),且 CAS 设置 锁状态成功,设置holdcount,并返回 1 。如果不满足任一条件,则调用 #fullTryAcquireShared(Thread thread) 方法。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
// 锁降级
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
// 读锁需要阻塞,判断是否当前线程已经获取到读锁
else if (readerShouldBlock()) {
//列头为当前线程
if (firstReader == current) {
}
//HoldCounter后面讲解
else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0) // 计数为 0 ,说明没得到读锁,清空线程变量
readHolds.remove();
}
}
if (rh.count == 0) // 说明没得到读锁
return -1;
}
}
//读锁超出最大范围
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//CAS设置读锁成功
if (compareAndSetState(c, c + SHARED_UNIT)) { //修改高16位的状态,所以要加上2^16
//如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
//如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程,则将firstReaderHoldCount+1
else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
//更新线程的获取“读取锁”的共享计数
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
该方法会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过 CAS 尝试获取锁,并返回 1 。所以,#fullTryAcquireShared(Thread) 方法,是 #tryAcquireShared(int unused) 方法的自旋重试的逻辑。
【写锁】tryRelease
protected final boolean tryRelease(int releases) {
//释放的线程不为锁的持有者
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
//若写锁的新线程数为0,则将锁的持有者设置为null
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
写锁释放锁的整个过程,和独占锁 ReentrantLock 相似,每次释放均是减少写状态,当写状态为 0 时,表示写锁已经完全释放了,从而让等待的其他线程可以继续访问读、写锁,获取同步状态。同时,此次写线程的修改对后续的线程可见。
【读锁】tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果想要释放锁的线程为第一个获取锁的线程
if (firstReader == current) {
//仅获取了一次,则需要将firstReader 设置null,否则 firstReaderHoldCount - 1
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
}
//获取rh对象,并更新“当前线程获取锁的信息”
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;
}
//CAS更新同步状态
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
tryWriteLock
#tryWriteLock() 方法,尝试获取写锁。
若获取成功,返回 true 。
若失败,返回 false 即可,不进行等待排队。
final boolean tryWriteLock(){
Thread current = Thread.currentThread();
int c = getState();
if(c != 0){
int w = exclusiveCount(c); // 获得现在写锁获取的数量
if(w == 0 || current != getExclusiveOwnerThread()){ // 判断是否是其他的线程获取了写锁。若是,返回 false
return false;
}
if(w == MAX_COUNT){ // 超过写锁上限,抛出 Error 错误
throw new Error("Maximum lock count exceeded");
}
}
if(!compareAndSetState(c, c + 1)){ // CAS 设置同步状态,尝试获取写锁。若失败,返回 false
return false;
}
setExclusiveOwnerThread(current); // 设置持有写锁为当前线程
return true;
}
tryReadLock
#tryReadLock() 方法,尝试获取读锁。
若获取成功,返回 true 。
若失败,返回 false 即可,不进行等待排队。
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
//exclusiveCount(c)计算写锁
//如果存在写锁,且锁的持有者不是当前线程,直接返回-1
//存在锁降级问题,后续阐述
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
// 读锁
int r = sharedCount(c);
/*
* HoldCount 部分后面讲解
*/
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (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 true;
}
}
}
锁降级
锁降级就意味着写锁是可以降级为读锁的,但是需要遵循先获取写锁、获取读锁在释放写锁的次序。注意如果当前线程先获取写锁,然后释放写锁,再获取读锁这个过程不能称之为锁降级,锁降级一定要遵循那个次序。
在获取读锁的方法 #tryAcquireShared(int unused) 中,有一段代码就是来判读锁降级的:
int c = getState();
//exclusiveCount(c)计算写锁
//如果存在写锁,且锁的持有者不是当前线程,直接返回-1
//存在锁降级问题,后续阐述
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//读锁
int r = sharedCount(c);
锁降级中读锁的获取释放为必要?肯定是必要的。试想,假如当前线程 A 不获取读锁而是直接释放了写锁,这个时候另外一个线程 B 获取了写锁,那么这个线程 B 对数据的修改是不会对当前线程 A 可见的。如果获取了读锁,则线程B在获取写锁过程中判断如果有读锁还没有释放则会被阻塞,只有当前线程 A 释放读锁后,线程 B 才会获取写锁成功。
4、volatile关键字底层原理
volatile关键字
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新(可见性),线程应该确保通过排他锁单独获得这个变量。通俗地讲,当一个变量被volatile关键字修饰了,则Java可以确保所有线程看到这个变量的值是一致的。如果某个线程对volatile修饰的共享变量进行了更新,那么其他线程可以立马看到这个更新,这就是所谓的线程可见性。
内存模型
操作系统语义:
计算机在运行程序时,每条指令都在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主内存中的,这时就会出现一个问题,读写主存的数据没有CPU执行指令的速度快,如果任何的交互都需要与主存打交道,则会大大影响效率,所以就有了CPU高速缓存。CPU高速缓存为某个CPU独有,只与在该CPU上执行的线程有关。
CPU高速缓存虽然解决了效率问题,但它又带出了一个新的问题:数据一致性问题。在程序运行过程中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时,CPU不再和主存打交道,而是直接从高速缓存中读写数据,只有运行结束后,才会将数据刷会主存中。
解决缓存一致性方案有两种:
1、通过在总线加Lock锁的方式。
2、通过缓存一致性协议
第一种方案,存在一个问题,它是采用一种独占的方式来实现的,即总线加LOCK#锁的话,只能有一个CPU能够执行,其他CPU都得阻塞,效率低下。
第二种方案,缓存一致性协议(MESI协议),它确保每个缓存中使用的共享变量是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中读取数据。
Java内存模型
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
并发编程的三个基本概念:原子性、可见性、有序性
原子性:即一个操作或者多个操作,要么全部执行并且执行过程中不会被其他任何因素打断,要么就全部不执行。
在单线程环境下我们可以认为整个步骤都是原子性操作,但是在多线程环境下则不同,Java 只保证了基本数据类型的变量和赋值操作才是原子性的(注:在 32 位的 JDK 环境下,对 64 位数据的读取不是原子性操作,例如:long、double)。要想在多线程环境下保证原子性,则可以通过锁、synchronized 来确保。
volatile是不保证原子性的。
可见性: 当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。
但Java提供了volatile来保证可见性。
当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程对共享变量进行修改后,它会立即被更新到主内存中;当其他线程读取共享变量时,它会直接从主内存中读取。
当然,synchronized和锁都可以保证线程可见性。
有序性: 即程序执行的顺序按照代码的先后顺序执行。
在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当前重排序它不会影响单线程的运行结果,但是会对多线程的运行结果造成影响。
Java提供volatile来保证一定的有序性。(通过内存屏障)。最著名的例子就是单例模式的DCL(双重检查锁)。
Happens-Before原则
该原则保证了程序的有序性,它规定如果两个操作的执行顺序无法从happens-before原则中推导出来,那么它们就不能保证有序性,可以随意进行重排序。
定义如下:
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作,happens-before 于书写在后面的操作。
- 锁定规则:一个 unLock 操作,happens-before 于后面对同一个锁的 lock 操作。
- volatile 变量规则:对一个变量的写操作,happens-before 于后面对这个变量的读操作。
- 传递规则:如果操作 A happens-before 操作 B,而操作 B happens-before 操作C,则可以得出,操作 A happens-before 操作C
- 线程启动规则:Thread 对象的 start 方法,happens-before 此线程的每个一个动作。
- 线程中断规则:对线程 interrupt 方法的调用,happens-before 被中断线程的代码检测到中断事件的发生。
- 线程终结规则:线程中所有的操作,都 happens-before 线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.isAlive() 的返回值手段,检测到线程已经终止执行。
- 对象终结规则:一个对象的初始化完成,happens-before 它的 finalize() 方法的开始
剖析volatile原理
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层,volatile是采用“内存屏障”来实现的。
1、保证可见性、不保证原子性。
2、禁止指令重排序。
volatile保证可见性
当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程对共享变量进行修改后,它会立即被更新到主内存中;当其他线程读取共享变量时,它会直接从主内存中读取。
volatile禁止指令重排序
指令重排序:
1、编译器重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2、处理器重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
我们着重看一下Happens-Before规则中的第三条,volatile规则:对volatile变量的写操作,happen-before与对该volatile变量的后序读操作。
JVM是如何禁止指令重排序的?
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。lock前缀指令,其实就相当于一个内存屏障。内存屏障是一组处理指令,用来实现对内存操作的顺序限制。
Lock前缀实现:它先对总线/缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。Lock后的写操作会让其他CPU相关的cache失效,从而从新从内存加载最新的数据,这个是通过缓存一致性协议做的。
内存屏障主要提供3个功能:
- 确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 强制将对缓存的修改操作立即写入主存,利用缓存一致性机制,并且缓存一致性机制会阻止同时修改由两个以上CPU缓存的内存区域数据;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。
内存屏障是CPU指令。如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。
下面是基于保守策略的JMM内存屏障插入策略:
1、在每个volatile写操作的前面插入一个StoreStore屏障。
2、在每个volatile写操作的后面插入一个StoreLoad屏障。
3、在每个volatile读操作的前面插入一个LoadLoad屏障。
4、在每个volatile读操作的后面插入一个LoadStore屏障。
内存屏障可以被分为以下几种类型
LoadLoad屏障: 对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障: 对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障: 对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障: 对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。
在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
单例模式的DCL(双重检查锁)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance(){
if(singleton == null){ // 1
synchronized (Singleton.class){ // 2
if(singleton == null){ // 3
singleton = new Singleton(); // 4
}
}
}
return singleton;
}
}
instance = new Singleton();这句代码并不是一个原子操作,实际上它可以被拆分为三步:
1、分配内存空间
2、实例化对象instance
3、把instance引用指向已分配的内存空间,此时instance有了内存地址,不再为Null。
但指令重排序可能会将指令的执行顺序排成1-3-2,在这种情况下,如果线程执行完1-3后备阻塞了,恰好线程B进来获取到instance不为null,然后线程B把instance返回了,但其实instance还没有进行实例化。所以在调用其后续的实例方法时就会得不到预期的结果。
1、基于volatile解决方案
所以当instance被定义为 private volatile static Singleton instance的话就会保证在创建对象的时候的执行顺序一定是1-2-3的步骤, 从而保证了instance要么为null 要么是已经完全初始化好的对象, 从而避免了场景三的情况出现。
ublic class Singleton {
// 通过volatile关键字来确保安全
private volatile static Singleton singleton;
private Singleton(){}
public static Singleton getInstance(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton = new Singleton();
}
}
}
return singleton;
}
}
2、基于类初始化的解决方案
该解决方案的根本就在于:利用 ClassLoder 的机制,保证初始化 instance 时只有一个线程。JVM 在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
public class Singleton {
private static class SingletonHolder{
public static Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.singleton;
}
}
这种解决方案的实质是:运行步骤 2 和步骤 3 重排序,但是不允许其他线程看见。
Java 语言规定,对于每一个类或者接口 C ,都有一个唯一的初始化锁 LC 与之相对应。从C 到 LC 的映射,由 JVM 的具体实现去自由实现。JVM 在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。
5、ThreadLocal底层原理
ThreadLocal提供了线程局部变量。ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享一个变量,而ThreadLocal是为每个线程创建一个独立的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程的变量副本。
ThreadLocal提供了4个方法:
- get():返回此线程局部变量的当前线程副本中的值。
- initialValue():返回此线程局部变量的当前线程的初始值。
- remove():移除此线程局部变量当前线程的值。
- set(T value):将此线程局部变量的当前线程副本中的值设置成指定值。
ThreadLocal内部使用的是静态内部类ThreadLoaclMap,该类是实现线程隔离机制的关键。ThreadLoaclMap提供了一种键值对方式存储每一个线程的变量副本的方法,key对应当前的ThreadLoacl对象,value则为对应线程的变量副本。
对于ThreadLocal需要注意两点:
1、ThreadLocal实例本身不存储值,它只是提供了一个在当前线程中找到副本的key。
2、是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中。
Thread类中定义了ThreadLocalMap:
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
ThreadLocal设置值的时候,是根据当前线程获得线程下的threadLocals。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
ThreadLocal的使用示例:
public class SeqCount {
private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){
// 实现initialValue()
public Integer initialValue() {
return 0;
}
};
public int nextSeq(){
seqCount.set(seqCount.get() + 1);
return seqCount.get();
}
public static void main(String[] args){
SeqCount seqCount = new SeqCount();
SeqThread thread1 = new SeqThread(seqCount);
SeqThread thread2 = new SeqThread(seqCount);
SeqThread thread3 = new SeqThread(seqCount);
SeqThread thread4 = new SeqThread(seqCount);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
private static class SeqThread extends Thread{
private SeqCount seqCount;
SeqThread(SeqCount seqCount){
this.seqCount = seqCount;
}
public void run() {
for(int i = 0 ; i < 3 ; i++){
System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
}
}
}
}
运行结果:
ThreadLocalMap源码解析
ThreadLocal虽然解决了这个多线程变量的复杂问题,但是它的源码实现却是比较简单的。ThreadLocalMap是实现ThreadLocal的关键,我们先从它入手。
ThreadLocalMap其内部利用Entry来实现key-value的存储,如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
从上面代码中可以看出Entry的key就是ThreadLocal,而value就是值。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用
set(ThreadLocal> key, Object value):
private void set(ThreadLocal<?> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
int i = key.threadLocalHashCode & (len-1);
// 采用“线性探测法”,寻找合适位置
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// key 存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
// key == null,但是存在值(因为此处的e != null),说明之前的ThreadLocal对象已经被回收了
if (k == null) {
// 用新元素替换陈旧的元素
replaceStaleEntry(key, value, i);
return;
}
}
// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
// cleanSomeSlots 清楚陈旧的Entry(key == null)
// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
这个set()操作和我们在集合了解的put()方式有点儿不一样,虽然他们都是key-value结构,不同在于他们解决散列冲突的方式不同。集合Map的put()采用的是拉链法,而ThreadLocalMap的set()则是采用开放定址法。如果当前数组下标对应的entry对象已经存在,对比key是否相同,如果相同则替换掉value,然后返回。如果key为null,当value不为null,说明key对象呗内存回收了,用新元素代替旧元素。否则下标往后移一位。继续寻找,直到找到。
set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义如下:
private final int threadLocalHashCode = nextHashCode();
从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量,从nextHashCode就可以看出他们的定义。
getEntry():
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);
}
由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:
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;
}
这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。
get()
返回当前线程所对应的线程变量
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的成员变量 threadLocal
ThreadLocalMap map = getMap(t);
if (map != null) {
// 从当前线程的ThreadLocalMap获取相对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 获取目标值
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
首先通过当前线程获取所对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前ThreadLocal的Entry,最后通过所获取的Entry获取目标值result。
getMap()方法可以获取当前线程所对应的ThreadLocalMap,如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
remove()
将当前线程局部变量的值删除。
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocal为什么会内存泄漏
前面提到每个Thread都有一个ThreadLocal.ThreadLocalMap的map,该map的key为ThreadLocal实例,它为一个弱引用,我们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收,因为他还与Current Thread存在一个强引用关系.
由于存在这个强引用关系,会导致value无法回收。如果这个线程对象不会销毁那么这个强引用关系则会一直存在,就会出现内存泄漏情况。所以说只要这个线程对象能够及时被GC回收,就不会出现内存泄漏。如果碰到线程池,那就更坑了。
那么要怎么避免这个问题呢?
在前面提过,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以显示调用ThreadLocal的remove()方法进行处理。
为什么要使用弱引用?
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap.
Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key.
每个key都弱引用指向threadlocal.
所以当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal就可以顺利被gc回收。
注意!假如每个key都强引用指向threadlocal,也就是上图虚线那里是个强引用,那么这个threadlocal就会因为和entry存在强引用无法被回收!造成内存泄漏 ,除非线程结束,线程被回收了,map也跟着回收。
下面再对ThreadLocal进行简单的总结:
1、ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
2、每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
3、ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要目的是为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。
InheritableThreadLocal底层实现
使用样例:
public class TestThreadLocal {
static final String VALUE01 = "VALUE01";
static final String VALUE02 = "VALUE02";
public static void main(String[] args) throws InterruptedException {
ThreadLocal<String> threadLocal = new ThreadLocal<String>();
threadLocal.set(VALUE01);
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<String>();
inheritableThreadLocal.set(VALUE01);
Thread thread_1 = new Thread_TestThreadLocal(threadLocal, inheritableThreadLocal);
thread_1.setName("Thread01");
thread_1.start();
thread_1.join();
System.out.println(Thread.currentThread().getName() + "******************************************");
System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get());
}
}
class Thread_TestThreadLocal extends Thread {
ThreadLocal<String> threadLocal;
InheritableThreadLocal<String> inheritableThreadLocal;
public Thread_TestThreadLocal(ThreadLocal<String> threadLocal, InheritableThreadLocal<String> inheritableThreadLocal) {
super();
this.threadLocal = threadLocal;
this.inheritableThreadLocal = inheritableThreadLocal;
}
public void run() {
System.out.println(Thread.currentThread().getName() + "******************************************");
System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get());
threadLocal.set(TestThreadLocal.VALUE02);
inheritableThreadLocal.set(TestThreadLocal.VALUE02);
System.out.println(Thread.currentThread().getName() + "(Reset Value)*****************************");
System.out.println(Thread.currentThread().getName() + "\tThreadLocal: " + threadLocal.get());
System.out.println(Thread.currentThread().getName() + "\tInheritableThreadLocal: " + inheritableThreadLocal.get());
}
}
InheritableThreadLocal类重写了ThreadLocal的3个函数:
/**
* 该函数在父线程创建子线程,向子线程复制InheritableThreadLocal变量时使用
*/
protected T childValue(T parentValue) {
return parentValue;
}
/**
* 由于重写了getMap,操作InheritableThreadLocal时,
* 将只影响Thread类中的inheritableThreadLocals变量,
* 与threadLocals变量不再有关系
*/
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
/**
* 类似于getMap,操作InheritableThreadLocal时,
* 将只影响Thread类中的inheritableThreadLocals变量,
* 与threadLocals变量不再有关系
*/
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
线程间传值实现原理
说到InheritableThreadLocal,还要从Thread类说起:
public class Thread implements Runnable {
......(其他源码)
/*
* 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,
* 主要用于父子线程间ThreadLocal变量的传递
* 本文主要讨论的就是这个ThreadLocalMap
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......(其他源码)
}
Thread类中包含 threadLocals 和 inheritableThreadLocals 两个变量,其中 inheritableThreadLocals 即主要存储可自动向子线程中传递的ThreadLocal.ThreadLocalMap。
线程初始化时:
/**
* 初始化一个线程.
* 此函数有两处调用,
* 1、上面的 init(),不传AccessControlContext,inheritThreadLocals=true
* 2、传递AccessControlContext,inheritThreadLocals=false
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
......(其他代码)
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
......(其他代码)
}
可以看到,采用默认方式产生子线程时,inheritThreadLocals=true;若此时父线程inheritableThreadLocals不为空,则将父线程inheritableThreadLocals传递至子线程。
摘录:
Java 8 并发篇 - 冷静分析 Synchronized(下)
【死磕 Java 并发】—– 深入分析 synchronized 的实现原理
【死磕 Java 并发】—– 深入分析 volatile 的实现原理