Lock、ReentrantLock、ReentrantReadWriteLock原理及应用深入解析

Lock、ReentrantLock、ReentrantReadWriteLock

Lock相比于synchronized具有更强大的功能,在jdk1.6之前,锁竞争激烈的情况下使用lock的实现类ReentrantLock甚至比synchronized具有更好的性能,1.6之后oracle对synchronized进行了优化,如今的jdk1.8中两者性能不相伯仲。一个工具类,没有使用机器指令,没有编译器的特殊优化,却具有和jvm层实现的关键字一样甚至更好的效率与更强大的功能。对Doug Lea大神再一次膜拜Orz 本文讲详细介绍高效锁结构的实现原理与使用方式。


Lock

Lock是一个接口,定义了锁的基本方法:

public interface Lock {
void lock();//加锁
void lockInterruptibly() throws InterruptedException;//加可中断锁
boolean tryLock();//加锁成功返回true,失败返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//加锁成功返回true,失败等待一段时间后若仍无法加锁则返回false,可响应中断
void unlock();//解锁
Condition newCondition(); //返回一个Condition,利用它可以如和Synchronizd配合使用的wait()和notify()一样对线程阻塞和唤醒,不同的是一个lock可以有多个condition.
}

最后的newCondition返回一个Condition对象,该对象是一个接口,我们来看看其中提供的方法。

public interface Condition {
 void await() throws InterruptedException;//类似于wait(),可以响应中断
 void awaitUninterruptibly();//不响应中断的等待
 long awaitNanos(long nanosTimeout) throws InterruptedException;//等待指定时间(单位是纳秒),在接到信号、被中断或到达指定等待时间之前一直处于等待状态。方法返回被唤醒后的剩余等待时间,若返回值小于等于0则代表此次等待超时。
boolean await(long time, TimeUnit unit) throws InterruptedException;//指定时间到达前结束等待返回true,否则返回false
boolean awaitUntil(Date deadline) throws InterruptedException;//指定日期到达前被唤醒返回true,否则返回false
void signal();//唤醒一个等待中的线程,类似于notify()
void signalAll();//唤醒所有等待中的线程,类似于notifyAll()
}

ReentrantLock

可重入锁是Lock接口的一个重要实现类。所谓可重入锁即线程在执行某个方法时已经持有了这个锁,那么线程在执行另一个方法时也持有该锁。首先我们来看看加锁方法lock的实现

public void lock() {
        sync.lock();
    }

sync是ReentrantLock中静态内部接口Sync的实例对象。在ReentrantLock中提供了两种Sync的具体实现,FairSync与NonfairSync。故名思意,两种不同的Sync分别用于公平锁和非公平锁。首先我们来看FairSync中lock的实现

final void lock() {
            acquire(1);
        }

acquire方法在Sync的父类AbstractQueuedSynchronizer中实现

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

感觉代码大致含义是:尝试获取锁,成功则直接返回(&&链接,前一个为fales不会再进入后一个),失败则尝试加入等待队列中,若还是失败则触发中断。下面我们做具体分析。
首先分析tryAcquire()方法

 protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

在这里只是一个空实现,具体的实现又交回了子类FairSync中,这里用到了模板方法设计模式,关于这个设计模式的相关知识可以戳这里http://blog.csdn.net/lovelion/article/details/8299794
那么为什么这个空方法Doug Lea不以抽象的方式声明呢?因为AbstractQueuedSynchronizer有两种功能:独占和共享,之后我们在分析ReentrantReadWriteLock将详细看到。若都定义为抽象方法会导致子类无论如何都需要实现另外一种场景的抽象方法,显然,这对子类来说是不友好的。到底是哪两种使用场景稍后我们可以具体看到。
继续来看看FairSync中的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;
        }

getState()是AbstractQueuedSynchronizer中的方法,其有一个标志位state

  private volatile int state;
  protected final int getState() {
        return state;
    }

这个标志位的作用就是表示有没有线程已经拿走了锁,即当前锁还是否存在。
state为0表示锁没被取走,接着进入下一层判断

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

hasQueuedPredecessors判断当前线程之前是否还有其他线程在等待锁。如果当前线程是等待队列中的头节点或线程等待队列为空则返回fales(表示当前线程之前无其他节点)

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

compareAndSetState方法用于将AbstractQueuedSynchronizer中的标志位state更新为指定值,这里即把state从0更新为1

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

检查和更新都通过后,设置变量值表明当前线程成功获取了锁,之后返回true

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

接下来再来看看state不为0即锁已被拿走的情况

        else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }

虽然锁已经被拿走了,但是,因为ReentrantLock是可重入锁,一个线程是可以重复lock,unlock的。因此这里还要再判断一次获取锁的线程是不是当前请求锁的线程,如果是的话将state字段增加(这里是加1)并返回true(因此线程中重复的lock和unlock必须成对出现,不然会导致state的值无法降为0或小于0抛出异常)
来总结一下tryAcquire方法:加锁通过CAS的方式进行,成功返回true,失败返回false。
如果返回false,回顾一下之前的代码,首先执行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;
    }

