JAVA同步器——ReentrantLock

清晰的记录,不辜负此刻的青春,日后的完善,成就更美好的未来


  •  concurrent包结构

最底层:

volatile变量:volatile保证变量在内存中的可见性。java线程模型包括线程的私有内存和所有线程的公共内存,这与cpu缓存和内存类似。线程对变量进行操作的时候一般是从公共内存中拷贝变量的副本,等修改完之后重新写入到公共内存,这存在并发风险。而被volatile标注的变量通过CPU原语保证了变量的内存可见性,也就是说一个线程读到工作内存中的变量一定是其他线程已经更新到内存中的变量。为简单起见,你可以想象成所有线程都在公共内存中操作volatile变量,这样一个线程对volatile的更改其他线程都看的见,即内存可见性!

CAS:compare and swap,比较-相等-替换新值,跟底层CPU有关,一般CAS操作的都是volatile变量。CAS操作包含内存位置(V)、预期原值(A)和新值(B), 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。如方法a.CAS(b,c),如果内存中a与预期值b相等,那么把a更新成c。cas操作在java中由sun.misc.Unsafe类全权代理了!

最底层结:concurrent包中用cas死循环修改volatile变量直到修改成功是最常见的手法!

第二层:

AtomicXXX类:常用基本类型和引用都有Atomic实现,其中最重要的两点当然是一个volatile变量和一个CAS操作。++i和i++操作就是volatile和cas的典型用法,incrementAndget和getAndincrement是两个实现方法,死循环:首先获取volatile变量值a,然后执行a.cas(a,a+1)直到修改成功。

AQS框架:总有面试官会问你AQS框架,尽管我怀疑他们并不真正了解所有细节,Doug lea最屌的思想应该都在AQS框架里了,直接上图(借用,侵删)


该类最主要的两部分就是state状态和Node节点(即队列中的元素),前一个是资源状态,后一个还是cas操作volatile的典型应用,Node还分为独占模式和共享模式,Node节点的waiteState表示线程的状态

  •  lock与sychronized的区别

 

     

 

  • lock源码分析

     lock锁是再java.util.concurrent.locks包下面的接口,它的子类实现了很多不同的锁,如ReentrantLock,ReadWriterLock

(实现类ReentrantReadWriteLock),它们都依赖java.utils.concurrent.locks包下的AbstractQueenSynchronizer,简称AQS,是lock实现锁机制的核心;

针对lock的锁,它分为

  • 独占锁  共享锁
  • 公平锁  非公平锁  重入锁
  • 条件锁  读写锁

ReentrantLock可重入锁源码分析

ReentrantLock相当于对sychronized一个实现,他与sychronized一样是一个独占锁,且是可重入锁,但是sychronized是一个非公平锁,对于竞态条件下的线程都可能得到该锁,ReentrantLock可以是公平锁,也可以是非公平锁,默认的是非公平锁;

 

从代码可以看出ReentrantLock实现了lock接口,并且内部维护了一个静态内部抽象类Sync,该内部类实现了AQS,而Sync又有俩个子类

非公平锁子类

公平锁子类

它们俩个实现大致相同,差别不大,ReentrantLock默认的采用非公平锁,非公平锁相对公平锁而言吞吐量较大,我们从非公平锁上分析源码

本文从ReentrantLock加锁到解锁源码流程来分析,ReentrantLock类码源码如图;

从图中可以看到,该类实现了AQS接口,AQS使用了一个整形volatile变量state来维护同步状态,这个state变量是所有实现lock的关键,加锁,解锁都是依靠这个state的状态位的值去实现;

下面来分析,ReentrantLock的非公平锁加锁lock()方法分析

      1.加锁lock()

 

公平锁加锁如下

非公平锁与公平锁的区别的是在于,非公平锁会使用CAS算法去尝试修改一次AQS字段的state变量,将其从0(无锁状态)置为1(有锁状态),(注:ReentrantLock用state表示“持有锁的线程已经重复获取该锁的次数”。当state等于0时,表示当前没有线程持有锁)如果成功,执行setExclusiveOwnerThread方法将持有锁的线程(ownerThread)设置为当前线程,否则就执行acquire方法,而公平锁线程不会尝试去获取锁,直接执行acquire方法。

2.acquire方法

acquire方法获取独占模式,忽略中断,通过调用至少一次tryAcquire方法,成功则返回,否则,线程可能排队,重复阻塞和接触阻塞,调用tryAcquire直到成功。

acquire方法的执行逻辑为:首先调用tryAcquire尝试获取锁,如果获取不到,则调用addWaiter方法将当前线程加入包装为Node对象加入队列队尾,之后调用acquireQueued方法不断的自旋获取锁。

