一篇搞懂之并发编程中的锁

之前的文章中也分类很多篇幅去介绍并发编程中的知识点,但相对分散,不够系统。

本文将是一个串联 & 汇总, 还原java中并发编程的全貌。

一、锁的分类

1.1 可重入锁、不可重入锁

synchronizedReentrantLockReentrantReadWriteLock都是可重入锁。

重入:当前线程获取到A锁,在获取之后尝试再次获取A锁是可以直接拿到的。可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。

不可重入:当前线程获取到A锁,在获取之后尝试再次获取A锁,无法获取到的,因为A锁被当前线 程占用着,需要等待自己释放锁再获取锁。不可重入锁也叫自旋锁。要实现不可重入锁,需要自定义一个锁类,并在其中禁止同一线程多次获取锁。

synchronized为例说明下可重入锁是啥意思:

public class ReentrantExample {
    public synchronized void method1() {
        System.out.println("method1");
        method2(); // 调用method2时,线程已经持有对象锁
    }

    public synchronized void method2() {
        System.out.println("method2");
    }

    public static void main(String[] args) {
        ReentrantExample example = new ReentrantExample();
        example.method1(); // 调用method1
    }
}

在上面的代码中,method1和method2都是同步方法,当一个线程调用method1时,它会获取ReentrantExample对象的锁。在method1中,线程再次调用了method2,由于method2也是同步方法,线程需要再次获取对象的锁。因为sychronized是可重入的,线程可以再次获取锁并进入method2,不用重新获取锁,从而不会导致阻塞或死锁。

可重入原理:

synchronized的可重入性是由监视器Monitor锁的计数器来实现的(Monitor有个count属性就是计数用的)。当一个线程获取一个对象的锁时,锁的计数器会加1;当线程退出同步块或方法时,计数器会减1。如果计数器为0,则表示锁已被释放。同一个线程, 锁重入, 会对state进行自增. 释放锁的时候, state进行自减; 当state自减为0的时候. 此时线程才会将锁释放成功, 才会进一步去唤醒其他线程来竞争锁。

计数器的工作原理

  • 第一次获取锁:当线程第一次进入synchronized块或方法时,监视器锁的计数器从0增加到1,并记录当前持有锁的线程ID。
  • 再次获取锁:如果该线程在持有锁的情况下再次进入另一个synchronized块或方法,计数器会再次增加,表明线程多次获取了同一个锁。
  • 锁的释放:每当线程退出synchronized块或方法时,计数器会减少。当计数器减少到0时,锁被真正释放,其他线程可以获取该锁。

1.2 乐观锁、悲观锁

synchronizedReentrantLockReentrantReadWriteLock都是悲观锁。

CAS操作,就是乐观锁的一种实现。

悲观锁:获取不到锁资源时,会将当前线程挂起(进入BLOCKED、WAITING),线程挂起会涉及到用户态和内核态的切换,而这种切换是比较消耗资源的。

  • 用户态:JVM可以自行执行的指令,不需要借助操作系统执行。
  • 内核态:JVM不可以自行执行,需要操作系统才可以执行。

乐观锁:获取不到锁资源,可以再次让CPU调度,重新尝试获取锁资源。

Atomic原子性类中,就是基于CAS乐观锁实现的。

1.3 公平锁、非公平锁

synchronized只能是非公平锁。

ReentrantLockReentrantReadWriteLock可以实现公平锁和非公平锁

公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,锁被A持有,同时线程B在排队。直接排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。

非公平锁:线程A获取到了锁资源,线程B没有拿到,线程B去排队,线程C来了,先尝试竞争一波拿到锁资源:开心,插队成功。没有拿到锁资源:依然要排到B的后面,等待B拿到锁资源或者是B取消后,才可以尝试去竞争锁资源。

1.4 互斥锁、共享锁

synchronizedReentrantLock是互斥锁。

ReentrantReadWriteLock,有互斥锁也有共享锁。

互斥锁:同一时间点,只会有一个线程持有者当前互斥锁。
共享锁:同一时间点,当前共享锁可以被多个线程同时持有。

可以看到锁的分类有多个维度的多种类型,但是主要的锁就那么几个,下面我们将分别介绍。 

二、synchronized的优化 

synchronized的锁是基于对象实现的。

在JDK1.5的时候,Doug Lee推出了ReentrantLock,lock的性能远高于synchronized,所以JDK团队就在JDK1.6中,对synchronized做了大量的优化。Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了。

2.1 锁消除

synchronized修饰的代码中,如果不存在操作临界资源的情况,会触发锁消除,即便写了synchronized,他也不会触发。 之前说过的逃逸流程,若某个变量没有逃逸,那么就是线程安全的,就会触发同步省略。

2.2 锁膨胀

如果在一个循环中,频繁的获取和释放做资源,这样带来的消耗很大,锁膨胀就是将锁的范围扩大,避免频繁的竞争和获取锁资源带来不必要的消耗。 

public void method(){  
    for(int i = 0;i < 999999;i++){  
        synchronized(对象){
        
        }
    }  

    // 这是上面的代码会触发锁膨胀  
    synchronized(对象){  
        for(int i = 0;i < 999999;i++){  
        }  
    }  
}

2.3 锁升级

ReentrantLock的实现,是先基于乐观锁的CAS尝试获取锁资源,如果拿不到锁资源,才会挂起线程。synchronized在JDK1.6之前,完全就是获取不到锁,立即挂起当前线程,所以synchronized性能比较差。 synchronized就在JDK1.6做了锁升级的优化:

  • 无锁、匿名偏向:当前对象没有作为锁存在
  • 偏向锁:如果当前锁资源,只有一个线程在频繁的获取和释放,那么这个线程过来,只需要判断,当前指向的线程是否是当前线程
    • 如果是,直接拿着锁资源走。
    • 如果当前线程不是我,基于CAS的方式,尝试将偏向锁指向当前线程。如果获取不到,触发锁升级,升级为轻量级锁。(偏向锁状态出现了锁竞争的情况)
  • 轻量级锁:会采用自旋锁的方式去频繁的以CAS的形式获取锁资源
    • 如果成功获取到,拿着锁资源走
    • 如果自旋了一定次数,没拿到锁资源,锁升级 
  • 重量级锁:就是最传统的synchronized方式,拿不到锁资源,就挂起当前线程

整个锁升级状态的转变:

综上所述,synchronized无论处理哪种锁,都是先尝试获取,获取不到才升级|| 放到队列上的,所以是非公平的。

 2.4 synchronized实现原理

展开MarkWord