传入的参数表示该节点的类型,是独占的(Node.EXCLUSIVE)还是共享的( Node.SHARED)。因为AbstractQueuedSynchronizer有两种模式。这里传入的是独占型。
创建好节点后,将节点加入到队列尾部,此处,在队列不为空的时候,先尝试通过cas方式修改尾节点为当前节点,修改成功后将原尾节点的子节点指向当前节点。
如果修改失败,意味着有并发,这个时候进入enq中死循环,以“自旋”方式进行cas修改。直到当前节点正常进入队列。

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

线程节点进入等待队列后,还有一件重要的事情:将线程挂起。由acquireQueued()方法实现,来看看

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

对于头节点这里已自旋转的方式获取锁,直到重新获得锁后将下一个节点设为头节点并返回。通过这里保证了以这种方式获取锁的节点都是头节点。
如果不是头节点或无法获取锁,则首先执行shouldParkAfterFailedAcquire方法

 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

该方法通过当前节点的前一个节点的状态判断当前节点是否需要被挂起。节点状态有四种:

        static final int CANCELLED =  1;//取消
        static final int SIGNAL    = -1;//等待触发
        static final int CONDITION = -2;//等待条件
        static final int PROPAGATE = -3;//节点状态向下一个节点传播

若不在这4种中,状态值将为0。当一个线程释放锁时,它的状态值将被设为0。
当上一个节点状态为SIGNAL时,直接返回true,当前节点线程可以被挂起。
当上一个节点状态为取消时,向上遍历上上个节点,直到节点状态不为取消为止,并设置其为当前节点的父节点。返回false,当前节点不可被挂起。
当上一个节点为其他两种状态时,通过cas修改上一节点状态为SIGNAL,并返回false,当前节点不可被挂起。
节点可以被挂起后,执行parkAndCheckInterrupt方法

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

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, 0L);
        setBlocker(t, null);
    }

可以看到,通过UNSAFE.park方法最终将线程挂起。

总结一下lock方法:若获取锁成功,直接返回。若失败当前线程会被加入到等待队列中并被阻塞。
看完了获取锁,在来看看释放锁。

 public void unlock() {
        sync.release(1);
    }

调用了AbstractQueuedSynchronizer中的release方法

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

AbstractQueuedSynchronizer中的tryRelease依然是空实现

   protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

这次交由AbstractQueuedSynchronizer的子类,NonfairSync和FairSync的共有父类Sync来实现

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

c值为state值减传入值(这里即减1,之后又把c设为了state值。因此可以看到加锁时state加1,释放锁减1,state即锁的标志变量)。如果释放锁的线程不是当前线程则抛出异常。如果c为0(因为有一个线程多次加锁的重入情况),设置当前线程不在拥有锁,重设状态值并返回true,若不为0则重设状态值返回false。
当前释放锁后,继续执行

Node h = head;
 if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);

如果存在头节点且状态不为0,则执行unparkSuccessor方法唤醒头节点线程

   private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

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


   public static void unpark(Thread thread) {
        if (thread != null)
            UNSAFE.unpark(thread);
    }

代码很清晰了,首先将释放锁节点的线程状态设为0。然后看该节点的下一个节点(在上面已经阐述了,此时释放锁的节点node就是头节点)状态是否满足要求,满足要求则将其唤醒。若是已取消状态的话从队尾倒着遍历直到获得最前面的状态不为取消状态的节点并将其唤醒(这段遍历循环可以仔细看看,注意循环结束的判断条件,通俗的解释此处就是倒着遍历队列并获得离头节点最近的状态小于等于0的节点s,由此便实现了等待最长时间的线程将最先获得锁)。
至此,ReentrantLock中公平锁的加解锁方式阐述完了,下面我们在看看ReentrantLock中的非公平锁模式NonfairSync的实现

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

可以看到非公平锁就是在加锁时先直接cas操作尝试获取锁,获取失败再通过上面所讲的公平锁获取锁的流程排队获取,由此达到抢占的目的。

本节参考资料:http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer


ReentrantReadWriteLock

看源码之前我们先明确一下读写锁的概念:加读锁时其他线程可以进行读操作但不可进行写操作,加写锁时其他线程读写操作都不可进行。
ReentrantReadWriteLock和ReentrantLock不同,ReentrantReadWriteLock实现的是ReadWriteLock接口,我们首先来看看该接口

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

只定义了两个方法,返回读锁与返回写锁。
先来看看返回读锁的实现

 public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

返回的类是ReadLock,下面我们来看看这个类中加锁和释放锁的实现原理。首先来看加锁

public void lock() {
            sync.acquireShared(1);
        }

依然是使用Sync结构,这里也是调用到AbstractQueuedSynchronizer中

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

