Java锁之ReentrantLock(二)

一、前言

上一篇《Java锁之ReentrantLock(一)》已经介绍了ReentrantLock的基本源码,分析了ReentrantLock的公平锁和非公平锁机制,最终分析ReentrantLock还是依托于AbstractQueuedSynchronizer同步队列器(以下简称同步器)实现,所以本篇开始分析同步器内部的代码实现,考虑到代码结构比较长,所以分析源码时会精简部分不重要的代码,但是最终还是会以不影响代码逻辑的情况下进行精简。

二、同步器分析

  • 同步器主要属性

image

根据上图源码我们可以知道,AbstractQueuedSynchronizer内部构建了一个Node节点对象,同时构造了一个具有volatile属性头节点与尾部节点,保证了多线程之间的可见性,同时最重要的是定义了一个int类型变量state,通过上一篇文章分析,我们知道了ReenTrantLock是否获取到锁的判断就是state是否大于0,等于0表示锁空闲,大于0,表示锁已经被获取。接下来我们重点分析下Node节点内部构造以及同步器的实现原理,Node源码如下:



        static final class Node {
        //共享模式
        static final Node SHARED = new Node();
        //独占模式
        static final Node EXCLUSIVE = null;
        //取消状态
        static final int CANCELLED =  1;
        //唤醒状态
        static final int SIGNAL    = -1;
        //等待条件状态
        static final int CONDITION = -2;
       //传播状态
        static final int PROPAGATE = -3;

       //等待状态
        volatile int waitStatus;

       //上一个节点
        volatile Node prev;

      //下一个节点
        volatile Node next;

      //获取同步状态的线程
        volatile Thread thread;

        //等待队列中的后继节点,如果当前节点是共享的,那么这个nextWaiter=SHARED
        Node nextWaiter;

        //判断当前后继节点是否是共享的
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        //返回当前节点的前一个节点
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

这里需要重点说明的属性是waitStatus,该状态就是包括节点内部声明的几个常量,如下:

常量名 功能
CANCELLED 值为1,当前节点进入取消状态,原因是由于被中断或者是等待超时而进入取消状态,需要说明的是,节点线程进入取消状态后,状态不会再改变,也就不会再阻塞获取锁
SIGNAL 值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使得后继节点的线程得以运行
CONDITION 值为-2,节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到同步状态的竞争中
PROPAGATE 值为-3,表示下一次共享式同步状态获取会无条件的传播下去,比如如果头节点获取到共享式同步状态,判断状态是PROPAGATE,会继续调用doReleaseShared,使得后继节点继续获取锁
INITIAL 值为 0,表示初始状态(这个应该是老版本中的代码中存在,目前查看jdk1.8已经没有显示声明INITIAL状态,因为初始化时候,int变量默认就是0)

分析同步器的属性,我们可以大概画出构造器的队列示意图,如下:

image

首先同步器声明了头节点和尾部节点,head节点指向一个node节点表示该节点是队列的头部节点,tail节点指向一个node节点表示该节点是尾部节点,同时,每个节点都有pre和next属性,指向node节点,然后如图所示构建成一个FIFO双向链表式队列。下面我们查看下同步器常用主要方法

  • 同步器主要方法列表

方法名称 功能
compareAndSetState(int expect, int update) CAS进行设置同步状态
enq(final Node node) 循环入等待队列,直到入队成功为止
addWaiter(Node mode) 以当前线程创建一个尾部节点,并加入到尾部
unparkSuccessor(Node node) 唤醒节点的后继节点
doReleaseShared() 释放共享模式下的同步状态
setHeadAndPropagate(Node node, int propagate) 设置头节点,并继续传播同步许可
release(int arg) 独占式释放同步状态
acquireShared(int arg) 共享式释放同步状态
hasQueuedPredecessors() 判断是否有比当前线程等待更久的线程(用于公平锁)
  • 同步器加锁

以上是同步器主要的方法,我们接下来会对上述部分方法进行重点分析,要了解同步器如何完成加锁,等待获取锁,释放锁的功能,我们先回顾上一篇文章分析ReentranLock的Lock()方法,实现源码如下:

  /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

我们发现重点是acquire(1)方法,该方法是父类也就是同步器提供的,源码如下:

 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

源码其实可以拆分为三部分:

  • tryAcquire(arg) 尝试获取锁
  • addWaiter(Node.EXCLUSIVE), arg) 以当前线程构建成节点添加到队列尾部
  • acquireQueued(final Node node, int arg)让节点以死循环去获取同步状态,获取成功就退出循环