MarkWord中标记着四种锁的信息:无锁、偏向锁、轻量级锁、重量级锁。

Monitor对象 :

ObjectMonitor() {  
    _header = NULL; // header存储着MarkWord  
    _count = 0; // 竞争锁的线程个数  
    _waiters = 0, // wait的线程个数  
    _recursions = 0; // 标识当前synchronized锁重入的次数  
    _object = NULL;  
    _owner = NULL; // 持有锁的线程  
    _WaitSet = NULL; // 保存wait的线程信息,双向链表  
    _WaitSetLock = 0 ;  
    _Responsible = NULL ;  
    _succ = NULL ;  
    _cxq = NULL ; // 获取锁资源失败后,线程要放到当前的单向链表中  
    FreeNext = NULL ;  
    _EntryList = NULL ; // _cxq以及被唤醒的WaitSet中的线程,在一定机制下,会放到EntryList中  
    _SpinFreq = 0 ;  
    _SpinClock = 0 ;  
    OwnerIsThread = 0 ;  
    _previous_owner_tid = 0;  
}  

 Monitor对象有三个集合属性:waitSet、EntryList、Owner

对象锁的基本工作机制:

  1. 当多个线程同时访问一段同步代码时,首先会进入 _EntryList队列中阻塞。

  2. 当某个线程获取到对象的对象锁后进入临界区域,并把对象锁中的 _owner变量设置为当前线程,即获得对象锁。

  3. 若持有对象锁的线程调用 wait() 方法,将释放当前持有的对象锁,_owner变量恢复为null,同时该线程进入 _WaitSet 集合中等待被唤醒。

  4. 在_WaitSet集合中的线程被唤醒,会被再次放到_EntryList队列中,重新竞争获取锁。

  5. 若当前线程执行完毕也将释放对象锁并复位变量的值,以便其他线程进入获取锁。

 三、ReentrantLock

3.1 ReentrantLock和synchronized的区别 

核心区别:

  • ReentrantLock是个类(由AQS框架实现,JUC包,可以说是基于JDK实现的)
  • synchronized是关键字(可以说是基于JVM实现的)

效率区别:

  • 如果竞争比较激烈,推荐ReentrantLock去实现,不存在锁升级概念。而synchronized是存在锁升级概念的,如果升级到重量级锁,是不存在锁降级的。

底层实现区别:

  • 实现原理是不一样,ReentrantLock基于AQS实现的,synchronized是基于ObjectMonitor

功能向的区别:

  • ReentrantLock的功能比synchronized更全面。
    • ReentrantLock支持公平锁和非公平锁
    • ReentrantLock可以指定等待锁资源的时间
    • ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程

    • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

选择哪个:如果并发编程特别熟练,推荐使用ReentrantLock,功能更丰富。如果掌握的一般般,使用synchronized会更好。很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized。

 四、AQS

AQS就是AbstractQueuedSynchronizer抽象类,AQS其实就是JUC包下的一个基类,JUC下的很多内容都是基于AQS实现了部分功能,比如ReentrantLockThreadPoolExecutor,阻塞队列,CountDownLatch,Semaphore,CyclicBarrier等等都是基于AQS实现。

AQS核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞+等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。(CLH队列的全称是Craig, Landin, and Hagersten lock queue‌。它是一种基于链表结构的自旋锁等待队列,用于存储被阻塞的线程信息)

  1. state:代表被抢占的锁的状态(valotile修饰,int类型)
  2. 队列:没有抢到锁的线程会包装成一个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;  
}  

再具体一点:

4.1 独占和共享两种模式加锁流程

独占(锁只会被一个线程独占)和共享(多个线程可同时执行)。

 独占锁加锁流程图:

共享锁的加锁 — acquireShared

   

4.2 加锁流程源码剖析 

4.2.1 三种加锁源码分析(公平与非公平)

lock方法

执行lock方法后,公平锁和非公平锁的执行套路不一样

// 非公平锁
final void lock() {  
    // 上来就先基于CAS的方式,尝试将state从0改为1  
    if (compareAndSetState(0, 1))  
        // 获取锁资源成功,会将当前线程设置到exclusiveOwnerThread属性,代表是当前线程持有着锁资源  
        setExclusiveOwnerThread(Thread.currentThread());  
    else  
        // 执行acquire,尝试获取锁资源  
        acquire(1);  
}  

// 公平锁  
final void lock() {  
    // 执行acquire,尝试获取锁资源  
    acquire(1);  
}

acquire方法,是公平锁和非公平锁的逻辑一样

public final void acquire(int arg) {  
    // tryAcquire:再次查看,当前线程是否可以尝试获取锁资源  
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
        // 没有拿到锁资源  
        // addWaiter(Node.EXCLUSIVE):将当前线程封装为Node节点,插入到AQS的双向链表的结尾  
        // acquireQueued:查看我是否是第一个排队的节点,如果是可以再次尝试获取锁资源,如果长时间拿不到,挂起线  
        // 如果不是第一个排队的额节点,就尝试挂起线程即可  
        
        // 中断线程的操作  
        selfInterrupt();  
}

tryAcquire方法竞争锁资源的逻辑,分为公平锁和非公平锁

// 非公平锁实现  
final boolean nonfairTryAcquire(int acquires) {  
    // 获取当前线程  
    final Thread current = Thread.currentThread();  
    // 获取了state熟属性  
    int c = getState();  
    // 判断state当前是否为0,之前持有锁的线程释放了锁资源  
    if (c == 0) {  
        // 再次抢一波锁资源  
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);  
            // 拿锁成功返回true  
            return true;  
        }  
    }  
    // 不是0,有线程持有着锁资源,如果是,证明是锁重入操作  
    else if (current == getExclusiveOwnerThread()) {  
        // 将state + 1  
        int nextc = c + acquires;  
        if (nextc < 0) // 说明对重入次数+1后,超过了int正数的取值范围  
            // 01111111 11111111 11111111 11111111  
            // 10000000 00000000 00000000 00000000  
            // 说明重入的次数超过界限了。  
            throw new Error("Maximum lock count exceeded");  
            
        // 正常的将计算结果,复制给state  
        setState(nextc);  
        // 锁重入成功  
        return true;  
    }  
    // 返回false  
    return false;  
}  

