AQS分析第三篇(借助ReentrantLock探索AQS独占模式的实现原理)

问题是最好的老师!

一、思考

问题一:关于Node节点的属性(waitStatus和nextWaiter)?

第一篇中我们了解到AQS中队列的Node节点的数据结构,其中Node有几个属性waitStatus和nextWaiter,我们只是从注释翻译了解到其基本概念,那么AQS中不同情况下这两个属性的取值是怎么样的,这一点需要我们取思考。

问题二:AQS内部是如何使用队列的?

 我们通过前面了解到AQS队列中存储的是线程,那么什么情况下将线程入队,怎样入队,入队之后怎样唤醒。

问题三:AQS中阻塞和唤醒线程采用的是什么方式?

 我们知道常用的阻塞唤醒有Object.wait/notify,但是AQS内部内部并没有一个共享对象Object,其只是维护了一个state,那么它是怎样阻塞和唤醒的。而且一般阻塞唤醒我们都采用线程间通信的方式,而线程间通信一般采用Condition.await/signal和Object.wait/notify,那么AQS内部是如何进行线程间通信的。

问题四:共享模式和独占模式究竟有什么区别,是如何实现的?

 独占模式比较好理解,当某个线程独占锁之后,其他线程阻塞即可。但是共享模式我们可能比较模糊,即一个线程获取锁之后,其他线程依然可以获取,那么这里就有疑问,既然其他线程都可以获取,那阻塞在哪里?阻塞谁?什么情况下阻塞?

问题五:ReentrantLock公平锁和非公平锁有什么区别?

我们将从实现原理去剖析一下这种差异性。

二、源码分析

要了解ReentrantLock,必然要了解如何使用它,而ReentrantLock的一般使用方式为:

//该锁默认为非公平锁
ReentrantLock lock=new ReentrantLock();

public void synchronizeMethod(){

    lock.lock();
    
    try{

        doSomeThing();

    }finally{

        lock.unlock();    

    }

}

在上面这段代码中,关于ReentrantLock有三个定义:

1.ReentrantLock的定义:我们可以定义为非公平锁(默认)或者公平锁(调用ReentrantLock(boolean fair)方法)。

2.线程获取锁:lock.lock()该方法不响应中断,若响应中断还可调用lock.lockInterruptibly()方法。

3.线程释放锁:lock.unlock()。

 

那么接下来我们将从这三个定义去分析ReentrantLock的源码和AQS的源码,因为ReentrantLock为独占锁,在第二篇中我们有提到,如果只需要独占模式,那么实现AQS的子类是不需要定义共享模式锁所涉及方法的,所以该篇先只分析独占模式的实现。

2.1非公平锁

  • Part1:ReentrantLock的定义

//默认为非公平锁
public ReentrantLock() {
        sync = new NonfairSync();
}

还记得我们第二节所讲的AQS使用方式吗,我们先通过ReentrantLock去论证前两点:

1.实现AQS的子类需要定义为内部类

2.需要实现AQS定义的受保护方法

//注意java中并不存在多个继承,这里只是为了看的更直观

//AQS使用方式一:子类需要定义为内部类
static final class NonfairSync extends Sync extends AbstractQueueSynchronizer{
    
    //这里只列举部分方法,说明AQS使用方式
    
    //以下这些方法的解释请看AQS第二篇,具体实现请看下文分析

    //这里主要通过构造函数初始化之后,AQS的state初始值为0,而ReentrantLock中state为0,则代表
    //锁未被占有
    NonfairSync() {
    }

    Sync() {
    }

    //AQS实现方式二:子类实现受保护方法tryAccquire
    protected final boolean tryAcquire(int acquires) {
    }

    //AQS实现方式二:子类实现受保护方法tryRelease
    protected final boolean tryRelease(int releases) {
    }

    //AQS实现方式二:子类实现受保护方法isHeldExclusively
    protected final boolean isHeldExclusively() {
    }
}

 

  •  Part2:lock.lock()实现原理

