前文(深入JVM锁机制-synchronized)分析了JVM中的synchronized实现,本文继续分析JVM中的另一种锁Lock的实现。与synchronized不同的是,Lock完全用Java写成,在java这个层面是无关JVM实现的。

在 java.util.concurrent.locks包中有很多Lock的实现类,常用的有ReentrantLock、 ReadWriteLock(实现类ReentrantReadWriteLock),其实现都依赖 java.util.concurrent.AbstractQueuedSynchronizer类,实现思路都大同小异,因此我们以 ReentrantLock作为讲解切入点。

1. ReentrantLock的调用过程

经过观察ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer:

  1. static abstract class Sync extends AbstractQueuedSynchronizer

Sync又有两个子类:

  1. final static class NonfairSync extends Sync
  1. final static class FairSync extends Sync

显然是为了支持公平锁和非公平锁而定义,默认情况下为非公平锁。

先理一下Reentrant.lock()方法的调用过程(默认非公平锁):

这 些讨厌的Template模式导致很难直观的看到整个调用过程,其实通过上面调用过程及AbstractQueuedSynchronizer的注释可以 发现,AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现。 tryAcquire方法的语义在于用具体子类判断请求线程是否可以获得锁,无论成功与否AbstractQueuedSynchronizer都将处理 后面的流程。

2. 锁实现(加锁)

简单说 来,AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时 会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,经过调查线程的显式阻塞是通过调用 LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进 一步,HotSpot在Linux中中通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞。

该队列如图:

与synchronized相同的是,这也是一个虚拟队列,不存在队列实例,仅存在节点之间的前后关系。令人疑惑的是为什么采用CLH队列呢?原生的CLH队列是用于自旋锁,但Doug Lea把其改造为阻塞锁。

当有线程竞争锁时,该线程会首先尝试获得锁,这对于那些已经在队列中排队的线程来说显得不公平,这也是非公平锁的由来,与synchronized实现类似,这样会极大提高吞吐量。

如 果已经存在Running线程,则新的竞争线程会被追加到队尾,具体是采用基于CAS的Lock-Free算法,因为线程并发对Tail调用CAS可能会 导致其他线程CAS失败,解决办法是循环CAS直至成功。AbstractQueuedSynchronizer的实现非常精巧,令人叹为观止,不入细节 难以完全领会其精髓,下面详细说明实现过程:

2.1 Sync.nonfairTryAcquire

nonfairTryAcquire方法将是lock方法间接调用的第一个方法,每次请求锁时都会首先调用该方法。

  1. final boolean nonfairTryAcquire(int acquires) {
  2. final Thread current = Thread.currentThread();
  3. int c = getState();
  4. if (c == 0) {
  5. if (compareAndSetState(0, acquires)) {
  6. setExclusiveOwnerThread(current);
  7. return true;
  8. }
  9. }
  10. else if (current == getExclusiveOwnerThread()) {
  11. int nextc = c + acquires;
  12. if (nextc < 0) // overflow
  13. throw new Error("Maximum lock count exceeded");
  14. setState(nextc);
  15. return true;
  16. }
  17. return false;
  18. }

该方法会首先判断当前状态,如果c==0说明没有线程正在竞争该锁,如果不c !=0 说明有线程正拥有了该锁。

如 果发现c==0,则通过CAS设置该状态值为acquires,acquires的初始调用值为1,每次线程重入该锁都会+1,每次unlock都会 -1,但为0时释放锁。如果CAS设置成功,则可以预计其他任何线程调用CAS都不会再成功,也就认为当前线程得到了该锁,也作为Running线程,很 显然这个Running线程并未进入等待队列。

如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能,并且实现的非常漂亮。

2.2 AbstractQueuedSynchronizer.addWaiter

addWaiter方法负责把当前无法获得锁的线程包装为一个Node添加到队尾:

  1. private Node addWaiter(Node mode) {
  2. Node node = new Node(Thread.currentThread(), mode);
  3. // Try the fast path of enq; backup to full enq on failure
  4. Node pred = tail;
  5. if (pred != null) {
  6. node.prev = pred;
  7. if (compareAndSetTail(pred, node)) {
  8. pred.next = node;
  9. return node;
  10. }
  11. }
  12. enq(node);
  13. return node;
  14. }