其实解析为三部分就很清楚这个方法的作用了,首先尝试获取锁,获取不到就把自己添加到尾部,然后在队列中死循环去获取锁,最重要的部分就是acquireQueued(final Node node, int arg),源码如下:

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);
        }
    }
//该方法主要靠前驱节点判断当前线程是否应该被阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 如果前任节点的状态等于SIGNAL,
             * 说明前任节点获取到了同步状态,当前节点应该被阻塞,返回true
             */
            return true;
        if (ws > 0) {
            /*
             * 前任节点被取消
             */
            do {//循环查找取消节点的前任节点,
            //直到找到不是取消状态的节点,然后剔除是取消状态的节点,
            //关联前任节点的下一个节点为当前节点
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * CAS设置前任节点等待状态为SIGNAL,
             * 设置成功表示当前节点应该被阻塞,下一次循环调用就会
            *  return  true
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
//把当前线程挂起,从而阻塞住线程的调用栈,
//同时返回当前线程的中断状态。
//其内部则是调用LockSupport工具类的park()方法来阻塞该方法。
   private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);//阻塞线程
        return Thread.interrupted();
    }

以上就是lock.lock()的加锁过程,我们总结分析下:

  • 首先,直接尝试获取锁,获取成功直接结束。
  • 如果获取锁失败,就把当前线程构造一个尾部节点,CAS方式加入到队列的尾部
  • 在队列中,死循环式的判断前任节点是否是头节点,如果是头节点就尝试获取锁,如果不是就把自己挂起,等待前任节点唤醒自己,这样可以避免多个线程死循环带来的性能消耗。
  • 同步器解锁

    lock.unlock()释放锁的过程,分析源码,老规矩,上源码:
    //释放锁
   public void unlock() {
        sync.release(1);
    }
    
     public final boolean release(int arg) {
        //尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            //如果头节点不为空,并且不是初始状态,也就是不在队列中了
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒后继节点
            return true;
        }
        return false;
    }
    //该方法和之前分析的代码类似,主要是设置Sate状态
     protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);//设置同步状态占有线程为null
            }
            setState(c);
            return free;
        }
        
    private void unparkSuccessor(Node node) {
       
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // 取到当前节点的下一个节点,如果节点不为空就唤醒阻塞的线程
        //如果节点为空,或者节点是取消状态,那么就循环从尾部节点找到
        //当前节点的下一个节点唤醒
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);//唤醒阻塞的线程
    }    

以上就是对lock.unlock()分析,同样我们总结分析下

  • 首先既然加锁成功与否判断是根据State不为0来判断,所以,释放锁就会把State设置为0,同时设置锁的所有者线程为null
  • 锁释放成功了,接着就会唤醒在队列的后继节点,通过调用LockSupport.unpark(s.thread)来唤醒线程的,LockSupport主要依托于sun.misc.Unsafe类来实现的,该类提供了操作系统硬件级别的方法,不在本文讨论中。

三、尾言

  • 本次主要分析了AQS同步器的加锁和解锁的实现,其实jdk很多同步工具类都是依赖于AQS同步器实现的,了解了AQS同步器的原理后,对理解其他并发工具的原理也很有帮助,比如CountDownLatchSemaphore,CyclicBarrier(依赖ReentrantLock)等。下一篇《Java锁之ReentrantReadWriteLock》继续细化分析,分析读锁和写锁,分析ReentrantLock是如何实现读锁的重复获取,锁降级等功能的。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容