// 公平锁实现  
protected final boolean tryAcquire(int acquires) {  
    // 获取当前线程  
    final Thread current = Thread.currentThread();  
    // ....  
    int c = getState();  
    if (c == 0) {  
        // 查看AQS中是否有排队的Node  
        // 没人排队抢一手 。有人排队,如果我是第一个,也抢一手  
        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;  
}  

// 查看是否有线程在AQS的双向队列中排队  
// 返回false,代表没人排队  
public final boolean hasQueuedPredecessors() {  
    // 头尾节点  
    Node t = tail;  
    Node h = head;  
    // s为头结点的next节点  
    Node s; 
    
    // 如果头尾节点相等,证明没有线程排队,直接去抢占锁资源  
    // s节点不为null,并且s节点的线程为当前线程(排在第一名的是不是我)  
    return h != t &&  (s == null || s.thread != Thread.currentThread());  
}

addWaite方法,将没有拿到锁资源的线程扔到AQS队列中去排队

// 没有拿到锁资源,过来排队, mode:代表互斥锁  
private Node addWaiter(Node mode) {  
    // 将当前线程封装为Node,  
    Node node = new Node(Thread.currentThread(), mode);  
    // 拿到尾结点  
    Node pred = tail;  
    // 如果尾结点不为null  
    if (pred != null) {  
        // 当前节点的prev指向尾结点  
        node.prev = pred;  
        // 以CAS的方式,将当前线程设置为tail节点  
        if (compareAndSetTail(pred, node)) {  
            // 将之前的尾结点的next指向当前节点  
            pred.next = node;  
            return node;  
        }  
    }  
    // 如果CAS失败,以死循环的方式,保证当前线程的Node一定可以放到AQS队列的末尾  
    enq(node);  
    return node;  
}  

private Node enq(final Node node) {  
    for (;;) {  
        // 拿到尾结点  
        Node t = tail;  
        // 如果尾结点为空,AQS中一个节点都没有,构建一个伪节点,作为head和tail
        if (t == null) {  
            if (compareAndSetHead(new Node()))  
            tail = head;  
        } else {  
            // 比较熟悉了,以CAS的方式,在AQS中有节点后,插入到AQS队列的末尾  
            node.prev = t;  
            if (compareAndSetTail(t, node)) {  
                t.next = node;  
                return t;  
            }  
        }  
    }  
}

acquireQueued方法,判断当前线程是否还能再次尝试获取锁资源,如果不能再次获取锁资源,或者又没获取到,尝试将当前线程挂起 

// 当前没有拿到锁资源后,并且到AQS排队了之后触发的方法。 中断操作这里不用考虑  
final boolean acquireQueued(final Node node, int arg) {  
    // 不考虑中断  
    // failed:获取锁资源是否失败(这里简单掌握落地,真正触发的,还是tryLock和lockInterruptibly)  
    boolean failed = true;  
    try {  
        boolean interrupted = false;  
        // 死循环............  
        for (;;) {  
            // 拿到当前节点的前继节点  
            final Node p = node.predecessor();  
            // 前继节点是否是head,如果是head,再次执行tryAcquire尝试获取锁资源。  
            if (p == head && tryAcquire(arg)) {  
                // 获取锁资源成功  
                // 设置头结点为当前获取锁资源成功Node,并且取消thread信息  
                setHead(node);  
                // help GC  
                p.next = null;  
                // 获取锁失败标识为false  
                failed = false;  
                return interrupted;  
            }  
            // 没拿到锁资源......  
            // shouldParkAfterFailedAcquire:基于上一个节点转改来判断当前节点是否能够挂起线程,如果可以返回true,  
            // 如果不能,就返回false,继续下次循环  
            // 这里基于Unsafe类的park方法,将当前线程挂起  
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())  
                // 这里基于Unsafe类的park方法,将当前线程挂起                  
                interrupted = true;  
        }
   } finally {  
        if (failed)  
            // 在lock方法中,基本不会执行。  
            cancelAcquire(node);  
   }  
}  
    
// 获取锁资源成功后,先执行setHead  
private void setHead(Node node) {  
    // 当前节点作为头结点 伪  
    head = node;  
    // 头结点不需要线程信息  
    node.thread = null;  
    node.prev = null;  
}  

// 当前Node没有拿到锁资源,或者没有资格竞争锁资源,看一下能否挂起当前线程  
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  
    // -1,SIGNAL状态:代表当前节点的后继节点,可以挂起线程,后续我会唤醒我的后继节点  
    // 1,CANCELLED状态:代表当前节点以及取消了  
    int ws = pred.waitStatus;  
    if (ws == Node.SIGNAL)  
        // 上一个节点为-1之后,当前节点才可以安心的挂起线程  
        return true;  
    if (ws > 0) {  
        // 如果当前节点的上一个节点是取消状态,我需要往前找到一个状态不为1的Node,作为他的next节点  
        // 找到状态不为1的节点后,设置一下next和prev  
        do {  
           node.prev = pred = pred.prev;  
        } while (pred.waitStatus > 0);  
                
        pred.next = node;  
    } else {  
        // 上一个节点的状态不是1或者-1,那就代表节点状态正常,将上一个节点的状态改为-1 
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
    }  
    return false;  
}  
tryLock方法
// tryLock方法,无论公平锁还有非公平锁。都会走非公平锁抢占锁资源的操作
// 就是拿到state的值, 如果是0,直接CAS浅尝一下
// state 不是0,那就看下是不是锁重入操作
// 如果没抢到,或者不是锁重入操作,告辞,返回false
public boolean tryLock() {
    // 非公平锁的竞争锁操作
    return sync.nonfairTryAcquire(1);
}

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {    
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        
        setState(nextc);
        return true;
    }
    return false;
}
tryLock(time, unit)
// tryLock(time,unit)执行的方法  
   public final boolean tryAcquireNanos(int arg, long nanosTimeout)throws InterruptedException {  
   // 线程的中断标记位,是不是从false,别改为了true,如果是,直接抛异常  
   if (Thread.interrupted())  
       throw new InterruptedException();  

   // tryAcquire分为公平和非公平锁两种执行方式,如果拿锁成功, 直接告辞,  
   return tryAcquire(arg) ||  
   // 如果拿锁失败,在这要等待指定时间  
   doAcquireNanos(arg, nanosTimeout);  
}  