其中参数mode是独占锁还是共享锁,默认为null,独占锁。追加到队尾的动作分两步:

  1. 如果当前队尾已经存在(tail!=null),则使用CAS把当前线程更新为Tail
  2. 如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq方法继续设置Tail

下面是enq方法:

  1. private Node enq(final Node node) {
  2. for (;;) {
  3. Node t = tail;
  4. if (t == null) { // Must initialize
  5. Node h = new Node(); // Dummy header
  6. h.next = node;
  7. node.prev = h;
  8. if (compareAndSetHead(h)) {
  9. tail = node;
  10. return h;
  11. }
  12. }
  13. else {
  14. node.prev = t;
  15. if (compareAndSetTail(t, node)) {
  16. t.next = node;
  17. return t;
  18. }
  19. }
  20. }
  21. }

该方法就是循环调用CAS,即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)。总而言之,addWaiter的目的就是通过CAS把当前现在追加到队尾,并返回包装后的Node实例。

把线程要包装为Node对象的主要原因,除了用Node构造供虚拟队列外,还用Node包装了各种线程状态,这些状态被精心设计为一些数字值:

  • SIGNAL(-1) :线程的后继线程正/已被阻塞,当该线程release或cancel时要重新这个后继线程(unpark)
  • CANCELLED(1):因为超时或中断,该线程已经被取消
  • CONDITION(-2):表明该线程被处于条件队列,就是因为调用了Condition.await而被阻塞
  • PROPAGATE(-3):传播共享锁
  • 0:0代表无状态

2.3 AbstractQueuedSynchronizer.acquireQueued

acquireQueued的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回

  1. final boolean acquireQueued(final Node node, int arg) {
  2. try {
  3. boolean interrupted = false;
  4. for (;;) {
  5. final Node p = node.predecessor();
  6. if (p == head && tryAcquire(arg)) {
  7. setHead(node);
  8. p.next = null; // help GC
  9. return interrupted;
  10. }
  11. if (shouldParkAfterFailedAcquire(p, node) &&
  12. parkAndCheckInterrupt())
  13. interrupted = true;
  14. }
  15. } catch (RuntimeException ex) {
  16. cancelAcquire(node);
  17. throw ex;
  18. }
  19. }


细看看这个方法是个无限循环,感觉如果p == head &&
tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,奥秘在于第12行的parkAndCheckInterrupt会把
当前线程挂起,从而阻塞住线程的调用栈。

  1. private final boolean parkAndCheckInterrupt() {
  2. LockSupport.park(this);
  3. return Thread.interrupted();
  4. }


前面所述,LockSupport.park最终把线程交给系统(Linux)内核进行阻塞。当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程
的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中:

  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  2. int ws = pred.waitStatus;
  3. if (ws == Node.SIGNAL)
  4. /*
  5. * This node has already set status asking a release
  6. * to signal it, so it can safely park
  7. */
  8. return true;
  9. if (ws > 0) {
  10. /*
  11. * Predecessor was cancelled. Skip over predecessors and
  12. * indicate retry.
  13. */
  14. do {
  15. node.prev = pred = pred.prev;
  16. } while (pred.waitStatus > 0);
  17. pred.next = node;
  18. } else {
  19. /*
  20. * waitStatus must be 0 or PROPAGATE. Indicate that we
  21. * need a signal, but don't park yet. Caller will need to
  22. * retry to make sure it cannot acquire before parking.
  23. */
  24. compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
  25. }
  26. return false;
  27. }

检查原则在于:

  • 规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞
  • 规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
  • 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同

总体看来,shouldParkAfterFailedAcquire就是靠前继节点判断当前线程是否应该被阻塞,如果前继节点处于CANCELLED状态,则顺便删除这些节点重新构造队列。

至此,锁住线程的逻辑已经完成,下面讨论解锁的过程。

3. 解锁

