在之前的文章中我也曾经介绍过Lock,像ReentrantLock(可重入锁)和ReentrantReadWriteLock(可重入读写锁),这些所我们在说的时候并没有详细的说明它们的原理,仅仅说明了它们的用法,今天我们就来看一看Java中Lock底层的原理,下一篇文章将分析ReentrantLock和ReentrantReadWriteLock!

以下大概就是我们本篇文章的内容:

  1. Lock的方法摘要
  2. 队列同步器
  3. 自定义同步组件(类似ReentrantLock的简单结构)
  4. 同步器队列的实现
  5. 三种不同的同步状态

1.Lock接口

说到Lock,我们立即会想到synchronized,它和锁一样同样起到同步的作用,经过jdk1.6优化过后,synchronized已经不是以前的那个不好用的东西了,它使得我们更容易使用多线程同步(例如里面添加了自旋锁,锁粗化,锁消除,轻量级锁和偏向锁),经过优化过后它的效率已经不再像以前那样了,但是今天我们的主角可不是它,而是我们的Lock:

Lock是一个接口,里面规范了一系列的方法:

大体就是以上的方法了。

我们经常使用的ReentrantLock就是Lock的子类,所以Lock使用的方法ReentrantLock都会实现。

Lock接口的实现通常聚合了一个同步器的子类来完成线程访问的控制。


2.队列同步器

在学习ReentrantLock之前我们必须先了解同步器,即:AbstractQueuedSynchronizer,它是构建锁和其他同步组件的基础框架,它使用一个int成员变量表示同步状态,通过FIFO(first input first output)队列来完成资源获取线程的排队工作。

我们看看它的命名是什么样的:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements Serializable

我们可以发现它是一个抽象类,继承了AbstractOwnableSynchronizer ,并可序列化!

我们来看看这个抽象类里面都有些什么方法:



(以上五个方法是同步器可重写的方法,这些方法在重写的时候会用到同步器的模板方法)

前两个方法是独占式获取同步状态和释放独占状态,三四方法是共享式获取同步状态,最后一个方法表示判断同步器是否被当前同步器所独占!

上面的五个方法是可重写的方法,同时同步器还为我们提供了模板方法:

  • void acquire(int arg)
  • void acquireInterruptibly(int arg)
  • boolean tryAcquireNanos(int arg,long nanos)
  • void acquireShared
  • void acquireSharedInterruptibly(int arg)
  • boolean tryAcquireNanos(int arg, long nanosTimeout)
  • boolean release(int arg)
  • boolean releaseShared(int arg)
  • Collection<Thread>getQueuedThreads

以上是同步器提供的模板方法,但是不要以为同步器的方法只有这些,还有一些不常用的方法,有兴趣的朋友可以自己打开文档瞧瞧!


3.自定义同步组件

方法我么都了解了,接下来看一个范例:

以下的这段代码是我参考jdk1.6文档的时候,关于AbstractQueuedSynchronizer 的使用范例:

class Mutex implements Lock, java.io.Serializable {

    // Our internal helper class
private static class Sync extends AbstractQueuedSynchronizer {
// Report whether in locked state
protected boolean isHeldExclusively() {
return getState() == 1;
} // Acquire the lock if state is zero
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
} // Release the lock by setting state to zero
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise unused
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
} // Provide a Condition
Condition newCondition() { return new ConditionObject(); } // Deserialize properly
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
setState(0); // reset to unlocked state
}
} // The sync object does all the hard work. We just forward to it.
private final Sync sync = new Sync(); //里面实现了Lock接口中的所有方法,并且方法里面使用到Sync的方法。
public void lock() { sync.acquire(1); }
public boolean tryLock() { return sync.tryAcquire(1); }
public void unlock() { sync.release(1); }
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively(); }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}

