JAVA多线程详解(三):ReentrantLock实现原理与源码分析

刚学java的多线程通信时,往往只能知道在多线程访问共享资源时加上锁,保证多线程访问共享资源时的同步操作。利用ReentrantLock的lock()方法加锁,ReentrantLock的unlock()方法释放锁。那么加锁和释放锁的内部是怎么实现的呢?其源码解释的比较清晰。下面将从源码的整体架构和具体细节来分析ReentraLock的实现原理。由于大量使用了CAS操作,先介绍CAS的实现原理。

一、CAS原理

是compare and swap的缩写,也就是比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 也就是在修改V位置的值之前,先将该位置的值与“认为”当前位置原先的值进行比较,若相等,则将内存V的值修改为新值B,否则认为值被修改了,则当前操作不修改V的值。(cas操作往往返回操作是否成功,也有返回当前V中的值)。
CAS仅仅只是一种修改值的手段,单独的CAS并不能保证多线程操作的安全性,其往往要和volatile变量和for循环合作才能保证多线程操作的正确性。volatile保证线程的可见性,for循环则会在CAS操作成功的情况下退出,在失败的情况下更新预期原值(A)为最新的V中存的值然后再次尝试修改直到修改成功,常见的操作如下所示:

//以下代码为AQS中的一段源代码,采用的CAS操作
//使用volatile变量保证线程可见性
private transient volatile Node tail;
private Node enq(final Node node) {
        for (;;) {   //循环进行多次操作
            Node t = tail;   //获得最新的尾结点值
            if (t == null) { // Must initialize   //若尾结点为空,则表示队列为空
                if (compareAndSetHead(new Node()))  //CAS操作设置头结点
                    tail = head;   //CAS操作成功,则将尾结点修改
            } else {
                node.prev = t;      
                if (compareAndSetTail(t, node)) {   //CAS操作设置尾结点成功
                    t.next = node;                      //则执行后续操作
                    return t;
                }
            }
        }
    }

以上就是常见的CAS操作利用循环和volatile变量保证设置的正确性。当前线程设置成功 后,其他线程则无法设置成功,必须第二次循环才有可能设置成功。

二、CLH队列锁介绍

CLH是一种种基于链表的可扩展、高性能、公平的自旋锁,申请锁的线程在本地变量上自旋,不断读取其前驱节点的锁状态,若前驱锁状态为false时则退出自旋开始执行。
CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程须要获取锁,且不释放锁。为false表示线程释放了锁。
JAVA多线程详解(三):ReentrantLock实现原理与源码分析_第1张图片
如上图所示,这是一种单向链表,每个节点有一个指向其前驱节点的引用。当一个线程申请锁时,会被包装为一个QNode并使用CAS操作加到队列尾部,然后判断前驱的状态开始自旋。

while (pred.locked) //若前驱为true则自旋,若为false则退出自旋';
{  
}

则利用此队列实现公平的自旋锁,先来先执行,后来后执行。
AQS在CLH队列的基础上进行了扩展。

三、ReentrantLock整体框架

先来看看ReentrantLock都用到了哪些设计模式
1、策略模式
以下为ReentrantLock的部分代码,首先其持有Sync引用,根据不同情况,分别创建了NonfairSync和FairSync的具体执行对象。然后调用的lock()和unlock()的实际执行者是Sync引用的实际对象。这种方式是典型的策略模式。

class ReentrantLock{
        private final Sync sync;
        public ReentrantLock() {
        sync = new NonfairSync();
    }
        public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
       public void lock() {
        sync.lock();
    }
      public void unlock() {
        sync.release(1);
    }
}

2、模板方法模式

abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync

以上为ReentrantLock中的几个静态内部类,锁原理的核心实现在AbstractQueuedSynchronizer抽象类中,同时此类中有5个模板方法,具体的实现类可以选择实现这些方法,来达到特定的锁机制。以一个抽象方法为例看源码

protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

这些模板方法并非抽象方法,所以也说明了具体的实现类中可以选择实现的原理。
下图为类的结构图。
JAVA多线程详解(三):ReentrantLock实现原理与源码分析_第2张图片

源码分析

ReentrantLock的加锁的入口函数为lock(),释放锁的入口函数为unlock(),那么我将由lock()为入口来一步一步看公平锁FairSync和非公平锁NonfairSync加锁的实现原理。

1、非公平锁NonfairSync加锁流程

整个加锁的流程如下图所示
JAVA多线程详解(三):ReentrantLock实现原理与源码分析_第3张图片
默认情况下ReentrantLock创建的是非公平锁,则在调用lock时调用的是NonfairSync的lock方法,方法首先尝试将state由0变为1,若成功,则表明当前线程抢到锁并设置拥有锁的线程为当前线程。若失败则执行acquire(1)。此处也是非公平锁可以插队的原理。当来了一个线程,不论等待队列里面有没有线程,则都具备申请锁的资格。

