Java多线程系列--“JUC锁”02之 互斥锁ReentrantLock

ReentrantLock介绍

ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。

顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取
ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。

ReentrantLock主要是以下四个点:

 (1)ReentrantLock是一个基于AQS来实现的可重入的互斥锁,它是一个互斥锁,并且支持可重入的。

 (2)ReentrantLock实现了Lock接口,实现了锁的语义。

 (3)ReentrantLock提供了两种锁模式,公平锁和非公平锁,默认是非公平锁。

 (4)ReentrantLock基于AQS之上的Condition机制,实现了多线程通过Condition进行睡眠、唤醒来控制线程行为

示例:

public class ReentrantLockDemo {
    // 获取一个Lock接口的实例,由ReentrantLock实现的
    public static Lock lock = new ReentrantLock();
    // 定义一个共享变量
    public static int value = 0;
   
    public static void add() {
        try {
            // 执行value++操作之前先获取锁
            lock.lock();
            value++;
        } finally {
            // 执行完操作之后释放锁
            lock.unlock();
        }
    }

    public static class AddThread extends Thread{
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                add();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 这里同时开启两个线程执行,验证ReentrantLock是否能保证线程安全
        // 也就是ReentrantLock是否能实现锁的作用
        AddThread addThread1 = new AddThread();
        AddThread addThread2 = new AddThread();

        addThread1.start();
        addThread2.start();

        // 这里主线程调用addThread1.join、addThread2.join的方法
        // 意思就等待这两个线程执行完成之后才能继续执行
        addThread1.join();
        addThread2.join();

        System.out.println("value 的值是:" + value);
    }
}

这里得到的结果是20000,跟预期值20000完全一致,是并发安全的。

根据上面的那个Demo样例,我理解的就是ReentrantLock实现了Lock接口,实现了锁的功能,也就是实现了锁的语义,这个是我目前知道的   

ReentrantLock锁机制源码剖析

ReentrantLock成员列表

private final Sync sync;

 

这个Sync的内部类继承了AQS,同时Sync又有两个子类,分别是NonfairSync非公平锁、FairSync公平锁。

然后ReentrantLock只是基于已有的公平锁和非公平所同步工具类之上再简单的封装了一层而已。大概的结构图是这样的:

 

 

 之前讲AQS的时候啊,说过AQS提供了四个空的方法,这四个方法分别是:
(1)分别是获取独占锁实现方法tryAcquire、释放独占锁实现方法tryRelease
(2)获取共享锁实现方法tryAcquireShared、释放共享锁实现方法tryReleaseShared
AQS的子类分别实现这四个不同的方法,就形成了不同的并发工具,

公平锁FairSync和非公平锁NonfairSync都是实现了tryAcquire和tryRelease方法,我们首先来看下FairSync公平锁的内部源码:

公平锁内部源码解析

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        // 这里的acquire方法其实就是AQS的acquire方法
        acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        // 获取当前线程
        final Thread current = Thread.currentThread();
        // 判断资源是否有人加锁,c == 0 没人加锁,c > 0 表示有人加锁了
        int c = getState();
        // c == 0 表示没人加锁
        if (c == 0) {
            // hasQueuedPrecessors这里时判断AQS的等待队列是否有人在等待
            // 其实公平锁和非公平锁实现的精髓就在这里,
            // 公平锁如果发现AQS中等待队列有人在等待,那么直接去排队,即时资源时空的也不争抢
            if (!hasQueuedPredecessors() &&
                // 如果AQS队列没线程在排队,则CAS开始争抢锁
                compareAndSetState(0, acquires)) {
                // 争抢成功则设置加锁的线程时自己
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果上面 c > 0 说明有人加锁了
        // 这里就获取当前加锁的线程是谁,如果加锁的竟然是自己,则直接重入
        else if (current == getExclusiveOwnerThread()) {
            // 之前加锁的是自己,现在直接重入,修改加锁的次数就好了
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

 

我们这里梳理一下FairSync公平锁的acquire方法的源码流程如下:


(1)调用ReentrantLock的公平锁的lock方法其实会调用到AQS的acquire方法。
acquire的方法内部其实调用子类的tryAcquire方法去实际获取锁,也就是回到了子类FairSync的tryAcquire方法,源码的流程如上图
(2)这里tryAcquire的方法其实做的事情很简单,如果判断c == 0 资源是空的,看一下等待队列有没有人,有人直接去队列后面等待,这就是公平锁啊!!!(公平锁就是判断等待队列有人就去乖乖排队了)。如果没人等待自己就去加锁。
如果资源 c > 0,说明已经有人加锁了, 则判断之前加锁的人是不是自己,如果是自己说明自己已经加过锁了,直接修改加锁的次数就好了,如果不是自己说明别人加锁了,自己自然就获取锁失败了
(3)一旦在tryAcquire获取锁失败,就会调用AQS的addWaiter方法进入等待队列排队,然后调用acquireQueued自旋的尝试获取锁或者将自己挂起,等待别人唤醒。这里的源码和核心机制,前几章我们已经非常透彻的分析过了。
FairSync公平锁的整个套路你理解了没?其实也就是实现了获取锁的tryAcquire方法逻辑而已。底层的进入等待队列addWaiter、以及阻塞等待的acquireQueued的这套机制还是AQS的,子类实现获取独占锁,只需要实现tryAcquire方法就可以了,其它的AQS都保障好了。

公平锁FairSync释放锁逻辑

我们看到ReentrantLock释放锁的方法为unlock,底层调用的还是Sync的release方法,源码如下:

public void unlock() {
    sync.release(1);
}

这里的release方法,其实就是AQS内部提供的release方法,作为释放独占锁资源的入口,我们之前讲过源码了,如下:

public final boolean release(int arg) {
    // 1. 这里会调用子类的tryRelease方法,实际去释放锁
    if (tryRelease(arg)) {
        Node h = head;
        // 2. 释放锁成功,唤醒后续AQS等待队列中等待的线程
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

释放锁的实际逻辑还是到子类的tryRelease方法里面,也就是FairSync的tryRelease方法里面。由于FairSync继承自Sync同步器,同时也继承了Sync定时的tryRelease方法,所以我们继续看下:
SynctryRelease释放锁的实际逻辑,源码如下:

protected final boolean tryRelease(int releases) {
    // 直接讲释放锁的次数减少releases次
    // 也就是你加锁多少次,就释放多少
    // 当state == 0 的时候说明锁已经空闲了,没人持有了
    int c = getState() - releases;
    // 这里释放之前判断之前是不是自己加锁的
    // 如果自己之前没加锁,不能胡乱释放,直接抛出异常
    // 谁加的锁,谁才能释放
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 当c == 0 说明锁已经完全释放了
    if (c == 0) {
        free = true;
        // 设置加锁的线程为null,表示没人加锁了
        setExclusiveOwnerThread(null);
    }
    // 设置state = 0,锁空闲了,让别的线程可以加锁
    setState(c);
    // free = true表示锁释放了,完全空闲了
    return free;
}

我们同样画个图来梳理一下释放锁的流程图:

 (1)释放锁的时候其实首先就会走入到AQS提供的release入口方法,然后release中再调用子类的tryRelease方法去释放锁
(2)tryRelease释放锁的时候会判断,是不是加锁的线程释放的,如果不是,那不行啊哥们,只能加锁的人释放锁,不然就乱了。
然后释放锁的时候会扣减加锁的次数,当state 加锁的次数被扣减为零,才是真正的完全释放锁,这个时候就需要设置一下加锁的线程为null,也就是没人加锁了。
(3)当锁释放了之后,就会调用AQS给你提供的unparkSuccessor方法去唤醒等待队列中正在等待的人了,哈哈,这里的源码之前我们非常详细的讲解过了
释放锁的过程简单讲:释放锁的时候就是扣减一下次数而已,然后判断一下state == 0 那就完全释放锁了,这个这是没人加锁了。然后到这里子类的事情干完了,交给AQS去调用unparkSuccessor唤醒等待锁的人。你基于AQS,只需要实现获取锁和释放锁的逻辑就好了,至于获取锁失败之后要干啥,AQS已经给你定义好了,那就是进入等待队列,然后你可能在队列里面沉睡挂起,或者自旋再次尝试获取锁。
同样的当你释放锁成功之后要干啥,AQS同样已经定义好了,那就是唤醒等待队列中的线程让他们去重新竞争锁;这些都是AQS内部已有的机制。

非公平锁NonFairSync的源码


上面公平锁FairSync的加锁实现,当state == 0的时候也就是没人加锁的时候,它会调用AQS的hasQueuedPredecessors()方法,判断等待队列里面是否还有人在等待。
如果有人在等待那它就不去加锁了,如果没人在等它就尝试去加锁,公平锁就是加锁的时候判断有没有人在等待,有的话它就放弃加锁,老老实实的去等待队列尾部等待。

接下来我们看一下非公平锁怎么实现:

首先看一下NonFairSync非公平锁的加锁方法lock方法,作为加锁的入口方法:

final void lock() {
    // 这里上来就直接尝试加锁,不管资源是不是空的,不管有没有人在等待
    // 这哥们不讲武德啊,上来就抢
    if (compareAndSetState(0, 1))
        // 抢夺成功之后,设置是自己加锁,然后就完事了
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

看上面非公平锁的加锁lock方法:
(1)首先进入的这个方法,立马执行compareAndSetState(0,1) 尝试去加锁,这个时候不管有没有人加锁,也不管等待队列中有没有人在等,完全不讲规矩,不讲武德啊
(2)然后尝试加锁失败直接调用AQS的acquire方法,我们继续看流程:

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

(3)然后acquire方法里面又会调用子类的tryAcquire方法,也就是调用NonfairSync的tryAcquire方法,实际尝试获取锁:

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}

(4)NonFairSync的tryAcquire方法直接内部就是调用到Sync的nonFairTryAcquire方法,也就是说调来调去,加锁的具体逻辑最核心的源码在nonfairTryAcquire方法内部,我们下面看看:

final boolean nonfairTryAcquire(int acquires) {
    // 获取当前线程
    final Thread current = Thread.currentThread();
    // 获取资源state的值,state > 0 表示已经有人加锁了
    // state = 0表示没人加锁,锁是空闲的
    int c = getState();
    // 如果c == 0 没人加锁,马上就去竞争锁,不管有没有人在等待
    if (c == 0) {
        // CAS尝试竞争锁
        if (compareAndSetState(0, acquires)) {
            // 加锁成功,设置加锁的线程是自己,
            // 这里的setExclusiveOwnerThread就是设置时哪个线程加锁的
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果上面c != 0 ,说明有人加锁了;这里判断之前加锁的线程是不是自己
    // 如果是自己的话,直接就重入,直接把自己加锁的次数增加就可以了
    // 如果不是自己加锁,说明是别人加锁了,此时就需要进入AQS的等待队列等待
    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;
}

这个源码看完了,对比公平锁FairSync的tryAcquire的源码,你发现了什么?

这里非公平锁NonFairSync的源码跟之前公平锁的tryAcquire方法源码几乎一致。

(1)公平锁:唯一不同的是公平锁在资源state == 0也就是没人加锁的时候,还要通过hasQueuedPrecessors()方法判断等待队列有没有在等待,如果有人在等待则它立马放弃去加锁。
(2)非公平锁:非公平锁在state == 0 也就是没人加锁的时候,才不管你等待队列有没有人在等待,它不在乎,比较自私一点,直接就去争抢锁,成功就返回了。

对于释放资源的实际方法tryRelease,公平锁和非公平所完全一样,都是使用Sync的tryRelease方法,这里我们上面已经讲解过了。

public void unlock() {
    // 调用到AQS的release方法释放
    sync.release(1);
}

然后AQS中的release方法调用到子类Sync的tryRelease方法:

public final boolean release(int arg) {
    // 调用到子类的tryRelease方法
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

 

上面已经详细讲解了tryRelease方法释放资源的流程了。

接下来我们讲解ReentrantLock的Condition机制,这个机制能实现类似wait和notify类似的控制线程沉睡、唤醒的行为功能。

ReentrantLock的Condition机制

ReentrantLock提供的这个Condition功能啊,底层还是基于AQS的Condition机制的,Condition必须要配合一个锁来使用。具有的功能跟我们之前讲解过的synchronized和wait、notify/notifyAll是一样的, 实现的功能是控制多线程的行为。

Condition例子:两个线程交替打印1、2(通过condition的await方法和singal方法来实现线程的沉睡和唤醒功能,从而来控制线程的行为)

public class ConditionDemo {
 
    // 声明一个变量,初始值为1,注意:这个变量是非线程安全的
    public static int value = 1;
    // 声明一个reentrantLock互斥锁
    public static ReentrantLock reentrantLock = new ReentrantLock();
    // reentrantLock创建一个Condition
    public static Condition condition = reentrantLock.newCondition();
    
    // 线程1:始终打印1的线程
    public static class PrintOneThread extends Thread {
        @Override
        public void run() {
            // 打印10000次1
            for (int i = 0; i < 10000; i++) {
                try {
                    // 由于变量value是非线程安全的,每次操作前需加锁
                    reentrantLock.lock();
                    // 当value的值不是奇数的时候,直接沉睡等待
                    while (value % 2 != 1) {
                        // 调用condition的await方法,释放锁,同时进入沉睡
                        // 等待别人调用singal/singalAll唤醒自己,然后重新获取锁
                        condition.await();
                    }
                    // 走到这里说明value是奇数,并且自己获取了锁
                    System.out.print("1");
                    // 执行value的++操作,
                    value++;
                    // 唤醒调用condition.await而陷入等待的线程;这里就是唤醒线程2
                    condition.signal();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 释放锁
                    reentrantLock.unlock();
                }

            }
        }
    }
    
    // 线程2:始终打印2的线程
    public static class PrintTwoThread extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                try {
                    // 获取独占锁
                    reentrantLock.lock();
                    // 当value的值不是偶数,就直接沉睡等待
                    while (value % 2 != 0) {
                        // 这里调用condition.await沉睡等待,同时释放独占锁
                        // 等待别人调用singal/singalAll唤醒自己,然后自己重新竞争锁
                        condition.await();
                    }
                    // 走到这里说明value是偶数,打印2
                    System.out.print("2");
                    // 执行value的++操作,让value变为奇数
                    value++;
                    // 唤醒因为调用condition.await而陷入等待的线程;这里是唤醒线程1
                    condition.signal();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 释放独占锁
                    reentrantLock.unlock();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 创建线程1、线程2
        PrintOneThread printOneThread = new PrintOneThread();
        PrintTwoThread printTwoThread = new PrintTwoThread();
        
        // 启动线程1、线程2,就会看到数字1和2交替打印的效果;实现了线程的控制
        printOneThread.start();
        printTwoThread.start();

        // 主线程等待printOneThread、printTwoThread线程运行结束再往后走
        printOneThread.join();
        printTwoThread.join();

        System.out.println("运行结束");
    }
}

程序的运行结果,数字1、2依次交替打印
上面的例子啊,就是使用Condtion来控制线程的行为,Condition必须是配合一个锁来使用的
(1)PrintOneThread线程获取锁之后,判断一下value是不是奇数,如果不是奇数,他就调用condition.await()方法,释放锁,同时沉睡等待(条件:只在value为奇数的时候打印)。
当被唤醒之后会重新尝试获取锁,获取锁成功之后才能继续运行。
如果是奇数的话,打印1,然后执行value++,然后调用condition.singal方法唤醒因为调用condition.await方法而陷入等待的线程(这里PrintTwoThread线程而因为调用了condition.await方法而沉睡等待,被唤醒)
(2)同样的在PrintTwoThread线程获取锁之后,如果发现value是奇数,自己也会调用condition.awai方法t,释放锁,沉睡等待(条件:只在value为偶数的时候打印)。
然后是偶数的话就打印2,然后执行value++操作,最后执行condition.singal唤醒因为调用condition.await方法而陷入等待的线程。
然后我针对上面的例子,给你画了个图,你来理解理解:

 有个疑问啊,我看到上面的例子condition的await方法和singal方法都是在获取ReentrantLock之后才调用的。是不是只有获取锁成功之后才能进行await和singal方法的调用,进行线程控制?

是的,这里的使用其实跟synchronized、wait、notify的使用是一样的,wait、notify方法也是必须在synchronized代码块里面使用的,也就是说必须获取锁之后才能调用。同样condition的使用也必须在一个锁里面。
调用Condition.await()方法线程释放ReentrantLock锁,然后才陷入沉睡的。那到底底层是怎么释放锁?怎么让线程沉睡的?同时你说沉睡之后被唤醒的线程还要再去竞争ReentrantLock,竞争成功之后才能继续业务执行代码?

这个其实就是condition.await内部机制的一个实现,Condition.singal是怎么唤醒因为调用Condition.await而陷入等待的线程

下面分析Condition.await和Condition.singal内部的机制是怎么样的?

Condition.await方法源码


我们看一下Condition的await方法的源码,其实就是AQS中await方法的源码(又是基于AQS),如下所示:

public final void await() throws InterruptedException {
    // 如果线程被中断了,直接抛出中断异常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 当前线程封装成Node节点,加入condition队列里面
    // 注意:这里是Condition队列,而不是AQS获取锁的等待队列,注意
    Node node = addConditionWaiter();
    // 这里是释放锁,完全释放锁资源,将state归于0
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 如果Node节点不在AQS获取锁的等待队列,这里一般都不会在
    while (!isOnSyncQueue(node)) {
        // 直接将线程挂起,让线程沉睡
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 走到这里说明有别的线程调用Condition.singal方法将你唤醒了
    
    // 这里这里调用AQS的acquireQueue方法,这个方法的作用之前讲过了
    // 就是将你放入AQS的等待队列里面,重新等待获取锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        // 这里就是删除一下无效的condition队列节点
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        // 由于等待时间可能太久了,被中断了
        reportInterruptAfterWait(interruptMode);
}

 

这里我整体画一个流程图,来让你理解一下:

  上面这个图形啊,就是Condition.await方法内部的大致流程了,由于Condition的await方法是直接调用AQS的await方法的,所以也是AQS的await方法内部的整体流程:
(1)第一步首先就是调用addConditionWaiter方法,将当前线程直接封装成一个Node加入Condition队列,注意:这里说的是Condition队列!!,不是AQS等待锁的等待队列!!,这里要注意,不要弄乱了!!!
(2)加入Condition队列之后,就是释放锁资源,这里fullyRelease就是完全释放锁,不管之前获取了锁多少次,这里都完全释放,将state 资源重新置为0。
(3)通过isOnSyncQueue方法判断是不是在AQS等待锁的等待队列,如果不是那就直接调用LockSupport.park方法将线程挂起,这里挂起之后需要别的线程调用Condition.singal或者Condition.singalAll方法才能将自己唤醒
(4)自己被唤醒之后,将自己加入AQS的等待队列,去等待队列重新等待锁!!!如果自己已经在AQS的等待队列了,跳出循环,然后在AQS的等待队列里面等待获取锁,注意:这里需要在AQS的等待队列里面等待锁,才能执行后面的业务逻辑方法。

加入Condition队列,然后释放锁,之后沉睡。当别的线程唤醒它之后,它要重新进入AQS获取锁的等待队列里面,只有重新获取锁成功才能执行业务逻辑方法。

上面的await方法的整个流程的源码中,加入等待队列等待锁的源码方法acquireQueue,之前讲AQS的时候重点分析过了,但是这里的addConditionWaiter方法、fullyRelease方法、isOnSyncQueue内部机制是怎么样的?这个我还不了解......

 
我们慢慢来一个一个分析,首先讲的是addConditionWaiter是怎么将线程封装成Node加入Condition队列的。
AQS等待队列Node类数据结构回顾

讲解addConditionWaiter源码之前,我们之前在AQS讲解过的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;
    // -2表示节点在Condition队列,等待别人唤醒
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    // Node节点的等待状态
    volatile int waitStatus;
    // AQS队列中,Node节点指向前一个节点的指针
    volatile Node prev;
    // AQS队列中,Node节点指向后一个节点的指针
    volatile Node next;
    // 线程
    volatile Thread thread;
    // 这里啊就是Condition队列的Node节点,指向下一个节点的指针了
    Node nextWaiter;

上面就是我们回顾Node节点的数据结构了,其中prev、next指针是用在AQS等待队列的,分别指向AQS等待队列的前一个节点和后一个节点。然而nextWaiter使用在Condition队列的!!
Condition是由单向链表构成的一个队列,nextWaiter表示当前Condtion队列节点的下一个节点。

AQS内部相当于有两个队列,一个是AQS等待锁的等待队列,是一个双向链表;另一个是Condition队列,只有用到Condition机制的时候才会创建这个队列,是一个单向链表

我记得之前在讲解AQS的等待队列的时候说过,AQS使用一个head和tail的指针管理着AQS等待队列,那这个Condition队列又是怎么来进行管理的?

也才一样的思路,ConditionObject也就是Condition接口的实现类,内部也是搞了两个指针来管理着Condition队列的,给你看下代码:
ConditionObject内部的数据结构

public class ConditionObject implements Condition, java.io.Serializable {
    // 这里就是指向Condition队列头节点的指针
    private transient Node firstWaiter;
    // 这里就是指向Condition队列尾节点的指针
    private transient Node lastWaiter;
}

 

它也是通过两个指针来管理Condition队列的;只不过命名不一样而已。AQS等待队列使用的头节点、尾节点指针叫做head、tail,而Condition队列使用头节点、尾节点的名字叫做firstWaiter、lastWaiter而已。
画个图对比一下AQS获取锁的等待队列、Condition队列:

好的,了解了这个,那我们接下来就进入addConditionWaiter方法的源码剖析。
addConditionWaiter方法源码

 

private Node addConditionWaiter() {
    // 获取Condition队列的尾结点
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    // 如果Condition队列中已经有些节点的线程因为超时或者中断原因被取消了
    // 这里的unlinkCancelledWaiters方法就是删除哪些无效的节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 创建一个Node节点
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 下面就是把node插入condition队列的尾部了
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

 

上面的代码很清晰吧,就是获取一下Condition队列的尾节点,然后将当前线程封装成一个Node节点,插入Condtion队列的尾部,就完事了。
当然如果发现lastWaiter节点状态不对,可能是由于超时或者中断的原因导致lastWaiter节点被取消了,此时就会调用unlinkCanceledWaiter方法删除一下无用节点。
再来看一下unlinkedCancelledWaiter方法源码:

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    // 这里就是从头结点开始遍历整个链表
    // 然后检查状态,状态不对的就删除
    // 这里就是最基础的链表遍历和删除代码了
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

这里就是一段基础的链表遍历和链表删除的操作,这里代码没啥太多的东西。
addConditionWaiter方法里面的源码都挺简单的,就是往Condition队列尾部插入一个节点,如果发现Condition队列尾节点状态不对,则遍历一下Condition队列,删除一下无用节点,就是基础的链表遍历、插入、删除操作而已。

接着继续,讲解到fullyRelease方法的源码,看看这个方法里面是怎么完全释放锁资源的?
fullyRelease方法源码:

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 首先获取加锁的次数
        int savedState = getState();
        // 这里就是调用AQS里面的release方法去释放资源,
        // 之前已经分析过了release方法的源码了
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

fullyRelease方法的源码就更简单了:
它就是直接调用release方法去全部释放资源,至于release方法的源码之前我们已经讲解过了。
我们这里再说一下,就是先调用子类的tryRelease方法去释放资源,如果释放成功了则调用AQS的unparkSuccessor方法去唤醒等待中沉睡的线程。

原来fullyRelease里面就是直接调用release方法去释放资源而已,我还以为有啥重要操作呢

AQS作为一个并发的框架,这些通用的东西肯定考虑过了,它已经定义好那么多获取资源、释放资源的入口和机制了,这里直接复用就好了,没必要都重新写一套吧。

我们接下来再分析一下,isOnSyncQueued方法,说白了,这个方法就是遍历一下等待队列,然后依次比较,看看节点是不是在队列里面而已,就是这么简单,不信你看看下面的代码:
isOnSyncQueued方法源码:

final boolean isOnSyncQueue(Node node) {
    // 如果Node节点状态是CONDITION,说明肯定在CONDITION队列,不在等待队列
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 如果他的next指着有值,说明肯定在等待队里了
    if (node.next != null)
        return true;
    // 这里就是遍历整个等待队列,一个个比较
    return findNodeFromTail(node);
}
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    // 从尾部往前遍历,一个个对比是否等于node节点
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

 

isOnSyncQueued方法的源码就更简单了,如果Node的next指针有值肯定在等待队列了,如果Node节点的waitStatus是CONDITION肯定就不在了,因为它会在Condition队列。然后上面的情况都不是的话就遍历整个等待队列一个个对比了。

感觉这就完了,Condition.await方法里面的源码都剖析过了,感觉都挺简单的呀。

嗯嗯,Condition.await方法的源码其实并不难,因为大量使用了之前我们分析过的AQS机制,你是有了之前的基础所以才会觉得那么简单的。
哈哈,感觉也是啊,如果我之前没有学过AQS的那些机制,让我看下去,感觉后会把自己给绕晕了。
讲解了addConditionWaiter、fullyRelease、isOnSyncQueue这几个方法的源码之后,我之前给你画的Condition.await方法的流程图再细化一下:

 

讲解到这里Condition.await方法内部的全部源码流程都讲解完了。
包括调用方法之后怎么通过addConditionWaiter放入到Condition队列的尾部,然后调用fullyRelease释放锁,然后就陷入沉睡。
当被唤醒之后就调用AQS的acquireQueud方法去重新竞争锁资源,获取锁成功之后才能继续执行业务代码,这一整套流程下来,你还有疑问的不?
既然Condition.await方法没有问题了,那我么就分析Condition的另外一个方法,Condition.singal是怎么唤醒Condition队列里面的线程的?放心这里的方法很简单,比Condiution.await的简单很多,我们接着看就是了。
Condition.singal方法源码

看看Condition的singal方法实现者,也就是AQS里面的singal方法源码:

public final void signal() {
    // 首先判断一下自己是不是拥有独占锁
    // 没有独占锁,不能调用singal方法,会抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    // 获取Condition队列的头结点firstWaiter
    Node first = firstWaiter;
    if (first != null)
        // 调用doSingal方法去唤醒
        doSignal(first);
}

我们接着看doSingal方法的源码:

private void doSignal(Node first) {
    do {
        // 这里的逻辑就是从头往后遍历Condition链表
        // 找到一个节点不是null的,然后调用唤醒,就那么简单
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
        // 这里的实际唤醒逻辑在transferForSingal方法里面
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

 


继续看transferForSingal方法源码

final boolean transferForSignal(Node node) {
    // 唤醒前将节点等待状态从CONDTION改为0
    // 因为后面唤醒之后还要进入等待队列去争抢锁,所以改为0也就是等待队列的初始状态
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 这里就是直接插入AQS等待队列了,之前讲解AQS的时候详细分析过enq源码
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        // 看,这里就是直接调用LockSupport.unpark方法将线程唤醒了
        LockSupport.unpark(node.thread);
    return true;
}

 

 就是Condtion.singal方法的全部源码了,大致流程小陈你看懂了没?

这里其实就是从Condition队列里面头节点开始尝试唤醒节点。唤醒之前会插入AQS的等待队列让他们再次尝试获取锁,然后就是直接调用LockSupport.unpark方法唤醒线程了,流程也不复杂。

总体来说确实不复杂,但是老规矩,为了你以后能记忆更加深刻,我还是跟你整了图:

 

Condtion.singal方法的全部核心的流程、源码实现都给你剖析过一遍了,到这里我们Condition.singal方法的源码就讲解结束了,其实还有一个Condtion.singalAll方法是唤醒Condition队列中所有等待线程的,你可以自己看一下,逻辑基本和Condition.singal一致,只是唤醒所有的而已。

给你一幅图总结一下整个Condition.await、Condition.singal的核心机制:

 

如果有啥步骤忘记了,就重新根据这个图再回忆一下好了。我们今天分析Condition机制的内部实现已经完全分析完了



ReentrantLock函数列表

// 创建一个 ReentrantLock ,默认是“非公平锁”。
ReentrantLock()
// 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
ReentrantLock(boolean fair)

// 查询当前线程保持此锁的次数。
int getHoldCount()
// 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner()
// 返回一个 collection,它包含可能正等待获取此锁的线程。
protected Collection<Thread> getQueuedThreads()
// 返回正等待获取此锁的线程估计数。
int getQueueLength()
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition)
// 返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition)
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread)
// 查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads()
// 查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition)
// 如果是“公平锁”返回true,否则返回false。
boolean isFair()
// 查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
// 查询此锁是否由任意线程保持。
boolean isLocked()
// 获取锁。
void lock()
// 如果当前线程未被中断,则获取锁。
void lockInterruptibly()
// 返回用来与此 Lock 实例一起使用的 Condition 实例。
Condition newCondition()
// 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
boolean tryLock()
// 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
boolean tryLock(long timeout, TimeUnit unit)
// 试图释放此锁。
void unlock()



使用场景

从使用场景的角度出发来介绍对ReentrantLock的使用,相对来说容易理解一些。

场景1:如果发现该操作已经在执行中则不再执行(有状态执行)

a、用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。
b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。

以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)

    private ReentrantLock lock = new ReentrantLock();//参数默认false,不公平锁
    
    public void test1() {
         if (lock.tryLock()) {  //如果已经被lock,则立即返回false不会等待,达到忽略操作的效果 
             try {
                 System.out.println("test1 获得锁");
                 System.out.println("test1 工作");
             } finally {
                 lock.unlock();
             }
         } else {
             System.out.println("test1 未获得锁");
         }
    }

场景2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)

这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。
但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。

这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)

ReentrantLock默认情况下为不公平锁

    private ReentrantLock lock = new ReentrantLock(true); //公平锁
    
    public void test2() {
        try {
            lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
            //操作
            System.out.println("test2 获得锁");
                System.out.println("test2 工作");
        } finally {
            lock.unlock();
        }
    }

不公平锁与公平锁的区别:

公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)
不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)

 

场景3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)

这种其实属于场景2的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。
用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。

    private ReentrantLock lock = new ReentrantLock(true); //公平锁
    
    public void test3() {
        try {
            if (lock.tryLock(5, TimeUnit.SECONDS)) {  //如果已经被lock,尝试等待5s,看是否可以获得锁,如果5s后仍然无法获得锁则返回false继续执行
                try {
                    //操作
                    System.out.println("test3 获得锁");
                        System.out.println("test3 工作");
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace(); //当前线程被中断时(interrupt),会抛InterruptedException                 
        }
        
    }

场景4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。

synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景2的另一种改进,没有超时,只能等待中断或执行完毕)

这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)

ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁与synchronized的区别,这给我们带来了很大的灵活性。比如:如果A、B 两个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了两种方式处理:

第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock 不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);

第二,B线程中断自己(或者别的线程中断它),ReentrantLock 处理了这个中断,并且不再等待这个锁的到来,继续向下执行B线程的其它工作。

package lock.demo4;

import java.util.concurrent.locks.ReentrantLock;

public class Test {
    //是用ReentrantLock,还是用synchronized
    public static boolean useSynchronized = false;

    public static void main(String[] args) {
        IBuffer buff = null;
        if (useSynchronized) {
            buff = new Buffer();
        } else {
            buff = new BufferInterruptibly();
        }
        final Writer writer = new Writer(buff);
        final Reader reader = new Reader(buff);
        writer.start();
        reader.start();
        new Thread(new Runnable() {
            public void run() {
                long start = System.currentTimeMillis();
                for (;;) {
                    // 等5秒钟去中断读
                    if (System.currentTimeMillis() - start > 5000) {
                        System.out.println("[主线程]调用reader.interrupt()发出“的尝试中断消息”,让Reader线程不再等了");
                        reader.interrupt();
                        break;
                    }

                }

            }
        }).start();
    }
}

interface IBuffer {
    public void write();

    public void read() throws InterruptedException;
}

class Buffer implements IBuffer {
    private Object lock;

    public Buffer() {
        lock = this;
    }

    public void write() {
        synchronized (lock) {
            long startTime = System.currentTimeMillis();
            System.out.println("[Writer线程]开始往这个buff写入数据…");
            for (;;)// 模拟要处理很长时间
            {
                if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)
                    break;
            }
            System.out.println("终于写完了");
        }
    }

    public void read() {
        synchronized (lock) {
            System.out.println("从这个buff读数据");
        }
    }
}

class BufferInterruptibly implements IBuffer {

    private ReentrantLock lock = new ReentrantLock();

    public void write() {
        lock.lock();
        try {
            long startTime = System.currentTimeMillis();
            System.out.println("[Writer线程]开始往这个buff写入数据…");
            for (;;)// 模拟要处理很长时间
            {
                if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)
                    break;
            }
            System.out.println("终于写完了");
        } finally {
            lock.unlock();
        }
    }

    public void read() throws InterruptedException {
        lock.lockInterruptibly();// 注意这里,可以响应中断
        try {
            System.out.println("从这个buff读数据");
        } finally {
            lock.unlock();
        }
    }

}

class Writer extends Thread {

    private IBuffer buff;

    public Writer(IBuffer buff) {
        this.buff = buff;
    }

    @Override
    public void run() {
        buff.write();
    }

}

class Reader extends Thread {

    private IBuffer buff;

    public Reader(IBuffer buff) {
        this.buff = buff;
    }

    @Override
    public void run() {

        try {
            buff.read();
        } catch (InterruptedException e) {
            System.out.println("[Reader线程]我不读了");
        }

        System.out.println("[Reader线程]读结束");

    }
}

结果:

[Writer线程]开始往这个buff写入数据…
[主线程]调用reader.interrupt()发出“的尝试中断消息”,让Reader线程不再等了
[Reader线程]我不读了
[Reader线程]读结束

可重入概念

若一个程序或子程序可以“安全的被并行执行(Parallel computing)”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,可以再次进入并执行它(并行执行时,个别的执行结果,都符合设计时的预期)。可重入概念是在单线程操作系统的时代提出的。

参考:https://mp.weixin.qq.com/s/oKvdct83NHbRMPjp0x9fkA

posted on 2016-11-14 22:07  duanxz  阅读(666)  评论(0编辑  收藏  举报