private boolean doAcquireNanos(int arg, long nanosTimeout)  
throws InterruptedException {  
   // 如果等待时间是0秒,直接告辞,拿锁失败  
   if (nanosTimeout <= 0L)  
       return false;  

   // 设置结束时间。  
   final long deadline = System.nanoTime() + nanosTimeout;  
   // 先扔到AQS队列  
   final Node node = addWaiter(Node.EXCLUSIVE);  
   // 拿锁失败,默认true  
   boolean failed = true;

   try {  
       for (;;) {  
           // 如果在AQS中,当前node是head的next,直接抢锁  
           final Node p = node.predecessor();  
           if (p == head && tryAcquire(arg)) {  
           setHead(node);  
           p.next = null; // help GC  
           failed = false;  
           return true;  
       }  

   // 结算剩余的可用时间  
   nanosTimeout = deadline - System.nanoTime();  
   // 判断是否是否用尽的位置  
   if (nanosTimeout <= 0L)  
       return false;  
   
   // shouldParkAfterFailedAcquire:根据上一个节点来确定现在是否可以挂起线程
   // 避免剩余时间太少,如果剩余时间少就不用挂起线程 
   if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)  
       // 如果剩余时间足够,将线程挂起剩余时间  
       LockSupport.parkNanos(this, nanosTimeout);  
       // 如果线程醒了,查看是中断唤醒的,还是时间到了唤醒的。  
       if (Thread.interrupted())  
           // 是中断唤醒的!  
           throw new InterruptedException();  
       }  
   } finally {  
       if (failed)  
           cancelAcquire(node);  
   }  
}

unlock()

public void unlock() {
  // 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
  sync.release(1);
}
// 释放锁的核心流程
public final boolean release(int arg) {
  // 核心释放锁资源的操作之一
  if (tryRelease(arg)) {
   // 如果锁已经释放掉了,走这个逻辑
  Node h = head;
  // h不为null,说明有排队的(录课时估计脑袋蒙圈圈。)
   // 如果h的状态不为0(为-1),说明后面有排队的Node,并且线程已经挂起了。
   if (h != null && h.waitStatus != 0)
    // 唤醒排队的线程
    unparkSuccessor(h);
   return true;
  }
  return false;
}

// ReentrantLock释放锁资源操作
protected final boolean tryRelease(int releases) {
  // 拿到state - 1(并没有赋值给state)
  int c = getState() - releases;
  // 判断当前持有锁的线程是否是当前线程,如果不是,直接抛出异常
 if (Thread.currentThread() != getExclusiveOwnerThread())
   throw new IllegalMonitorStateException();
 
  // free,代表当前锁资源是否释放干净了。
  boolean free = false;
  if (c == 0) {
   // 如果state - 1后的值为0,代表释放干净了。
   free = true;
   // 将持有锁的线程置位null
   setExclusiveOwnerThread(null);
  }
 // 将c设置给state
  setState(c);
  // 锁资源释放干净返回true,否则返回false
  return free;
}

// 唤醒后面排队的Node
private void unparkSuccessor(Node node) {
  // 拿到头节点状态
  int ws = node.waitStatus;
  if (ws < 0)
  // 先基于CAS,将节点状态从-1,改为0
  compareAndSetWaitStatus(node, ws, 0);
  // 拿到头节点的后续节点。
  Node s = node.next;
  // 如果后续节点为null或者,后续节点的状态为1,代表节点取消了。
  if (s == null || s.waitStatus > 0) {
   s = null;
   // 如果后续节点为null,或者后续节点状态为取消状态,从后往前找到一个有效节点环境
   for (Node t = tail; t != null && t != node; t = t.prev)
   // 从后往前找到状态小于等于0的节点
   // 找到离head最新的有效节点,并赋值给s
   if (t.waitStatus <= 0)
    s = t;
  }
  // 只要找到了这个需要被唤醒的节点,执行unpark唤醒
  if (s != null)
   LockSupport.unpark(s.thread);
}

4.3 Condition

Condition是用来替代传统的0bject的wait()、notify()实现现线程间的协作,使用Condition中的await()、signal()实现线程间协作更加安全和高效。

比如,这里线程一先获取锁,然后使用 await()方法挂起当前线程并释放锁,线程二获取锁后使用signal 唤醒线程一:

AbstractQueueSynchronizer 中实现了 Condition 中的方法,主要对外提供awaite(object.wait())和 signal(object.notify())调用。

五、CountDownLatch

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。

它使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成了任务,在CountDownLatch上 await() 的线程就会被唤醒,就可以恢复执行任务。

所以,CountDownLatch不是完美的,它是一次性的,计数器的值只能在初始化时构造一次,之后没有任何机制能够对它重设值。也就是说,当CountDownLatch使用完毕之后不能再次使用。

典型用法1

一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。

某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为 n

CountDownLatch countDownLatch = new CountDownLatch(n) 

每当一个任务线程执行完毕,就将计数器减1 

countdownlatch.countDown()

当计数器的值变为0时,在CountDownLatch上 await() 的线程就会被唤醒。

public class CountdownLatchTest1 {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        final CountDownLatch latch = new CountDownLatch(3);
        for (int i = 0; i < 3; i++) {
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("子线程" + Thread.currentThread().getName() + "开始执行");
                        Thread.sleep((long) (Math.random() * 1000));
                        System.out.println("子线程" + Thread.currentThread().getName() + "执行完成");
                        latch.countDown();//当前线程调用此方法,则计数减一
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });


        }
        try {
            System.out.println("主线程" + Thread.currentThread().getName() + "等待子线程执行完成...");
            latch.await();//阻塞当前线程,直到计数器的值为0
            System.out.println("主线程" + Thread.currentThread().getName() + "开始执行...");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

典型用法2

实现多个线程开始执行任务的最大并行性。

类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计数器初始化为1,多个线程在开始执行任务前首先 coundownlatch.await(),当主线程调用 countDown() 时,计数器变为0,多个线程同时被唤醒。await() 方法使当前线程在 CountDownLatch 计数减至零之前一直等待,除非线程被中断。

举例一: 

    private final static int threadCount = 200;
    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);

        for (int i = 0; i < threadCount; i++) {
            final int threadNum = i;
            exec.execute(() -> {
                try {
                    test(threadNum);
                } catch (Exception e) {
                    LOGGER.warning("execption:" + e);
                } finally {
                    countDownLatch.countDown();
                }
            });
        }
        countDownLatch.await();
        // countDownLatch.await(10, TimeUnit.MILLISECONDS);    // 指定时间内完成
        LOGGER.info("finish !!!");
        exec.shutdown();
    }
......
七月 12, 2018 5:17:51 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 7
七月 12, 2018 5:17:51 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 6
七月 12, 2018 5:17:51 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 4
七月 12, 2018 5:17:51 下午 com.snowalker.test.aqs.CountDownLatchDemo main
信息: finish !!!

