Lock 源码分析 --ReentrantLock

本文详细分析了ReentrantLock的源码,包括内部类FairSync和NonfairSync,以及它们如何继承Sync和AbstractQueuedSynchronizer。通过双向链表Node实现线程的序列化访问,公平锁和非公平锁的区别在于获取锁的策略。在获取锁失败时,线程会被添加到链表尾部并进入等待状态,当锁被释放时,会唤醒下一个节点的线程。

ReentrantLock 重入锁,从这里开始解析Lock的源码及机制

首先从一个demo开始,这段代码循环5次,每次起一个线程,获取锁,执行逻辑,解锁.这篇的重点不在这个demo,无需过度关注.

 

public class ThreadMain {
    private  static int sum=0;
    private static ReentrantLock lock=new ReentrantLock(true);
    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<5;i++){
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    lock.lock();
                    if (finalI%2==0){
                        try {
                            System.out.println("--------"+finalI+"--------");
                            Thread.sleep(2000);
                            System.out.println("--------"+finalI+"--------");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    sum++;
                    System.out.println(sum+"-----"+ Thread.currentThread().getName());
                    lock.unlock();
                }
            }).start();
        }
    }

}

先从代码结构看起,ReentrantLock 类存在三个内部类分别是

Sync,FairSync,NonfairSync

其中FairSync,NonfairSync继承了Sync,而这个Sync继承了AbstractQueuedSynchronizer

AbstractQueuedSynchronizer的结构如下,AbstractQueuedSynchronizer中存在一个内部类Node,这个node就是本篇的重点了.


接下来重点分析 AbstractQueuedSynchronizer与Node.

Node结构如下,可以看到Node存在prev(前一个Node),next(后一个Node),thread(记录线程),nextWaiter(等待属性,后文会继续讲)等属性,从这个结构可以看出来,Node为双向链表,每个Node都会记录它的前一个节点与后一个节点,每个节点都会有thread属性,用来记录当前线程.

 然后是AbstractQueuedSynchronizer,可以看到里面存在一个head(头节点)与一个tail(尾节点).

至此,可以放出一个结论 (过程分析下部分继续说明),重入锁(ReentrantLock)通过双向链表(Node)的方式使得线程序列化访问临界资源.如下图所示,node的双向链表,每个node记录前驱及后续节点,AbstractQueuedSynchronizer中的head记录头节点,tail记录尾节点.


接下来分析流程

打上断点,跟进,此时走的是第一个线程"Thread-0"

进入断点看到源码,由于测试main方法中定义的是公平锁,所以进入FairSync中的lock()方法

这里插一句,公平锁与非公平锁的区别就在于新的线程获取锁的时候,公平锁会排队到队尾,非公平锁会先尝试获取锁,没有获取到再排到队尾,非公平锁的源码如下,标记部分即为区别,这里可以看到进行了CAS操作,如果成功了,则将当前线程设置为锁的独占线程.

 

回到公平锁的源码 ,最后会执行acquire(1)这行代码.

 acquire(1)里面的逻辑如下

这里稍微说道说道

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



//尝试获取锁,获取成功返回false,失败则返回true
!tryAcquire(arg)

//添加一个新节点Node,并将其置于链表尾部
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)


//给当前线程设置一个中断标识位,线程并不会中断,只是一个标识位
selfInterrupt();

 总结起来就是:试图获取锁,如果获取失败则添加一个Node对象置于链表尾部,并给当前线程设置一个中断标识位,如果锁获取成功,则不执行后续逻辑

当第一个线程进入时,此时锁并未被占有,所以能够获取成功

同样来分析一下这段代码

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");
                //更新锁的状态值(重入锁,每次线程进入+1,释放则-1)
                setState(nextc);
                return true;
            }
            return false;
        }

分析一下: 判断当前锁的状态值state,如果等于0,则意味着当前没有线程占有该锁,接下来执行以下逻辑

if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }



//判断当前的Node链表头(head)与链表尾(tail)是否不等,如果不等则判断链表头的下一个Node是否为空或者链表头的下一个Node记录的线程是否等于当前线程,以此来校验结构是否正确
public final boolean hasQueuedPredecessors() {
        Node t = tail; 
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }



//通过CAS更新State的值
compareAndSetState(0, acquires)

//将当前线程设置为锁的独占线程
setExclusiveOwnerThread(current)

 将当前线程设置为锁的当前独占线程后,意味着锁获取成功,至此,第一个线程获取了锁,目前没有释放,下一个线程去获取锁时,必定无法获取成功,接下来分析下一个线程获取锁时发生了什么.

当下一个线程进入时,同样走入了tryAcquire(arg)这行代码,但必定无法获取成功,此时线程0正在持有锁,并且没有释放,那么线程1只能在后续排队,接下来分析,这个队是怎么排的.

 接下来分析acquireQueued(addWaiter(Node.EXCLUSIVE), arg)这行代码

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



//添加一个新节点,并将其放入链表中
 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

由此先看addWaiter(Node.EXCLUSIVE), arg)这行代码

添加一个独占节点,具体逻辑,进入断点查看

这里添加了一个节点,并将当前线程赋值给该Node的thread属性,之后判断当前链表的尾节点 ,

