博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java并发编程系列-ReentrantLock原理分析
阅读量:6996 次
发布时间:2019-06-27

本文共 11529 字,大约阅读时间需要 38 分钟。

hot3.png

ReentrantLock全程为可冲入锁,可重入意思就是线程可以多次获取锁,ReentrantLock是基于AQS而设计的AQS全称AbstractQueuedSynchronizer,中文名抽象队列同步器,是并发包JUC中最基本的组件,JUC包中大部分类都基于它实现的并发处理故其为一个抽象类,使用AQS需要去继承该类并重写其被protected修饰的几个方法。AQS内部实现了一个FIFO队列,队列中的节点维护了当先线程的引用及其状态,每个线程都可以视为AQS中的一个节点,AQS中节点的定义如下:

static final class Node {        volatile int waitStatus;        volatile Node prev;        volatile Node next;        volatile Thread thread;        Node nextWaiter;}

既然是队列其实现无非就是数组或者链表,在AQS中实现为双向队列,其中prev为节点的前驱,next为节点的后驱,thread为当前线程,nextWaiter为存储condition队列中的后继节点,waitStatus为节点等待状态,主要有以下几种状态:

waitStatus=1,线程被取消

waitStatus=2,线程正在等待一个condition。

waitStatus=-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark。

waitStatus=3,表示当前场景下后续的acquireShared能够得以执行。

waitStatus=0,表示当前节点在sync队列中,等待着获取锁。

简单介绍了AQS,下面我们将从源码分析ReentrantLock的工作原理。

ReentrantLock实现了Lock接口主要有以下方法可以使用,通常我们使用Lock时会配合finally一起使用,原因是不管什么情况下必须要释放锁不然后续资源就得不到释放。

获取非公平锁

我们首先看lock()接口,ReentrantLock支持公平锁和非公平锁,默认构造函数是非公平锁,下面我们将通过下面代码例子类解析非公平锁:

try{  Lock lock=new ReentrantLock();  lock.lock();  //todo 业务代码}finally{  lock.unlock();}

默认构造函数实现的如下,实例化了一个非公平同步器。

public ReentrantLock() {        sync = new NonfairSync();    }

NonfairSync是什么东西呢,NonfairSync是一个简单的内部静态类继承了Sync。

static final class NonfairSync extends Sync {        private static final long serialVersionUID = 7316153563782823691L;        /**         * Performs lock.  Try immediate barge, backing up to normal         * acquire on failure.         */        final void lock() {            if (compareAndSetState(0, 1))                setExclusiveOwnerThread(Thread.currentThread());            else                acquire(1);        }        protected final boolean tryAcquire(int acquires) {            return nonfairTryAcquire(acquires);        }    }

而Sync是一个抽象内部静态类继承了AbstractQueuedSynchronizer并重写了其protected方法。

我们首先看lock.lock();代码是如何执行:lock()方法直接调用的是Sync中的抽象方法,这个抽象方法在NonfairSync中被实现具体代码如上述。我们知道线程是否获取到锁是通过AbstractQueuedSynchronizer中的下属字段来描述,如果state=0说明没有线程获取到锁,如果state大于0说明有线程获取到了锁。

private volatile int state;

compareAndSetState指令就是通过CAS将state设置为1,如果设置成功了认为线程获取到了锁,并且设置占用排它锁的线程为当前线程。否则尝试非公平获取锁即调用下面方法。

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

该方法为AbstractQueuedSynchronizer中一个被final修饰的方法,显然不允许被子类重写。这个方法比较重要,我们分别分析其是如何工作的。

首先调用tryAcquire这个方法,如果成功了那么获取到锁,该方法是一个protected修饰的方法用于被子类重写,最终调用的是:

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;        }

在这个方法中首先再次判断是否有现成获取到了锁,如果没有则再次尝试CAS获取设置锁状态位为1,成功则说明获取到了锁,否则判断当前获取锁的线程是否是当前线程是的话说明是重入锁,这样将获取锁的状态为+1并返回获取锁,否则获取锁失败,再次回到这个方法中:

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

