文章40 | 阅读 14799 | 点赞0
ReentrantLock的源码涉及到的类比较多,如果想真正掌握ReentrantLock的原理,建议独自去阅读源码,会有更好的效果。
我们都知道ReentrantLock
简称(可重入锁
),我们也知道ReentrantLock可以实现非公平锁和公平锁机制(转载的一篇公平锁和非公平机制)。默认(指无参构造得到的ReentrantLock实例
)是非公平锁,通过有参构造器传入一个boolean值,如果传入true则是公平锁。
在阅读ReentrantLock前,我们将ReentrantLock的整体先掌握个大概,然后将大的一块逐步分成一个个小块,进一步掌握ReentrantLock源码的原理。
第一个问题:读源码的必要性回答
为什么要阅读源码,如果只是做一个api调用工程师,那么源码似乎无多大用处。但对于读过源码的小伙伴我相信他们都会和我有一样的感受(发自内心的感受、由内到外的透露):我们不一样!~~~
。
读过源码的小伙伴对ReentrantLock的原理成竹在胸
。
有没有读过源码区别就在于两者的基础不一样,读源码有助于个人的成长!也能建立自信心。
第二个问题:ReentrantLock简不简单?
这个问题仁者见仁智者见智。
读ReentrantLock的源码 说难也难,说不难也不难。真正区别在于你有没有耐心去读下去,遇到困难时是选择放弃,还是坚持磨刀霍霍不放弃!
话不多说,我们省点笔墨灌鸡汤和扯皮。
为了省篇幅和排版,以下知识点我大多采用链接方式引用我之前写过的一些文章,如果是哪里自己不是很熟悉的可以先阅读我之前的文章(有我的一些总结)。同时自己也通过搜索引擎查阅一下相关资料,我们争取每一个知识点都不要错过!
第一个知识点:
可重入锁和不可重入锁的区别第二个知识点:
公平锁和不公平锁的区别第三个知识点:
LockSupport源码和原理以及坑第四个知识点(非常重要但对于本篇文章非必须掌握):
以ReentrantLock的非公平锁为例深入解读AQS源码ReentrantLock的源码涉及到的类是比较多的,要想真正掌握下来需要花一段时间好好消化。
首先我们按照常规逻辑去跟踪一下源码。
常规的逻辑是:
先lock(),然后unlock()
我们看下lock()
到底做了哪些事情
下面是ReentrantLock内部类的整体结构
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync; // 同步器
/** * Sync是公平锁和非公平锁的父类 */
abstract static class Sync extends AbstractQueuedSynchronizer {
...//省略部分代码,具体要用到的时候我再粘出来。先有一个整体概念
}
/** * 非公平锁 */
static final class NonfairSync extends Sync {
...//省略部分代码
}
/** * 公平锁 */
static final class FairSync extends Sync {
...//省略部分代码
}
/** 无参构造方法 * 默认创建一个非公平锁 */
public ReentrantLock() {
sync = new NonfairSync();
}
/** 带布尔值的有参构造方法 * true则是公平锁,false则是非公平锁 */
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
/** * 尝试加锁 * 如果没有线程加锁则立即将锁计数记为1 * 如果是同一个线程调用这个方法则计数+1 * 如果已经被其它线程上锁了,则禁止当前线程调度。知道其它线程释放了锁 */
public void lock() {
sync.lock();
}
/** * 释放锁 */
public void unlock() {
sync.release(1);
}
}
假设我们创建的是非公平锁NonfairSync
那么成员变量被赋值sync= new NonfairSync();
当我们调用ReentrantLock的lock()
的时候回发现实际上调用的是内部类NonfairSync中的lock()
而这个lock方法是由父类Sync
继承过来的。这个时候我们将Sync的lock方法粘贴出来看下
/** * Sync是公平锁和非公平锁的父类 */
abstract static class Sync extends AbstractQueuedSynchronizer {
@ReservedStackAccess
final void lock() {
if (!initialTryLock())
acquire(1);//这个方法来自父类AbstractQueuedSynchronizer
}
// 尝试加锁,由子类实现具体功能
abstract boolean initialTryLock();
}
会发现判断需要调用子类NonfairSync的initialTryLock()返回值
这个时候粘贴除非公平锁的这个方法做了哪些事情
static final class NonfairSync extends Sync {
//初始化锁
final boolean initialTryLock() {
Thread current = Thread.currentThread(); // 获取当前线程
if (compareAndSetState(0, 1)) { // 如果之前没有加锁,则将state更新为1,并进行加锁 setExclusiveOwnerThread(current);
setExclusiveOwnerThread(current);// 将当前线程设置为独占意思就是加锁
return true; // 加锁成功
} else if (getExclusiveOwnerThread() == current) {// 之前加过锁,相当于重入锁(也意味着当前线程本来就获得到了锁),第二次进入则进入代码块
int c = getState() + 1; // 将锁计数+1,相当于多次加锁,每加一次锁就会+1
if (c < 0) // 小于0,超过int的上限导致变成负数,就抛异常
throw new Error("Maximum lock count exceeded");
setState(c);//更新state值
return true; //加锁成功
} else //这个分支说明当前线程所操作的资源已经被加锁了,需要等待释放锁后获得锁。所以返回false
return false; // 加锁失败
}
}
通过阅读代码会发现这个initialTryLock()
内部由3个分支,分别是处理
第一次加锁
重入锁
其它线程尝试加锁
这个逻辑就是判断当前线程是第一个获得到锁的线程?还是获得到锁的线程再一次获得锁?还是其它线程想要获得锁?三种情况。
他们的作用好比我之前文章中提到的:可重入锁与不可重入锁的区别
如果是重入锁,则通过这个state成员属性记录加锁次数,想要防止死锁现象,就得加几次锁就得解几次锁。也就是最终将这个state更新为0。
这里由于我们是第一次进入lock()
也就是第一个分支返回true,我们回到之前得代码会发现由于!true
也就是不执行if
的代码块acquire(1);
@ReservedStackAccess
final void lock() {
if (!initialTryLock()) //最终为false
acquire(1);//这个方法来自父类AbstractQueuedSynchronizer
}
那么意味着我们第一次加锁成功了!
在上面的代码中我们遇到了一行未知的方法setExclusiveOwnerThread(current);
。我们深入这个方法看下这个方法到底在哪里,做了什么事情。
通过追踪发现是AQS的父类AbstractOwnableSynchronizer
中定义的final方法
public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
/** * 成员变量 */
private transient Thread exclusiveOwnerThread;
// 发现是final修饰的方法,不可以被继承而且里面只有一行代码
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
}
对于这类方法我们只需要知道它是做什么的就行了。通过英文注释翻译大致的意思就是将传入的线程设置为独占线程(实际也是Thread的实例,只是做一个标记而已,表示这个线程是独占的,相当于加锁的含义)
回过头来,第一次加锁我们加锁成功了。那么其它线程执行同一段加锁代码段会经过哪些逻辑呢?
大胆的猜测加锁的本质。
线程和锁都是比较抽象的概念,为了更好的理解,我们读一小段加锁的字节码,然后再结合上面分析的过程,相互对照和验证。
测试代码类的源码
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockTest {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.out.println("这是一段加锁代码段");
} finally {
lock.unlock();
}
}
}
javap -c ReentrantLockTest.class
反编译得到(去除了部分字节码)public class top.huashengshu.java8.ReentrantLockTest {
static java.util.concurrent.locks.ReentrantLock lock;
public top.huashengshu.java8.ReentrantLockTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
3: invokevirtual #3 // Method java/util/concurrent/locks/ReentrantLock.lock:()V
6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
9: ldc #5 // String 这是一段加锁代码段
11: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
14: getstatic #2 // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
17: invokevirtual #7 // Method java/util/concurrent/locks/ReentrantLock.unlock:()V
20: goto 32
23: astore_1
24: getstatic #2 // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
27: invokevirtual #7 // Method java/util/concurrent/locks/ReentrantLock.unlock:()V
30: aload_1
31: athrow
32: return
Exception table:
from to target type
6 14 23 any
static {};
Code:
0: new #8 // class java/util/concurrent/locks/ReentrantLock
3: dup
4: invokespecial #9 // Method java/util/concurrent/locks/ReentrantLock."<init>":()V
7: putstatic #2 // Field lock:Ljava/util/concurrent/locks/ReentrantLock;
10: return
}
Process finished with exit code 0
我们很容易得知源码的lock.lock()
对应着main方法中的Code标号为3的3: invokevirtual #13
invokevirtual意思就是调用实例方法的指令lock.unlock()
方法对应Code标号为9的代码,又由于lock.unlock()
放在finally中,即使try中出异常也会再次执行finally块中的代码,也就是说还会执行解锁,防止线程死锁。
同时也会发现Code 9和19是一样的指令并且都是#17(调用的地址或者称为行号)
根据字节码的显示,我们就更加理解了lock()和unlock()
的作用。
ReentrantLock就像是站岗的交警,它指挥着线程的通行,只不过只能允许一俩汽车通行。而有的汽车通过后,可能绕了一个圈,接着又通行了。达到了可重入锁的效果。
而有的线程则由于执行到lock()
时被拦住了,不让通行(因为没有获得到锁,就必须等待)。底层是将线程进行自旋,就是不断的原地踏步(本质就是死循环的含义,但又可以退出循环。需要等其它线程释放锁,才又机会获得锁并退出自旋)
讲了那么多,那么大致可重入锁的lock()
我们基本上是理解了,而且也跟踪了一下源码。
同理,unlock()
方法,你也可以自己分析出来,即使不用看源码,大致也能猜到,有哪些步骤是一定要走的
比如:执行一次unlock()
state的值就得state= state -1
。相当于重入锁释放一次锁,计数-1。
这里面无非就是有一个可重入锁的概念夹杂在其中。
此番分析源码并没有将ReentrantLock的所有源码都解读完毕,ReentrantLock涉及的内容比较多,一篇文章完全不够讲。因此我只是站在一个角度将源码浅略的分析了一下。真正想要掌握ReentrantLock就真的需要自己去发掘,反问一下ReentrantLock难道 就只有lock()、unlock()
方法?其它方法你都用过?
分析完ReentrantLock后,更深入挖掘源码。比如AQS的源码。AbstractQueuedSynchronizer是juc底层的框架,如果想要更深入的理解多线程,那么就需要掌握aqs的原理和源码。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://yumbo.blog.csdn.net/article/details/109439491
内容来源于网络,如有侵权,请联系作者删除!