喻红叶《Java并发-ReentrantReadWriteLock源码分析》

来源:互联网 发布:网络互联设备有哪些 编辑:程序博客网 时间:2024/06/10 07:41

ReentrantLock实现了标准的互斥重入锁,任一时刻只有一个线程能获得锁。考虑这样一个场景:大部分时间都是读操作,写操作很少发生;我们知道,读操作是不会修改共享数据的,如果实现互斥锁,那么即使都是读操作,也只有一个线程能获得锁,其他的读都得阻塞。这样显然不利于提供系统的并发量。在JDK1.5中,Doug Lea又给我们带来了读写锁ReentrantReadWriteLock,在读-写锁的实现加锁策略中,允许多个读操作同时进行,但每次只允许一个写操作。

ReentrantReadWriteLock的结构

ReentrantReadWriteLock(简称RRWL)的实现也是通过AQS来实现的,它的内部也与一个Sync类,继承自AQS,读写锁都是依赖它来实现。在RRWL内部有两个类来分别实现读锁和写锁,ReadLock和WriteLock。RRWL有两个方法分别用于返回读锁和写锁:

ReentrantReadWriteLock.ReadLockreadLock()
返回读锁
  ReentrantReadWriteLock.WriteLockwriteLock()
返回写锁
同样的,RRWL使用FairSync实现公平策略,NonFairSync实现非公平策略。RRWL包含的类:


Sync类

在读写锁中最重要的就是Sync类,它继承自AQS,还记得吗,AQS使用一个int型来保存状态,状态在这里就代表锁,它提供了获取和修改状态的方法。可是,这里要实现读锁和写锁,只有一个状态怎么办?Doug Lea是这么做的,它把状态的高16位用作读锁,低16位用作写锁,所以无论是读锁还是写锁最多只能被持有65535次。所以在判断读锁和写锁的时候,需要进行位运算:

(1)由于读写锁共享状态,所以状态不为0,只能说明是有锁,可能是读锁,也可能是写锁;

(2)读锁是高16为表示的,所以读锁加1,就是状态的高16位加1,低16位不变,所以要加的不是1,而是2^16,减一同样是这样。

(3)写锁用低16位表示,要获得写锁的次数,要用状态&2^16-1,结果的高16位全为0,低16位就是写锁被持有的次数。

在Sync中还有几个属性,会在后面的代码中用到。

[java] view plaincopy
  1. /** 实现ReentrantReadWriteLock的同步器,分别用子类来实现公平和非公平策略 */  
  2.    abstract static class Sync extends AbstractQueuedSynchronizer {  
  3.        private static final long serialVersionUID = 6317671515068378041L;  
  4.   
  5.        //最多支持65535个写锁和65535个读锁;低16位表示写锁计数,高16位表示持有读锁的线程数  
  6.        static final int SHARED_SHIFT   = 16;  
  7.        //由于读锁用高位部分,读锁个数加1,其实是状态值加 2^16  
  8.        static final int SHARED_UNIT    = (1 << SHARED_SHIFT);  
  9.        static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;  
  10.        /**写锁的掩码,用于状态的低16位有效值 */  
  11.        static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;  
  12.   
  13.        /** 读锁计数,当前持有读锁的线程数,c的高16位 */  
  14.        static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }  
  15.        /** 写锁的计数,也就是它的重入次数,c的低16位*/  
  16.        static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }  
  17.   
  18.        /** 
  19.         * 每个线程持有读锁的计数 
  20.         */  
  21.        static final class HoldCounter {  
  22.            int count = 0;  
  23.            //使用id而不是引用是为了避免保留垃圾。注意这是个常量。  
  24.            final long tid = Thread.currentThread().getId();  
  25.        }  
  26.   
  27.        /** 
  28.         * 采用继承是为了重写 initialValue 方法,这样就不用进行这样的处理: 
  29.         * 如果ThreadLocal没有当前线程的计数,则new一个,再放进ThreadLocal里。 
  30.         * 可以直接调用 get。 
  31.         * */  
  32.        static final class ThreadLocalHoldCounter  
  33.            extends ThreadLocal<HoldCounter> {  
  34.            public HoldCounter initialValue() {  
  35.                return new HoldCounter();  
  36.            }  
  37.        }  
  38.   
  39.        /** 
  40.         * 当前线程持有的可重入读锁的数量,仅在构造方法和readObject(反序列化) 
  41.         * 时被初始化,当持有锁的数量为0时,移除此对象。 
  42.         */  
  43.        private transient ThreadLocalHoldCounter readHolds;  
  44.   
  45.        /** 
  46.         * 最近一个成功获取读锁的线程的计数。这省却了ThreadLocal查找, 
  47.         * 通常情况下,下一个释放线程是最后一个获取线程。这不是 volatile 的, 
  48.         * 因为它仅用于试探的,线程进行缓存也是可以的 
  49.         * (因为判断是否是当前线程是通过线程id来比较的)。 
  50.         */  
  51.        private transient HoldCounter cachedHoldCounter;  
  52.       
  53.        /**firstReader是第一个获得读锁的线程; 
  54.         * firstReaderHoldCount是firstReader的重入计数; 
  55.         * 更准确的说,firstReader是最后一个把共享计数从0改为1,并且还没有释放锁。 
  56.         * 如果没有这样的线程,firstReader为null; 
  57.         * firstReader不会导致垃圾堆积,因为在tryReleaseShared中将它置空了,除非 
  58.         * 线程异常终止,没有释放读锁。 
  59.         *  
  60.         * 跟踪无竞争的读锁计数时,代价很低 
  61.         */  
  62.        private transient Thread firstReader = null;  
  63.        private transient int firstReaderHoldCount;  
  64.   
  65.        Sync() {  
  66.            readHolds = new ThreadLocalHoldCounter();  
  67.            setState(getState()); // ensures visibility of readHolds  
  68.        }  
