Java可重入锁详解

作者: 一字马胡 | 来源:发表于2017-10-14 13:09 被阅读643次

    作者: 一字马胡
    转载标志 【2017-11-03】

    更新日志

    日期 更新内容 备注
    2017-11-03 添加转载标志 持续更新

    前言

    在java中,锁是实现并发的关键组件,多个线程之间的同步关系需要锁来保证,所谓锁,其语义就是资源获取到资源释放的一系列过程,使用lock的意图就是想要进入临界区对共享资源执行操作,使用unlock说明线程已经完成了相关工作或者发生了异常从而离开临界区释放共享资源,可以说,在多线程环境下,锁是一个必不可少的组件。我们最为常用的并发锁是synchronized关键字,在最新的jdk中,synchronized的性能已经有了极大的提升了,而且未来还会对它做更进一步的优化,最为重要的是synchronized使用起来特别方便,基本不需要要我们考虑太多的内容,只需要将临界区的代码放在synchronized关键字里面,然后设定好需要锁定的对象,synchronized就会自动为进入的并发线程lock和unlock。在大多数情况下,我们写并发代码使用synchronized就足够了,而且使用synchronized也是首选,不过如果我们希望更加灵活的使用锁来做并发,那么java还提供了一个借口Lock,本文并不会对synchronized进行分析总结,本文的重点在Lock接口,以及实现了Lock接口的一些子类的分析总结。

    为了本文的完整性,可以参考Java同步框架AbstractQueuedSynchronizer,这个俗称为AQS的东西是Lock接口实现的根本,它实现了Lock的全部语义,可以说,java的Lock接口的子类就是借助AQS来实现了lock和unlock的,理解了AQS,就可以很好的理解java中的锁了。

    Lock接口以及ReadWriteLock接口

    下面首先展示出了Lock接口的内容,然后是ReadWriteLock的接口,本文主要分析这两个接口的几个子类的实现细节。

    Lock接口
    
    public interface ReadWriteLock {
        /**
         * Returns the lock used for reading.
         *
         * @return the lock used for reading
         */
        Lock readLock();
    
        /**
         * Returns the lock used for writing.
         *
         * @return the lock used for writing
         */
        Lock writeLock();
    }
    
    

    Lock接口提供了lock和unlock方法,提供加锁和释放锁的语义,lockInterruptibly方法可以响应中断,lock方法会阻塞线程直到获取到锁,而tryLock方法则会立刻返回,返回true代表获取锁成功,而返回false则说明获取不到锁。newCondition方法返回一个条件变量,一个条件变量也可以做线程间通信来同步线程。多个线程可以等待在同一个条件变量上,一些线程会在某些情况下通知等待在条件变量上的线程,而有些变量在某些情况下会加入到条件变量上的等待队列中去。

    ReadWriteLock是读写锁,可以对共享变量的读写提供并发支持,ReadWriteLock接口的两个方法分别返回一个读锁和一个写锁。本文将基于上面提到的两个接口Lock和ReadWriteLock,对Lock的子类ReentrantLock和ReadWriteLock的子类ReentrantReadWriteLock进行一些分析总结,以备未来不时之需。

    ReentrantLock

    Java同步框架AbstractQueuedSynchronizer提到了两个概念,一个是独占锁,一个是共享锁,所谓独占锁就是只能有一个线程获取到锁,其他线程必须在这个锁释放了锁之后才能竞争而获得锁。而共享锁则可以允许多个线程获取到锁。具体的分析不再本文的分析范围之内。

    ReentrantLock翻译过来为可重入锁,它的可重入性表现在同一个线程可以多次获得锁,而不同线程依然不可多次获得锁,这在下文中会进行分析。下文会分析它是如何借助AQS来实现lock和unlock的,本文只关注核心方法,比如lock和unlock,而不会去详细的描述所有的方法。ReentrantLock分为公平锁和非公平锁,公平锁保证等待时间最长的线程将优先获得锁,而非公平锁并不会保证多个线程获得锁的顺序,但是非公平锁的并发性能表现更好,ReentrantLock默认使用非公平锁。下面分公平锁和非公平锁来分析一下ReentrantLock的代码。

    锁Sync

    在文章Java同步框架AbstractQueuedSynchronizer中已经提到了如何通过AQS来实现锁的方法,那就是继承AbstractQueuedSynchronizer类,然后使用它提供的方法来实现自己的锁。ReentrantLock的Sync也是通过这个方法来实现锁的。

    Sync有一个抽象方法lock,其子类FairSync和NonfairSync分别实现了公平上锁和非公平上锁。nonfairTryAcquire方法用于提供可重入的非公平上锁,之所以把它放在Sync中而不是在子类NonfairSync中(FairSync中有公平的可重入上锁版本的实现),是因为nonfairTryAcquire不仅在NonfairSync中被使用了,而且在ReentrantLock.tryLock里面也使用到了。对于它的分析留到NonfairSync里面再分析。

    Sync中还需要注意的一个方法是tryRelease,执行这个方法说明线程在离开临界区,下面是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;
            }
    
    

    tryRelease方法重写了父类的tryRelease方法,而父类的tryRelease方法在release方法中被调用,而release方法最后会被用于实现ReentrantLock的unlock方法。所以理解了该方法,就理解了ReentrantLock的unlock逻辑。

    从上面展示的代码分析,getState方法获取当前的共享变量,getState方法的返回值代表了有多少线程获取了该条件变量,而release代表着想要释放的次数,然后根据这两个值计算出最新的state值,接着判断当前线程是否独占了锁,如果不是,那么就抛出异常,否则继续接下来的流程。如果最新的state为0了,说明锁已经被释放了,可以被其他线程获取了。然后更新state值。

    公平锁FairSync

    FairSync实现了公平锁的lock和tryAcquire方法,下面分别看一下这两个方法的实现细节:

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

    可以看到,FairSync的lock实现使用了AQS提供的acquire方法,这个方法的详细解析见Java同步框架AbstractQueuedSynchronizer

    下面是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;
            }
        }
    
    

    可以看到,公平锁的tryAcquire实现和非公平锁的tryAcquire实现的区别在于:公平锁多加了一个判断条件:hasQueuedPredecessors,如果发现有线程在等待获取锁了,那么就直接返回false,否则在继承尝试获取锁,这样就保证了线程是按照排队时间来有限获取锁的。而非公平锁的实现则不考虑是否有节点在排队,会直接去竞争锁,如果获取成功就返回true,否则返回false。

    当然,这些分支执行的条件是state为0,也就是说当前没有线程独占着锁,或者获取锁的线程就是当前独占着锁的线程,如果是前者,就按照上面分析的流程进行获取锁,如果是后者,则更新state的值,如果不是上述的两种情况,那么直接返回false说明尝试获取锁失败。

    非公平锁NonfairSync

    公平锁的lock使用了AQS的acquire,而acquire会将参与锁竞争的线程加入到等待队列中去按顺序获得锁,队列头部的节点代表着当前获得锁的节点,头结点释放锁之后会唤醒其后继节点,然后让后继节点来竞争获取锁,这样就可以保证锁的获取是按照一定的优先级来的。而非公平锁的实现则会首先尝试去竞争锁,如果不成功,再走AQS提供的acquire方法,下面是NonfairSync的lock方法:

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

    非公平锁的tryAcquire方法使用了父类的nonfairTryAcquire方法来实现。

    ReentrantLock上锁和释放锁

    说完了Sync类和其两个子类,现在来看一下ReentrantLock是如何使用这两个类来实现lock和unlock的。首先是ReentrantLock的构造方法:

    
        /**
         * Creates an instance of {@code ReentrantLock}.
         * This is equivalent to using {@code ReentrantLock(false)}.
         */
        public ReentrantLock() {
            sync = new NonfairSync();
        }
    
        /**
         * Creates an instance of {@code ReentrantLock} with the
         * given fairness policy.
         *
         * @param fair {@code true} if this lock should use a fair ordering policy
         */
        public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }
    
    

    默认构造函数使用了非公平锁来提高并发度,一般情况下使用默认构造函数即可。而在一些特殊的情景下,需要使用公平锁的话就传递一个true的参数。下面是lock和unlock方法的细节,lock使用了Sync的lock方法,而unlock使用了AQS的release方法,而release方法使用了其tryRelease方法,而这个方法在Sync类中被重写,上面我们已经有过分析。

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

    newCondition方法

    newCondition这个方法需要一些篇幅来描述一下,而newCondition方法的返回内容涉及AQS类中的内部类ConditionObject,所以也就是分析一下ConditionObject这个类的一些细节。下面的图片展示了ConditionObject这个类的类图,可以看出,它实现了Condition的所有方法。

    ConditionObject类图

    关于Condition接口的描述,可以参考下面的文档内容:

    
     * Conditions (also known as condition queues or
     * condition variables) provide a means for one thread to
     * suspend execution (to wait) until notified by another
     * thread that some state condition may now be true.  Because access
     * to this shared state information occurs in different threads, it
     * must be protected, so a lock of some form is associated with the
     * condition. The key property that waiting for a condition provides
     * is that it atomically releases the associated lock and
     * suspends the current thread, just like {@code Object.wait}.
    
    

    await和await(long time, TimeUnit unit)方法

    接下来分析一下ConditionObject类是如何实现Condition接口的方法的。首先是await方法,这个方法的意思是让当前线程等待直到有别的线程signalled,或者被interrupted。调用此方法的线程会被阻塞直到有其他的线程唤醒或者打断它。下面是它的实现。

    
            public final void await() throws InterruptedException {
                if (Thread.interrupted())
                    throw new InterruptedException();
                Node node = addConditionWaiter();
                int savedState = fullyRelease(node);
                int interruptMode = 0;
                while (!isOnSyncQueue(node)) {
                    LockSupport.park(this);
                    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                        break;
                }
                if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                    interruptMode = REINTERRUPT;
                if (node.nextWaiter != null) // clean up if cancelled
                    unlinkCancelledWaiters();
                if (interruptMode != 0)
                    reportInterruptAfterWait(interruptMode);
            }
    
         
    

    首先,如果线程被中断了,那么抛出异常。否则调用addConditionWaiter方法生成一个Node,下面来看一下addConditionWaiter这个方法的细节:

    
            private Node addConditionWaiter() {
                Node t = lastWaiter;
                // If lastWaiter is cancelled, clean out.
                if (t != null && t.waitStatus != Node.CONDITION) {
                    unlinkCancelledWaiters();
                    t = lastWaiter;
                }
                Node node = new Node(Thread.currentThread(), Node.CONDITION);
                if (t == null)
                    firstWaiter = node;
                else
                    t.nextWaiter = node;
                lastWaiter = node;
                return node;
            }
    
    

    lastWaiter是等待在该Condition上的队列末尾的线程,根据代码,首先,如果最后一个线程从Condition上被取消了,并且当前线程并没有在该Condition的等待队列上,那么就将当前线程作为该Condition上等待队列的末尾节点。如果上面的条件不成立,那么就使用当前线程生成一个新的Node,然后将其状态变为Node.CONDITION代表其等待在某个Condition上,然后将该新的节点添加到队列的末尾。

    现在回头看await方法,我们发现addConditionWaiter的作用就是将当前线程添加到Condition的等待队列上去。接下来的步骤特别关键。await方法调用了fullyRelease方法,我们来看一下这个方法是干嘛用的:

    
        final int fullyRelease(Node node) {
            boolean failed = true;
            try {
                int savedState = getState();
                if (release(savedState)) {
                    failed = false;
                    return savedState;
                } else {
                    throw new IllegalMonitorStateException();
                }
            } finally {
                if (failed)
                    node.waitStatus = Node.CANCELLED;
            }
        }
    
    

    fullyRelease会调用release方法来是释放当前线程的同步状态,并且返回释放之后的状态值,这个值在await方法中作为了acquireQueued方法参数,这个方法在稍后分析。现在来看一下接下来的步骤,在获得了当前线程的state值了之后,就会进入一个while循环中去,while循环停止的条件是isOnSyncQueue(node)这个方法返回true,这个方法是用来判断一个Node是否在AQS的等待队列中的,下面是其方法内容:

    
        final boolean isOnSyncQueue(Node node) {
            if (node.waitStatus == Node.CONDITION || node.prev == null)
                return false;
            if (node.next != null) // If has successor, it must be on queue
                return true;
     
            return findNodeFromTail(node);
        }
    
        private boolean findNodeFromTail(Node node) {
            Node t = tail;
            for (;;) {
                if (t == node)
                    return true;
                if (t == null)
                    return false;
                t = t.prev;
            }
        }
    

    也就是说,只要当前线程的Node还在Condition上等待的话,就会一直在while循环中等待,而这个等待被破除的关键是signal方法,后面会分析到。我们现在假设signal方法运行完了,并且当前线程已经被添加到了AQS的SYNC等待队列中去了,那么接下来就使用我们一开始获取到的state值来竞争锁了,而这个竞争就是AQS的逻辑,下面的方法就是这个竞争去获取锁的方法:

    
        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. 该节点的前驱节点是头结点,头结点代表的是获得锁的节点,只有它释放了state其他线程才能获得这个变量的所有权
    2. 在条件1的前提下,方法tryAcquire返回true,也就是可以获得同步资源state

    整个await方法总结起来就是首先释放当前线程的条件变量,然后获取到释放完了之后的state值,我们假设这就是这个线程当前上下文的一部分内容,然后进入阻塞等待,一直在while循环里面等待,如果当前线程的Node被添加到了Sync队列中去了,那么就可以开始去竞争锁了,否则一直在等待,在await方法的整个过程中,可以相应中断。

    上面分析了await方法,await(long time, TimeUnit unit)方法只是在await方法上加了一个超时时间,await会死等直到被添加到Sync队列中去,而await(long time, TimeUnit unit)方法只会等设定的超时时间,如果超时时间到了,会自己去竞争锁。

    还有awaitUninterruptibly方法是await方法的简化版本,它不会相应中断。awaitUntil(Date deadline)方法让你可以设定一个deadline时间,如果超过这个时间了还没有被添加到Sync队列中去,那么线程就会自作主张的去竞争锁了。

    signal和signalAll方法

    上面分析了如何使用await等一系列方法来block线程,现在来分析如何使线程冲破block从而参与到获取锁的竞争中去。首先分析一下signal方法,使用这个方法可以使得线程被添加到Sync队列中去竞争锁。

    
            public final void signal() {
                if (!isHeldExclusively())
                    throw new IllegalMonitorStateException();
                Node first = firstWaiter;
                if (first != null)
                    doSignal(first);
            }
            
            private void doSignal(Node first) {
                do {
                    if ( (firstWaiter = first.nextWaiter) == null)
                        lastWaiter = null;
                    first.nextWaiter = null;
                } while (!transferForSignal(first) &&
                         (first = firstWaiter) != null);
            }
            
        final boolean transferForSignal(Node node) {
            if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
                return false;
    
            Node p = enq(node);
            int ws = p.waitStatus;
            if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
                LockSupport.unpark(node.thread);
            return true;
        }
     
        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;
                    }
                }
            }
        }          
    

    signal方法首先要唤醒的是等待在它的Condition的等待队列上的第一个节点,signal方法调用了doSignal方法,而doSignal方法调用了transferForSignal方法,transferForSignal方法调用enq方法将节点添加到了Sync队列中去,至此,await方法的while循环将不满足继续循环的条件,会执行循环之后的流程,也就是会去竞争锁,而之后的流程已经在Java同步框架AbstractQueuedSynchronizer中有分析,不在此赘述。

    signalAll方法的意思是把所有等待在条件变量上的线程都唤醒去竞争锁,下面是它的流程。

    
         public final void signalAll() {
                if (!isHeldExclusively())
                    throw new IllegalMonitorStateException();
                Node first = firstWaiter;
                if (first != null)
                    doSignalAll(first);
            }
            
           private void doSignalAll(Node first) {
                lastWaiter = firstWaiter = null;
                do {
                    Node next = first.nextWaiter;
                    first.nextWaiter = null;
                    transferForSignal(first);
                    first = next;
                } while (first != null);
            }        
    
    

    在方法doSignalAll中遍历每一个等待在条件变量上的Node,然后调用transferForSignal方法将它们添加到Sync队列中去。关于Condition的内容就分析这么多,介于后文还要对java的可重入读写锁进行分析,所以篇幅不宜过长,日后会对Condition进行更为深入的学习和总结。

    ReentrantReadWriteLock

    ReentrantReadWriteLock即可重入读写锁,下文将分析它在什么情况下是可重入的,而在什么情况下是独占的。ReentrantReadWriteLock 类实现了ReadWriteLock接口,它提供的读写锁是分离的,读锁和写锁分别是独立的锁。而读锁和写锁的实现也是不一样的,ReentrantReadWriteLock使用了两个内部类ReadLock和WriteLock来分别表示读锁和写锁,而这两种锁又依赖于基于AQS的类Sync来实现,Sync也是一个内部类,它继承了AQS类来实现了lock和unlock的语义。首先来分析一下其Sync内部类。

    Sync内部类

    首要解决的一个问题是,我们知道,AQS是使用了一个int类型的值来表示同步变量的,现在要使用一个int值来表示读锁和写锁两种类型的同步,怎么办呢?我们知道,一个int是32位的,ReentrantReadWriteLock使用高16位代表了读锁同步变量,而低16位代表了写锁同步变量,所以读锁与写锁的可重入数量限定在了(2^16-1)个,当然AQS还有一个使用long变量来实现的版本AbstractQueuedLongSynchronizer,它的实现和AQS除了使用了long类型的变量来代表同步变量之外没有区别。下面我们来看一下Sync是如何获取重入的读线程数量和写线程数量的:

    
            static final int SHARED_SHIFT   = 16;
            static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
            static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
            static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    
            /** Returns the number of shared holds represented in count  */
            static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
            /** Returns the number of exclusive holds represented in count  */
            static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
    
    

    我们来实践一个,比如现在的c是589826,则 (589826 >>> 16 = 9),说明有9个读可重入数量,而(589826 & (1 << 16 - 1)) = 2,说明有写重入的数量为2。需要注意的一点是,读锁可以有多个线程获取,而写锁只允许一个线程获取,那如何使用16位来代表多个读锁呢?ReentrantReadWriteLock使用了ThreadLocal来保存每个线程的重入数量,关于ThreadLocal的分析总结,可以参考Java中的ThreadLocal和 InheritableThreadLocal,ReentrantReadWriteLock的做法如下:

    
            /**
             * A counter for per-thread read hold counts.
             * Maintained as a ThreadLocal; cached in cachedHoldCounter
             */
            static final class HoldCounter {
                int count = 0;
                // Use id, not reference, to avoid garbage retention
                final long tid = getThreadId(Thread.currentThread());
            }
    
            /**
             * ThreadLocal subclass. Easiest to explicitly define for sake
             * of deserialization mechanics.
             */
            static final class ThreadLocalHoldCounter
                extends ThreadLocal<HoldCounter> {
                public HoldCounter initialValue() {
                    return new HoldCounter();
                }
            }
    
        private transient HoldCounter cachedHoldCounter;
        
    

    Sync类实现了一些子类通用了方法,下面重点分析几个方法。

    tryRelease和tryReleaseShared方法

    tryRelease方法重写了AQS的tryRelease方法,而tryRelease这个方法会在release方法中使用到,也就是在unlock的时候用到。下面展示了它的细节:

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

    tryRelease很好理解,它的任务就是去更新state值,它调用了我们上面分析过的exclusiveCount方法来计算写重入的数量。到这里需要提出的是,ReentrantReadWriteLock在实现上实现了读锁和写锁,读锁允许多个线程重入,使用了AQS的共享模式,而写锁只允许一个线程获得锁,使用了AQS的独占模式,所以这个tryRelease方法会在WriteLock的unlock方法中被用到,而ReadLock中的unlock使用的是AQS的releaseShared方法,而这个方法会调用AQS的tryReleaseShared方法,而这个方法在Sync被重写,也就是接下来分析的方法:

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

    可以很明显的感觉得出来,读锁的释放要比写锁的释放要麻烦很多,因为写锁只有一个线程获得,而读锁则有多个线程获取。释放需要获取到当前线程的ThreadLocal变量,然后更新它的重入数量,更新state值。可以看到,因为使用了ThreadLocal,使得多个线程的问题变得简单起来,就好像是操作同一个线程一样。

    tryAcquire和tryAcquireShared方法

    ReadLock在调用lock方法的时候,会调用AQS的releaseShared方法,而releaseShared方法会调用AQS的tryReleaseShared方法,而tryReleaseShared方法在Sync中被重写了。WriteLock在lock的时候,会调用AQS的acquire方法,而acquire方法会调用AQS的tryAcquire方法,而tryAcquire方法在Sync中被重写了,所以接下来分析一下这两个被重写的方法来认识一下WriteLock和ReadLock是如何通过AQS来lock的。

    首先是tryAcquire方法:

    
            protected final boolean tryAcquire(int acquires) {
                /*
                 * Walkthrough:
                 * 1. If read count nonzero or write count nonzero
                 *    and owner is a different thread, fail.
                 * 2. If count would saturate, fail. (This can only
                 *    happen if count is already nonzero.)
                 * 3. Otherwise, this thread is eligible for lock if
                 *    it is either a reentrant acquire or
                 *    queue policy allows it. If so, update state
                 *    and set owner.
                 */
                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;
            }
    
    

    这个方法用于写锁上锁,我们知道,只有一个线程可以获取到写锁,如果w为0,说明已经有线程获得了读锁,而在有读线程在读取数据的时候,写锁是无法获得的,所以w为0直接回失败,或者w不为0则说明了已经有线程获得了写锁,那么因为只允许有一个线程获取到写锁,所以如果当前线程不是那个获得了写锁的独占锁的话,也就直接失败,否则,如果上面两条检测都通过了,也就是说,当前没有线程获得读锁和写锁,那么判断重入数量是否超过了最大值,如果是则抛出异常,否则上锁成功。上面的分析都是基于c不为0,也就是说已经有线程获得了读锁或者写锁的情况下分析的,那如果c为0呢?说明当前环境下没有线程占有锁,那么接下来就分公平锁和非公平锁了,Sync有两个抽象方法需要子类来实现为公平锁还是非公平锁:

    
            /**
             * Returns true if the current thread, when trying to acquire
             * the read lock, and otherwise eligible to do so, should block
             * because of policy for overtaking other waiting threads.
             */
            abstract boolean readerShouldBlock();
    
            /**
             * Returns true if the current thread, when trying to acquire
             * the write lock, and otherwise eligible to do so, should block
             * because of policy for overtaking other waiting threads.
             */
            abstract boolean writerShouldBlock();
    
    

    具体的细节到公平锁和非公平锁的分析上再讲细节。上面分析完了WriteLock使用的lock需要的tryAcquire方法,下面来分析一下ReadLock的lock需要的tryReleaseShared方法:

    
            protected final int tryAcquireShared(int unused) {
                /*
                 * Walkthrough:
                 * 1. If write lock held by another thread, fail.
                 * 2. Otherwise, this thread is eligible for
                 *    lock wrt state, so ask if it should block
                 *    because of queue policy. If not, try
                 *    to grant by CASing state and updating count.
                 *    Note that step does not check for reentrant
                 *    acquires, which is postponed to full version
                 *    to avoid having to check hold count in
                 *    the more typical non-reentrant case.
                 * 3. If step 2 fails either because thread
                 *    apparently not eligible or CAS fails or count
                 *    saturated, chain to version with full retry loop.
                 */
                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);
            }
    
    

    同样比WriteLock的要复杂很多,这个方法会返回1或者-1代表lock的结果,1代表lock成功了,-1代表lock失败了,来分析一下在上面情况下会失败:

    1、如果有线程已经获得了写锁,那么肯定会失败,因为写锁是排斥锁,不允许其他线程获得任意类型的锁
    2、如果重入的数量已经超过了限定,那么也会失败,如果你还想要支持更多的重入数量,那么使用AbstractQueuedLongSynchronizer来代替AQS

    而在下面的情况下是会成功的:

    1、没有线程获得写锁
    2、获得写锁的线程就是当前想要获得读锁的线程
    3、重入数量没有超过上限

    总结起来就是,只要有线程获得了写锁,那么其他线程都获取不到写锁,如果获得写锁的线程想要获取读锁,那么可以成功。在获取读锁的时候,多个线程可以同时获得读锁,读锁是共享锁,而写锁是独占锁。

    FairSync和NonFairSync的实现

    这两个类仅仅是继承了父类然后实现了两个抽象方法,来表示读线程是否需要阻塞和写线程是否需要阻塞,以这样的方式来达到公平锁和非公平锁的目的。

    下面是公平锁的实现:

    
            final boolean writerShouldBlock() {
                return hasQueuedPredecessors();
            }
            final boolean readerShouldBlock() {
                return hasQueuedPredecessors();
            }
    
    

    可以看出,对于公平锁来说,读锁和写锁都是查看Sync队列中是否有排队的线程,如果没有,则可以放行,否则就得排队。下面是非公平锁的实现:

    
    
           final boolean writerShouldBlock() {
                return false; // writers can always barge
            }
            final boolean readerShouldBlock() {
                /* As a heuristic to avoid indefinite writer starvation,
                 * block if the thread that momentarily appears to be head
                 * of queue, if one exists, is a waiting writer.  This is
                 * only a probabilistic effect since a new reader will not
                 * block if there is a waiting writer behind other enabled
                 * readers that have not yet drained from the queue.
                 */
                return apparentlyFirstQueuedIsExclusive();
            }
    
        final boolean apparentlyFirstQueuedIsExclusive() {
            Node h, s;
            return (h = head) != null &&
                (s = h.next)  != null &&
                !s.isShared()         &&
                s.thread != null;
        }
    

    在非公平锁中,写锁的获取不需要阻塞,而读锁的获取在apparentlyFirstQueuedIsExclusive中判断是否需要阻塞。所谓公平锁和非公平锁只是希望能对所有的线程都不区别对待,但是使用公平锁的代价是吞吐量没有非公平锁那么大,所以,如果我们的需求没有特别的原因,应该使用非公平锁。

    ReadLock和WriteLock

    上面介绍了Sync类,现在来分析一下ReadLock和WriteLock是如何通过Sync提高的方法来实现lock和unlock的。首先是ReadLock。它的lock和unlock方法如下:

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

    而WriteLock的lock和unlock方法如下:

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

    如果想要获取更多关于AQS的相关知识,可以去阅读AQS的源代码,或者参考Java同步框架AbstractQueuedSynchronizer,上文中也对lock和unlock的流程有所分析,再次也不做赘述。

    可重入锁使用示例

    最后,在分析了ReentrantLock和ReentrantReadWriteLock之后,来看一下如何使用它们。

    ReentrantLock使用示例

    
    /**
     * how to use ReentrantLock lock
     */
    class LockX {
        private final Lock LOCK = new ReentrantLock(); // non-fair lock
        
        public void lockMethod() {
            LOCK.lock();
            try {
                doBiz();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
        }
        
        public void doBiz() {
            
        }
        
    }
    
    

    你需要注意的是你应该总是在try块中执行你的业务代码,然后在finally中unlock掉。

    ReentrantReadWriteLock使用示例

    
    abstract class CachedData {
        Object data;
        volatile boolean cacheValid;
        final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        
        void processCachedData() {
          rwl.readLock().lock();
          if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
              // Recheck state because another thread might have
              // acquired write lock and changed state before we did.
              if (!cacheValid) {
                data = loadData();
                cacheValid = true;
              }
              // Downgrade by acquiring read lock before releasing write lock
              rwl.readLock().lock();
            } finally {
              rwl.writeLock().unlock(); // Unlock write, still hold read
            }
          }
     
          try {
            consumeData(data);
          } finally {
            rwl.readLock().unlock();
          }
        }
        
        abstract Object loadData();
        abstract void consumeData(Object data);
      }
    
    

    别忘了在lock之后要unlock,否则如果一个写锁被获取之后没有释放的话,就不可能有锁获得锁了除非它自己本身。通用的做法是在try 块中进行业务处理,然后在finally中释放锁。Lock接口的使用相比于synchronized的使用要复杂很多,所以在大部分情况下,你应该使用synchronized来做并发控制,而不是Lock,但是如果想要做更加灵活的锁控制,你就可以选择使用Lock接口的具体实现类来应用,或者继承AQS来实现自己的同步器。

    相关文章

      网友评论

        本文标题:Java可重入锁详解

        本文链接:https://www.haomeiwen.com/subject/bsvxuxtx.html