目录
前言
synchronized
Monitor
锁优化
轻量级锁
锁膨胀
自旋优化
偏向锁
锁消除
synchronized缺陷
ReentrantLock
AQS原理
ReentrantLock原理
异同
前言
在Java多线程的学习中绕不过去的一点就是锁,那么这些锁又分为哪些呢?
1.公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,即先到先得
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,而是随机争夺的。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
2.可重入锁
即一个线程在首次获得锁之后,再调用其他方法的时候去获取锁还是可以获取到的;不可重入的化即使是自己再去获取也会被阻塞,也就是自己会把自己锁死
这里就涉及到了死锁现象:
-
就是当多把锁时,线程A在获取锁1之后又去获取锁2而线程B在获取锁2之后又去获取锁1那么这个时候就会发生线程AB互相等待对方的现象此时就发生了死锁
-
定位死锁可以使用jsp查看IP然后jstack id去查看具体的死锁信息
3.独占锁/共享锁
独占锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java ReentrantLock/Synchronized而言,是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独占锁。
4.乐观锁和悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
5.偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java 6通过引入锁升级的机制来实现高效Synchronized。
-
在jdk1.6后synchronized的锁有四种状态:无锁,偏向锁,轻量级锁,重量级锁
偏向锁是指锁对象的MarkWorld不再存储锁记录地址而是存储线程ID,当发生锁重入的时候就会发现这个线程ID是自己的不需要重新CAS,那么只要不发生竞争就会一直属于该线程 。默认情况新建对象都是偏向锁。
轻量级锁是指当线程A拥有偏向锁时,在A执行完后又有B来获取锁,此时发生了锁膨胀进入轻量级锁。具体指:线程的栈帧中会有一个所记录的对象(Lock Record)其中包含了对象指针(Object reference)和锁记录地址(Lock Record地址00);当这个线程执行到锁时(synchronized(obj))时就会将对象头的Mark World和锁记录地址进行交换,成功后对象头存放的是锁记录地址和状态00此时代表cas交换成功
重量级锁是指线程A已经拥有了轻量级锁正在执行中,而此时线程B发生了竞争此时就会发生锁膨胀直接升级为重量级锁。也就是monitor对象了
自旋优化当线程A持有锁并且在执行过程中另一个CPU的线程B来访问时并不会立即进入阻塞状态而是多次尝试获取锁,若期间线程A的执行完了同步代码块那么线程B就会获得锁并执行代码这就是自旋;当然如果多次尝试还是无法获取到锁就失败了进入阻塞状态
以上就是常见的一些锁的基本概念了,下面我们着重查看一下synchronized和ReentrantLock
6.多把锁
-
当有两个业务不相关的时候如果我们上的时同一把锁那么这个时候并发性会很低,此时我们可以将锁细粒度化;也就是多把锁各做各的互不干扰又保证线程安全
-
多把锁的好处时提高了并发性;但是坏处就是一个线程可以获得多把锁那么这个时候容易发生死锁现象
死锁
-
就是当多把锁时,线程A在获取锁1之后又去获取锁2而线程B在获取锁2之后又去获取锁1那么这个时候就会发生线程AB互相等待对方的现象此时就发生了死锁
-
定位死锁可以使用jsp查看IP然后jstack id去查看具体的死锁信息
synchronized
-
俗称对象锁,采用互斥的方式让同一时刻至多只有一个线程拥有对象锁,其他线程想要获取的时候就会阻塞,直到当前线程使用完毕释放之后再唤醒阻塞状态的线程重新竞争锁
-
syschronized就像是一把锁,线程A来执行共享资源的时候,他会进行上锁的操作,这是其他线程想要执行会发现被锁了只能进入阻塞状态,在这期间即使线程A的时间片被用完了,也不会释放锁而是重新进入,因为其他线程再阻塞状态自然也不会被分配时间片;直到线程A执行完临界区代码释放锁后会唤醒阻塞状态的线程
-
syschronized加在静态方法上锁的是类对象(xx.class);加在非静态方法上面锁的是this对象
俗称对象锁,采用互斥的方式让同一时刻至多只有一个线程拥有对象锁,其他线程想要获取的时候就会阻塞,直到当前线程使用完毕释放之后再唤醒阻塞状态的线程重新竞争锁
syschronized就像是一把锁,线程A来执行共享资源的时候,他会进行上锁的操作,这是其他线程想要执行会发现被锁了只能进入阻塞状态,在这期间即使线程A的时间片被用完了,也不会释放锁而是重新进入,因为其他线程再阻塞状态自然也不会被分配时间片;直到线程A执行完临界区代码释放锁后会唤醒阻塞状态的线程
syschronized加在静态方法上锁的是类对象(xx.class);加在非静态方法上面锁的是this对象
那么究竟什么是锁呢;上面提到的锁是指Monitor
Monitor
我们将synchronized字节码编译后会发现两个指令:Monitorenter和Monitorexit指令
Monitorenter和Monitorexit指令,会让对象在执行,使其锁计数器加1或者减1。每一个对象在同一时间只与一个monitor(锁)相关联,而一个monitor在同一时间只能被一个线程获得,一个对象在尝试获得与这个对象相关联的Monitor锁的所有权的时候,monitorenter指令会发生如下3中情况之一:
- monitor计数器为0,意味着目前还没有被获得,那这个线程就会立刻获得然后把锁计数器+1,一旦+1,别的线程再想获取,就需要等待
- 如果这个monitor已经拿到了这个锁的所有权,又重入了这把锁,那锁计数器就会累加,变成2,并且随着重入的次数,会一直累加;这也就是可以重入的原因
- 这把锁已经被别的线程获取了,等待锁释放
monitorexit指令:释放对于monitor的所有权,释放过程很简单,就是讲monitor的计数器减1,如果减完以后,计数器不是0,则代表刚才是重入进来的,当前线程还继续持有这把锁的所有权,如果计数器变成0,则代表当前线程不再拥有该monitor的所有权,即释放锁。
而monitor可以翻译成监视器或者管程,当Java给一个对象加锁的时候就会把这个对象和monitor(操作系统提供)进行关联
-
刚开始Monitor中的Owner是null当有一个线程执行到synchronized的时候就会将Owner设置成自己,即该锁的主人;之后再有其他线程访问时进入EntryList中编程阻塞状态;等到Thread2执行完后释放了锁就会将阻塞的线程叫醒来竞争(非公平)成为下一个Owner
锁优化
-
在jdk1.6后synchronized的锁有四种状态:无锁,偏向锁,轻量级锁,重量级锁
-
重量级锁用的是上面提到的Monitor;轻量级锁用的是栈帧中的锁记录(Lock Record);偏向锁
CAS
-
compareAndSet(比较交换值)方法就是CAS的缩写,它由底层的CPU原子指令实现;内部是原子的
-
CAS方法需要传入两个参数,第一个是当前线程获取到的共享变量的值,第二个是将要修改为的值,之后会去再次获取当前共享变量的值为多少,若是不一致了就代表交换失败了,若是相同就会修改
-
CAS要配合volatile才能获取到最新的共享变量
-
CAS比synchronized的效率要高,但实在多核CPU的情况下,否则CAS会由于分配不到时间片而导致上下文切换;正常情况下CAS是一直运行的不会停歇,而synchronized在没有获取锁的情况下是会发生上下文切换的进入阻塞
-
CAS是无锁并发,无阻塞并发的;无锁是指它不需要额外加锁来实现对共享资源的并发,而是通过不断的while(true)循环来解决;相比synchronized因为得不到锁会进入阻塞状态CAS一直在运行就是无阻塞的
轻量级锁
-
在JDK 1.6之后引入的轻量级锁,但轻量级锁并不是替代重量级锁的,而是对在大多数情况下同步块并不会出现竞争时提出的一种优化。它可以减少重量级锁对线程的阻塞带来的线程开销。从而提高并发性能。
-
线程的栈帧中会有一个所记录的对象(Lock Record)其中包含了对象指针(Object reference)和锁记录地址(Lock Record地址00);当这个线程执行到锁时(synchronized(obj))时就会将对象头的Mark World和锁记录地址进行交换,成功后对象头存放的是锁记录地址和状态00此时代表cas交换成功
-
若线程执行到锁时发现对象头中的锁记录状态已经时00了说明锁已经被其他线程拥有了,此时发生了锁竞争那么就需要锁膨胀了
-
若方法A调用了方法B而B中对同一锁对象进行了加锁操作,那么此时cas是失败的,但是没有关系因为都是一线线程中的方法,因此该线程还会创建出一个锁记录对象只是其中的锁记录地址不再会交换而是设为null(也就是锁重入),主要是记录重入的次数
-
synchronized代码块解锁的时候如果读取为null说明发生了锁重入那么需要进行减一;若不为null则使用cas将Mark World恢复给对象,若是失败了说明轻量锁已经变成重量锁了需要按重量级锁解锁了
锁膨胀
-
在上述过程中线程A已经拥有了轻量级锁,而此时线程B发生了竞争此时就会发生锁膨胀直接升级为重量级锁;此时将obj的Mark World指向Monitor将状态00改为10,Owner指向线程A;并且让线程B进入阻塞状态
-
在解锁时线程A必然会cas失败,那么此时按重量级锁的流程:设置Monitor中的owner为null唤醒EntryList中的线程
自旋优化
-
自旋在1.7之后由jvm自己控制我们不再能决定是否开始;自旋后发生再多核CPU中否则单核的CPU是没有意义的;1.6之后自旋的次数比较智能如果成功过就多自旋几次,若总是失败就会少自旋
-
当线程A持有锁并且在执行过程中另一个CPU的线程B来访问时并不会立即进入阻塞状态而是多次尝试获取锁,若期间线程A的执行完了同步代码块那么线程B就会获得锁并执行代码这就是自旋;当然如果多次尝试还是无法获取到锁就失败了进入阻塞状态
-
因为进入阻塞状态会发生上下文切换比较耗性能
偏向锁
-
由于轻量级锁中会发生锁重入的现象,而锁重入就会发生CAS操作,这一操作是会耗费性能的,所以jdk6中有引入了偏向锁的概念
-
即:当第一次加轻量锁时,锁对象的MarkWorld不再存储锁记录地址而是存储线程ID,当发生锁重入的时候就会发现这个线程ID是自己的不需要重新CAS,那么只要不发生竞争就会一直属于该线程
在jdk1.6后synchronized的锁有四种状态:无锁,偏向锁,轻量级锁,重量级锁
重量级锁用的是上面提到的Monitor;轻量级锁用的是栈帧中的锁记录(Lock Record);偏向锁
compareAndSet(比较交换值)方法就是CAS的缩写,它由底层的CPU原子指令实现;内部是原子的
CAS方法需要传入两个参数,第一个是当前线程获取到的共享变量的值,第二个是将要修改为的值,之后会去再次获取当前共享变量的值为多少,若是不一致了就代表交换失败了,若是相同就会修改
CAS要配合volatile才能获取到最新的共享变量
CAS比synchronized的效率要高,但实在多核CPU的情况下,否则CAS会由于分配不到时间片而导致上下文切换;正常情况下CAS是一直运行的不会停歇,而synchronized在没有获取锁的情况下是会发生上下文切换的进入阻塞
CAS是无锁并发,无阻塞并发的;无锁是指它不需要额外加锁来实现对共享资源的并发,而是通过不断的while(true)循环来解决;相比synchronized因为得不到锁会进入阻塞状态CAS一直在运行就是无阻塞的
-
在JDK 1.6之后引入的轻量级锁,但轻量级锁并不是替代重量级锁的,而是对在大多数情况下同步块并不会出现竞争时提出的一种优化。它可以减少重量级锁对线程的阻塞带来的线程开销。从而提高并发性能。
-
线程的栈帧中会有一个所记录的对象(Lock Record)其中包含了对象指针(Object reference)和锁记录地址(Lock Record地址00);当这个线程执行到锁时(synchronized(obj))时就会将对象头的Mark World和锁记录地址进行交换,成功后对象头存放的是锁记录地址和状态00此时代表cas交换成功
-
若线程执行到锁时发现对象头中的锁记录状态已经时00了说明锁已经被其他线程拥有了,此时发生了锁竞争那么就需要锁膨胀了
-
若方法A调用了方法B而B中对同一锁对象进行了加锁操作,那么此时cas是失败的,但是没有关系因为都是一线线程中的方法,因此该线程还会创建出一个锁记录对象只是其中的锁记录地址不再会交换而是设为null(也就是锁重入),主要是记录重入的次数
-
-
synchronized代码块解锁的时候如果读取为null说明发生了锁重入那么需要进行减一;若不为null则使用cas将Mark World恢复给对象,若是失败了说明轻量锁已经变成重量锁了需要按重量级锁解锁了
锁膨胀
-
在上述过程中线程A已经拥有了轻量级锁,而此时线程B发生了竞争此时就会发生锁膨胀直接升级为重量级锁;此时将obj的Mark World指向Monitor将状态00改为10,Owner指向线程A;并且让线程B进入阻塞状态
-
在解锁时线程A必然会cas失败,那么此时按重量级锁的流程:设置Monitor中的owner为null唤醒EntryList中的线程
自旋优化
-
自旋在1.7之后由jvm自己控制我们不再能决定是否开始;自旋后发生再多核CPU中否则单核的CPU是没有意义的;1.6之后自旋的次数比较智能如果成功过就多自旋几次,若总是失败就会少自旋
-
当线程A持有锁并且在执行过程中另一个CPU的线程B来访问时并不会立即进入阻塞状态而是多次尝试获取锁,若期间线程A的执行完了同步代码块那么线程B就会获得锁并执行代码这就是自旋;当然如果多次尝试还是无法获取到锁就失败了进入阻塞状态
-
因为进入阻塞状态会发生上下文切换比较耗性能
偏向锁
-
由于轻量级锁中会发生锁重入的现象,而锁重入就会发生CAS操作,这一操作是会耗费性能的,所以jdk6中有引入了偏向锁的概念
-
即:当第一次加轻量锁时,锁对象的MarkWorld不再存储锁记录地址而是存储线程ID,当发生锁重入的时候就会发现这个线程ID是自己的不需要重新CAS,那么只要不发生竞争就会一直属于该线程
在上述过程中线程A已经拥有了轻量级锁,而此时线程B发生了竞争此时就会发生锁膨胀直接升级为重量级锁;此时将obj的Mark World指向Monitor将状态00改为10,Owner指向线程A;并且让线程B进入阻塞状态
在解锁时线程A必然会cas失败,那么此时按重量级锁的流程:设置Monitor中的owner为null唤醒EntryList中的线程
-
自旋在1.7之后由jvm自己控制我们不再能决定是否开始;自旋后发生再多核CPU中否则单核的CPU是没有意义的;1.6之后自旋的次数比较智能如果成功过就多自旋几次,若总是失败就会少自旋
-
当线程A持有锁并且在执行过程中另一个CPU的线程B来访问时并不会立即进入阻塞状态而是多次尝试获取锁,若期间线程A的执行完了同步代码块那么线程B就会获得锁并执行代码这就是自旋;当然如果多次尝试还是无法获取到锁就失败了进入阻塞状态
-
因为进入阻塞状态会发生上下文切换比较耗性能
-
偏向锁
-
由于轻量级锁中会发生锁重入的现象,而锁重入就会发生CAS操作,这一操作是会耗费性能的,所以jdk6中有引入了偏向锁的概念
-
即:当第一次加轻量锁时,锁对象的MarkWorld不再存储锁记录地址而是存储线程ID,当发生锁重入的时候就会发现这个线程ID是自己的不需要重新CAS,那么只要不发生竞争就会一直属于该线程
由于轻量级锁中会发生锁重入的现象,而锁重入就会发生CAS操作,这一操作是会耗费性能的,所以jdk6中有引入了偏向锁的概念
即:当第一次加轻量锁时,锁对象的MarkWorld不再存储锁记录地址而是存储线程ID,当发生锁重入的时候就会发现这个线程ID是自己的不需要重新CAS,那么只要不发生竞争就会一直属于该线程
| Mark World | State |
|---|---|
| unused:25|hashcode:31|unused:1|age:4|biased_lock:0|01 | Normal(无偏向锁) |
| thread:54(线程ID)|epoch:2|unused:1|age:4|biased_lock:1|01 | Biased(偏向锁) |
| ptr_to_lock_record:62(轻量级锁指针) | 00 | Lightweight Locked |
| ptr_to_heavyweight_montior:62(重量级锁指针) | 10 | Heavyweight Locked |
-
默认情况下新建一个对象都是开启偏向锁的,但是偏向锁都是有延迟的,不是立即启动,如果想要立即启动可以VM添加参数:
-XX:BiasedLockingStartupDelay=0来禁用延迟;-XX:-UseBiasedLocking取消使用偏向锁
-
当我们调用了偏向锁的hashcode()方法时会导致偏向锁失效被替换为轻量级锁
-
因为偏向锁存的时线程id而因此调用hashcode的时候会发现放不下了,就把线程id等清掉将markWorld中替换为正常状态下的hashcode等
-
-
撤销偏向锁:一种时上面提到的调用hashcode方法;一种是当有其他线程使用锁对象(不能发生两个线程的竞争那就是重量级锁了,也就是说一个线程执行结束后另一个线程再使用)的时候也会撤销偏向锁;也就是将偏向锁状态从1变成0;以及调用wait/notify因为这个方法只有重量级锁才有,自然也会把锁升级为重量级锁
-
批量重偏向:一开始偏向锁偏向线程A,之后当线程B执行时违反了偏向锁的原理发生了多个线程使用锁,所以会将原来的偏向锁撤销变成轻量级锁;但是撤销是需要耗费性能的,于是当撤销的次数超过了阈值20就会做出优化jvm会认为这个锁不再适合偏向于之前的线程,会将其之后的全部偏向于这个新线程
-
批量撤销:但是当撤销的次数一直增加,例如线程B对线程A的锁执行了20次撤销操作,再线程B执行完后线程C执行,当线程C的撤销超过40的时候也就是低40次时jvm会认为这个对象竞争过于激励不再适合作为偏向锁,因此会将其置为不可偏向的锁,之后创建的都是不可偏向的锁
-
锁消除
-
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除;比如我们通过逃逸分析发现这个锁是方法中的局部变量,不可能会发生逃逸是方法私有的,那么这个锁就加的没有意义,此时可以将这个锁消除以此提升性能
synchronized缺陷
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除;比如我们通过逃逸分析发现这个锁是方法中的局部变量,不可能会发生逃逸是方法私有的,那么这个锁就加的没有意义,此时可以将这个锁消除以此提升性能
效率低:锁的释放情况少,只有代码执行完毕或者异常结束才会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时
不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活
无法知道是否成功获得锁,相对而言,Lock可以拿到状态
ReentrantLock
-
可重入锁;相比于synchronized可打断,可以设置超时时间,可以设为公平锁,支持多个条件变量;相同点是都支持可重入
-
要先获取对象再使用lock方法加锁,在try中写临界区代码finally中unlock来解锁;synchronized的锁jvm会帮我们自动释放,而lock需要自己释放
ReentrantLock lock = new ReentrantLock();//参数加true就是公平锁
Thread t1 = new Thread(()->{
lock.lock();//上锁
try{
for (int i = 0; i < 500; i++) {
count++;
}
}finally {
lock.unlock();//解锁
}
},"t1");
可重入锁;相比于synchronized可打断,可以设置超时时间,可以设为公平锁,支持多个条件变量;相同点是都支持可重入
要先获取对象再使用lock方法加锁,在try中写临界区代码finally中unlock来解锁;synchronized的锁jvm会帮我们自动释放,而lock需要自己释放
可重入
-
即一个线程在首次获得锁之后,再调用其他方法的时候去获取锁还是可以获取到的;不可重入的化即使是自己再去获取也会被阻塞
可打断
-
不管是synchronized还是lock方法都是不可打断的,也就是说以一个线程去尝试获取锁,由于另外一个资源始终没有放弃锁,那么他就会一直等下去;
-
如果要可打断的锁那么要使用lockinterruptibly方法来加锁,此时若获取不到锁还是会进入阻塞,但是其他线程可以使用该线程的interrupt方法来打断这个阻塞;为了避免死等可以有效避免死锁
Thread t3 = new Thread(()->{
try {
//可打断锁,获取不到锁进入阻塞但是可以被其他线程打断
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("阻塞了,并且被打断了未获取到锁");
e.printStackTrace();
return;
}
try{
System.out.println("t3拿到锁了");
}finally {
lock.unlock();
}
});
锁超时
-
可打断是被动的避免,而锁超时是主动的避免死锁,防止死等
-
需要使用trylock方法加锁,这个方法的意思就是去尝试获得锁,如果获取了返回true否则false;如果不加参数就是立刻做出反应;如果带了参数,参数是(时间,单位)并且到时间了还没有获取到那就退出,在等待的时间中也可以被提前打断,但是在等待时间内获得了锁会马上执行代码
-
trylock可以很好的解决死锁问题,也可以解决线程的饥饿问题(多线程在竞争时有的线程总是获得锁,有的线程总是无法获取锁)
Thread t4 = new Thread(()->{
//尝试获取锁,获取到返回真没有获取到返回false
try {
if(lock.tryLock(500, TimeUnit.SECONDS)){//不加参数就是直接放弃,带了参数第一个是时间第二个是单位,时间到了还没有就放弃
try {
System.out.println("t4,获取到了锁");
}finally {
lock.unlock();
}
}else {
System.out.println("t4,没有获取到锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("获取不到 被打断");
}
});
公平锁
-
在创建ReentrantLock的时候默认是不公平的,可以new ReentrantLock(true)就会创建一个公平锁,先等待的就先获得锁
-
但是没必要,会降低并发性,并且trylock可以很好的解决饥饿问题
条件变量
-
之前的synchronized的wait方法调用后线程会进入waitSet中等待,并且只有一个,当唤醒时会去waitSet中唤醒一个或全部唤醒;而lock支持多个这样的waitSet在唤醒时也可以指定唤醒某一个waitSet的线程
Condition condition1 = lock.newCondition();//创建多个休息室
Condition condition2= lock.newCondition();
lock.lock();//await之前要加锁
condition1.await();//进入等待
condition1.signal();//唤醒指定等待室中的某一个还有一个signalall可以全部唤醒
AQS原理
-
AQS全称AbstractQueuedSynchronizer,抽象同步队列,是实现同步器的基础组件,底层数据结构是一个FIFO的双向队列(这个队列中的元素是AQS中的一个内部类Node元素)。
-
AQS维护被volatile修饰的state来标识资源的状态--是否被占有(分为分享模式和独占模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。
-
getState()获取state状态
-
setState()设置state状态
-
compareAndSetState-乐观锁机制设置state状态
-
独占模式是只有一个线程能够访问资源,共享模式可以允许多个线程访问资源
-
内部的FIFO等待队列,类似于Monitor的EntryList
-
条件变量未来实现等待,唤醒机制,支持多个条件变量,类似于Monitor的WatiSet
-
需要子类去实现这些方法,这样一个完整的同步器类就实现了
-
tryAcquire(尝试获取独占锁) / tryAcquireShared(尝试获取共享锁)---->使用park unpark来实现阻塞和恢复
-
tryRelease(尝试释放独占锁) / tryReleaseShared(尝试释放共享锁)
-
isHeldExclusively(该线程是否在独占资源)
ReentrantLock原理
AQS全称AbstractQueuedSynchronizer,抽象同步队列,是实现同步器的基础组件,底层数据结构是一个FIFO的双向队列(这个队列中的元素是AQS中的一个内部类Node元素)。
AQS维护被volatile修饰的state来标识资源的状态--是否被占有(分为分享模式和独占模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁。
-
getState()获取state状态
-
setState()设置state状态
-
compareAndSetState-乐观锁机制设置state状态
-
独占模式是只有一个线程能够访问资源,共享模式可以允许多个线程访问资源
-
内部的FIFO等待队列,类似于Monitor的EntryList
-
条件变量未来实现等待,唤醒机制,支持多个条件变量,类似于Monitor的WatiSet
需要子类去实现这些方法,这样一个完整的同步器类就实现了
-
tryAcquire(尝试获取独占锁) / tryAcquireShared(尝试获取共享锁)---->使用park unpark来实现阻塞和恢复
-
tryRelease(尝试释放独占锁) / tryReleaseShared(尝试释放共享锁)
-
isHeldExclusively(该线程是否在独占资源)
-
NoFairSync继承了Sync而Sync继承了AQS实现了其中的子类方法
加锁
-
当没有竞争时,Thread0获取锁将state修改为1,并将自己设为当前锁的主人
-
当有竞争发生会执行acquire方法,该方法会先尝试CAS加锁如果成功返回true;否则的话判断是否时当前线程是的话执行重入操作,不是的话就返回false;在等待队列中创建Node结点
-
Node的创建是惰性的(首次创建添加两个节点);其中第一个Node称为Dummy(哨兵)节点,用来占位,并不关联线程,哨兵节点即为头结点
-
之后进入acquireQueued逻辑,该逻辑会在一个死循环中不断尝试获取锁,失败后进入park阻塞
-
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;
}
//如果失败进入shouldParkAfterFailedAcquire逻辑,首次循环将前驱Node,即head的waitStatus改为-1,且返回false
//-1的意思是当前结点有义务唤醒后置结点;结束后会再次for(;;)再次失败后来到shouldParkAfterFailedAcquire此时返
//回的就是true了执行parkAndCheckInterrupt操作即将当前线程park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
释放锁
-
当前占有锁的线程tryRelease成功后将state置为0;将owner线程置为null;并且如果发现head不为null并且wai'tStatus不为0那么,找到队列中离head最近的一个Node(没取消的),unpark恢复其运行
-
此时如果被唤醒的结点没有线程与其竞争那么他会把前置结点置为null把自己的结点置为哨兵结点,将state设为1并将自己设为owner线程;如果有其他新来的线程与其发生了竞争并且被其他线程抢夺了资源,那么他继续原地呆着(还是在队列的最前面)
可重入
-
如果是同一线程进来尝试获取锁,把state加1,并且tryAcquire方法返回true
-
如果是相同线程进来尝试释放锁,则把state减1,直到state=0时release成功
可打断
-
有两种模式一种是不可打断模式一种是可打断模式
-
在不可打断模式中,会记录有线程打断过,然后去尝试获取锁,若失败了还是继续留在队列中,如果成功了那么就补上这次的打断
-
在可打断模式中,回直接抛出异常从而导致线程不会继续在aqs的队列中继续等待
-
公平锁FairSync
-
区别于非公平锁,当公平锁发现state是0的时候会先检查AQS队列中是否有其他的线程,如果在当前线程前有前任结点那么返回true就不会继续执行后面的compareAndSetState了
if (c == 0) {
if (!hasQueuedPredecessors() &&//查看是否有前继结点
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
条件变量
-
每一个条件变量就是一个等待队列,其实现类是AQS中的ConditionObject类
await
-
一开始Thread0拥有锁,之后调用await方法,进入ConditionObject的addConditionWaiter流程创建新的Node状态为-2(Node.CONDITION),关联Thread-0,加入等待队列尾部
-
接下来进入AQS的fullyRelease(因为有可能发生锁重入所以需要使用fullRelease)流程,释放掉当前节点对应的锁标记(即state);并且unpark阻塞队列中的下个线程重新竞争锁,同时park线程0进入等待
signal
-
进入ConditonObject执行doSignal方法,取得等待队列中第一个Node,即Thread-0所在Node(由于每次都是取第一个,所以是公平)
-
执行transferForSignal方法,将该Node加入AQS队列尾部,将Thread-0的waitStatus改为0,它的前置结点的waitStatus改为-1
-
调用signal的线程释放锁,进入unlock
异同
-
synchronized是Java关键字而lock是一个接口;
-
在使用时synchronized要对一个对象进行上锁,可以加在代码段也可以加在方法上,lock需要先创建一个实现类对象然后直接lock方法加锁之后再try块中编写临界区代码在finally中释放锁;
-
synchronized会自动释放锁而lock要调用unlock方法解锁(所以要在finally中unlock);
-
lock时可打断的可以使用lockinterruptibly来获取锁这样就可以使用interrupt方法来打断正在等待的锁;而synchronized时不可打断的,如果一个线程在等另一个线程的锁而另一个线程阻塞了这个线程会一直等下去(所以有可能会造成死锁问题)
-
lock有锁超时即获取锁时使用trylock方法,如果没有得到锁就会放弃执行;也可以设置超时时间;synchronized不可以
-
lock可以设置为公平锁,即每个线程先到先得,而synchronized时非公平的需要多个线程去竞争
-
lock可以有多个条件变量(Condition),synchronized只能有一个即waitSet的使用lock可以细分
-
在性能上二者差距不大,但官方更推荐synchronized
-
synchronized中的锁是一个monitor对象是底层的重量级锁,ReentrantLock是通过AQS来实现的锁
synchronized是Java关键字而lock是一个接口;
在使用时synchronized要对一个对象进行上锁,可以加在代码段也可以加在方法上,lock需要先创建一个实现类对象然后直接lock方法加锁之后再try块中编写临界区代码在finally中释放锁;
synchronized会自动释放锁而lock要调用unlock方法解锁(所以要在finally中unlock);
lock时可打断的可以使用lockinterruptibly来获取锁这样就可以使用interrupt方法来打断正在等待的锁;而synchronized时不可打断的,如果一个线程在等另一个线程的锁而另一个线程阻塞了这个线程会一直等下去(所以有可能会造成死锁问题)
lock有锁超时即获取锁时使用trylock方法,如果没有得到锁就会放弃执行;也可以设置超时时间;synchronized不可以
lock可以设置为公平锁,即每个线程先到先得,而synchronized时非公平的需要多个线程去竞争
lock可以有多个条件变量(Condition),synchronized只能有一个即waitSet的使用lock可以细分
在性能上二者差距不大,但官方更推荐synchronized
synchronized中的锁是一个monitor对象是底层的重量级锁,ReentrantLock是通过AQS来实现的锁