private Node addWaiter(Node mode) {
        //获取当前线程,并将其赋值给Node
        Node node = new Node(Thread.currentThread(), mode);
        //获取尾节点
        Node pred = tail;
        if (pred != null) {
            //如果尾节点不为空,则意味着当前队列中还有其他线程,那么把node对象的前置节点赋值为当前的尾节点
            node.prev = pred;
            //通过CAS将当前节点修改为tail(尾节点标识)
            if (compareAndSetTail(pred, node)) {
                //修改完成后将之前尾节点的next指向当前的node
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }




private Node enq(final Node node) {
        for (;;) {
            //获取尾节点
            Node t = tail;
            if (t == null) { // Must initialize
                //如果尾节点为空,则通过CAS将头节点设置为一个空的Node
                if (compareAndSetHead(new Node()))
                    //并把头节点赋值给尾节点
                    tail = head;
            } else {
                //如果尾节点不为空,则将node的前置节点赋值为当前的tail(尾节点)
                node.prev = t;
                //通过CAS赋值tail(尾节点)为当前的node
                if (compareAndSetTail(t, node)) {
                    //将之前尾节点的next指向当前的node
                    t.next = node;
                    return t;
                }
            }
        }
    }

这里通过一个自旋来完成节点结构的维护,第一次进入时通过CAS给head(头节点)赋值一个空的Node,并将这个Node赋值给tail(尾节点),下一次进入时t就不为空了,进入else里面的逻辑,将node的前置赋值为t(尾节点),并将t的next赋值为node,这样就完成了一次双向链表的插入操作,将node插入到了当前双向链表的尾部,并将tail指向它.

接下来继续分析acquireQueued(addWaiter(Node.EXCLUSIVE), arg)中acquireQueued(final Node node, int arg)这部分的代码.

 这里也存在一个自旋,该方法的作用是维护队列中的线程包括更新waitStatus状态.

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取节点的前置节点 (prev)
                final Node p = node.predecessor();
                //如果前一个节点是head,那么尝试获取一次锁
                if (p == head && tryAcquire(arg)) {
                    //获取锁成功则将node设置为head(在这可以知道head节点即为获取了当前锁的节点)
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    //结束自旋
                    return interrupted;
                }
                //更新waitStatus状态,删除已经失效的线程对应的node,完成更新后阻塞该线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


//通过多次循环来维护waitStatus的值
 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            //waitStatus为-1 代表着这个node的下一个节点(next)对应的线程处于可以唤醒状态
            return true;
        if (ws > 0) {
            //waitStatus>0 代表着node的下一个节点对应的线程为失效状态,删除它
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //通过CAS更新waitStatus的值
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

正如上文代码注释所示,通过自旋维护waitStatus值之后,下一次再进入时会执行到如图所示代码,这行代码的作用即为阻塞当前线程

在当前线程被唤醒之前,无法执行到断点的下一行代码,线程阻塞

 至此ReentrantLock的加锁过程执行完毕,下一步看看解锁时发生了什么.


跟入断点,进入unlock()源码,可以看到调用了release()方法

 release()源码如下

接下来分析一下逻辑 

public final boolean release(int arg) {
        //尝试释放锁
        if (tryRelease(arg)) {
            //获取当前的head节点
            Node h = head;
            //head节点不为空,并且head节点的waitStatus不为0
            if (h != null && h.waitStatus != 0)
                //唤醒下一个节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }



//尝试释放锁的方法
protected final boolean tryRelease(int releases) {
            //计算更新state值
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                //将当前锁的独占线程置为空
                setExclusiveOwnerThread(null);
            }
            //更新state值
            setState(c);
            return free;
        }


//唤醒node对应的下一个节点
private void unparkSuccessor(Node node) {
        //拿到node的waitStatus值,用于判断该node的下一个节点是否为可唤醒的
        int ws = node.waitStatus;
        if (ws < 0)
            //如果状态<0则通过CAS修改waitStatus的值
            compareAndSetWaitStatus(node, ws, 0);
        //获取node的下一个节点
        Node s = node.next;
        //这里需要注意,如果node的下一个节点的waitStatus >0意味着线程为失效的,此时唤醒的是尾节点的前一个(可被唤醒的)节点对应的线程
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            //唤醒s.thread 也就是头节点的下一个节点对应的线程
            LockSupport.unpark(s.thread);
    }

最后通过

LockSupport.unpark(s.thread);

这行代码将下一个节点唤醒.

唤醒后看看现象,此时线程1从阻塞中被唤醒

 

 当线程1被唤醒后,进入下一次的循环,node的前一个节点为head节点,然后尝试去获取锁

于是回到了本文开始讲到的获取锁的流程

 这里说一个特殊点,当释放锁时,会去唤醒当前节点的下一个节点对应的线程,如果那个线程是失效的,则会唤醒尾节点的前一个(可唤醒的)线程

 

 也就意味着,被唤醒的这个线程,其实是"插队"的,它本应该是倒数第二个节点,执行顺序应该排在末位,但被提前唤醒了.当这个"插队线程"释放锁时,会重新唤醒head节点的下一个节点.


总结一下:

1:ReentrantLock通过AbstractQueuedSynchronizer的一个内部类Node来维护一个双向链表结构,每个node中记录了上一个节点和下一个节点以及waitStatus

2:公平锁获取锁时,会将自己的node置于链表队尾,非公平锁在获取锁时会直接尝试CAS操作(尝试插队),操作失败才会去排队(置于链表队尾)

3:node对象中的线程能否被唤醒,取决于该节点的上一个节点的waitStatus

4:waitStatus的变化状态为0 =>-1 =>0

 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值