Java并发系列之一 Lock源码解析

1. Lock接口简介

Lock接口是Java concurrent包中比较重要的接口。Lock的实现类有ReentrantLock、WriteLock、ReadLock。Lock类中定义了六个方法
void lock();

void lockInterruptibly() throws InterruptedException;

boolean tryLock();

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

void unlock();

Condition newCondition();

复制代码
  1. lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。
  2. unlock()方法是用来释放锁的。调用了lock()必须成对的调用unlock(),否则会出现死锁的情况。
  3. newCondition()方法是给锁创建一个ConditionObject对象,ConditionObject的await()方法能让当前线程释放锁,并且让线程处于阻塞状态。==signal()方法是通知其他正在该ConditionObject上阻塞的线程加入竞争锁的队列中==

锁分为排他锁和共享锁

  • 排他锁同一时间只能被一个线程获取到
  • 共享锁可以被多个线程同时获取

ReentrantLock和WriteLock是排他锁,ReadLock是共享锁

2. Lock的使用

通过Lock来实现同步功能,我们一般会潇洒地写出以下代码

Lock lock = new ReentrantLock();
try{
    lock.lock();//标记1
    doSomeThing();
}finally{
    lock.unlock();
}
复制代码

通过lock.lock()方法就能实现代码的同步。就问你神不神奇。可能大家面试的时候也经常会被问到如何用Lock实现同步,很大一部分同学应该都知道使用lock()方法。正当你洋洋得意,以为这就是整个答案的时候,面试官也会轻轻点头露出一份诡异的笑容同时抛出下一个问题,那么请问lock方法的实现原理是什么,为什么调用这个方法就能实现线程间同步呢?那么下面由我带大家进入lock()方法的揭秘之旅

3. lock()方法揭秘

lock()方法是Lock接口中定义的方法。要想探索lock()方法的奥秘,我们得先从Lock接口的实现类中找到一个突破口才行啊。好吧那就从ReentrantLock类开始吧。ReentrantLock的含义是指==重入锁==,重入锁的意思是指同一线程可以多次获取到同一把锁,每次获取到锁,锁的状态会加1,每次释放锁,锁状态会减1。后面我们会讲解到重入锁的实现原理。

我们接着来探索ReentrantLock的lock()方法

//ReentrantLock.java
public void lock() {
        sync.lock();
}
复制代码

1.原来只是调用了sync对象的lock()方法,so easy找到sync对象的定义

    private final Sync sync;

    abstract static class Sync extends AbstractQueuedSynchronizer {
            abstract void lock();
    }
复制代码

2.至此我们引出了一个非常重要的类AbstractQueuedSynchronizer。中文意思就是抽象队列同步器(下文简称AQS)。顾名思义队列同步器,那内部结构肯定是用队列来实现的。队列最重要的性质就是先进先出。这个给各位先剧透下,如果一个线程获取锁失败后,会封装成一个Node节点加入sync的队列中。注意Sync类是ReentrantLock的静态内部类,也就意味着一把锁只有一个sync同步器,一个同步器内部维护了一个双向列表(也就是队列)。队列里面维护的是获取锁失败的线程组成的节点。当锁被释放后,根据先进先出的性质,在队首的节点的线程将优先有获取锁的权利。==当然也有意外== ==当然也有意外== ==当然也有意外== 并不是所有的锁都是在队首的节点优先获取锁。这种情况就是,有个运气爆表的线程我们暂且叫它线程T吧,在刚运行到lock.lock()方法的时候,占有锁的线程刚好执行完,T去尝试获取了下锁,因为它运气爆表嘛,刚好他获取到了锁。刚才我们讲了,如果一个线程获取锁失败,它会被封装成Node加入sync同步器的队列中,等待被唤醒再次获取锁。因为线程T很幸运在第一时间获取到了锁,那么它根本就不需要到sync队列中等待。那么有线程幸运就自然有线程不公平,在sync队列中苦哈哈排队的队首线程本来是锁释放后第一个能获取到锁的线程。至此我们引出两个非常重要的概念 ==公平锁和非公平锁==。接下来我们去代码中探究下公平锁和非公平锁的实现原理

4. 公平锁和非公平锁