该部分比较重要,也是AQS的实现原理,会分析的比较细,故内容比较多,请大家耐心看完,分析不对的地方请大家多指点。

Method:lock()

//线程独占锁
final void lock() {
            //类似快速失败方式
            //尝试设置sate为1(sate为0上文解释过),获取锁

            //注意这里可能有多个线程并发调用,那么我们知道这里必须要保证state值的可见性,看AQS中 
            //state定义:private volatile int state(volatile保证可见性);

            //具体compareAndSetState实现请看下文
            if (compareAndSetState(0, 1))
                //如果通过快速尝试的方式获取锁成功,则设置当前占有锁的线程为当前线程
                //具体setExclusiveOwnerThread请看下文
                setExclusiveOwnerThread(Thread.currentThread());
            else
                //通过快速尝试的方式获取锁失败,则要么再次尝试(可能还会成功),要么入队

                //在第二篇中也将到调用accquire方法的传参一般都是1,代表将要获取锁,因为为0则代表
                //锁没有被占用

                //具体accquire方法实现请看下文
                acquire(1);
}

Method:compareAndSetState(int state)

//expect:期望原值   update:新值
//语义:如果原值为expect,则更新为update
//返回值:true代表更新成功,失败则代表其他线程可能已经更新了原值
protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

Method:setExclusiveOwnerThread(Thread thread)

protected final void setExclusiveOwnerThread(Thread thread) {
    //独占线程,null代表无线程占用锁    
    exclusiveOwnerThread = thread;
}

Method:acquire(int arg)

//在第二篇tryAccquire方法介绍中有提到,线程需要通过调用accquire方法从而调用tryAccquire方法,这里
//得到论证