指定了计数器为200,让主线程await,等待200个线程执行完毕。每个子线程在执行完毕都对计数器进行减一操作。当200个子线程都执行完毕,主线程才执行。

如果我们不想一直等待,可以指定线程等待时间,通过

countDownLatch.await(10, TimeUnit.MILLISECONDS);    // 指定时间内完成
七月 12, 2018 5:18:36 下午 com.snowalker.test.aqs.CountDownLatchDemo main
信息: finish !!!
七月 12, 2018 5:18:36 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 5
七月 12, 2018 5:18:36 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 4
七月 12, 2018 5:18:36 下午 com.snowalker.test.aqs.CountDownLatchDemo test
信息: threadNum = 7

我指定为10毫秒,可以看到我们的主线程没有等待所有线程执行完毕才被唤醒,而是等待10毫秒后恢复执行,输出finish!!!

举例二:

public class CountdownLatchTest2 {
    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch cdOrder = new CountDownLatch(1);
        final CountDownLatch cdAnswer = new CountDownLatch(4);
        for (int i = 0; i < 4; i++) {
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        System.out.println("选手" + Thread.currentThread().getName() + "正在等待裁判发布口令");
                        cdOrder.await();
                        System.out.println("选手" + Thread.currentThread().getName() + "已接受裁判口令");
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("选手" + Thread.currentThread().getName() + "到达终点");
                        cdAnswer.countDown();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }
        try {
            Thread.sleep((long) (Math.random() * 10000));
            System.out.println("裁判"+Thread.currentThread().getName()+"即将发布口令");
            cdOrder.countDown();
            System.out.println("裁判"+Thread.currentThread().getName()+"已发送口令,正在等待所有选手到达终点");
            cdAnswer.await();
            System.out.println("所有选手都到达终点");
            System.out.println("裁判"+Thread.currentThread().getName()+"汇总成绩排名");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}

CountDownLatch核心实现类是Sync,它是一个继承自AbstractQueuedSynchronizer的内部类。 

// Sync 类继承自 AbstractQueuedSynchronizer,提供了共享锁的功能
private static final class Sync extends AbstractQueuedSynchronizer {

    // 构造方法,设置初始计数值
    Sync(int count) {
        // 使用 AQS 的 setState 方法来设置同步器的状态,这里用 count 来初始化状态
        // 计数值即锁的可用次数
        setState(count);
    }

    // 获取当前的计数值(即剩余的共享资源数量)
    int getCount() {
        // 调用 AQS 的 getState 获取当前状态,即共享资源的剩余数量
        return getState();
    }

    // 试图以共享模式获取锁(acquireShared),即检查是否可以获取共享资源
    protected int tryAcquireShared(int acquires) {
        // 状态为0时,返回为1,表示资源获取成功。返回为-1时,表示资源不可用,线程需要挂起等待。
        return (getState() == 0) ? 1 : -1;
    }

    // 试图以共享模式释放锁(releaseShared),即释放一个共享资源
    protected boolean tryReleaseShared(int releases) {
        for (;;) { // 循环尝试,直到成功
            // 获取当前的状态值(即共享资源数量)
            int c = getState();

            //如果当前状态(即共享资源的数量)为 0,表示已经没有共享资源可以释放,因此返回 false,表示释放操作失败。
            if (c == 0)
                return false;  // 没有资源,返回 false

            // 计数器的值减1
            int nextc = c - 1;

            // 尝试更新状态(使用 CAS 操作)
            if (compareAndSetState(c, nextc)) {
                // 如果计数器的值减为0,说明所有线程已经执行完毕,返回 true,否则返回false。
                return nextc == 0;
            }
        }
    }
}

 六、CyclicBarrier

CyclicBarrier可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。

举个例子,比如小明,小美,小华,小丽几人相约一起聚餐,然而他们每个人到达约会地点的耗时都不一样,有的人会早到,有的人会晚到,但是他们要都到了以后才可以决定点那些菜。这里每个人相当于一个线程,而餐厅就是 CyclicBarrier。

CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

使用举例:

public class CyclicBarrierDemo1 {
    public static CyclicBarrier cyclicBarrier = new CyclicBarrier(10);

    public static class T extends Thread {
        int sleep;

        public T(String name, int sleep) {
            super(name);
            this.sleep = sleep;
        }

        @Override
        public void run() {
            try {
                //模拟休眠
                TimeUnit.SECONDS.sleep(sleep);
                long starTime = System.currentTimeMillis();
                //调用await()的时候,当前线程将会被阻塞,需要等待其他员工都到达await了才能继续
                cyclicBarrier.await();
                long endTime = System.currentTimeMillis();
                System.out.println(this.getName() + ",sleep:" + this.sleep + " 等待了" + (endTime - starTime) + "(ms),开始吃饭了!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 10; i++) {
            new T("员工" + i, i).start();
        }
    }
}
员工10,sleep:10 等待了0(ms),开始吃饭了!
员工1,sleep:1 等待了8996(ms),开始吃饭了!
员工7,sleep:7 等待了2998(ms),开始吃饭了!
员工9,sleep:9 等待了1000(ms),开始吃饭了!
员工6,sleep:6 等待了3997(ms),开始吃饭了!
员工4,sleep:4 等待了6000(ms),开始吃饭了!
员工5,sleep:5 等待了4999(ms),开始吃饭了!
员工2,sleep:2 等待了7999(ms),开始吃饭了!
员工3,sleep:3 等待了7002(ms),开始吃饭了!
员工8,sleep:8 等待了1999(ms),开始吃饭了!

 

CyclicBarrier和CountDownLatch的区别

CountDownLatch 主要用来解决一个线程等待多个线程的场景,可以类比旅游团团长要等待所有游客到齐才能去下一个景点。

而 CyclicBarrier 是一组线程之间的相互等待,可以类比几个驴友之间的不离不弃,共同到达某个地方,再继续出发,这样反复。

