美文网首页
多线程知识梳理(13) - ReentrantReadWrite

多线程知识梳理(13) - ReentrantReadWrite

作者: 泽毛 | 来源:发表于2019-05-13 11:13 被阅读0次

    一、基本概念

    ReentrantReadWriteLockJava并发包中提供的读写锁实现,它维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排它锁有了提升,写锁和读锁的特点总结如下。

    1.1 写锁的特点

    • 写锁是可重入的。
    • 如果存在读锁,则写锁不能获取,只有等待其它读线程都释放了读锁,写锁才能被当前线程获取。
    • 当前线程获取写锁后,它仍然可以获取读锁,而 其它线程 对于读锁和写锁的获取均被阻塞。
    • 当前线程获取写锁后,再获取到读锁,随后释放掉写锁,这种操作称为锁降级。

    1.2 读锁的特点

    • 读锁是可重入。
    • 读锁是共享的,它能够被多个线程同时获取。
    • 在没有其它写线程访问时,读锁总会被成功获取。

    1.3 示例

    /**
     * @author lizejun
     **/
    public class ReadWriteDemo {
    
        private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
        private final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        private final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    
        private int data;
    
        private void set(int data) {
            writeLock.lock();
            System.out.println("write lock begin");
            try {
                System.out.println("write data=" + data);
                this.data = data;
            } finally {
                System.out.println("write lock end");
                writeLock.unlock();
            }
        }
    
        private void get() {
            readLock.lock();
            System.out.println("read lock begin");
            try {
                System.out.println("read data=" + data);
            } finally {
                readLock.unlock();
                System.out.println("read lock end");
            }
        }
    
        public static void run() {
            final ReadWriteDemo demo = new ReadWriteDemo();
            for (int i = 0; i < 5; i++) {
                Thread pThread = new Thread() {
    
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(Math.round(Math.random() * 5));
                            demo.set((int) Math.round(Math.random() * 100));
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                };
                pThread.start();
            }
            for (int i = 0; i < 5; i++) {
                Thread cThread = new Thread() {
    
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1);
                            demo.get();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                    }
                };
                cThread.start();
            }
        }
    }
    

    运行结果:


    读写锁示例

    二、读写锁的实现

    2.1 读写状态的设计

    虽然我们在使用ReentrantReadWriteLock的时候,是通过从ReentrantReadWriteLock获得两个不同的锁writeLock()readLock(),但在其内部都是通过同一个AQS的实现类Sync来关联到同一个同步队列。

    其中读写状态依赖的也是同步器的同步状态state,读写锁将变量切割成了两个部分,高16位表示读,低16位表示写,通过位运算判断当前的读写状态。

    2.2 公平 & 非公平模式

    Sync中提供了两个抽象的方法是子类需要实现的,它们表示当有别的线程也在尝试获取锁时,是否应该阻塞,它决定了锁是公平还是非公平的,用于写锁和读锁的获取逻辑中。

    • boolean writerShouldBlock()
    • boolean readerShouldBlock()

    公平版本FairSync的实现为:

        static final class FairSync extends Sync {
    
            final boolean writerShouldBlock() {
                return hasQueuedPredecessors();
            }
            final boolean readerShouldBlock() {
                return hasQueuedPredecessors();
            }
        }
    

    hasQueuedPredecessors表示当前是否有线程在等待。

    非公平版本NonfairSync的实现为:

        static final class NonfairSync extends Sync {
    
            final boolean writerShouldBlock() {
                return false;
            }
            
            final boolean readerShouldBlock() {
                return apparentlyFirstQueuedIsExclusive();
            }
        }
    

    apparentlyFirstQueuedIsExclusive表示当前获取到同步状态的线程是否占用了写锁,如果是,那么返回true

    2.3 写锁的获取 & 释放

    2.3.1 获取

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

    获取写锁时分为三个步骤:

    • 当前有读锁或者已经被其它线程占有了写锁,那么获取失败。
    • 写锁数量大于65535,抛出异常。
    • 如果writerShouldBlock()返回true(如前所述,对于公平锁来说,指的是当前是否有其它线程在排队;对于非公平锁来说,始终是false),或者CAS设置失败,那么获取失败。

    如果获取失败,那么将会进入同步队列中去排队;如果获取成功,一个线程可以多次地获取写锁,并增加同步状态的值。

    2.3.2 释放

        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,写锁才能够被释放。

    2.4 读锁的获取 & 释放

    2.4.1 获取

        protected final int tryAcquireShared(int unused) {    
            for (;;) {            
                int c = getState();            
                int nextc = c + (1 << 16);            
                if (nextc < c)                    
                    throw new Error("Maximum lock count exceeded");            
                if (exclusiveCount(c) != 0 && owner != Thread.currentThread())                    
                    return -1;            
                if (compareAndSetState(c, nextc))                     
                    return 1;    
            } 
        }
    

    上面是获取读锁的简略版,保留了获取的核心的逻辑。当读锁获取成功后,会把读状态+1。这里面有一个要点:如果当前写锁已经被一个线程占有,那么只有该线程可以获取读锁,其它线程都无法获取读锁。

    2.4.2 释放

        protected final boolean tryReleaseShared(int unused) {
            //...
            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;
            }
        }
    

    读锁的每次释放,就是通过CAS减少读状态,每次减少的值是(1 << 16)

    相关文章

      网友评论

          本文标题:多线程知识梳理(13) - ReentrantReadWrite

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