public final void acquire(int arg) {
        //如果尝试获取锁失败,则将该线程入队并阻塞(这是重点)

        //如果阻塞被唤醒之后acquireQueued方法返回true则中断自己,这里我们先不着急,一步一步看

        //这里具体各个方法的解释请看下文
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

Method:tryAcquire(int acquires)

//该方法由子类自己实现
protected final boolean tryAcquire(int acquires) {
            //请看下文nonfairTryAcquire方法
            return nonfairTryAcquire(acquires);
}

Method:nonfairTryAcquire(int acquires)

//通过非公平方式尝试获取锁
final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //上文中有讲到快速尝试,这里再进行一次尝试获取锁
                if (compareAndSetState(0, acquires)) {
                    //获取成功,设置独占线程为当前线程
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //因为ReentrantLock为可重入锁,支持同一线程多次获取锁
            //判断如果是同一线程再次获取锁
            else if (current == getExclusiveOwnerThread()) {
                //对于同一线程获取锁state依次递增
                int nextc = c + acquires;
                //int最大值加一会变成负数
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                //这里因为是同一线程,其已经占用锁,所以必然是现场安全的,直接设置state值即可。
                //setSate方法解释请看下文
                setState(nextc);
                return true;
            }
            //获取锁失败,请移步accquire方法,看下一步对应的方法调用
            return false;
}

Method:setState(int newState)

//为state赋值
protected final void setState(int newState) {
        state = newState;
}

Method:addWaiter(Node mode)

//采用指定的模式为当前线程创建新的Node,并将其入队

//mode:当前线程占用锁的模式,accquire方法中传入为Node.EXCLUSIVE

//返回值:代表当前线程的Node
private Node addWaiter(Node mode) {
        //创建一个新的Node,请看下文Node的构造函数
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure

        //通过快速尝试的方式入队,如果失败,则执行enq()方法
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            //如果尾节点不为空,则通过CAS入队,这里队列的相关知识,不再进行介绍
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //快速尝试的方式失败,则调用enq方法入队,请看下文enq方法
        enq(node);
        //成功入队之后,请移步acquireQueued方法
        return node;
}

Method:Node(Thread thread,Node mode)

Node(Thread thread, Node mode) {     // Used by addWaiter
            //这里我们设置了nextWaiter的值,如果是独占模式就是Node.EXCLUSIVE
            //共享模式就是null
            //那么该值是如何用的呢,请接着看
            this.nextWaiter = mode;
            this.thread = thread;
}

 Method:enq(Node node)

//将指定Node入队
private Node enq(final Node node) {
        //CAS加失败重试机制
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                //队列为空,队列初始化,具体请看下文compareAndSetHead方法
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                //队列不为空,入队具体请看下文compareAndSetTail方法
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

 Method:compareAndSetHead(Node update)

//CAS设置队头节点
private final boolean compareAndSetHead(Node update) {
        return unsafe.compareAndSwapObject(this, headOffset, null, update);
}

Method:compareAndSetTail(Node update)

//CAS设置队尾节点
private final boolean compareAndSetTail(Node expect, Node update) {
        return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}

Method:acquireQueued(Node node,int arg)

//在看这个方法之前,我们先思考一下整个流程

//前面我们判断当前线程无法获取到锁,通过addWaiter已经将node加入了队列。

//那既然该线程无法获取锁,我们需要做的就是将该线程阻塞。但是如果直接阻塞的话,我们试想这种情况, 
//此时其他线程已经执行完毕,释放了锁,那么该线程就没必要阻塞了。或者该线程在判断无法获取锁之后到 
//入队,这中间有一个时间差,如果在这个时间差内其它线程都已经执行完毕 并释放了锁,然后判断队列中还 
//没有该线程,自然无法唤醒它,那么该线程将会一直阻塞下去。

//通过以上分析,我们可以猜测该方法做了以下事情:
//
//1.阻塞之前判断该线程是否需要阻塞。
//2.如果需要阻塞则阻塞当前线程,否则直接返回。

//返回值:该方法在队列中阻塞获取锁的过程中有没有被中断
final boolean acquireQueued(final Node node, int arg) {
        //最终是否成功获取到锁
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋直到获取到锁为止
            for (;;) {
                //获取到当前node的前驱节点,详情请看下文predecessor方法。
                final Node p = node.predecessor();
                
                //如果其前一个节点为队头节点,则尝试获取锁
               
                //记得我们在第一篇分析AQS数据结构时,讲到head就代表当前拥有锁的线程,原因就在于
                //以下这段代码

                //如果p当前正在运行的节点,就尝试获取,因为p为head说明没有其他线程等待了
                //而且p有可能刚好执行完成了
                if (p == head && tryAcquire(arg)) {
                    //如果获取锁成功,则将head设置为当前获取锁的线程
                    setHead(node);
                    //将前一个执行成功的线程进行垃圾回收
                    p.next = null; // help GC
                    //成功获取到锁
                    failed = false;
                    //未被中断
                    return interrupted;
                }
                //两种情况将走到这里
                //1.该Node前一个节点不是head,即当前线程的前一个线程不是正在运行的线程,那么
                //就不进行尝试,因为可能还有其他线程在等待。
                //2.前一个线程是正在运行的节点,但是该线程获取锁失败,说明锁还被前一个线程占用
                
                //这里在阻塞之前先判断是否应该阻塞,如果需要则阻塞
                //详情请看下文shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //parkAndCheckInterrupt返回true,表示等待的过程中被中断
                    //但是此时我们并不能直接返回,因为可能会有后续的线程已经把当前线程设置为
                    //前驱节点,所以我们仍然自旋让该线程获取到锁,但是获取到锁之后,会中断该
                    //线程,然后在释放锁的时候唤醒其后继线程,不然其后继线程是没法被唤醒的。
                    interrupted = true;
            }
        } finally {
            //前面如果正常执行结束,线程正常获取到锁,那么failed必然为false,只有在有异常抛出的
            //情况下,faied将为true。
            if (failed)
                //该线程在获取锁的过程中异常(这里AQS自己是不会抛出异常的,但是tryAccqure方法 
                //由子类实现,所以要兼容可能抛出异常的可能性),则取消获取锁
                
                //详情请看cancelAcquire方法
                cancelAcquire(node);
        }
}

看完该方法请移步accquire方法继续。

Method:predecessor()

final Node predecessor() throws NullPointerException {
            //prev指针指向前驱节点
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
}

Method:shouldParkAfterFailedAcquire()

//在分析该方法之前,我们也做一个思考

//在这之前,我们已经判断该线程的前一个线程不是head或者是head但是获取锁失败,那么这种情况下为什么
//不能直接进行阻塞呢,还需要干什么?

//假设我们直接阻塞,那么可能该node的前一个节点已经被中断或者取消等待,那么其前一个节点是没法通知
//该节点的,该线程将无法唤醒。


//该方法将会涉及到我们第一篇中锁提到的waitStatus属性,这里再回忆一下其取值代表的意思:
//0:默认
//1(CANCELLED):由于超时或者中断等原因取消等待,一旦取消,该线程将不会再获取锁
//-1(SIGNAL):表示该线程的后继线程需要被唤醒
//-2(CONDITION):表示该线程在某个条件上等待
//-3(PROPAGATE):表名下一个获取锁的线程需要无条件传播,只有在共享锁才会用到

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果前驱节点已经知道其后继节点是需要被唤醒的则可以安全阻塞,这样保证了该阻塞的线程一定
        //能正确的被唤醒。

        //这里一为什么会为-1,因为该线程第一次执行该方法时,判断该值不为-1时(一般为默认值0), 
        //并不会立即阻塞该线程,而是先设置为-1,然后再重新尝试获取一次锁。
        
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        //如果前驱节点已经因为等待超时或者中断原因取消,不再获取锁,那么其将不会通知该线程
        //此时需要一直往前遍历,找到一个未被取消的线程,将其设置为该节点的前驱节点
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            //如果其前驱节点为0(默认就是0,也是绝大多数情况)或者-2,-3(什么情况下会为这两
            //个值后面再分析),则将其前驱节点的waitStatus设置为SIGNAL,让其释放锁之后唤醒该线
            //程。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        //如果成功设置了前驱节点的waitStatus为-1之后,代表线程可以被正确唤醒,但是这是并不立即
        //阻塞该线程,而是再重新尝试获取一次锁看看(这是有可能成功的,此时其他线程已经释放锁)
        return false;
}