请求锁不成功的线程会被挂起在acquireQueued方法的第12行,12行以后的代码必须等线程被解锁锁才能执行,假如被阻塞的线程得到解锁,则执行第13行,即设置interrupted = true,之后又进入无限循环。


无限循环的代码可以看出,并不是得到解锁的线程一定能获得锁,必须在第6行中调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线
程获得,从而导致刚被唤醒的线程再次被阻塞,这个细节充分体现了“非公平”的精髓。通过之后将要介绍的解锁机制会看到,第一个被解锁的线程就是Head,
因此p == head的判断基本都会成功。

至此可以看到,把tryAcquire方法延迟到子类中实现的做法非常精妙并具有极强的可扩展性,令人叹为观止!当然精妙的不是这个Templae设计模式,而是Doug Lea对锁结构的精心布局。

解锁代码相对简单,主要体现在AbstractQueuedSynchronizer.release和Sync.tryRelease方法中:

class AbstractQueuedSynchronizer

  1. public final boolean release(int arg) {
  2. if (tryRelease(arg)) {
  3. Node h = head;
  4. if (h != null && h.waitStatus != 0)
  5. unparkSuccessor(h);
  6. return true;
  7. }
  8. return false;
  9. }

class Sync

  1. protected final boolean tryRelease(int releases) {
  2. int c = getState() - releases;
  3. if (Thread.currentThread() != getExclusiveOwnerThread())
  4. throw new IllegalMonitorStateException();
  5. boolean free = false;
  6. if (c == 0) {
  7. free = true;
  8. setExclusiveOwnerThread(null);
  9. }
  10. setState(c);
  11. return free;
  12. }

tryRelease与tryAcquire语义相同,把如何释放的逻辑延迟到子类中。tryRelease语义很明确:如果线程多次锁定,则进行多次释放,直至status==0则真正释放锁,所谓释放锁即设置status为0,因为无竞争所以没有使用CAS。

release的语义在于:如果可以释放锁,则唤醒队列第一个线程(Head),具体唤醒代码如下:

  1. private void unparkSuccessor(Node node) {
  2. /*
  3. * If status is negative (i.e., possibly needing signal) try
  4. * to clear in anticipation of signalling. It is OK if this
  5. * fails or if status is changed by waiting thread.
  6. */
  7. int ws = node.waitStatus;
  8. if (ws < 0)
  9. compareAndSetWaitStatus(node, ws, 0);
  10. /*
  11. * Thread to unpark is held in successor, which is normally
  12. * just the next node.  But if cancelled or apparently null,
  13. * traverse backwards from tail to find the actual
  14. * non-cancelled successor.
  15. */
  16. Node s = node.next;
  17. if (s == null || s.waitStatus > 0) {
  18. s = null;
  19. for (Node t = tail; t != null && t != node; t = t.prev)
  20. if (t.waitStatus <= 0)
  21. s = t;
  22. }
  23. if (s != null)
  24. LockSupport.unpark(s.thread);
  25. }


段代码的意思在于找出第一个可以unpark的线程,一般说来head.next ==
head,Head就是第一个线程,但Head.next可能被取消或被置为null,因此比较稳妥的办法是从后往前找第一个可用线程。貌似回溯会导致性
能降低,其实这个发生的几率很小,所以不会有性能影响。之后便是通知系统内核继续该线程,在Linux下是通过pthread_mutex_unlock
完成。之后,被解锁的线程进入上面所说的重新竞争状态。

4. Lock VS Synchronized

AbstractQueuedSynchronizer通过构造一个基于阻塞的CLH队列容纳所有的阻塞线程,而对该队列的操作均通过Lock-Free(CAS)操作,但对已经获得锁的线程而言,ReentrantLock实现了偏向锁的功能。

synchronized
的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程
的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优
化,而Lock则完全依靠系统阻塞挂起等待线程。

当然Lock比synchronized更适合在应用层扩展,可以继承
AbstractQueuedSynchronizer定义各种实现,比如实现读写锁(ReadWriteLock),公平或不公平锁;同时,Lock对
应的Condition也比wait/notify要方便的多、灵活的多。