上面就是一个同步组件,Mutex实现Lock,并通过调用同步器的模板方法来实现了Lock的所有方法,在Mutex类中我们添加了一个静态内部类,里面重写了同步器的三个方法(这个Mutex组件是一个独占式同步组件,所以只重写了tryAcquire和tryRelease两个方法) 在tryAcquire方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,在tryRelease方法中只需要将同步状态设置为0就可以了。

当我打开ReentrantLock的源码的时候,毫不意外的发现这个范例和ReentrantLock的结构相似!

所以现在你应该知道ReentrantLock是如何实现的了吧!


4.同步器队列的实现

通过介绍上面的自定义同步组件,我们会想:当获取同步状态失败时怎么办?我们想象一下,我们去食堂打饭,如果你率先到达食堂,食堂的阿姨会立即为你打饭,但是如果窗口被占用怎么办?对,你只好老老实实的在后边排队,我们的同步器的内部也是这样,它维护了一个FIFO队列来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程和等待状态这些信息构造成一个节点Node(这个Node类是同步器的内部类),并把它放入同步队列中,同时会阻塞当前线程,当同步状态空闲时,即头结点的同步状态结束时,会激活首节点中的线程,让其再次尝试获取同步状态。

接线来我们来看看这个队列的节点中都包含哪些内容(字段):

  • int waitStatus 等待状态
  • Node prev 前驱节点
  • Node next 后继节点
  • Node nextWaiter 等待队列中的后继节点,节点类型和后继节点公用一个字段
  • Thread thread 获取同步状态的线程

    以下为同步队列的结构图:

    (博主才疏学浅,不会绘图,就用Visio将就花了一下)

里面还包含了一些对节点的操作方法和构造方法,这里就不再说明了!

相信到这里大家已经对同步队列有一个大概的认识了,首先一个线程想获取同步状态,如果标识位标识为空闲,那么当前线程获得同步状态,否则同步器把它包装成一个节点放入同步队列中等待,一直到头节点内的线程释放同步状态,然后唤醒首节点的线程,尝试获取同步状态(在这个过程中这个双向队列会进行改变,例如有节点添加到尾部,或者有节点退出这个队列,具体的操作过程参照链表那样照葫芦画瓢即可)

5.同步状态

同步状态可以分为三种:

1.独占式同步状态:

独占式同步状态是指当一个线程获取到同步状态后,其他线程在同步状态未释放之前就不能够再获取这个同步状态,就像写操作那样!

2.共享式同步状态

共享式同步状态是指一个线程获取了同步状态后,其他线程(具有一定的条件下)可以继续获取这个同步状态,就像读操作,这个同步状态允许多个线程同时读取同一段数据(需要同步的)。

3.独占式超时同步状态

首先这个同步状态是独占的,其次这个同步状态需要在一定的时间内完成获取同步状态,如果成功则返回true,否则返回false。

接下来我们分别来看这些同步状态的获取和释放操作:

独占式获取同步状态:



安利一个绘图软件:processOn

/**
其实这里是一大堆的英文注释,我们接下来就来看看这个acquire方法具体是干嘛的吧:
文档说了,这是一个独占式的获取模式
**/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