看完该方法请继续移步accquireQueued方法,继续往下看。

Method:parkAndCheckInterrupt()

//阻塞当前线程,并判断中断标志位
//至于LockSupport方法请移步博文“LockSupport分析”,这里不做过多解释
private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        //大家想一下,为什么会走到这行代码,线程中断之后,如果当前线程处于阻塞状态不是会抛出异常吗
        //阻塞中断抛异常的问题,请移步博客“线程中断”,这里不做过多解释
        
        //那这里唯一的解释就是,LockSupport.park方法的在被中断之后是不会抛出异常的,而只是设置了
        //线程中断标志位
        return Thread.interrupted();
}

看完该方法请继续移步accquireQueued方法

Method:cancelAcquire()

//这里我们先思考一下,如果当前线程取消等待,那么可能有这种情况:该线程已经入队列,而且其后继线
//还等着它去唤醒,而唤醒是在释放锁的时候,但是该线程根本不会拥有锁,无法唤醒,所以在这里要找到一
//个能唤醒后继线程的线程。然后将该节点从队列中移除 
private void cancelAcquire(Node node) {
        if (node == null)
            return;

        node.thread = null;

        //如果当前节点已经是CANCELLED状态,则往前遍历,找到一个waitStatus小于0的节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        Node predNext = pred.next;

        //将当前Node设置为取消等待,先这样做的目的是,避免在执行该方法时,其他线程突然进来,设置
        //该Node为其前驱节点
        node.waitStatus = Node.CANCELLED;

        //如果当前需要需要取消等待的节点是尾节点,则通过CAS设置队尾节点为其前一个节点
        if (node == tail && compareAndSetTail(node, pred)) {
            //通过CAS将当前节点的前一个节点设置为队尾只有,通过CAS设置尾节点的下一个节点为空
            compareAndSetNext(pred, predNext, null);
        } else {
            //如果当前节点不是尾节点
            int ws;
            //前面找的这个pred节点是waitStatus不为1的节点
            //当满足以下两点时,将当前节点的前驱节点的next指向当前节点的后继节点 
            //使当前节点的后继可以被正常唤醒
            //1.当前节点的前驱节点不为头节点
            //2.当前节点的前驱节点为SIGNAL或者通过CAS设置为SIGNAL成功
            //3.当前节点的后继节点存在且不为CANCELLD。
            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 {
                //详情请看下文unparkSuccessor方法
                //注意这里唤醒后继节点只是让后继节点继续自旋去获取锁而已(请看accquireQueue方 
                //法),这是pred线程可能还没执行完成,后继节点依然可能会获取锁失败。
                unparkSuccessor(node);
            }

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

Method:unparkSuccessor()

//唤醒Node节点的后继节点(未被取消)所在线程
private void unparkSuccessor(Node node) {
       
        int ws = node.waitStatus;
        //如果当前节点的waitStatus小于0,则将其设置为0,因为小于0代表其需要通知其他线程,但是该方
        //会执行通知操作
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //找到当前节点的后继可被唤醒节点
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            //当该节点的下一个节点为空时,从队尾节点往前遍历找到一个第一个(从前往后)未被取消的节 
            //点。
            
           //为什么从队尾开始找:因为s可能为null
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        //唤醒找到的节点所在线程
        if (s != null)
            LockSupport.unpark(s.thread);
    }

至此,ReentrantLock中获取独占锁的源码已经分析完毕,我们会在文章结尾进行统一总结,接下来,我们复习一下ReentrantLock是如何释放锁的。

  • Part3:lock.unlock()的实现原理

释放锁的过程相对于获取锁的过程更简单一些,接下来我们看看其实现原理。

Method:unlock()

public void unlock() {
        //详情请看下文Sync类的release方法
        sync.release(1);
}

Method:release()

//前面有讲过线程通过调用release方法从而调用tryRelease方法释放锁
public final boolean release(int arg) {
        //如果尝试释放锁成功,则唤醒后继线程
        //详情请看下文tryRelease方法
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //从头结点(代表当前线程)开始往后找到一个可以被唤醒的线程
                //详情请看上文unparkSuccessor方法
                unparkSuccessor(h);
            return true;
        }
        return false;
}

Method:tryRelease()

//尝试释放锁
//一般情况下release值为1 
//返回值:是否成功释放锁
//注意:这里线程已经持有锁,所以调用线程安全的方法取修改state状态
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果state为0代表没有线程占用锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //更新state的值
            setState(c);
            return free;
}

