Concurrent
https://www.wwwbuild.net/JavaAmazing/112187.html
并发操作:原子性、可见性、有序性
1、原子性
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
如java.util.concurrent.atomic包下的原子类,就是用CAS(Compare And Swap)保证原子性和可见性(内存屏障)
虽然
java.util.concurrent.atomic提供了原子操作,但这些解决方案主要针对单一变量。对于涉及多个变量时的原子性操作,仍然需要使用高级同步机制(如synchronized块或ReentrantLock)。Java 内存模型(JMM)确保在使用原子类和 CAS 操作时,数值的更新对其他线程是可见的。这是通过内存屏障来实现的。
2、可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
如volatile能保证被修饰变量的可见性、有序性;原子类可以保证原子性和可见性;
- 可见性:
volatile关键字确保变量的更新对所有线程立即可见,避免线程读取到变量的过期值。- 禁止指令重排序优化:编译器和运行时不会把
volatile变量的写操作与之前的内存操作重排序,也不会把volatile变量的读操作与之后的内存操作重排序。
3、有序性
即程序执行的顺序按照代码的先后顺序执行。(指令编排可能会导致多线程下执行结果不一致)
如volatile能保证被修饰变量的可见性、有序性
synchronized关键字三者都能保证。
Volatile 和synchronized的区别:
java.util.concurrent.atomic包的原子类可以保证数据的原子性、可见性;volatile关键字能保证数据的可见性、有序性,但不能保证数据的原子性。synchronized关键字三者都能保证。
1:并发特性比较:
volatile关键字能保证数据的可见性、有序性,但不能保证数据的原子性(即volatile int x; x++ 是三步操作:一取x值,二加一,三赋值回x)。synchronized关键字两者都能保证。
有序性则volatile和synchronized都能保证,volatile关键字禁止JVM编译器已及处理器对其进行重排序,
synchronized保证顺序性是串行化的结果,但同步块里的语句是会发生指令从排。
2:volatile 的原理
1). 修改volatile变量时会强制将修改后的值刷新的主内存中。
2). 修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。
3). 禁止指令重排序优化:编译器和运行时不会把 volatile 变量的写操作与之前的内存操作重排序,也不会把 volatile 变量的读操作与之后的内存操作重排序。
(Intel 的MESI协议:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的高速缓存置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取重新加载到高速缓存。)
3:阻塞与否
多线程访问volatile关键字不会发生阻塞(2所述原理),而synchronized关键字可能会发生阻塞(重量级锁时会阻塞)
4:性能
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用synchronized关键字的场景还是更多一些。
可重入锁和不可重入的区别
可重入锁也叫递归锁,是在一个线程获取锁后,内部如果还需要获取锁,可以直接获取的锁(前提锁对象得是同一个对象或者class)。
不可重入锁也就是相反,线程获取锁后,内部不能再获取锁,由于之前已经获取过还没释放而阻塞,会导致线程死锁。
所以可重入锁的一个优点是可一定程度避免死锁。
可重入锁有ReentrantLock和synchronized。
非可重入锁有NonReentrantLock(Netty框架)。
可重入锁的实现原理:
二者类似
synchronized:
synchronized底层的实现原理是利用计算机系统的mutex Lock实现。每一个可重入锁都会关联一个线程ID和一个锁状态status。
当一个线程请求方法时,会去检查锁状态,如果锁状态是0,代表该锁没有被占用,直接进行CAS操作获取锁,将线程ID替换成自己的线程ID。如果锁状态不是0,代表有线程在访问该方法。此时,如果线程ID是自己的线程ID,如果是可重入锁,会将status自增1,然后获取到该锁,进而执行相应的方法。如果是非重入锁,就会进入阻塞队列等待。
释放锁时,可重入锁,每一次退出方法,就会将status减1,直至status的值为0,最后释放该锁。
释放锁时,非可重入锁,线程退出方法,直接就会释放该锁。
ReentrantLock:
ReentrantLock的可重入功能基于AQS的同步状态:state。
其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。
原子类&CAS算法&乐观锁&悲观锁
简述:
乐观锁是无锁的,因为它认为自己使用数据时不会有别的线程来修改数据,所以不会对资源加锁。实现乐观锁的代表是原子类(java.util.concurrent.atomic包),原子类中的递增操作就是由CAS自旋实现的。
悲观锁则是在使用数据前悲观的认为一定会有别的线程来修改数据,所以在每次使用数据时都会对资源加锁,未拿到锁的都需要排队阻塞等候。
乐观锁&悲观锁
对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
而乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。
乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
- 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
- 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。
CAS原理
CAS算法全程 Compare And Swap,在不加锁的请况下实现多线程变量同步:
简述:当且仅当内存值(内存地址)等于预估值(备份的旧数据)时,将更新值(新数据)写入内存,否则什么都不做
CAS 包含了三个操作数:
内存值 V
预估值 A
更新值 B当且仅当 V == A 时,V 将被赋值为 B,否则什么都不做,
自旋锁:循环的CAS算法:当然如果需要的话,可以设计成自旋锁的模式,循环着不断进行判断 V 与 A 是否相等。
CAS比较与交换的伪代码可以表示为:
1 | do{ |
CAS存在的问题
CAS虽然很高效,但是它也存在三大问题:
ABA问题
CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从“A-B-A”变成了“1A-2B-3A”。
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
只能保证一个共享变量的原子操作
对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
synchronized原理及锁升级过程
synchronized关键字的锁升级机制:资源没被访问前,无锁;一旦被一个线程访问了,升级偏向锁;被第二个线程访问了,升级轻量锁;被三个及以上线程访问了或轻量锁一直CAS失败,升级重量锁。
synchronize重量锁其实就是 竞争 锁这个java对象的对象头中MarkWord指向的monitor的过程,一个线程获得锁其实就是锁的monitor中owner字段指向了这个线程。
简述:
无锁&偏向锁:对象头中的MarkWord的标志位都是01,偏向模式位 为0则无锁,为1则偏向锁;无锁也就是CAS原理及应用,不加锁,对修改资源进行循环的CAS操作。偏向锁则是因为同步代码一直被一个线程所访问,那么该线程会自动获取偏向锁,获取锁的线程的id被对象MarkWord记录,后不再通过CAS加解偏向锁,而是通过检测id是否相符(也就是不释放偏向锁直到有其他线程尝试竞争锁时升级轻量级锁)
轻量级锁:MarkWord的标志位00,一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁),其他线程通过自旋CAS操作尝试获取轻量级锁
重量级锁:MarkWord的标志位10,一旦尝试超过10次不成功或有三个线程同时竞争,升级重量级锁。此时,synchronized通过Monitor来实现线程同步,Monitor是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的线程同步。加锁的过程其实就是竞争 monitor 的过程,当线程进入字节码 monitorenter 指令(sychronized前后插入monitorenter&monitorentexit)之后,线程将持有 monitor 对象, 执行 monitorexit 时释放 monitor 对象,当其他线程没有拿到 monitor 对象时,则需要阻塞 等待获取该对象,阻塞由于涉及用户态和内核态的切换,线程的阻塞和恢复,故较为耗时
四种锁状态对应的的Mark Word内容:
| 锁状态 | 存储内容 | 存储内容 |
|---|---|---|
| 无锁 | 对象的hashCode、 对象分代年龄、是否是偏向锁(0) | 01 |
| 偏向锁 | 偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1) | 01 |
| 轻量级锁 | 指向栈中锁记录(lock_record)的指针 | 00 |
| 重量级锁 | 指向Monitor的指针 | 10 |
对象头

