概述
可重入锁
首先结合以下两个例子理解以下可重入锁的概念。
/**
* 可重入锁:
* 1、可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。
* 2、是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个
* 对象),不会因为之前已经获取过还没释放而阻塞
*/
public class ReEnterLockDemo {
static Object objectLockA = new Object();
public static void m1(){
new Thread(() -> {
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"t"+"------外层调用");
synchronized (objectLockA){
System.out.println(Thread.currentThread().getName()+"t"+"------中层调用");
synchronized (objectLockA)
{
System.out.println(Thread.currentThread().getName()+"t"+"------内层调用");
}
}
}
},"t1").start();
}
public static void main(String[] args) {
m1();
}
}
public class ReEnterLockDemo {
public synchronized void m1(){
System.out.println("=====外层");
m2();
}
public synchronized void m2() {
System.out.println("=====中层");
m3();
}
public synchronized void m3(){
System.out.println("=====内层");
}
public static void main(String[] args) {
new ReEnterLockDemo().m1();
}
}
相关知识的了解
3种让线程等待和唤醒的方法
- 方式1: 使用
Object
中的wait()
方法让线程等待, 使用Object中的notify()
方法唤醒线程 - 方式2: 使用JUC包中
Condition
的await()
方法让线程等待,使用signal()
方法唤醒线程 - 方式3:
LockSupport
类可以阻塞当前线程以及唤醒指定被阻塞的线程
Object类提供的等待唤醒机制的缺点
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"t"+"------被唤醒");
}
},"A").start();
new Thread(() -> {
synchronized (objectLock)
{
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"t"+"------通知");
}
},"B").start();
}
}
结果:
A ------come in
B ------通知
A ------被唤醒
Process finished with exit code 0
异常情况①:去掉同步代码块
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"t"+"------被唤醒");
// }
},"A").start();
new Thread(() -> {
// synchronized (objectLock){
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"t"+"------通知");
// }
},"B").start();
}
}
结果:
A ------come in
Exception in thread "A" Exception in thread "B" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$0(LockSupportDemo1.java:16)
at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$1(LockSupportDemo1.java:26)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
异常情况②:先唤醒,再等待。
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"t"+"------被唤醒");
}
},"A").start();
new Thread(() -> {
synchronized (objectLock)
{
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"t"+"------通知");
}
},"B").start();
}
}
结果:
B ------通知
A ------come in
Process finished with exit code -1
死循环,A无法被唤醒了。
两种异常:
Object类提供的
wait
和notify
1、只能在
synchronized
同步代码块里使用2、只能先等待(wait),再唤醒(notify)。顺序一旦出错,那个等待线程就无法被唤醒了。
Condition类提供的等待唤醒机制的缺点
这里也有两个缺点,而且和Object类里的wait,notify几乎一样。
1、只能在
lock
同步代码块里使用,不然就报错2、只能先等待(await),再唤醒(signal)。顺序一旦错了,那个等待线程就无法被唤醒了。
但相对于wait,notify改进的一点是,可以绑定lock进行定向唤醒,或者说精确唤醒。
LockSupport(本节重点)
首先直接看示例
异常情况①:无同步代码块
public class LockSupportDemo3 {
public static void main(String[] args) {
/**
LockSupport:俗称 锁中断
LockSupport它的解决的痛点
1。LockSupport不用持有锁块,不用加锁,程序性能好,
2。不需要等待和唤醒的先后顺序,不容易导致卡死
*/
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "t ----begin-时间:" + System.currentTimeMillis());
LockSupport.park();//阻塞当前线程
System.out.println(Thread.currentThread().getName() + "t ----被唤醒-时间:" + System.currentTimeMillis());
}, "t1");
t1.start();
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "t 通知t1...");
}
}
结果:
t1 ----begin-时间:1603376148147
t1 ----被唤醒-时间:1603376148147
main 通知t1...
Process finished with exit code 0
没有出现任何问题。
异常情况②:先唤醒,再阻塞(等待)。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "t ----begin-时间:" + System.currentTimeMillis());
LockSupport.park();//阻塞当前线程
System.out.println(Thread.currentThread().getName() + "t ----被唤醒-时间:" + System.currentTimeMillis());
}, "t1");
t1.start();
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "t 通知t1...");
}
结果:
main 通知t1...
t1 ----begin-时间:1603376257183
t1 ----被唤醒-时间:1603376257183
Process finished with exit code 0
可以看到,如果提前对线程进行唤醒。那么后面执行的LockSupport.park();
就相当于瞬间被唤醒了,不会和之前一样程序卡死。
为什么呢?
static void park() //除非许可证可用,否则禁用当前线程以进行线程调度。
static void unpark(Thread thread) //如果给定线程尚不可用,则为其提供许可。
- LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
- LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),
permit只有两个值1和零,默认是零。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
public static void park() {
UNSAFE.park(false, 0L);
}
LockSupport底层是通过UNSAFE。
-
permit默认是0,所以一开始调用
park()
方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。 -
调用
unpark(thread)
方法后,就会将thread线程的许可permit设置成1(多次调用unpark方法,不会累加,permit值还是1
),然后程序就会自动唤醒thread线程,也就是之前阻塞中的LockSupport.park()方法会立即返回。 -
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就将0变成1,调用一次park会消费permit,也就是将1变成o,同时park立即返回。如再次调用park会变成阻塞(
因为permit为零了会阻塞在这里,一直到permit变为1
),这时调用unpark会把permit置为1。
每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。 -
简单的理解就是,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时: 如果有凭证,则会直接消耗掉这个凭证然后正常退出; 如果无凭证,就必须阻塞等待凭证可用。 而unpark则相反: 它会增加一个凭证,但凭证最多只能有1个,累加无效。
因此上述问题的答案就是:
1、先执行unpark()
,将许可证由0变为1。
2、然后park()
来了发现许可证此时为0(也就是有许可证),那么他就不会阻塞,马上就往后执行。同时消耗许可证(也就是将1又变为0)。
回归正题
Java中的synchronized 和 ReentrantLock 都是可重入锁。可重入锁的意义在于防止死锁。
先看下ReentrantLock的继承关系图:
ReentrantLock实现了Lock接口,对外提供Lock接口的方法。有一个同步器属性,上锁、释放锁都是通过调用同步器的相关方法实现的。构造时,同步器可以选择公平锁/非公平锁,它们都继承了抽象父类Sync,而Sync又继承了AQS。
抽象类AQS维护了等待队列,而ReentrantLock只需要定义共享资源的获取与释放的方式。
可重入功能的实现原理
ReentrantLock的可重入功能基于AQS的同步状态:state。
其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。
以下是非公平锁(默认下我们使用的都是非公平锁)中可重入的实现原理。
public ReentrantLock() {
// 默认非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
这是非公平锁的部分代码,这里只为说明可重入
的实现原理(详见注释),对于非公平锁更详细分析见后文。
// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state的值
int c = getState();
// 如果state的值等于0,表示当前没有线程持有锁
// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// state的值不等于0,表示已经有其他线程持有锁
// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
// 如果不是当前线程,获取锁失败,返回false
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;
}
ReentrantLock中非公平和公平的实现原理
因为ReentrantLock同时支持公平锁和非公平锁,上文也提到, ReentrantLock默认无参构造函数使用的是非公平锁,有参构造函数可指定使用公平锁。
对于公平锁:是指在获取锁之前会检查队列中有没有线程在等待,如果有的话就不会去获取锁,而是会从尾结点加入队列。
对于非公平锁:就是在获取锁之前不会去检查队列中有没有线程在等待,而是直接去获取锁,这里其实是一种插队
的表现。如果锁没有线程占用,则队列中被唤醒的线程和新来的线程会同时竞争锁。此时,队列中被唤醒的线程并不一定能优先获得锁,当队列中被唤醒的线程被新来的线程抢占了资源,这种插队也就表现出了非公平的特性。
非公平锁的获取
注意和后面公平锁的对比
static final class NonfairSync extends Sync {
final void lock() {
// 和公平锁相比,这里会直接先进行一次CAS(尝试插队),成功就返回了。这是第一处不一样【对比请看下方公平锁的lock】
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer类的acquire(int arg)方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state的值
int c = getState();
// 如果state的值等于0,表示当前没有线程持有锁
// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true
if (c == 0) {
// 这里没有对队列进行判断,直接CAS抢,这是第二点不一样【对比请看下方公平锁的lock】
if (compareAndSetState(0, acquires)) {
//获取成功就设置线程变量
setExclusiveOwnerThread(current);
return true;
}
}
// state的值不等于0,表示已经有其他线程持有锁
// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
// 如果不是当前线程,获取锁失败,返回false
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
// 如果小于0,那么表示重入次数过多(超过了Integer.MAX_VALUE),直接抛出异常
// 新的state小于0的情况,只有c为Integer.MAX_VALUE时才会发生,
// 由于计算机二进制的计算原理,此时加上1反而会变成int类型的最小值,从而小于0
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//这里不需要CAS,因为在else if条件中,当前线程就是已经获取到锁的线程了
setState(nextc);
return true;
}
return false;
}
公平锁的获取
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 获取锁,与非公平锁的不同的地方在于,这里直接调用的AQS的acquire方法,没有先尝试获取锁
// acquire又调用了下面的tryAcquire方法,核心在于这个方法
final void lock() {
acquire(1);
}
// 这个方法和nonfairTryAcquire方法只有一点不同,就是 !hasQueuedPredecessors()
// 多了一个判断hasQueuedPredecessors,这个方法是判断当前AQS的同步队列中是否还有等待的线程
// 如果有,返回false,否则返回true。
// 由此可知,当队列中没有等待的线程时,当前线程才能尝试通过CAS的方式获取锁。
// 否则就让这个线程去队列后面排队。
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;
}
}
// state的值不等于0,表示已经有其他线程持有锁
// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
// 如果不是当前线程,获取锁失败,返回false
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
公平锁中AQS提供的同步队列机制:把线程封装成节点,在一个队列中进行排队。(队列是一个双向链表)
线程在排队过程中,可能存在多个线程共同竞争尾节点的位置,这个过程其实也是通过CAS+自旋来实现的,因此其实公平锁并非绝对公平。
注意:第一个节点是空节点(哨兵),指的不是Node为空,而是Thread = null 。获得锁的线程不在队列中,第二个位置才是即将获得资源的线程。释放锁的时候,是通过类似head.unpark(head.next.Thread)
这个逻辑的伪代码来实现的,也就是线程1来unpark(线程2)
。
注意线程节点入队后的操作流程理解:
入队后,需要把前面一个节点(线程)的waitstates改为-1,然后park(this),进入阻塞等待;
waitstates = -1的意义:如果该节点后面有节点(线程阻塞等待),需要cur.unpark(next)后面的线程,停止阻塞。
waitstates = 0的意义: 队列后面没有节点了,不需要unpark()其他线程。
当线程从队列中获取到了锁,执行后,该线程的节点 thread = null ,就是队列中的第一个节点。
锁的释放
解锁过程,unlock()
方法是在AQS中实现的:
public void unlock() {
// 直接调用了AQS中的release方法,参数为1表示解锁一次state值-1
sync.release(1);
}
public final boolean release(int arg) {
// 和tryAcquire一样,也得子类去重写,释放锁操作
if (tryRelease(arg)) {
// 释放锁成功后,获取新的头结点
Node h = head;
// 如果新的头结点不为空并且不是刚刚建立的结点(初始状态下status为默认值0,而上面在进行了shouldParkAfterFailedAcquire之后,会被设定为SIGNAL状态,值为-1)
if (h != null && h.waitStatus != 0)
// 唤醒头节点下一个节点中的线程
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 将等待状态waitStatus设置为初始值0
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 获取下一个结点
Node s = node.next;
// 如果下一个结点为空或是等待状态是已取消,那肯定是不能通知unpark的,这时就要遍历所有节点再另外找一个符合unpark要求的节点
if (s == null || s.waitStatus > 0) {
s = null;
//这里是从队尾向前,因为enq()方法中的t.next = node是在CAS之后进行的,而 node.prev = t 是CAS之前进行的,所以从后往前一定能够保证遍历所有节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
//如果找到了,就直接unpark,如果还是没找到,只能就此作罢
if (s != null)
LockSupport.unpark(s.thread);
}
再来看看tryRelease()
方法是怎么实现的,具体实现在Sync
中:
protected final boolean tryRelease(int releases) {
// 先计算本次解锁之后的状态值
int c = getState() - releases;
// 因为是独占锁,那肯定这把锁得是当前线程持有才行
if (Thread.currentThread() != getExclusiveOwnerThread())
// 否则直接抛异常
throw new IllegalMonitorStateException();
boolean free = false;
// 如果解锁之后的值为0,表示已经完全释放此锁
if (c == 0) {
free = true;
// 将独占锁持有线程设置为null
setExclusiveOwnerThread(null);
}
// 状态值设定为c
setState(c);
// 如果不是0表示此锁还没完全释放,返回false,是0就返回true
return free;
}
总结
Java中的
synchronized
和ReentrantLock
都是可重入锁(可递归锁)。可重入锁的意义在于防止死锁;由于
synchronized
是基于monitor
机制实现的,它只支持非公平锁;
ReentrantLock
同时支持公平锁和非公平锁,它依赖LockSupport
的park()
和unpark()
方法实现。
最后
以上就是炙热发夹为你收集整理的可重入锁的理解及公平锁和非公平锁的具体实现可重入锁的全部内容,希望文章能够帮你解决可重入锁的理解及公平锁和非公平锁的具体实现可重入锁所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复