java多线程之:深入JVM锁机制2-Lock (转载)的更多相关文章

  1. Java 多线程之:偏向锁,轻量级锁,重量级锁

    一:java多线程互斥,和java多线程引入偏向锁和轻量级锁的原因? --->synchronized的重量级别的锁,就是在线程运行到该代码块的时候,让程序的运行级别从用户态切换到内核态,把所有 ...

  2. Java多线程之synchronized线程锁

    package org.study2.javabase.ThreadsDemo.sync; /** * @Auther:GongXingRui * @Date:2018/9/18 * @Descrip ...

  3. Java多线程之Runnable与Thread

    Java多线程之Thread与Runnable 一.Thread VS Runnable 在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口:Thread类和 ...

  4. 转:synchronized和LOCK的实现原理---深入JVM锁机制

    JVM底层又是如何实现synchronized的? 目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug ...

  5. JAVA多线程之volatile 与 synchronized 的比较

    一,volatile关键字的可见性 要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图如下: 从图中可以看出: ①每个线程都有一个自己的本地内存空间--线程栈空 ...

  6. 深入JVM锁机制2-Lock

    前文(深入JVM锁机制-synchronized)分析了JVM中的synchronized实现,本文继续分析JVM中的另一种锁Lock的实现.与synchronized不同的是,Lock完全用Java ...

  7. 深入JVM锁机制1-synchronized

    目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增加的内容,其作者是大名鼎鼎的并发专家Doug Lea.本文并不比较synchronized与Loc ...

  8. JAVA多线程之wait/notify

    本文主要学习JAVA多线程中的 wait()方法 与 notify()/notifyAll()方法的用法. ①wait() 与 notify/notifyAll 方法必须在同步代码块中使用 ②wait ...

  9. java多线程之yield,join,wait,sleep的区别

    Java多线程之yield,join,wait,sleep的区别 Java多线程中,经常会遇到yield,join,wait和sleep方法.容易混淆他们的功能及作用.自己仔细研究了下,他们主要的区别 ...

随机推荐

  1. BCP 导入导出数据库数据

    使用 bcp 将数据库迁移到 Azure SQL Database --所有 都是在本机sql上运行--先开启cmdshellEXEC sp_configure 'show advanced opti ...

  2. Allow windows service to "Interact with desktop"

    Typically, services are designed to run unattended without any UI with any need to interact with des ...

  3. Python单元测试——unittest

    unittest是python自带的一个模块 python344\Lib\unittest 官方参考文档: http://docs.python.org/2.7/library/unittest.ht ...

  4. PHP Array 函数

    PHP Array 简介 array 函数允许您对数组进行操作. PHP 支持单维和多维的数组.同时提供了用数据库查询结果来构造数组的函数. 安装 array 函数是 PHP 核心的组成部分.无需安装 ...

  5. Ajax方法实现登录页面

    Note: ajax技术 不用刷新页面,做局部刷新不用form表单,因为不需要提交,通过JQuery控制必须要有id如果要用ajax可以用JQuery也可以用js写,推荐JQuery 因为简单,直接引 ...

  6. android onclick onLongClick ontouch dispatchTouchEvent onInterceptTouchEvent

    android onclick onLongClick ontouch dispatchTouchEvent onInterceptTouchEvent 按ACTION_DOWN -> onLo ...

  7. LeetCode---- 二叉树中,找出和为某值的所有路径

    Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals the given su ...

  8. IIS发布错误

    发布程序时遇到的错误:

  9. 10、网页制作Dreamweaver(扩展:各浏览器对 onunload 事件的支持与触发条件实现有差异)

    标准参考 在 HTML 4.01 规范中关于 onunload 事件的描述是:当 document 从 window 中移除时,触发 onunload 事件. 关于 HTML 4.01 规范中 onu ...

  10. mysql实现高效率随机取数据

    从数据库中(mysql)随机获取几条数据很简单,但是如果一个表的数据基数很大,比如一千万,从一千万中随机产生10条数据,那就相当慢了,如果同时一百个人访问网站,处理这些个进程,对于一般的服务器来说,肯 ...