和前面类似tryAcquireShared也委托给子类实现

     protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            int r = sharedCount(c);
            if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }
            return fullTryAcquireShared(current);
        }

一步一步进行分析。

     if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;

exclusiveCount()方法会返回独占锁的持有线程数,即写锁持有线程数。这里有值得注意的一点:state即用来保存等待读锁的线程数也用来保留等待写锁的线程数,高16位表示共享锁,低16位表示独占锁。通过不同的移位运算对其中保存的读写线程数进行加减操作,有兴趣的读者可自行研究。
这段代码判断如果写锁已被获取且获取的线程不是当前线程则获取锁失败。
继续往下

      if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                if (r == 0) {
                    firstReader = current;
                    firstReaderHoldCount = 1;
                } else if (firstReader == current) {
                    firstReaderHoldCount++;
                } else {
                    HoldCounter rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current))
                        cachedHoldCounter = rh = readHolds.get();
                    else if (rh.count == 0)
                        readHolds.set(rh);
                    rh.count++;
                }
                return 1;
            }

readerShouldBlock()是为了控制公平性:可以直接获取锁或者需要排队等待。若其为false并且获取读锁的线程没有超过最大值,通过cas操作修改获取读锁的线程数目,将当前锁加入获取了读锁的线程队列中并最终返回获取锁成功。
如果if中的条件不成立会进入fullTryAcquireShared,在该方法中简单来说就是:如果获取锁的线程超过了最大数目,抛出error,如果是cas失败则循环重试直到成功。
尝试获取读锁的方法说完了,简单总结一下就是:已经有其他线程加了写锁则获取失败。结下来看看获取失败的处理:

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

既然获取锁失败当然是要等待了,不过和之前ReentrantLock中的略有不同,当节点是头节点时采用的是自旋锁也就是忙等的等待方式:不停的尝试获取锁,一旦获取成功后不仅会将下一个节点设置为头节点还会将头节点唤醒(ReentrantLock只将下一个节点设为头节点,唤醒通过unlock进行。这里因为读锁只用等待写锁被释放后就畅通无阻即一个线程获取了读锁,那么所有等待读锁的线程都应该能获取读锁并被唤醒,因此这样设计)。当节点非头节点时才尝试将线程挂起。这部分和ReentrantLock一样就不阐述了。
加锁方法就分析完了,接下来在来看看解锁unlock方法

 public void unlock() {
            sync.releaseShared(1);
        }

   public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

        protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
                if (firstReaderHoldCount == 1)
                    firstReader = null;
                else
                    firstReaderHoldCount--;
            } else {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                int count = rh.count;
                if (count <= 1) {
                    readHolds.remove();
                    if (count <= 0)
                        throw unmatchedUnlockException();
                }
                --rh.count;
            }
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

HoldCounter是每个线程自己持有的,标记它当前所持有的锁数目的标记变量。这段代码就很简单了,校验线程确实持有锁可以释放之后通过cas的方法减少state中记录的读锁数目,读锁为0则返回true。
再来看看返回true即所有读锁都释放后执行的doReleaseShared()方法

  private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

唤醒等待队列中的头节点。从上面分析我们可以知道此时等待队列中只有等待写锁的线程,因此就是唤醒第一个等待写锁的节点。
读锁的加锁和解锁到此分析完了,接下来看看写锁的加解锁过程。
首先来看加锁

 public void lock() {
            sync.acquire(1);
        }

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

和ReentrantLock的加锁没有区别,主要看看tryAcquire()的实现逻辑

   protected final boolean tryAcquire(int acquires) {
            Thread current = Thread.currentThread();
            int c = getState();
            int w = exclusiveCount(c);
            if (c != 0) {
                // (Note: if c != 0 and w == 0 then shared count != 0)
                if (w == 0 || current != getExclusiveOwnerThread())
                    return false;
                if (w + exclusiveCount(acquires) > MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                // Reentrant acquire
                setState(c + acquires);
                return true;
            }
            if (writerShouldBlock() ||
                !compareAndSetState(c, c + acquires))
                return false;
            setExclusiveOwnerThread(current);
            return true;
        }

如果存在写锁或是持有写锁的线程不是当前线程,返回false,超出最大数目error,否则修改锁标志位并返回true,成功获取锁。
这里的writerShouldBlock()是为来控制公平性:在没有锁时写操作是可直接获取锁还是需进入队列排队等待。
在看看释放锁:

 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) {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            int nextc = getState() - releases;
            boolean free = exclusiveCount(nextc) == 0;
            if (free)
                setExclusiveOwnerThread(null);
            setState(nextc);
            return free;
        }

很简单,如果持有写锁的是当前线程并且释放后持有写锁的线程数目为0(之所以加入这层判断是因为该线程可能多次申请写锁,直到全部释放后才能唤醒其他线程)返回true,之后唤醒等待的线程。

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011479540/article/details/52013187
个人分类: JAVA并发编程
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