其中tryAcquire方法、addWaiter方法、acquireQueued方法我们接下来逐个分析。

3.tryAcquire

非公平锁的tryAcquire 直接调用Sync的nonfairTryAcquire方法

nofairTryAcquire方法的逻辑:

  •   获取当前的锁标记位state,如果state为0表名此时没有线程占用锁,直接进入if中获取锁的逻辑,与非公平锁lock方法的前半部分一样,将state标记CAS改变为1,设置获取独占锁的线程为当前线程。
  •  获取当前的锁标记位state,如果state为0表名此时没有线程占用锁,直接进入if中获取锁的逻辑,与非公平锁lock方法的前半部分一样,将state标记CAS改变为1,设置获取独占锁的线程为当前线程。
  •  如果state锁标记位既不为0,也不是当前线程,表示其他线程来争夺锁,结果当然是失败。

我们回到第二步,当tryAcquire方法返回true,说明没有线程占用锁,当前线程获取锁成功,后续的addWaiter方法与acquireQueued方法不再执行并返回,线程执行同步块中的方法。若tryAcquire方法返回false,说明当前有其他线程占用锁,此时将会触发执行addWaiter方法与acquireQueued方法

公平锁中的tryAcquire方法与非公平锁基本相同,只不过比非公平锁在第一次获取锁时的判断中多了hasQueuedPredecessors方法

hasQueuedPredecessors用于判断

前线程是否为head节点的后续节点线程(预备获取锁的线程节点)。

或者说:判断“当前线程”是不是CLH队列中的第一个线程(head节点的后置节点),若是的话返回false,不是返回true。

4.addWaiter方法

addWaiter方法的主要目的是将当前线程包装为一个独占模式的Node隐式队列,在分析方法前我们需要了解Node类的几个重要的参数:

prev:前置节点

next:后置节点

wiatStatus:是等待链表(队列)中的状态,状态分一下几种

在AQS中,记录了head节点和tail节点

 

首先将当前线程保障成一个独占模式的Node节点对象,然后判断当前队尾(tail节点)是否有节点,如果有,则通过CAS将队尾节点设置为当前节点,并将当前节点的前置节点设置为上一个尾节点。

如果tail尾节点为null,说明当前节点为第一个入队节点,或者CAS设置当前节点为尾节点失败,将调用enq方法。

 

enq方法也是一个CAS方法,当第一次循环tail尾节点为null时,说明当前节点为第一个入队节点,此时将新建一个空Node节点为傀儡节点,并将其设置为队首,然后再次循环时,将当前节点设置为tail尾节点,失败将循环设定直至成功。若是addWaiter方法中设置tail尾节点失败的话,进入enq方法后直接将进入else模块将当前节点设置为tail尾节点,循环设定直至成功。

接了方便理解addWaiter方法的作用,以及后续acquireQueued的理解,我们通过3个线程来画图演示从第1步到第4步AQS中Node队列的情况:

     1.假设当前有一个线程 thread-1执行lock方法,由于此时没有其他线程占用锁,thread-1得到了锁

     2.此时thread-2执行lock方法,由于此时thread-1占用锁,因此thread-2执行acquire方法,并且thread-1不释放锁,tryAcquire方法失败,执行addWaiter方法

由于thread-2第一个进入队列,此时AQS中head以及tail为null,因此进入执行enq方法,根据上面描述的enq方法逻辑,执行之后等待队列为

3.接下来thread-3执行lock方法,thread-1依然没有释放锁,此时对接就变成这样

addWaiter方法的的作用就是将一个个没有获取锁的线程,包装成为一个等待队列。

5.acquireQueued方法

acquireQueued方法的作用就是CAS循环获取锁的方法,并且如果当前节点为head节点的后续节点,则尝试获取锁,如果获取成功则将当前节点置为head节点,并返回,如果获取失败或者当前节点并不是head节点的后续节点,则调用shouldParkAfterFailedAcquire方法以及parkAndCheckInterrupt方法,将当前节点的前置节点状态位置为SIGNAL(-1) ,并阻塞当前节点。