public ReentrantLock() {
        sync = new NonfairSync();
    }
final void lock() {
	//尝试将锁状态由0设置为1
    if (compareAndSetState(0, 1)) 
      //设置拥有锁线程为当前线程        
        setExclusiveOwnerThread(Thread.currentThread());  
    else
        acquire(1);   //申请锁操作
 }

接下来就是执行acquire()方法了。

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

此处的if中的条件是短路与操作,首先执行tryAcquire

protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
 //得到锁返回true,得不到锁返回false
final boolean nonfairTryAcquire(int acquires) {   
	//获得当前线程
	final Thread current = Thread.currentThread();  
            int c = getState(); //得到锁状态
            if (c == 0) { //若为0,则表示锁空置
            //利用CAS操作修改state状态,若成功则当前线程得到锁
                if (compareAndSetState(0, acquires)) { 
                		//设置占有锁的线程为当前线程     
                    setExclusiveOwnerThread(current);       
                    return true;
                }
            }
            //这部分为可重入的操作,当state不为0同时当前线程
            //与得到锁的线程是同一个线程进入条件
            else if (current == getExclusiveOwnerThread()) {     
            //锁状态增加,可重入
                int nextc = c + acquires; 
                //如果小于0抛出异常                  
                if (nextc < 0) // overflow                
                    throw new Error("Maximum lock count exceeded");
                    //设置锁状态的值
                setState(nextc);                          
                return true;
            }
            return false;
        }

由上可知如果得到锁,则tryAcquire(1)返回true,取反则为false,后面的操作被短路不执行,acquire(1)结束退出,线程得到锁。当未得到锁时,则tryAcquire(1)则返回false,取反为true,则执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)。首先先看addWaiter(Node.EXCLUSIVE)。

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; //新创建的节点的前驱指向尾结点
            //cas操作将尾结点指针指向新的节点
            if (compareAndSetTail(pred, node)) { 
            	//若成功,则将原来的尾结点指向的next指向新节点    
                pred.next = node;
                //返回新创建的节点                         
                return node;                              
            }
        }
        //以上操作失败或者pred为空时,调用此函数
        enq(node);
        //返回新创建的节点                              
        return node;                             
    }

接下来执行enq,是典型的CAS+循环+volatile操作

private Node enq(final Node node) {
        for (;;) {//循环
            Node t = tail; //得到尾结点
            //判断尾结点是否为空
            if (t == null) { // Must initialize 
             //CAS操作设置一个新的头结点,并将尾结点指向头结点
                if (compareAndSetHead(new Node()))   
                    tail = head;
            } else { //若尾结点不为空的时候
                node.prev = t;//将新创建的节点前驱指向尾结点
                //cas操作将尾结点指针指向新的节点
                if (compareAndSetTail(t, node)) {
                   //若成功,则将原来的尾结点指向的next指
           //向新节点,如果失败则一直循环直到成功后才能推出。  
                    t.next = node;      
                    return t; 
                }
            }
        }
    }

以上则为加入锁队列的过程,可以参考下图。同时加入的不走只能严格按照这2,3,4步执行,否则在多线程状态下回出问题。
JAVA多线程详解(三):ReentrantLock实现原理与源码分析_第4张图片
节点加入后会返回当前node节点,调用acquireQueued(node,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)                   
        //如果上述循环退出,并且failed为true时,则取消当前节点
                cancelAcquire(node);
        }
    }

以下程序是当前线程阻塞的准备工作,只有此函数返回true时,当前线程才能安心的去等待,否则则会一直在上面函数的for循环中自旋

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus; //得到前驱节点的waitStatus状态
        //如果状态为-1,则表示释放锁时会通知下一个节点醒来,则下一个节点可以放心的阻塞了
        if (ws == Node.SIGNAL)                  
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
             //返回true,此时才会执行parkAndCheckInterrupt()方法,见上一个函数
            return true; 
            //如果状态大于0,则只能为1,表示前驱节点需要被删除                            
        if (ws > 0) {                      
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
            //执行删除前驱节点的操作
                node.prev = pred = pred.prev;
                //只要状态大于0则一直删除,知道找到一个状态不大于0 的           
            } while (pred.waitStatus > 0);             
            pred.next = node;//从新得到前驱节点
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
             //执行操作将前驱节点状态设置为Node.SIGNAL。
             //用CAS操作是防止节点的自操作造成数据错误
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);    
        }
        return false; //返回false则会继续循环,然后再次执行此函数。
    }

上述函数的设计非常巧妙,为何在最后已经将前驱节点设置为Node.SIGNAL了却不返回true,而是返回false呢。主要原因有两个:1、由于多线程的操作,放置在修改后其他线程又修改了这个值 2、考虑到设置完了之后可能当前节点又具备了抢锁的资格,则再循环一次让其去获得锁,那么避免了上下文的切换而提高性能。上述函数返回true之后,则执行parkAndCheckInterrupt()函数,函数如下:

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