  1. CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景;
  2. CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;
  3. CountDownLatch允许一个或多个线程等待一组事件的产生,而CyclicBarrier用于等待其他线程运行到栅栏位置。
  4. 都可以用于“主线程阻塞一直等待,直到子任务完成,主线程才继续执行”的情况。

七、Semaphore

[ˈseməfɔːr]

其实可以将Semaphore看成一个计数器,当计数器的值小于许可最大值时,所有调用acquire方法的线程都可以得到一个许可从而往下执行。而调用release方法则可以让计数器的值减一。

信号量的主要应用场景是控制最多N个线程同时地访问资源,其中计数器的最大值即是许可的最大值N。以停车场为例,假设停车场一共有8个车位,其中6个车位已被停放,然后来了两辆汽车,此时因为刚好剩下两个车位所以这两辆车都能停放。接着又来了一辆车,现在已经没有空位了所以只能等待其它车离开。此时刚好一辆红色汽车离开停车场,来开后黄车刚好可以停进去,假如又有一辆汽车进来则该车又得等待。如此往复。这个过程中停车场就是公共资源,车位数就是信号量最大许可数,车辆就好比线程。 

举例:

import java.util.Random;
import java.util.concurrent.Semaphore;

public class MyThread extends Thread {
    private final Semaphore semaphore;
    private final Random random = new Random();
    public MyThread(String name, Semaphore semaphore) {
        super(name);
        this.semaphore = semaphore;
    }
    @Override
    public void run() {
        try {
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName() + " - 抢座成功,开始写作业");
            Thread.sleep(random.nextInt(1000));
            System.out.println(Thread.currentThread().getName() + " - 作业完成,腾出座位");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        semaphore.release();
    }
}

public static void main(String[] args) throws InterruptedException {
        Semaphore semaphore = new Semaphore(2);
        for (int i = 0; i < 5; i++) {
            new MyThread("学⽣-" + (i + 1), semaphore).start();
        }
 }
学⽣-1 - 抢座成功,开始写作业
学⽣-2 - 抢座成功,开始写作业
学⽣-2 - 作业完成,腾出座位
学⽣-3 - 抢座成功,开始写作业
学⽣-1 - 作业完成,腾出座位
学⽣-4 - 抢座成功,开始写作业
学⽣-3 - 作业完成,腾出座位
学⽣-5 - 抢座成功,开始写作业
学⽣-5 - 作业完成,腾出座位
学⽣-4 - 作业完成,腾出座位

上面主方法中,调用new Semaphore(2)定义了2份共享资源,或者两份许可证,也就是同一时刻最多只允许2个线程执行。 

Semaphore 基于 AbstractQueuedSynchronizer (AQS) 实现,其实现方式和 CountDownLatch 类似,但使用了 AQS 的共享模式,并对许可计数进行精确管理。其中 AQS 的 state 表示剩余许可数。acquireShared() 方法用于申请许可,releaseShared() 方法用于释放许可。CountDownLatch的实现同样基于AQS,但其计数器只能递减到零,一旦计数器为零,所有等待的线程将被唤醒。CountDownLatch的核心方法包括await()countDown(),分别用于等待所有操作完成和减少计数器的值‌

Semaphore 的核心实现依赖于其内部类 Sync,Sync 是 AbstractQueuedSynchronizer 的子类。根据是否是公平模式,有两种实现:NonfairSync 和 FairSync

  • NonfairSync:非公平模式。线程调用 acquire 时直接尝试获取许可,不保证顺序。
  • FairSync:公平模式。线程获取许可遵循队列顺序。
abstract static class Sync extends AbstractQueuedSynchronizer {
    // 设置初始许可数
    Sync(int permits) {
        setState(permits);  
    }
 
    final int getPermits() {
        return getState();
    }
 
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {
            int available = getState();
            // 加锁state许可减一
            int remaining = available - acquires;
            if (remaining < 0 || compareAndSetState(available, remaining))
                return remaining;
        }
    }
 
    protected final boolean tryReleaseShared(int releases) {
        for (;;) {
            int current = getState();
            // 释放锁state许可加一
            int next = current + releases;
            if (compareAndSetState(current, next))
                return true;
        }
    }
}

 八、ReentrantReadWriteLock

 读写锁的内部包含两把锁:一把是读锁,是一种共享锁;另一把是写锁,是一种独占锁

  • 在没有写锁的时候,读锁可以被多个线程同时持有
  • 写锁被一个线程持有,其他的线程不能再持有写锁,抢占写锁会阻塞,抢占读锁也会阻塞 

读写互斥原则:

  1. 读读相容
  2. 读写互斥
  3. 写写互斥

JUC包中的读写锁接口为ReadWriteLock:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();//返回读锁

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();//返回写锁
}

ReentrantReadWriteLock类图

  

WriteLock、Sync、ReadLock、FairSync、NonfaruSyns都是ReadWritrLock的静态内部类。

AQS 中只维护了 一个 state 状态,而 ReentrantReadWriteLock 则 需要维护读状态和写状态:用 state 的高16 位表示读状态,也就是获取到读锁的次数;使用低 16 位表示获取到写锁的线程的可重入次数 。

 	    static final int SHARED_SHIFT   = 16;
 	    
 	    //共享锁(读锁)状态单位值 65536  1<<16  2^16
        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
        
        //共享锁线程最大个数 65535
        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
        
		//排它锁(写锁)掩码,二进制,15 个 1
        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

		//返回读锁线程数
         /** Returns the number of shared holds represented in count  */
        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

		//返回写锁可重入个数
        /** Returns the number of exclusive holds represented in count  */
        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

		//firstReader 用来记录第一个获取到读锁的线程
 		private transient Thread firstReader = null;
 		//firstReaderHoldCount 则记录第 一个获取到读锁的线程获取读锁的可重入次数 
        private transient int firstReaderHoldCount;
        
	//cachedHoldCounter 用来记录最后 一个获取读锁的线程获取读锁 的可重入次数 
		private transient HoldCounter cachedHoldCounter;

 8.1 写锁的获取和释放

ReentrantReadWriteLock 中 写锁使用 WriteLock 来实现

public static class WriteLock implements Lock, java.io.Serializable {
	 public void lock() {
            sync.acquire(1);
        }
}
//AbstractQueuedSynchronizer类
 public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
//Sync类
protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            //c!=0说明读锁或者写锁已经被某线程获取
            if (c != 0) {//代码1处
                // (Note: if c != 0 and w == 0 then shared count != 0)
                // w=O说明已经有线程获取了读锁
                // w!=O 并且当前线程不是写锁拥有者则返回false
                if (w == 0 || current != getExclusiveOwnerThread())//代码2处
                    return false;
                //说明当前线程获取了写锁,判断可重入次数
                if (w + exclusiveCount(acquires) > MAX_COUNT)//代码3处
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);//设置可重入次数  代码4处
                return true;
            }
            //第 一个写线程获取写锁  
            //代码5处
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