6.shouldParkAfterFailedAcquire方法

 /
     * @param pred 前继节点
     * @param node 当前节点
      /
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)

        /*
         * 
         * 前继节点还在等待触发,还没当前节点的什么事儿,所以当前节点可以被park
         */
        return true;
    if (ws > 0) {
        /*
         * 前继节点是CANCELLED ,则需要充同步队列中删除,并检测新接上的前继节点的状态,若还是为CANCELLED ,还需要重复上述步骤
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 到这一步,waitstatus只有可能有2种状态,一个是0,一个是PROPAGATE,无论是哪个都需要把当前节点的状态设置为SIGNAL
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

 

shouldParkAfterFailedAcquire方法根据源码分许我们可以得知,该方法就是用来设置前置节点的状态位为SIGNAL(-1),而SIGNAL(-1)表示节点的后置节点处于阻塞状态。首次进入该方法时,前置节点的waitStatus为0,因此进入else代码块中,通过CAS将waitStatus设置为-1,当外围方法acquireQueued再次循环时,将直接返回true。这时将满足判断条件执行parkAndCheckInterrupt方法。

而中间这块判断逻辑则是前置节点状态为CANCELLED(1),则继续查找前置节点的前驱节点,因为当head节点唤醒时,会跳过CANCELLED(1)节点(CANCELLED(1):因为超时或中断或异常,该线程已经被取消)。

摘取网上大神的shouldParkAfterFailedAcquire方法的逻辑总结:

  1. 如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞

  2. 如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞(parkAndCheckInterrupt)

  3. 如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与第2点相同

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

7.parkAndCheckInterrupt方法

很简单,就是讲将前线程中断,返回中断状态。

那么此时我们来通过画图,总结下步骤5~步骤7:

在thread-2与thread-3执行完步骤4后

此时thread-2执行步骤5,由于他的前置节点为Head节点因此它有了一次tryAcquire获取锁的机会,如果成功则设置thread-2的Node节点为head节点然后返回,由于当前节点没有被中断,因此返回的中断标记位为false。

如果tryAcquire获取锁依然失败,则调用shouldParkAfterFailedAcquire方法以及parkAndCheckInterrupt方法对线程进行阻塞,直到持有锁的线程释放锁时被唤醒(具体后续说明,现在只需要知道前置节点获取释放锁后,会唤醒他的后置节点的线程),此时执行了shouldParkAfterFailedAcquire方法后将会变成这样

当锁被释放thread-2被唤醒后再次执行tryAcquire获取锁,此时由于锁已释放获取将会成功,但由于当前节点被中断过interrupted为true,因此返回的中断标记位为true。

回到上面步骤2中,此时将会执行selfInterrupt方法将当前线程从阻塞状态唤醒。

而thread-3则和thread-2经历差不多,区别在于thread-3的前置节点不是head节点,因此进入acquireQueued方法后thread-3直接被阻塞,直到thread-2获取锁后变为head节点并且释放锁之后,thread-3才会被唤醒。thread-3进入acquireQueued方法后变为

(为了避免大家理解不了,此处再次说明,前置节点的waitStatus为-1时表示当前节点处于阻塞态)

下面来说说步骤5中acquireQueued方法的finally代码块

cancelAcquire方法:如果出现异常或者出现中断,就会执行finally的取消线程的请求操作,核心代码是node.waitStatus = Node.CANCELLED;将线程的状态改为CANCELLED。

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // 获取前置节点并判断状态,如果前置节点已被取消,则将其丢弃重新指向前置节点,直到指向一个距离当前节点最近的有效节点,这种处理非常巧妙让人佩服
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    //获取新的前置节点的后置节点(此时新的前置节点的next还没有指向当前节点)
    Node predNext = pred.next;

    //将当前节点设置为取消状态
    node.waitStatus = Node.CANCELLED;

    // 如果当前节点为尾部节点,直接丢弃
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        //如果前置节点不是head,则后置节点需要一个状态,来对标记当前节点的状态,此处是设置新的前置节点的waitStatus为SIGNAL(-1),并且将新的前置节点的next指向当前节点,当前节点不会再此处被丢弃,而是在shouldParkAfterFailedAcquire方法中丢弃
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            //如果前置节点为head,则直接唤醒距离当前节点最近的有效后置节点
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

unparkSuccessor方法源码如下:

首先,将当前节点waitStatus设置为0,然后获取距离当前节点最近的有效后置节点,最后unpark唤醒后置节点的线程,此时后置节点线程就有机会获取锁了

至此,所有的lock逻辑全部走完了,下面来说说解锁。

ReentrantLock的unlock方法

其实是调用的AQS的release方法

release方法的逻辑为,首先调用tryRelease方法,如果返回true,就执行unparkSuccessor方法唤醒后置节点线程。接下来我们看看tryRelease方法,在ReentrantLock中实现。

我们看到在tryRelease方法中首先会获取state锁标记,将其进行-1操作,并且返回结果根据state锁标记位是否为0,如果为0则返回true,否则返回false。

我们知道ReentrantLock是一个可重入锁,前面分析了同一个线程,每次获取锁,重入锁,都会为state锁标记+1,state记录了线程获取了多少次锁。那么同一个线程获取了多少次锁,就要进行多少次解锁,直到全部解锁,state锁标记为0时,表示解锁成功,tryRelease方法返回true,后续唤醒后置节点线程。

 

网上找俩张图,帮助加深理解

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值