如果tryAcquire获取成功了就不会执行后面判断了,如果失败了则会执行acquireQueued语句,首先分析addWaiter:

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.prev = pred;            if (compareAndSetTail(pred, node)) {                pred.next = node;                return node;            }        }        enq(node);        return node;    }

这段代码的主要意思是将当前线程作为一个节点加入到同步队列中,首先将当前线程构造一个排它型节点,判断尾节点是否为null,如果不为null说明尾节点存在,这时候只需要将当前节点添加到尾节点后面主要逻辑如下:

尾节点存在

1.将当前节点的前驱节点指向尾节点。

2.CSA设置当前节点为尾节点,成功则设置当前节点的前驱节点指向尾节点同时并返回该节点。失败则进直接在队列末尾插入。

尾节点不存在或者尝试加入尾节点失败

尾节点不存或者尝试直接加入尾节点失败在进入下面逻辑:

private Node enq(final Node node) {        for (;;) {            Node t = tail;            if (t == null) { // Must initialize                if (compareAndSetHead(new Node()))                    tail = head;            } else {                node.prev = t;                if (compareAndSetTail(t, node)) {                    t.next = node;                    return t;                }            }        }    }

1.首先判断尾节点是否为null,如果为null CAS分配一个头结点并头尾指向起来。

2.循环判断判断尾节点不为null,重复当时将当前节点加入到尾节点后面直到成功并返回该尾节点。

成功将当前线程加入到尾节点后则执行下面方法,尝试获取锁(访问控制)或者休眠线程

final boolean acquireQueued(final Node node, int arg) {        boolean failed = true;        try {            boolean interrupted = false;            for (;;) {                final Node p = node.predecessor();                if (p == head && tryAcquire(arg)) {                    setHead(node);                    p.next = null; // help GC                    failed = false;                    return interrupted;                }                if (shouldParkAfterFailedAcquire(p, node) &&                    parkAndCheckInterrupt())                    interrupted = true;            }        } finally {            if (failed)                cancelAcquire(node);        }    }

1.首先设定获取队列访问结果为true,同时线程中断为false。

2.获取当前节点的前驱节点且当前线程获取到锁成功(获取锁的构成跟前面获取锁一样),则设置当前节点为头节点同时设置头结点的后继节点为null并且设置加入队列为失败同时返回false,这时候线程就获取到了锁。

3.如果当前线程没有获取到则判断是否需要休眠该线程调用下面方法来判断:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {        int ws = pred.waitStatus;        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.             */            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);        }        return false;    }

就是判断节点的的状态来决定是否需要休眠。

  • 如果当前节点已经被摄氏需要等待唤醒状态即Node.SIGNAL则直接挂起该线程。
  • 如果当前节点的前驱节点被取消则跳过这些取消的节点即节点状态为Node.CANCELLED,直接返回false并尝试再次获取锁。
  • 如果当前节点不是Node.SIGNAL和Node.CANCELLED则CAS将当前接单设置为Node.SIGNAL并在挂起前再次尝试获取锁(返回false)。

4.如果当前线程需要休眠则调用用下面方法中断该线程将线程从线程调度器中摘除,如果该线程被唤醒则判断该线程是否处于中断状态并返回是否处于中断状态,如果是处于中断状态就将中断状态设置为true,如果线程成功获取到了则会返回中断状态位true。

private final boolean parkAndCheckInterrupt() {        LockSupport.park(this);        return Thread.interrupted();    }

接着回到acquire方法中:

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

如果线程是直接获取到了锁没有被中断过则说明acquireQueued返回的是false,这样线程就直接获取到了锁。如果线程获取到了锁且线程是被中断过则acquireQueued返回的是true,

则将将线程设置状态标志。

以上就是ReentrantLock获取非公平锁的过程。总结点非公平锁的获取过程:

0d6614859a7b785882f928b14d3fce1972b.jpg

释放锁非公平锁

从unlock开始

public void unlock() {        sync.release(1);    }    public final boolean release(int arg) {        if (tryRelease(arg)) {            Node h = head;            if (h != null && h.waitStatus != 0)                unparkSuccessor(h);            return true;        }        return false;    }

首先调用tryRelease释放锁

protected final boolean tryRelease(int releases) {            int c = getState() - releases;            if (Thread.currentThread() != getExclusiveOwnerThread())                throw new IllegalMonitorStateException();            boolean free = false;            if (c == 0) {                free = true;                setExclusiveOwnerThread(null);            }            setState(c);            return free;        }
  • 获取到锁状态标志位-1=c
  • 判断当前线程是否是锁的独占线程不是抛出异常。
  • 判断c是否等于0,不等于0说明是重入锁,等于0话设置当前锁的独占线程为null并返回是完全是否成功,同时设置锁标志位-1.返回是否结果。

接着判断头结点是否为null以及节点是否为需要挂去,满足条件则调用unparkSuccessor方法唤醒线程否则释放失败。

private void unparkSuccessor(Node node) {        /*         * If status is negative (i.e., possibly needing signal) try         * to clear in anticipation of signalling.  It is OK if this         * fails or if status is changed by waiting thread.         */        int ws = node.waitStatus;        if (ws < 0)            compareAndSetWaitStatus(node, ws, 0);        /*         * Thread to unpark is held in successor, which is normally         * just the next node.  But if cancelled or apparently null,         * traverse backwards from tail to find the actual         * non-cancelled successor.         */        Node s = node.next;        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)            LockSupport.unpark(s.thread);    }

如何节点状态小于0说明节点是SIGNAL、CONDITION以及PROPAGATE节点则CAS清除器状态全部设置为0,且不管是否成功。

如果头结点的后继节点没有被取消且不为null则直接换成该线程,否则从后往前遍历找到最早没有被取消的节点并将其唤醒成功。当FIFO队列中等待锁的第一个节点被唤醒之后,会返回到到节点所在线程的acquireQueued()方法中,继续下一轮循环,这时当前节点正好时头节点的第一个后继节点,并且使用CAS修改状态持有锁成功,那么当前节点则晋升为头结点,并返回。

final boolean acquireQueued(final Node node, int arg) {        boolean failed = true;        try {            boolean interrupted = false;            for (;;) {                final Node p = node.predecessor();                if (p == head && tryAcquire(arg)) {                    setHead(node);                    p.next = null; // help GC                    failed = false;                    return interrupted;                }                if (shouldParkAfterFailedAcquire(p, node) &&                    parkAndCheckInterrupt())                    interrupted = true;            }        } finally {            if (failed)                cancelAcquire(node);        }    }

以上就是非公平锁释放锁的过程。

公平锁

公平获取锁

公平锁的获取则要简单

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

直接调用tryAcquire尝试获取锁

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;        }

如果有其他线程获取锁且当前现在不是锁的独占线程直接返回false进入到acquireQeued代码中再次尝试获取锁获取不到直接将线程作为节点添加到队列尾部,这不部分公平锁和非公平锁是一样的。

如果没有前程获取到对象的锁,首先判断是否有任何线程比当前线程等待更长时间,其实就是判断当前线程是否是队列头部。逻辑如下:

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;        return h != t &&            ((s = h.next) == null || s.thread != Thread.currentThread());    }

只有当当前线程结点为头结点是才可能返回false。返回fals我说当前线程等待的时间最长CASE设置获取到锁成功了设置当前线程为对象的独占线程。一般情况下这步肯定会设置成功的。两个队列为null且两个线程在争用该对象的锁,这时候会设置失败。

公平释放锁

释放锁和公平锁一样。

 

转载于:https://my.oschina.net/wenbo123/blog/1827299

你可能感兴趣的文章