至此我们分析完了ReentrantLock非公平锁的定义,获取锁和释放锁的实现原理。现在我们来总结一下:

  • 非公平锁定义

定义内部类NonfairSync继承AQS并重写其受保护的方法(这里我们并不比较其和公平锁的差异,等讲完公平锁之后,再进行比较)作为ReentrantLock同步辅助类。

  • 获取锁

1.state值为0代表锁未被占用,使用CAS设置state值为1,并设置独占锁的线程为当前线程。

2.state值不为0,代表锁已经被占用,如果获取锁的线程为当前线程,则可重入;当其他线程竞争该锁时将其加入队列,并进行阻塞。

  • 释放锁

拥有锁的线程设置将state的值减1,如果state为0,设置占用锁线程为null;然后从队列头节点开始唤醒后继节点所代表的线程。

2.2公平锁

AQS分析第三篇(借助ReentrantLock探索AQS独占模式的实现原理)_第1张图片

上图是公平锁和非公平锁的实现差别,我们看到唯一不同的是获取锁,而释放锁的过程是相同的,接着我看下获取锁的差异性体现在哪里。

Method:lock()

final void lock() {
            //非公平是先采用快速尝试的方式去获取锁,那么可能后执行的线程会先抢锁成功,为了直观把非
            //公平锁的获取方式贴到下面,可以对比
            
            //对于该方法的实现请看上文
            acquire(1);
}


