深入剖析ReentrantLock公平锁与非公平锁源码实现

来源:互联网 发布:淘宝网商银行怎么开通 编辑:程序博客网 时间:2024/06/07 07:22


原文:http://blog.csdn.net/lsgqjh/article/details/63685058


本文以公平与非公平锁的加锁缩成为主线,分析整个加锁过程。

准备知识简介

ReentrantLock类图: 
这里写图片描述 
NonfairSync继承关系: 
这里写图片描述 
这里写图片描述 
Node结点:作为获取锁失败线程的包装类, 组合了Thread引用, 实现为FIFO双向队列。 下图为Node结点的属性描述 
这里写图片描述
这里写图片描述

锁的创建

非公平锁(默认)

final ReentrantLock lock = new ReentrantLock();final ReentrantLock lock = new ReentrantLock(false);
  • 1
  • 2

公平锁

final ReentrantLock lock = new ReentrantLock(true);
  • 1

非公平锁加锁过程

    static final class NonfairSync extends Sync {        private static final long serialVersionUID = 7316153563782823691L;        /**         *逻辑: 多个线程调用lock()方法, 如果当前state为0, 说明无锁, 那么只有一个线程会CAS获得锁, 并设置此线程为独占锁线程。其它线程会调用acquire方法来竞争锁(后续会全部加入同步队列中自旋或挂起)。当有其它线程A又进来lock时, 恰好此前的某一线程恰好释放锁, 那么A恰好在同步队列所有等待获取锁的线程之前抢先获取锁。也就是说所有已经在同步队列中的尚未被取消获取的线程是绝对保证串行获取锁。后面代码解释。         *          */        final void lock() {            if (compareAndSetState(0, 1))                setExclusiveOwnerThread(Thread.currentThread());            else                acquire(1);        }/*** 此为AQS的protected方法,允许子类重写*/        protected final boolean tryAcquire(int acquires) {            return nonfairTryAcquire(acquires);        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

先看acquire方法: 
逻辑:tryAcquire方法仍然尝试获取锁(快速获取锁机制),成功返回false,如果没有成功, 那么就将此线程包装成Node加入同步队列尾部。并且在队列中不会响应中断。Node.EXCLUSIVE 表示独占模式。

    public final void acquire(int arg) {    // tryAcquire()方法也是让新来的线程进行第二次插队的机会!!        if (!tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

再看addWaiter 逻辑:

  1. Node包装当前线程
  2. pred 尾指针不为null,即队列不为空, 则快速CAS添加到尾结点
  3. 如果队列为空, 或CAS设置失败, 则调用enq入队
 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;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

下面是enq方法:

private Node enq(final Node node) {        for (;;) {            Node t = tail;            if (t == null) { // 判断队列是否为空                if (compareAndSetHead(new Node()))                    tail = head;//头尾共同指向头结点                    //注意此节点并不是当前线程包装结点            } else {//CAS添加并允许失败, 走for(;;)                node.prev = t;                if (compareAndSetTail(t, node)) {                    t.next = node;                    return t;                }            }        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

入队完成后看下acquireQueued 逻辑:

failed 标记最终acquire是否成功, interrupted标记是否曾被挂起过。注意到for(;;) 跳出的唯一条件就是if (p == head && tryAcquire(arg)) 即当前线程结点是头结点且获取锁成功。也就是说只有p结点的所有前置结点都执行完毕,p == head && node请求成功

final boolean acquireQueued(final Node node, int arg) {        boolean failed = true;        try {            boolean interrupted = false;            for (;;) {// 获取前置结点                final Node p = node.predecessor();                //如果第一次循环就获取成功那么返回的interrupted是false,不需要自我中断。否则 说明在获取到同步状态之前经历过挂起(返回true)那么就需要自我中断,可以 看acquire方法中的代码。 注意: 中断不是中断当前线程的执行                if (p == head && tryAcquire(arg)) {                    setHead(node);                    p.next = null; // help GC                    failed = false;                    return interrupted;                }                //如果当前node线程是可挂起的,就调用parkAndCheckInterrupt挂起 ,interrupted设置为true,                if (shouldParkAfterFailedAcquire(p, node) &&                    parkAndCheckInterrupt())                    interrupted = true;            }        } finally {            if (failed)//如果tryAcquire出现异常那么取消当前结点的获取                cancelAcquire(node);        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

看下shouldParkAfterFailedAcquire 方法的逻辑:

   private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {        int ws = pred.waitStatus;        if (ws == Node.SIGNAL)      /*      如果前置结点是SIGNAL状态, 那么当前置结点是执行完成可以唤醒后续结点的,此时可以安全的挂起当前结点, 不需要进行不必要的for(;;),前置结点自然会通知。      */            return true;        if (ws > 0) {      //   如果ws>0说明前置结点是被自己取消获取同步的结点(只有线程本身可以取消自己)。那么do while循环一直往头结点方向找waitStatus < 0;含义就是去除了FIFO队列中的已经自我取消申请同步状态的线程。            do {                node.prev = pred = pred.prev;            } while (pred.waitStatus > 0);            pred.next = node;        } else {             //如果是其它状态waitStatus要不是0或着PROPAGATE,意味着当前结点需要一个signal但是尚未park,所以调用者必须确保在挂起之前不能获取同步状态, 所以return false。并强行设置前置结点为SIGNAL。之所以CAS设置是因为,pred线程也会操作cancelAcquire来取消自己和node线程对pred的操作产生竞争条件。            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);        }        return false;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

看ReentrantLock中的NonfairSync对同步器方法 tryAcquire 的重写:

protected final boolean tryAcquire(int acquires) {            return nonfairTryAcquire(acquires);        }
  • 1
  • 2
  • 3

下面是nonfairTryAcquire 

final boolean nonfairTryAcquire(int acquires) {            final Thread current = Thread.currentThread();            int c = getState();            if (c == 0) { //如果没有线程获取锁,则当前线程CAS获取锁。并设置自己为当前锁的独占线程                if (compareAndSetState(0, acquires)) {                    setExclusiveOwnerThread(current);                    return true;                }            }            else if (current == getExclusiveOwnerThread()) {// 如果存在锁竞争,判断获取锁的线程是否是当前线程, 如果是 那么根据可重入的含义, 使当前state+1;                int nextc = c + acquires;                if (nextc < 0) // overflow                    throw new Error("Maximum lock count exceeded");                setState(nextc);                return true;            }如果不是当前线程,则不能获取同步状态            return false;        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

公平锁的加锁过程

公平锁对tryAcquire 的实现:

 static final class FairSync extends Sync {        private static final long serialVersionUID = -3000897897090466540L;//lock方法对比非公平锁, 没有了if else  也就意味着新来的线程没有插队的机会, 所有来的线程必须扔到队列尾部, acquire方法会像非公平锁一样首先调用tryAcquire插队试试(如果队列为空或着本身就是head,那么可能成功, 队列非空那么肯定被扔到队列尾部去了, 插个毛线)。        final void lock() {            acquire(1);        }      //只有队列为空或着当前tryAcquire的node线程是head        protected final boolean tryAcquire(int acquires) {            final Thread current = Thread.currentThread();            int c = getState();//拿到当前的同步状态, 如果是无锁状态, 则进行hasQueuedPredecessors方法逻辑,逻辑含义是:当前队列为空或本身是同步队列中的头结点。如果满足条件CAS获取同步状态,并设置当前独占线程。            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;        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

hasQueuedPredecessors() 
和非公平锁对比多了这个方法逻辑, 也就以为着没有了新来线程插队的情况,保证了公平锁的获取串行化。

 public final boolean hasQueuedPredecessors() {        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());    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

公平锁和非公平锁的释放逻辑

公平锁和非公平锁的释放逻辑是一致的 都是通过sync.release(1); 
ReentrantLock中的unlock()

public void unlock() {        sync.release(1);    }
  • 1
  • 2
  • 3
  • 4

AbstractQueuedSynchronizer中的release方法: 
release之后还要调用unparkSuccessor() 方法唤醒后继结点

    public final boolean release(int arg) {        if (tryRelease(arg)) {            Node h = head;// 当前这个头结点是同步队列中的头结点            //并且不一定是包装的当前要释放锁的执行线程, 因为可能是非公平锁的释放,压根就没插入队列就特么插队获得同步了            if (h != null && h.waitStatus != 0)                unparkSuccessor(h);            return true;        }        return false;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

释放锁的逻辑tryRelease

  protected final boolean tryRelease(int releases) {            int c = getState() - releases;            //  只有获得锁的线程自己才能释放锁            if (Thread.currentThread() != getExclusiveOwnerThread())                throw new IllegalMonitorStateException();            boolean free = false;            if (c == 0) {//c==0说明已经无锁                free = true;                setExclusiveOwnerThread(null);            }            setState(c);            //否则更新state为state-1, 也就是加锁多少次,就得释放多少次, lock unlock配对使用。            return free;        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

unparkSuccessor 方法逻辑:

   private void unparkSuccessor(Node node) {        int ws = node.waitStatus;        if (ws < 0)//同样是因为node线程和当前线程有竞争,node本身也可以修改自身状态嘛,因此CAS            compareAndSetWaitStatus(node, ws, 0);        Node s = node.next;        if (s == null || s.waitStatus > 0) {            s = null;            //发现这里竟然从tail结点往前找最靠近head结点的且处于非取消状态的结点? 留个疑问下面解释            for (Node t = tail; t != null && t != node; t = t.prev)                if (t.waitStatus <= 0)                    s = t;        }        //s是后继结点 因为后继结点很可能是满足条件的, 如果满足即s!=NULL 直接unpark后继结点。        if (s != null)            LockSupport.unpark(s.thread);    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

为什么在unparkSuccessor 方法中对于需要unpark的线程结点进行从tail 往前查找?再来看一下enq 方法:

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

首先node.prev = t 新节点的prev指向tail结点, 几点CAS将tail 指向新node结点, 当这一步执行完后, 在t.next = node 执行之前, 也就是之前的tail 结点的next 指针尚未指向新 node, 此时是不会出现问题的, 我觉得网上说出现问题是错误的。因为当调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 时, 方法中的if (p == head && tryAcquire(arg)) p == head 不会被满足, 然后调用shouldParkAfterFailedAcquire() 将取消的结点删掉,并将前置结点标记状态为SIGNAL , 那么p结点为head的时候 就进入了if语句块内是没有任何问题的。

问题的根结在于cancelAcquire() 方法

 private void cancelAcquire(Node node) {        // Ignore if node doesn't exist        if (node == null)            return;        node.thread = null;        // 跳过所有被取消的前置结点        Node pred = node.prev;        while (pred.waitStatus > 0)            node.prev = pred = pred.prev;        Node predNext = pred.next;        node.waitStatus = Node.CANCELLED;        // 如果是尾结点直接删掉        if (node == tail && compareAndSetTail(node, pred)) {            compareAndSetNext(pred, predNext, null);        } else {            // 判断如果需要唤醒要取消的结点的下一个结点, 那么就设置pred的next指针。 因为前面那个while循环只是设置的前置指针            int ws;            // 如果前置结点状态是SIGNAL || 如果不是CAS设为SIGNAL 那么接下来的执行自然就会唤醒下一个结点            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)//设置next-link                    compareAndSetNext(pred, predNext, next);            } else {  // 如果是头结点 p == head                unparkSuccessor(node);            }            node.next = node; // help GC            //关键问题就在这里 最终把取消acquire的 node的next指针指向的它自己!!!!!!        }    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

那么问题来了, 因为队列中的每个结点, 随时都可能自己取消自己, 一旦我们从前往后unpark玩意遇到哪个线程突然把自己给取消了, next指针指向自己, 那么我们就没办法去继续向后查找满足条件的线程去unpark。所以从tail 往前来查找就不会出现问题~


原创粉丝点击