公平锁和非公平锁的区别就好比大家在食堂排队打菜。打菜阿姨就是那把锁。阿姨正在给同学打菜就当是上锁,阿姨给一个同学打完菜就当做是释放锁。阿姨给一个同学打完菜后干嘛呢,当然是给下一个同学打菜了,排着长队呢!but 但是这期间是有几秒钟到几十秒中不等的间隔时间哦。那万一说时迟那时快就在这个时候,有个同学不守规矩想插队,臭不要脸的把饭盒凑到阿姨手前,而阿姨又是个老实的女子,一个不忍心给插队同学打了菜(上了锁)那这个阿姨就是非公平锁。如果阿姨是个有原则的女子,喝到,这位同学请排队。那么这个阿姨就是公平锁。

//公平锁
static final class FairSync extends Sync {
        
        final void lock() {
            acquire(1);//公平锁没有插队现象,正常请求锁
        }
        
        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;
        }

    }
//非公平锁
 static final class NonfairSync extends Sync {
       
        final void lock() {
            if (compareAndSetState(0, 1))
            //非公平锁if里面的条件就是插队,
            //compareAndSetState(0, 1)
            //意思就是插队看能不能拿到锁
            //能拿到把当前线程设为sync的独占线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
            //插队没竞争过其他同学,被脾气不好的同学吓到了
            //正常请求锁
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
复制代码

大家在看公平锁和非公平锁的源码时一定要对比看他们的区别在哪里。经过以上对sync的lock()方法讲解,想必大家对公平锁和非公平锁有了一个基本的认识。至此两个问题想考考读者

  1. 线程获取锁失败后他是怎么进入AQS队列的
  2. 非公平锁只要插队失败了就立马进入AQS队列吗

咱们接着看代码,仔细对比后发现不管是FairSync还是NonFairSync的lock()方法最终还是会调用acquire(1)方法。那么acquire(1)方法又是何方神圣呢?

//AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
复制代码

只有三行的一个方法,可以说是很简单了。但是却有大玄机,乍一看easy,深一看有点晕。selfInterrupt()很是迷惑,这不是中断线程的吗,既然中断了那还玩个屁,线程都挂了,上锁有毛用。笔者刚开始看这段代码也是很疑惑,但是仔细想想,没那么简单。

首先看tryAcquire(1)方法了。他的具体实现是在Sync子类中

//公平锁
static final class FairSync extends Sync {
        
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//看锁是否空闲(打菜阿姨是否空闲)
            //hasQueuedPredecessors表示是否在队首(我们还没讲入队,有个概念就行)
            //老实孩子如果不是在队首,直接走else if
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    //如果是在队首,而且得到了公平阿姨的锁,设置当前线程为锁sync的线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
            //已经上锁了,而且是已经获取到锁的线程请求锁,前面讲到的重入锁,状态+1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
//不是队首或者是队首但是或取锁失失败或者上锁情况下,不是获取锁的线程,返回false            
    return false;
        }

    }
    
    //非公平锁
    static final class NonfairSync extends Sync {

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    
    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {//如果当前锁空闲状态
                //对比公平锁哈!!!
                //看你是非公平锁(好说话的阿姨)不管自己在不在队首,先插个队如果获取到了返回true
                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;
            }
            //如果没上锁的情况插队失败,或者上锁情况下,不是获取锁的线程,返回true
            return false;
        }
复制代码

再回看acquire(1)方法

//AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    
复制代码
  1. 如果tryAcquire(arg)返回true,后面的判断不执行,直接整个方法返回,再拿下面的代码做讲解,如果lock方法直接返回,那么说明锁获取成功,然后接着调用doSomeThing()方法,这就是为什么获取锁后,可以做后面的事情
    Lock lock = new ReentrantLock();
    try{
        lock.lock();
        doSomeThing();
    }finally{
        lock.unlock();
    }
复制代码
  1. 如果tryAcquire(arg)返回false呢,那就会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法了。这个方法就比较复杂了。先提前剧透下,让读者有个概念,后面就不那么稀里糊涂了。首先这个方法其实是分为两个部分。第一、调用addWaiter(Node.EXCLUSIVE),这个方法是什么意思呢,还记得我们前面讲过的AQS里面会有队列吧,这个方法做的事情也比较复杂,简单来讲就是,把获取锁失败的线程封装成Node,并加入到队列中,but,队列可能初始的时候是空的,所以还会有个初始化的操作哦。第二、acquireQueued(),简而言之就是,线程在队列中尝试获取锁,但是要注意,在队列里面的线程可都是规规矩矩的,必须是排在队首的才有优先获取锁的权利,这个后面源码解析部分会讲到

3.曾经天真烂漫的我以为acquireQueued()可能会很快执行完,那最终还是要调用selfInterrupt()的呀,那还不是会中断线程,在lock()方法里中断线程,不是我想要的呀,doSomeThing()我都还没机会执行呢。如果你也有同样的疑惑,那么我在剧透下,在acquireQueued()如果在队列中获取锁失败,当前线程是会被LockSupport.park()掉的,也就是阻塞,也就是说selfInterrupt()是没有执行机会,如果线程在acquireQueued()在队列中获取到了锁,线程还会判断是否被中断了,如果是被中断才会调用selfInterrupt()

我们先来总结下lock.lock()的流程,然后再详细讲解addWaiter()入队和acquireQueued()线程Node在AQS队列中尝试获取锁的详细实现

5. addWaiter入队列

通过前面的分析我们知道,当线程获取锁失败后,AQS会把线程封装成Node对象,加到AQS的队列中,详细代码如下

 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添加到队列尾端
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果队列没有初始化调用enq
        //enq做了两件事 初始化队列并把Node入队
        enq(node);
        return node;
    }
    
    private Node enq(final Node node) {
        for (;;) {//注意死循环
            Node t = tail;
            if (t == null) { //如果没有初始化尝试初始化
                if (compareAndSetHead(new Node()))
                    tail = head; //初始化成功接着循环,入队
                //走到这里说明初始化失败接着死循环初始化
            } else {
                //初始化成功 入队 并终止循环
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
复制代码

==注意!!!== ==AQS队列 Head指向的节点是一个空节点node,它不代表任何线程,它只是为了辅助找到队列中等待最久的Node节点,node的下一个节点才是真正意义上的队首节点==

6. 线程节点在AQS队列中获取锁

前面我们讲了线程加入到AQS等待队列中。当线程被加入到AQS队列时,它会判断线程所在的节点是否是队首节点(==注意不是head所指向的节点,而是head指向的节点的下一个节点==),如果是队首节点,会调用tryAcquire()获取锁,如果获取成功结束上锁。如果获取失败,会调用LockSupport.park()让该线程阻塞

  final boolean acquireQueued(final Node node, int arg) {
        try {
            boolean interrupted = false;
            for (;;) {//又是死循环 只有获取到锁才会终止
            //获取当前线程所代表的node的前面一个节点
                final Node p = node.predecessor();
                //如果前面节点是头节点,尝试获取锁
                if (p == head && tryAcquire(arg)) {
                //如果当前线程获取锁成功,将当前节点设为头节点(再次强调下,头节点只是辅助的,真正的等待最久的节点是头节点的下一个节点),并且直接returnsetHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                //获取锁失败,判断是否需要阻塞线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            throw t;
        }
    }
复制代码

7.unlock()释放锁

我们都知道,当一个线程调用lock.unlock(),那么该线程会释放掉对锁的使用权,其他新来的线程可以通过lock.lock()获取锁。那么那些曾经想索取lock的使用权失败的线程,他们被存储在lock的sync对象的队列中了,并且在多次尝试加锁失败后,线程被强制阻塞了,他们是在什么时候被唤醒并且再次得到竞争锁的机会呢,下面通过探究unlock()方法来一探究竟。

unlock()相对lock()简单一些。它的调用逻辑如下

Lock.unlock() --> sync.release(1) --> sync.tryRelease(1) 我们重点来讲一下release 和tryRelease

public final boolean release(int arg) {
        //尝试释放锁
        if (tryRelease(arg)) {
            //释放成功,获取到AQS队列的头节点
            Node h = head;
            if (h != null && h.waitStatus != 0)
            //从头节点往队尾找到一个没有被取消的线程节点,唤醒它(只找一个不是所有的后面的节点)
                unparkSuccessor(h);
            return true;
        }
        return false;
}

 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            //只有获取到了锁的线程才有资格调用该方法,完美的解释了没有获取锁的线程直接调用unlock()会报IllegalMonitorStateException
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }
    
复制代码

8. 小结&一些唠叨的话

到这里已经基本上把Lock(主要是ReentrantLock)的lock()和unLock()方法做了一个比较全面的讲解。如果有讲错或者遗漏的地方欢迎大家评论与我互动。接下来会讲解下Condition在java同步包中的实现,todo list如下

  • [ ] 讲解Condition的原理,Condition的队列,

  • [ ] condition await()释放锁,park线程

  • [ ] condition signal(),把当前ConditionObject中的Node队列enque到AQS队列中去

最后欢迎添加我的微信,一起互动交流Java,一起成长

你可能感兴趣的:(Java并发系列之一 Lock源码解析)