对象头中的markWord
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。

32位标记字段详情
1 | |-------------------------------------------------------|--------------------| |
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。thread:持有偏向锁的线程ID。epoch:偏向时间戳。ptr_to_lock_record:指向栈中锁记录的指针。ptr_to_heavyweight_monitor:指向管程Monitor的指针。
对象头中的Klass Point
Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Monitor
Monitor实现
在Java虚拟机(HotSpot)中,Monitor是基于C++实现的,由ObjectMonitor实现的,其主要数据结构如下:
1 | ObjectMonitor() { |
源码地址:objectMonitor.hpp
ObjectMonitor中有几个关键属性:
_owner:指向持有ObjectMonitor对象的线程
_WaitSet:存放处于wait状态的线程队列
_EntryList:存放处于等待锁block状态的线程队列
_recursions:锁的重入次数
_count:用来记录该线程获取锁的次数
当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即获得对象锁。
Monitor可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个Java对象就有一把看不见的锁,称为内部锁或者Monitor锁。
Monitor是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联,同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

synchronized锁升级过程
无锁
也就是CAS原理及应用,不加锁,对修改资源进行循环的CAS操作
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
偏向锁
同步代码一直被一个线程所访问,那么该线程会自动获取锁,获取锁的线程的id被对象MarkWord记录,后不再通过CAS加解锁(也就是不释放锁直到有其他线程尝试竞争锁时升级轻量级锁),而是通过检测id是否相符
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。
当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
轻量级锁
一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁),其他线程通过自旋CAS操作尝试获取锁。一旦尝试超过10次不成功或者有三个及以上的锁竞争,升级重量级锁
是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
重量级锁
等待锁的线程都进入阻塞状态,直到锁被释放后线程唤醒竞争锁 ,过程涉及用户态和内核态的切换,较耗时
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
整体的锁状态升级流程如下:

综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
总结
- synchronized特点:保证内存可见性、操作原子性
- synchronized影响性能的原因:
- 1、加锁解锁操作需要额外操作;
- 2、互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的挂起线程和恢复线程的操作都需要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)
- synchronized锁:对象头中的Mark Word根据锁标志位的不同而被复用
- 偏向锁:在只有一个线程执行同步块时提高性能。Mark Word存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单比较ThreadID。特点:只有等到线程竞争出现才释放偏向锁,持有偏向锁的线程不会主动释放偏向锁。之后的线程竞争偏向锁,会先检查持有偏向锁的线程是否存活,如果不存货,则对象变为无锁状态,重新偏向;如果仍存活,则偏向锁升级为轻量级锁,此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁
- 轻量级锁:在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,尝试拷贝锁对象目前的Mark Word到栈帧的Lock Record,若拷贝成功:虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向对象的Mark Word。若拷贝失败:若当前只有一个等待线程,则可通过自旋稍微等待一下,可能持有轻量级锁的线程很快就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁
- 重量级锁:指向互斥量(mutex),底层通过操作系统的mutex lock实现。等待锁的线程会被阻塞,由于Linux下Java线程与操作系统内核态线程一一映射,所以涉及到用户态和内核态的切换、操作系统内核态中的线程的阻塞和恢复。
synchonized的无锁和其他状态下调用 hashcode区别
- 无锁状态:调用
hashCode直接返回哈希值,没有额外开销(已存过hashCode的情况)。 - 偏向锁状态:调用
hashCode会导致锁升级为轻量级锁,生成并存储哈希码。 - 轻量级锁和重量级锁状态:调用
hashCode会导致额外的开销,锁升级为重量级锁,并且可能需要额外的内存空间来存储哈希码。
HotSpot VM 的锁实现机制是:
- 当一个对象已经调用默认 hashCode() 或者 System.identityHashCode(),即计算过 identity hash code 后,它就无法进入偏向锁状态。这意味着,如果要在不发生争用的对象上进行同步,则最好覆盖默认hashCode()实现,否则JVM不会优化。
- 当一个对象当前正处于偏向锁状态,并且需要计算其 identity hash code 的话,则它的偏向锁会被撤销,并且锁会膨胀为轻量级锁或者重量锁;
- 轻量级锁的实现中,会通过线程栈帧的锁记录存储 Displaced Mark Word;
- 重量锁的实现中,ObjectMonitor 类里有字段可以记录非加锁状态下的 mark word,其中可以存储 identity hash code 的值。
synchronized关键字可以实现什么类型的锁
悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。
非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。
可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。
独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。
Lock与ReentrantLock
Lock 接是 java并发包的顶层接口。
可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。ReentrantLock 在默认情况下是非公平的,可以通过构造方法指定公平锁。一旦使用了公平锁,性能会下降。
Synchronized 和 Lock 的主要区别
存在层面:Syncronized 是Java 中的一个关键字,存在于 JVM 层面,Lock 是 Java 中的一个接口
锁的释放条件:1. 获取锁的线程执行完同步代码后,自动释放;2. 线程发生异常时,JVM会让线程释放锁;Lock 必须在 finally 关键字中释放锁,不然容易造成线程死锁
锁的获取: 在 Syncronized 中,假设线程 A 获得锁,B 线程等待。如果 A 发生阻塞,那么 B 会一直等待。在 Lock 中,会分情况而定,Lock 中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待
锁的状态:Synchronized 无法判断锁的状态,Lock 则可以判断
锁的类型:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是 可重入,可判断,可公平锁
锁的性能:Synchronized 适用于少量同步的情况下,性能开销比较大。Lock 锁适用于大量同步阶段:
Lock 锁可以提高多个线程进行读的效率(使用 readWriteLock)
在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReetrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。
ReentrantLock与synchronized的区别
- 「锁的实现:」 synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(一般是lock()和unlock()方法配合try/finally 语句块来完成。)
- 「性能:」 在JDK1.6锁优化以前,synchronized的性能比ReenTrantLock差很多。但是JDK6开始,增加了适应性自旋、锁消除等,两者性能就差不多了。
- 「功能特点:」 ReentrantLock 比 synchronized 增加了一些高级功能,如等待可中断、可实现公平锁、可实现选择性通知。
❝
- ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
- ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
- synchronized与wait()和notify()/notifyAll()方法结合实现等待/通知机制,ReentrantLock类借助Condition接口与newCondition()方法实现。
- ReentrantLock需要手工声明来加锁和释放锁,一般跟finally配合释放锁。而synchronized不用手动释放锁。
❞
ReentrantLock 有如下特点:
相同点:
- 可重入: 可重入锁 ReentrantLock 是 Lock 最常见的实现,与 synchronized 一样可重入。 不过两者实现原理稍有差 别, RetrantLock 利用 AQS 的的 state 状态来判断资源是否已锁,同一线程重入加锁, state 的状态 +1 ; 同一线程重入解锁, state 状态 -1 (解锁必须为当前独占线程,否则异 常); 当 state 为 0 时解锁成功。
不同点:
- 需要手动加锁、解锁,而 synchronized 关键字是自动进行加锁、解锁的。而 ReentrantLock 需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成,来手动加锁、解锁。
- 支持设置锁的超时时间,而 synchronized 关键字无法设置锁的超时时间。如果一个获得锁的线程内部发生死锁,那 么其他线程就会一直进入阻塞状态,而 ReentrantLock 提供 tryLock 方法,允许设置线 程获取锁的超时时间,如果超时,则跳过,不进行任何操作,避免死锁的发生。
- 支持公平/非公平锁(默认非公平),而 synchronized 关键字是一种非公平锁。先抢到锁的线程先执行。而 ReentrantLock 的 构造方法中允许设置 true/false 来实现公平、非公平锁,如果设置为 true ,则线程获取 锁要遵循”先来后到”的规则,每次都会构造一个线程 Node ,然后到双向链表的”尾 巴”后面排队,等待前面的 Node 释放锁资源。
- 可中断锁, ReentrantLock 中的 lockInterruptibly() 方法使得线程可以在被阻塞时响应中断。比 如一个线程 t1 通过 lockInterruptibly() 方法获取到一个可重入锁,并执行一个长时间 的任务,另一个线程通过 interrupt() 方法就可以立刻打断 t1 线程的执行,来获取t1持 有的那个可重入锁。而通过 ReentrantLock 的 lock() 方法或者 Synchronized 持有锁 的线程是不会响应其他线程的 interrupt() 方法的,直到该方法主动释放锁之后才会响应 interrupt() 方法。
可重入锁和不可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。下面用示例代码来进行分析:
1 | public class Widget { |
在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,doSomething()方法中调用doOthers()方法。因为内置锁是可重入的,所以同一个线程在调用doOthers()时可以直接获得当前对象的锁,进入doOthers()进行操作。
如果是一个不可重入锁,那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