利用LockSupport.park(this)函数使得线程阻塞,则当前线程会停在此位置等待唤醒。注意唤醒的方式有两种,首先是正常唤醒,那么函数返回flase。如果是由于中断唤醒,那么将会造成中断标志位位true。则返回值为true,并且修改原先的中断标志位为flase。也就是说返回true时,回到acquireQueued函数则会执行interrupted = true表示线程曾经中断过。然后线程继续执行acquireQueued函数中的for循环去争抢锁,如果没有抢到,则会重复上述过程自旋或者阻塞。如果抢到锁,那么会退出,acquireQueued()函数的返回值为interrupted。若interrupted为false,则为正常状态,在acquire()中则不会执行if中selfInterrupt()函数,但是如果interrupted为true,上述分析则表示曾经中断过,那么必须保证中断标志位位true,则执行selfInterrupt()将线程的中断标志位设置为true。

static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

此处则为LockSupport.park(this),Thread.interrupted()和Thread.currentThread().interrupt()的妙用。上述过程的目的其实就是为了保证由于中断导致线程唤醒的线程的中断标志位位true。那么有人就会疑问,为什么不采用return Thread.isInterrupted();函数呢,这样的话返回中断标志位位true而不修改中断标志位,则线程的中断标志位不就为true了吗,何须后面再麻烦重新设置呢。此处如果使用Thread.isInterrupted()返回,那么中断标志位为true,之后线程去争抢锁,如果没能挣到,线程会去等待,调用LockSupport.park(this)。但是如果当前线程的中断标志位为true时,则函数LockSupport.park(this)无法使得线程阻塞等待,那么线程会一直自旋,造成cpu消耗,性能较差。
以上就是获得锁的全部过程,其实也比较简单,首先来了一个线程,则拥有抢锁的资格,如果抢到锁则线程无需入队列,如果没有抢到锁,则线程入队列然后自旋或者阻塞等待,等待自己被唤醒然后再去抢锁。

公平锁FairSync加锁流程

有了上述非公平锁抢锁的过程,则公平锁就简单的缩了,此处只分析他们之间的不同,相同的部分则不作分析,实际上他们之间大部分采用的是相同的操作。

// An highlighted block
var foo = 'bar';
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;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }

以上就是非公平锁与公平锁的所有区别,首先lock时调用acquire(1)函数,但是非公平锁则会先判断当前线程是否可以得到锁,如果可以则无需后续操作了,这就是插队的原理,但是公平锁则不可以插队。锁以后面的tryAcquire()时,当状态为0时,if中的短路与的第一个条件为!hasQueuedPredecessors() 。当为false时则表示不具备抢锁资格,后面无需执行,只有为true时才能继续执行。hasQueuedPredecessors()函数如上所示,表示头结点和尾结点不相等并且(头结点的下一个节点为空或者当前线程为占有线程时)。此函数可知只有当队列为空或者当前线程为占有锁的线程时或者头结点的下一个节点为空时,新来的线程才能去抢锁。则表现出公平锁的特性。

3、释放锁的流程

无论公平锁还是非公平锁,释放锁的流程都是一致的,执行流程图如下
在这里插入图片描述

public void unlock() {
        sync.release(1);
    }
 public final boolean release(int arg) {
        if (tryRelease(arg)) {     //如果释放了锁则返回true,否则返回false,则if中不执行。
            Node h = head;    //得到头结点
            if (h != null && h.waitStatus != 0)   //头结点不为空并且头结点的状态不为0
                unparkSuccessor(h);  //唤醒后继节点
            return true;
        }
        return false;
    }
    
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;   //得到释放后的状态
            if (Thread.currentThread() != getExclusiveOwnerThread())  //只有获得锁的线程才能操作
                throw new IllegalMonitorStateException();
            boolean free = false;    //将释放状态设置为false
            if (c == 0) {   //如果释放后c为0,则表示锁状态全部释放
                free = true;
                setExclusiveOwnerThread(null);   //将持有锁的线程设置为空
            }
            setState(c);   //更新当前的状态值,无需CAS操作,此时只有持有锁的线程才能操作
            return free;   //返回是否已经完全释放了锁
        }
private void unparkSuccessor(Node node) {   //传入的是头结点
	int ws = node.waitStatus;      //得到状态
        if (ws < 0)                  //状态小于0,则修改状态为0
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;       //获得后继节点
        if (s == null || s.waitStatus > 0) {    //如果后继节点为空,或者后继节点的状态大于0,则从后向前查找,找到最靠近头结点的状态小于等于0的节点。
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)  //由后向前遍历
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)      //当s存在时
            LockSupport.unpark(s.thread);  //唤醒s对应的线程
}

上述过程唤醒线程后,线程执行并去抢锁,则进入之前分析的acquireQueued()函数抢锁的循环之中,这样线程抢到锁后会重新设置头结点,完成锁的控制

你可能感兴趣的:(JAVA编程语言)