它的两个子类,FairSync和NonFairSync比较简单,它们就是决定在某些情况下读锁或者写锁是否需要阻塞,通过两个方法的返回值决定:

 final boolean writerShouldBlock();//写锁是否需要阻塞

final boolean readerShouldBlock();//读锁是否需要阻塞

获取读锁

读锁是共享锁,同一时刻可以被多个线程获得,下面是获得读锁的代码:

[java] view plaincopy
  1. /** 
  2.  * 获取读锁,如果写锁不是由其他线程持有,则获取并立即返回; 
  3.  * 如果写锁被其他线程持有,阻塞,直到读锁被获得。 
  4.  */  
  5. public void lock() {  
  6.     sync.acquireShared(1);  
  7. }  
  8. //ASQ的acquireShared  
  9. /** 
  10.  * 以共享模式获取对象,忽略中断。通过至少先调用一次 tryAcquireShared(int)  
  11.  * 来实现此方法,并在成功时返回。否则在成功之前,一直调用 tryAcquireShared(int) 
  12.  *  将线程加入队列,线程可能重复被阻塞或不被阻塞。 
  13.  */  
  14. public final void acquireShared(int arg) {  
  15.     if (tryAcquireShared(arg) < 0)  
  16.         doAcquireShared(arg);  
  17. }  
  18. //Sync中的tryAcquireShared  
  19. protected final int tryAcquireShared(int unused) {  
  20.             Thread current = Thread.currentThread();  
  21.             int c = getState();  
  22.             //持有写锁的线程可以获得读锁  
  23.             if (exclusiveCount(c) != 0 &&  
  24.                 getExclusiveOwnerThread() != current)  
  25.                 return -1;//写锁被占用,且不是由当前线程持有,返回-1  
  26.             //执行到这里表明:写锁可用,或者写锁由当前线程持有  
  27.             //获得读锁的数量  
  28.             int r = sharedCount(c);  
  29.               
  30.             /** 如果不用阻塞,且没有溢出,则使用CAS修改状态,并且修改成功 */  
  31.             if (!readerShouldBlock() &&  
  32.                 r < MAX_COUNT &&  
  33.                 compareAndSetState(c, c + SHARED_UNIT)) {//修改高16位的状态,所以要加上2^16  
  34.                 //这是第一个占有读锁的线程,设置firstReader  
  35.                 if (r == 0) {  
  36.                     firstReader = current;  
  37.                     firstReaderHoldCount = 1;  
  38.                 } else if (firstReader == current) {//重入计数加1  
  39.                     firstReaderHoldCount++;  
  40.                 } else {  
  41.                     // 非 firstReader 读锁重入计数更新  
  42.                     //将cachedHoldCounter设置为当前线程  
  43.                     HoldCounter rh = cachedHoldCounter;  
  44.                     if (rh == null || rh.tid != current.getId())  
  45.                         cachedHoldCounter = rh = readHolds.get();  
  46.                     else if (rh.count == 0)  
  47.                         readHolds.set(rh);  
  48.                     rh.count++;  
  49.                 }  
  50.                 return 1;  
  51.             }  
  52.             //获取读锁失败,放到循环里重试  
  53.             return fullTryAcquireShared(current);  
  54.         }  
重点关注Sync中的tryAcquireShared(int),注意,在所有的读写锁中,获取锁和释放锁每次都是一个计数行为,锁其计数都说是1,而在获得读锁的过程中,参数根本就没有意义。上面的代码包含的逻辑:

(1)如果当前写锁被其他线程持有,则获取读锁失败;

(2)写锁空闲,或者写锁被当前线程持有(写锁可降级为读锁),在公平策略下,它可能需要阻塞,那么tryAcquireShared()就可能失败,则需要进入队列等待;如果是非公平策略,会尝试获取锁,使用CAS修改状态,修改成功,则获得读锁,否则也会进入同步队列等待;