tryAcquire方法中:

  • 如果当前 AQS 状态值不为 0 则说明 当前己经有线程获取到了读锁或者写锁。
  • 如果 w==0 说明状态值 的低16 位为0(低16位表示写状态) ,而 AQS 状态值不为 0,则说明高16位(高16位表示读状态)不为 0 ,这暗示己经有线程获取了读锁 ,所以直接返回 false
  • 如果 w! =0 则说明 当前已经有线程获取了该写锁,再看当前线程是不是该锁的持有者 ,如果不是则返回 false
  • 若当前线程之前已经获取到了该锁,判断该线程可重入次数是否超过了最大值,是则抛异常
  • 增加当前线程的可重入次数,返回true
  • AQS 的状态值等于 0则说明目前没有线程获取到读锁和写锁,让第 一个写线程获取写锁

 释放锁

//WriteLock类
 public void unlock() {
            sync.release(1);
        }
//AQS类
 public final boolean release(int arg) {
 //调用ReentrantReadWriteLock中sync类实现的tryRelease方法
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)//激活阻塞队列里面的一个线程
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
//Sync类
protected final boolean tryRelease(int releases) {
            if (!isHeldExclusively())//看是否是写锁拥有者调用的unlock
                throw new IllegalMonitorStateException();
            //获取可重入值,这里没有考虑高16位,因为获取写锁时读锁状态值肯定为0
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)//如采写锁可重入值为0则释放锁,否则只是简单地更新状态值
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }
  • 如果当前线程持有该锁,调用该方法会让该线程对该线程持有的 AQS状态值减1
  • 如果减去1后当前状态值为0则当前线程会释放该锁 ,否则仅仅减1而己
  • 如果当前线程没有持有该锁而调用了该方法则 会抛出 Illega!MonitorStateException 异常

8.2 读锁的获取和释放

ReentrantReadWri teLock 中的读锁是使用 ReadLock 来实现的。 

//ReadLock类
 public void lock() {
            sync.acquireShared(1);
        }
//AQS类
 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

//java.util.concurrent.locks.ReentrantReadWriteLock.Sync.tryAcquireShared(int)
protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();//获取当前状态值  代码1
					//代码2处
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)//判断是否写锁被占用
                return -1;
            int r = sharedCount(c);//获取读锁计数  代码3处
            //尝试获取锁 ,多个读线程只有一个会成功,不成功的进入fullTryAcquireShared进行重试
            if (!readerShouldBlock() && //代码4处
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {//第 一个线程获取读锁  代码5处
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {//如果当前线程是第一个获取读锁的线程  代码6处
                    firstReaderHoldCount++;
                } else {
                //记录最后一个获取读锁的线程或记录其他线程读锁的可重入数
                    HoldCounter rh = cachedHoldCounter; //代码7处
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);//代码8处
        }

tryAcquireShared方法解释:

  • 1. 代码1处首先获取了当前 AQS 的状态值
  • 2. 代码2处查看是否有其他线程获取到了写锁,如果是则直接返回-1
  • 3. 代码3处,得到获取到的读锁的个数 , 到这里说明目前没有线程获取到写锁 ,但是可能有线程持有读锁
  • 4. 代码4处的中非公平锁的 readerShouldBlock 实现代码如下:
//java.util.concurrent.locks.ReentrantReadWriteLock.NonfairSync.readerShouldBlock()
final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
//java.util.concurrent.locks.AbstractQueuedSynchronizer.apparentlyFirstQueuedIsExclusive()
 final boolean apparentlyFirstQueuedIsExclusive() {
        Node h, s;
        return (h = head) != null &&
            (s = h.next)  != null &&
            !s.isShared()         &&
            s.thread != null;
    }

如果队列里面存在一个元素,则判断第一个元素是不是正在尝试获取写锁,如果不是(!readerShouldBlock()=true),则当前线程判断当前获取读锁的线程是否达到了最大值( r < MAX_COUNT)。 最后执行CAS 操作将 AQS 状态值的高 16 位值增1(compareAndSetState(c, c + SHARED_UNIT)) 。

  • 5. 代码5处和代码6处记录第一个获取读锁的线程并统计该线程获取读锁的可重入数
  • 6. 代码7处使用 cachedHoldCounter 记录最后一个获取到读锁的线程和该线程获取读锁的可重入数
  • readHolds记录了当前线程获取读锁的可重入数
  • 7. 如果可以到代码8处,说明readerShouldBlock()=true,说明有线程正在获取写锁,fullTryAcquireShared的代码与tryAcquireShared 类似(尝试获取共享锁),它们的不同之处在于,前者通过循环自旋获取

8.3 使用案例 

补充一、怎么实现公平和非公平锁?

公平锁可以把竞争的线程放在一个先进先出的队列上,只要持有锁的线程执行完了,唤醒队列的下一个线程去获取锁就好了。

非公平锁是后到的线程可能比前到临界区的线程获取得到锁,线程先尝试能不能获取得到锁,如果获取得到锁了就执行同步代码了;如果获取不到锁,那就再把这个线程放到队列。

所以公平和非公平的区别就是:线程执行同步代码块时,是否会去尝试获取锁。如果会尝试获取锁,那就是非公平的。如果不会尝试获取锁,直接进队列,再等待唤醒,那就是公平的。

补充二、共享锁 源码

AQS 共享锁主要 API

  • tryAcquireShared() 获取共享锁
  • tryAcquireSharedNanos() 获取超时可中断共享锁
  • releaseShared() 释放共享锁

tryAcquireShared():

// AQS的acquireShared方法实现
public final void acquireShared(int arg) {
    // 尝试获取共享资源,返回值大于等于0表示成功,小于0表示失败
    if (tryAcquireShared(arg) < 0) {
        // 获取失败,则加入到等待队列中
        doAcquireShared(arg);
    }
}
 
// 子类需要重写此方法以实现具体的获取共享资源的逻辑
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}
 
// 将当前线程加入到等待队列中
private void doAcquireShared(int arg) {
    // 添加到队列的节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            // 获取前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头节点,尝试再次获取共享资源
            if (p == head) {
                // 尝试加锁,假设成功了,肯定是返回0或者是大于0的正数
                // 如果是0,加锁成功,该线程出队
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 获取成功,将当前节点设置为头节点
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // shouldParkAfterFailedAcquire表示是否应该将线程挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) {
                throw new InterruptedException();
            }
        }
    } finally {
        if (failed) {
            cancelAcquire(node);
        }
    }
}

看到上述代码,可以和独占锁的代码对比下,非常相近,无非就有两个地方不一样,如下图示:

共享锁模式下调用的是 setHeadAndPropagate() ,这个方法主要是会去唤醒其他 CLH 中排队的线程。

    private void setHeadAndPropagate(Node node, int propagate) {
    	// 将 AQS 的 head 赋值给 h 临时变量放着,后面要使用
        Node h = head; 
        // 主要是把 AQS 的 head 头指针往后移动,表示有 Node 节点出队列。
        setHead(node);
		
		// 前面提到的 Doug Lea 大佬对 方法 tryAcquireShared() 规定了返回值,负数,和 0 、大于0的正数
		// propagate 就是这个方法的返回值,如果你在子类实现这个 tryAcquireShared() 方法返回大于 0 的正数
		// 这下面的 if 逻辑就直接进入,后面的判断都不会走了。
		// 返回大于0的正数,表示当前线程可以加锁成功,而且还会通知其他线程也过来尝试加这把锁,下面就是干了这个事情
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            // s == null 在并发场景下,还没有来得及给 node 后驱指向赋值,不就是 null 嘛
            // 然后要判断该节点是否为共享模式还是独占模式,在 addWaiter() 方法已经封装了这个属性,这里就用到了
            if (s == null || s.isShared())
            	// 看后面代码分析,摘到外面去分析了,看下面,看完之后回来
                doReleaseShared();
        }
    }
    // 主要是把 AQS 的 head 头指针往后移动,表示有 Node 节点出队列。
    private void setHead(Node node) {
    	// 将 AQS 的 head 指向了当前线程运行的 Node 节点
        head = node;
        // 因为 Node 封装的线程已经加锁成功了,自然而然要把 thread 置成 null 值
        node.thread = null;
        // 同时也要把 Node 的前驱指向断开,方便上一个完成使命的哨兵 GC 回收
        node.prev = null;
    }

releaseShared()

    public final boolean releaseShared(int arg) {
        // 假设这里释放共享锁成功
        if (tryReleaseShared(arg)) {
        	// 释放成功,后面肯定是要去看看唤醒阻塞挂起的线程呗,叫他们醒来过来加锁了
            doReleaseShared();
            return true;
        }
        return false;
    }
    private void doReleaseShared() {
		// 又是一个自旋,肯定有个地方退出
        for (;;) {
            // 释放都是从 head 开始找
            Node h = head;
            // 该节点肯定不是哨兵节点,也不是尾节点,从中间开始
            if (h != null && h != tail) {
                // 正常情况,被阻塞挂起的线程waitStatus 都是-1
                int ws = h.waitStatus;
                // 所以这里成立
                if (ws == Node.SIGNAL) {
                	// 死命的去把 waitStatus 恢复成线程初始状态 0,直到成功,否则一直在这里转转
                    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;
        }
    }

tryAcquireSharedNanos()

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        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;
                }
                // nanosTimeout 表示剩余时间够不够超时时间
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                // 这里注意下,如果你设置的超时时间很多,都已经大于了系统默认 1000 ns 时间
                // 那系统肯定不会傻乎乎的在这里用你的超时时间疯狂自旋,会用自己的默认时间作为超时时间
                // 然后超过了,就去挂起不会去空转。这是个优化的地方,超时时间不要比系统默认的 1000ns 长
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                // 如果修改了线程中断标识,那么会被阻塞线程会响应你的请求,直接给你一个意外异常,然后退出for循环
                // 但是会执行 finally 逻辑哦,注意
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
            	// 主要是去维护
                cancelAcquire(node);
        }
    }
    private void cancelAcquire(Node node) {
        // 首先判断被中断的线程 Node 是否为 null
        if (node == null)
            return;
		// 因为该 Node 是被中断然后结束退出了,所以 Node 的 Thread 属性自然而然就是 null
        node.thread = null;

        // 下面这个 while 循环是维护 CLH 队列完整性,把取消的队列踢出队列
        Node pred = node.prev;
        // 循环去找,知道找到有一个前驱节点不被取消的
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // 因为该 Node 被中断退出了,所以 waitStatus 设置成取消状态
        node.waitStatus = Node.CANCELLED;

        // 如果改 Node 就是尾节点,那就直接踢出即可
        // 把 AQS 的 tail 指向该 Node 的前驱节点即可,该 Node 的前驱节点通过上面的 while
        // 循环已经保证了这个 pred 是正常状态的
        if (node == tail && compareAndSetTail(node, pred)) {
            // 然后 pred 的后驱指针赋值为 null,因为 pred 作为了新的尾节点,后面没有节点了
            compareAndSetNext(pred, predNext, null);
        } else {
            // 假设不是尾节点,那就说明该 Node 为中间节点然后被中断退出了
            int ws;
            // 首先排除哨兵节点,假设不是 head 节点,继续做后面的判断
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                // 假设进来了,该 Node 节点就要被踢出队列了,然后重新维护 CLH 队列完整性
                // 保存该 Node 节点的后驱指针
                Node next = node.next;
                // 如果后驱节点不为 null,说明该 Node 存在后节点而且 waitStatus 是正常的,正常值 0或者-1 
                if (next != null && next.waitStatus <= 0)
                    // 把 pred 该 Node 的前驱节点,predNext 为该 Node 的后驱节点
                    // 然后这里直接把 pred 的后驱指针指向了 predNext,把 Node 直接踢出 CLH 队列了。
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }
            // 把该 Node 的后驱节点指向自己
            node.next = node; // help GC
        }
    }
  • 正常情况下,独占锁只有持有锁的线程运行完了,释放锁了,独占锁才会出队列
  • 共享锁是唤醒下一个节点,并且,下一个节点成功设置成新的 head 头节点,自己就出队列
  • 独占锁是只有在释放锁的时候,才会去看看要不要唤醒下一个节点,而共享锁在两个地方去看看要不要唤醒下一个节点,一个是在获取锁成功时,去调用 setHeadAdnPropagate() 方法尝试是否唤醒下一个线程,另一个是在释放共享锁成功之后会去唤醒线程。

补充三、AbstractOwnableSynchronizer(AOS)

 在JDK1.6时发布,主要用于表示持有者与锁之间的关系‌。

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {    
    
    protected AbstractOwnableSynchronizer() { }
    //私有的不会被序列化的独占thread
    private transient Thread exclusiveOwnerThread;
    //set
    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
    //get
    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

由于AOS是一个抽象类不能直接实例化,我们定义一个子类实例化:

public class AosClient extends AbstractOwnableSynchronizer {
    public static void main(String[] args) {
        AosClient client = new AosClient();
        client.setExclusiveOwnerThread(Thread.currentThread());
        Thread exclusiveOwnerThread = client.getExclusiveOwnerThread();
        System.out.println(exclusiveOwnerThread);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沙滩de流沙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值