从代码层面看,首先他调用了自定义同步器实现的tryAcquire方法进行同步,这个方法保证线程安全的获取同步状态(可以参照前面的自定义同步器范例代码),其次如果获取失败,那么使用addWaiter方法构造同步节点,里面的参数Node.EXCLUSIVE是常量,表示构造的节点是独占式的,并传入当前的arg状态参数,最后使用acquireQueued方法使得该节点以死循环的方式获取同步状态,如果if判断成功(即未获取同步状态并且acquireQueued方法返回true创建节点成功,那么执行selfInterrupt方法来阻塞节点中的线程,我们通过源码可知执行:Thread.currentThread().interrupt();发出中断状态

addWaiter代码:

/**
* Creates and enqueues node for current thread and given mode.
* 为当前线程和所给模式创建节点并入队
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* 参数 模式 Node.EXCLUSIVE 为独占式
* @return the new node
* 返回一个新的节点
*/
//mode是Node类型的,之前我们在说Node的结构的时候有提及到这个
private Node addWaiter(Node mode) {
//为当前线程创建节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure//尝试快速在尾部添加
//找到头结点
Node pred = tail;
if (pred != null) {
//当前节点的前驱节点为头结点
node.prev = pred;
//compareAndSetTail方法底层调用的是Unsafe的compareAndSwapObject方法,来保证并发的安全性,这个方法的作用是:pred是预期值,使得node和它进行比较,如果相等就更新(更新为node)返回true,不相等就不更新返回false。
/**
* compareAndSwapObject(Object var1, long var2, Object var3, Object var4)
* var1 操作的对象
* var2 操作的对象属性
* var3 var2与var3比较,相等才更新
* var4 更新值
*/
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}

然后便是入队操作了:


/**
* Inserts node into queue, initializing if necessary. See picture above.
* 将节点插入队列,如果必要的话,进行初始化
* @param node the node to insert
* 参数node为插入的节点
* @return node's predecessor
* 返回为节点的前任,即为原来的尾节点
*
*/
private Node enq(final Node node) {
//以死循环的方式设置尾节点
for (;;) {
Node t = tail;
if (t == null) { // Must initialize,如果为节点为空,必须进行初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

最后要节点内的线程需要获取同步状态是怎么样的?

这就需要同过acquireQueued方法来进行设置:

/**
* Acquires in exclusive uninterruptible mode for thread already in
* 为已进入的线程以独占式连续模式来获取同步状态(翻译的比较蹩脚→.→)
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
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;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

上面的代码通过死循环的方式不断的判断当前节点的前一个节点是否是头结点,并且尝试获取同步状态,如果成功获取,那么释放原来的节点。

这里的死循环其实就是一个自旋的过程!

独占式获取同步就为以上的步骤了,接下来我们看独占式的release方法

同步器的release方法

/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*
* @param arg the release argument. This value is conveyed to
* {@link #tryRelease} but is otherwise uninterpreted and
* can represent anything you like.
* @return the value returned from {@link #tryRelease}
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

上面的方法除了释放原来的头结点,还起到了唤醒头结点的下一个节点的作用(unparkSuccessor),限于篇幅唤醒方法这里就不详解了。


共享式同步状态的获取与释放

    public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

线程通过tryAcquireShared获取共享式同步状态,当返回值大于零时表示能够获取同步状态,小于零无法获取同步状态,所以tryAcquireShared(arg) < 0 即获取失败,调用doAcquireShared方法为这个线程构造节点,并入队!

    private void doAcquireShared(int arg) {
//为当前线程以共享式构造节点
final Node node = addWaiter(Node.SHARED);
//addWaiter参照上面的
boolean failed = true;
try {
boolean interrupted = false;
//以死循环方式获取同步状态
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

在doAcquireShared方法中进行自旋获取同步状态的过程中,如果当前节点的前一个节点为头结点,那么尝试获取同步状态,如果r>0,那么获取成功,退出自旋,否则继续自旋!

共享式和独占式的区别是:是否允许在同一时刻有多个线程获取同步状态。

共享式同步状态也需要释放:

 public final boolean releaseShared(int arg) {
//
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}

和独占式不同,共享式的各个占用线程是多个不同的线程,所以要保证线程安全释放,所以通过CAS来保证这个线程安全,我将这个过程贴在了下面,限于篇幅不做过多解释(后续文章我会深入的分析CAS):

private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}

独占式超时同步状态:

先给大家个图瞧瞧大概是什么流程:

关于独占式超时同步状态,相信从名字上就能看出他的特性,以及我们上面做过简单的介绍,大家可以看出来他是在一定的时间内获取同步状态(这个时间由我们指定),而且初次获取失败后会继续获取,并不会调用addWaiter方法来生成节点,一直到规定的时间才获取失败!

在详细解析它之前我们首先要了解一下“响应中断同步状态的获取”,在Java1.5之前,当一个线程获取同步状态失败时都会被阻塞在synchronized方法之外,会对这个线程的中断标志位进行修改,从而对线程进行中断操作,Java1.5中(以后),同步器提供了acquireInterruptibly(int arg)方法,这个方法在等待获取同步状态时,如果线程被中断,那么会立刻返回并抛出InterruptedException。

超时获取,这个超时的时间为nanosTimeout - = now -lastTime,(now为当前唤醒时间,last为上次唤醒时间,注意:nanosTimeout后面有个减号),那么当nanosTimeout大于零的时候表示未超时,小于零的时候就超时了(博主笨拙,推敲了一下发现这个设计真的是很精妙,虽然只是一个很小的加减法,但是它能够多次判断)在这里敬仰一下Doug Lea(不要问我他是谁→.→),但是我下边的代码好像有它自己的想法,其实我用的是jdk1.8,所以它的源码和参考的书有些小不同!

下面我们看看同步器是怎样超时获取的(我又来贴代码啦):

 private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//首先对超时时间进行合法性判断(我得版本是jdk1.8,我参考的书没有进行判断,算是一种代码健全吧)
if (nanosTimeout <= 0L)
return false; final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//deadline=now+nanosTimeout
//上面我们说过老版本jdk是这样的
//nanosTimeout - = now -lastTime,这样有个缺点:
//就是每次都要记录lastTime的时间,所以jdk进行优化,变成下边这个样子
//啦,这样就免去每次记录的烦恼了(对于处理器而言,当然这样也算是代码优化吧)
nanosTimeout = deadline - System.nanoTime();
//判断是否小于零,小于零就直接返回false
if (nanosTimeout <= 0L)
return false;
//这个就应该是获取同步状态了
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
//如果线程中断,那么抛出异常
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

看了上面的代码是不是还有一处比较疑惑?

if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);

相信就是这一段:

①shouldParkAfterFailedAcquire是为获取失败的节点检查和更新状态,如果线程阻塞,那么返回true。

②spinForTimeoutThreshold是一个常亮为1000纳秒,如果nanosTime最后小于1000纳秒时,不会使该线程进行超时等待,而是进入快速自旋的过程(也是出于效率的角度和精准角度考虑,因为十分段的超时时间无法精确,如果再进行超时等待,那么nanosTimeout反而会不精确,所以这一做法毫无疑问非常明智)。

③LockSupport.parkNanos(this, nanosTimeout)这个是阻塞当前线程nanosTimeout,我们将在下一节说一下这个LockSupport,是如何阻塞和唤醒线程的。

本片文章就到这里了,可能写的不好,望见谅,有分析不到之处还请赐教,不胜感激!

2018.3.25. 16:34

参考书目:《并发编程的艺术》

从源码浅析Java中的Lock和AbstractQueuedSynchronizer的更多相关文章

  1. 通过源码浅析Java中的资源加载

    前提 最近在做一个基础组件项目刚好需要用到JDK中的资源加载,这里说到的资源包括类文件和其他静态资源,刚好需要重新补充一下类加载器和资源加载的相关知识,整理成一篇文章. 理解类的工作原理 这一节主要分 ...

  2. 从源码看java中Integer的缓存问题

    在开始详细的说明问题之前,我们先看一段代码 public static void compare1(){ Integer i1 = 127, i2 = 127, i3 = 128, i4 = 128; ...

  3. String 源码浅析————终结篇

    写在前面 说说这几天看源码的感受吧,其实 jdk 中的源码设计是最值得进阶学习的地方.我们在对 api 较为熟悉之后,完全可以去尝试阅读一些 jdk 源码,打开 jdk 源码后,如果你英文能力稍微过得 ...

  4. java并发:jdk1.8中ConcurrentHashMap源码浅析

    ConcurrentHashMap是线程安全的.可以在多线程中对ConcurrentHashMap进行操作. 在jdk1.7中,使用的是锁分段技术Segment.数据结构是数组+链表. 对比jdk1. ...

  5. Android源码浅析(四)——我在Android开发中常用到的adb命令,Linux命令,源码编译命令

    Android源码浅析(四)--我在Android开发中常用到的adb命令,Linux命令,源码编译命令 我自己平时开发的时候积累的一些命令,希望对你有所帮助 adb是什么?: adb的全称为Andr ...

  6. 我对java String的理解 及 源码浅析

    摘要: 摘要: 原创出处: http://www.cnblogs.com/Alandre/ 泥沙砖瓦浆木匠 希望转载,保留摘要,谢谢! 每天起床告诉自己,自己的目标是 ”技术 + 英语 还有生活“! ...

  7. 2018-10-08 Java源码英翻中进展-内测上线

    创建了一个子域名: http://translate.codeinchinese.com/ 欢迎试用, 如有建议/发现问题欢迎在此拍砖: program-in-chinese/code_transla ...

  8. 2018-09-24 Java源码英翻中网页演示

    在线演示地址: 源代码翻译 两部分如下. 独立的Java代码翻译库 续前文代码翻译尝试-使用Roaster解析和生成Java源码 源码库: program-in-chinese/java_code_t ...

  9. HashSet其实就那么一回事儿之源码浅析

    上篇文章<HashMap其实就那么一回事儿之源码浅析>介绍了hashMap,  本次将带大家看看HashSet, HashSet其实就是基于HashMap实现, 因此,熟悉了HashMap ...

随机推荐

  1. hive上mysql元数据库配置

    hive调试信息显示模式: ./hive -hiveconf hive.root.logger=DEBUG,console 非常有用. 默认情况下,Hive元数据保存在内嵌的 Derby 数据库中,只 ...

  2. NLP+词法系列(二)︱中文分词技术简述、深度学习分词实践(CIPS2016、超多案例)

    摘录自:CIPS2016 中文信息处理报告<第一章 词法和句法分析研究进展.现状及趋势>P4 CIPS2016 中文信息处理报告下载链接:http://cips-upload.bj.bce ...

  3. dojo实现省份地市级联报错(二)

  4. phpcmsv9更换模板介绍

    先分享下大概的步骤: 1.上传模版文件到服务器:2.在站点管理 里边[模板风格配置]选择新模板:3.设置不同模型对应模板:4.修改现有的栏目,匹配新模板:5.更新栏目缓存.系统缓存,更新HTML静态页 ...

  5. Struts2实现文件上传报错(四)

    1.具体错误如下 2014-5-2 21:38:29 com.opensymphony.xwork2.util.logging.jdk.JdkLogger error 严重: Exception oc ...

  6. My97 DatePicker图标触发

    My97 DatePicker图标触发 1.设计源码 <%@ page language="java" import="java.util.*" page ...

  7. Linux开发-makefile

    makefile 介绍 make命令执行时,需要一个 makefile 文件,以告诉make命令如何去编译和链接程序. 首先,我们用一个示例来说明makefile的书写规则.以便给大家一个感性认识.这 ...

  8. 二叉树与AVL树

    二叉树 什么是二叉树? 父节点至多只有两个子树的树形结构成为二叉树.如下图所示,图1不是二叉树,图2是一棵二叉树. 图1 普通的树                                    ...

  9. Git Compare with base,比较大文件时,长时间等待,无法加载

    问题 当使用Git比较一个大文件(几十兆数量级)版本见差异时,会一直等待加载,且内存消耗很大,导致其他进程很难执行.任务管理器中,可以看到此时的TortoiseGitMerge吃掉3G左右的内存. 原 ...

  10. 【BZOJ4195】【NOI2015】程序自动分析(并查集)

    [BZOJ4195][NOI2015]程序自动分析(并查集) 题面 Description 在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足. 考虑一个约束满足问题的简化版本:假设 ...