final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
}

Method:tryAccquire()

//采用非公平的方式获取锁,请大家对比公平锁的获取锁方式看,这里我们只讲唯一区别的地方
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                //通过对比发现其与非公平锁只差这一行代码,这里会进行如下校验:
                //hasQueuedPredecessors(队列终是否已经有非自身的等待线程)
                
                //详情请看下文:hasQueuedPredecessors方法
                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;
}

Method:hasQueuedPredecessors()

//查询是否有其他线程在该线程之前等待
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;

        //当满足以下情况认为当前线程需要等待
       //1.h!=t:因为h==t说明要么队列为空,此时线程当然可以获取锁。或者队列中只有一个节点在运行, 
       //这时锁被队头元素占用(当前占用锁的线程),其可以去尝试获取一下,因为队头线程可能刚好执行完 
       //成。

        //2.h.next==null:该点暂未理解

        //3.h.next!=null且h.next代表的线程不是当前线程(不是可重入情况),因为可重入是可以获取锁 
        //的。
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
}

至此公平锁和非公平锁已经分析完毕,接下来我们解答一下开篇的几个问题。

三、开篇解答

问题一:关于Node节点的属性(waitStatus和nextWaiter)?

答:当某个线程获取不到锁时,会创建代表该线程的Node,该Node默认的默认waitStatus取值为0。但是此时并不能安全将其入队,因为该Node的前驱节点锁代表的线程可能已经因为异常等原因取消等待,这将导致该线程无法被唤醒,所以我们需要为该线程找到一个可以正常唤醒自己的节点,并设置该节点的waitStatus为SIGNAL(-1),代表其需要通知其后继线程。在某个线程释放锁时,会从head节点(持有锁的线程)开始找到其后继的一个waitStatus<=0的节点,将其唤醒。对于nextWaiter取值,在ReentrantLock中,对于每个节点都是Node.EXCLUSIVE,但是该值并未使用到,我们将在后续博文中发现其作用。

问题二:AQS内部是如何使用队列的?

答:关于AQS的数据结构在第一篇中我们已经讲到过,这里我们注意一下以下几点:

1.队列的头结点代表当前持有锁的线程(具体请看acquireQueued方法,在线程获取锁之后会调用setHead方法设置对头为当前节点)。

2.阻塞入队时,将Node加入到队尾之后,会为其找到一个可唤醒自己的前驱节点,并将其前驱节点到自己之间已取消等待的节点移除。

3.释放锁,唤醒线程时,从队头开始,找到一个未取消等待的节点将其唤醒。

问题三:AQS中阻塞和唤醒线程采用的是什么方式?

答:采用LockSupport.park()和LockSupport.unpark(),具体请看【博文LockSupport分析(还没写)】。注意这里的唤醒,只是让唤醒其获取锁的阻塞,唤醒之后,其仍然要重新竞争锁。

问题四:共享模式和独占模式究竟有什么区别,是如何实现的?

答:通过ReentrantLock我们了解到了独占模式,即锁只能被一个线程独占,当其他线程获取锁时,将阻塞。对于共享模式请看博文【AQS分析第四篇(通过CountDownLatch分析AQS共享模式实现原理】

问题五:ReentrantLock公平锁和非公平锁有什么区别? 

答:其区别在于获取锁时的条件判断。

公平锁:某个线程获取锁时,如果队列中已经有线程等待,则当前线程获取锁必然失败,保证了先获取锁的线程先被通知。

非公平锁:当某个线程获取锁时,即使队列中有线程等待,依然会去竞争锁,这样可能后进来的线程先获取到锁。

你可能感兴趣的:(杂记---并发)