(3)进入同步队列后,就是由AQS来完成唤醒。

释放读锁

一般来说,释放锁比获取锁要容易一些,看一下释放读锁的代码:

[java] view plaincopy
  1. protected final boolean tryReleaseShared(int unused) {  
  2.         //将当前线程的读锁计数器的值减1  
  3.            Thread current = Thread.currentThread();  
  4.            /** 
  5.             * 当前线程是第一个获取到锁的,如果此线程要释放锁了,则firstReader置空 
  6.             * 否则,将线程持有的锁计数减1 
  7.             */  
  8.            if (firstReader == current) {  
  9.                // assert firstReaderHoldCount > 0;  
  10.                if (firstReaderHoldCount == 1)  
  11.                    firstReader = null;  
  12.                else  
  13.                    firstReaderHoldCount--;  
  14.            } else {  
  15.                HoldCounter rh = cachedHoldCounter;  
  16.                //如果cachedHoldCounter为空,或者不等于当前线程  
  17.                if (rh == null || rh.tid != current.getId())  
  18.                    rh = readHolds.get();  
  19.                int count = rh.count;  
  20.                if (count <= 1) {  
  21.                    readHolds.remove();  
  22.                    if (count <= 0)//如果没有持有读锁,释放是非法的  
  23.                        throw unmatchedUnlockException();  
  24.                }  
  25.                --rh.count;  
  26.            }  
  27.            //有可能其他线程也在释放读锁,所以要确保释放成功  
  28.            for (;;) {  
  29.                int c = getState();  
  30.                int nextc = c - SHARED_UNIT;//高16位-1  
  31.                if (compareAndSetState(c, nextc))  
  32.                 // 释放读锁对其他读线程没有任何影响,  
  33.                    // 但可以允许等待的写线程继续,如果读锁、写锁都空闲。  
  34.                    return nextc == 0;  
  35.            }  
  36.        }  
释放读锁很简单,就是把状态的高16位减1,同时把当前线程持有锁的计数减1。在释放的过程中,其他线程可能也在释放读锁,所以修改状态有可能失败,把修改状态放到循环里做,直到成功为止。

写锁的获取

[java] view plaincopy
  1. protected final boolean tryAcquire(int acquires) {  
  2.            Thread current = Thread.currentThread();  
  3.            //获取状态,是读写锁共有的  
  4.            int c = getState();  
  5.            //写锁被持有的次数,通过与低16位做与操作得到  
  6.            int w = exclusiveCount(c);  
  7.            //c!=0,说明存在锁,可能是读锁,也可能是写锁  
  8.            if (c != 0) {  
  9.                // c!=0,w==0,说明读锁存在  
  10.             //w != 0 && current != getExclusiveOwnerThread() 表示其他线程获取了写锁。  
  11.                if (w == 0 || current != getExclusiveOwnerThread())  
  12.                    return false;  
  13.                //如果超过了最大限制,则抛出异常  
  14.                if (w + exclusiveCount(acquires) > MAX_COUNT)  
  15.                    throw new Error("Maximum lock count exceeded");  
  16.                  
  17.                //执行到这里,说明存在写锁,且由当前线程持有  
  18.                // 重入计数  
  19.                setState(c + acquires);  
  20.                return true;  
  21.            }  
  22.            //执行到这里,说明不存在任何锁  
  23.            //WriterShouldBlock留给子类实现公平策略  
  24.            //使用CAS修改状态  
  25.            if (writerShouldBlock() ||  
  26.                !compareAndSetState(c, c + acquires))  
  27.                return false;  
  28.              
  29.            setExclusiveOwnerThread(current);  
  30.            return true;  
  31.        }  
其包含的逻辑:

(1)首先获得状态,保存到c中,获得写锁的计数保存到w中;这个时候需要根据c的值来判断是否存在锁

(2)如果c!=0,说明存在锁,如果w==0,说明存在读锁,获取写锁不能成功;如果w!=0,但是写锁是由其他线程持有的,那么当前线程获取写锁也不能成功;在这种情况下(存在写锁),只有写锁是由当前线程持有的,才能获得成功;

(3)如果c==0,说明不存在锁,如果是公平策略,还需要进入同步队列;如果是非公平策略,会尝试获得写锁。

释放写锁

[java] view plaincopy
  1. protected final boolean tryRelease(int releases) {  
  2.             if (!isHeldExclusively())  
  3.                 throw new IllegalMonitorStateException();  
  4.             int nextc = getState() - releases;  
  5.             boolean free = exclusiveCount(nextc) == 0;  
  6.             //如果锁是可用的  
  7.             if (free)  
  8.                 setExclusiveOwnerThread(null);  
  9.             setState(nextc);  
  10.             return free;  
  11.         }  
释放写锁很简答,就是状态的低16为减1,如果为0,说明写锁可用,返回true,如果不为0,说明当前线程仍然持有写锁,返回false;
0 0