JAVA并发之ReentrantReadWriteLock原理解析(一)

210 阅读5分钟

本篇文章主要介绍下Java并发包内的ReentrantReadWriteLock工具类的读锁。首先它具有可重入锁的功能(可重入锁的介绍可以参考JAVA并发之ReentrantLock原理解析),同时也实现了ReadWriteLock接口,具有读锁(共享锁)和写锁(排他锁)两把锁。

读锁(共享锁)

读锁是共享锁,也分公平锁和非公平锁,在没有写锁在执行或者在AQS队列排队的时候,两者没有区别,会直接将读锁的count加1(线程退出读锁的时候调用unlock会让state减1)。

这里需要注意的是读锁和写锁的count是共用了AQS的state变量的,他是一个32位的int型变量,高16位表示读锁的count,低16位表示写锁的count,再理解下面的源码就能明白了。

//AQS
static final int SHARED_SHIFT   = 16;
//获取读锁的count,将state右移16位,也就是取高16位
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }

static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
//下面是tryAcquireShared方法当中判断读锁能否获取到锁的部分逻辑
//其中compareAndSetState(c, c + SHARED_UNIT)表示将state加上1左移16位,反应到sharedCount方法就是相当于加了1
if (!readerShouldBlock() &&
                r < MAX_COUNT &&
                compareAndSetState(c, c + SHARED_UNIT)) {
  ...
}

下图是读锁调用的整个过程,当只有读锁的时候,方法走到第四步fullTryAcquireShared就返回了,这种情况下肯定能获得读锁。

下面我们来看下每一步大致的逻辑。

JAVA并发之ReentrantReadWriteLock原理解析(一)

其中主要的方法有两个:

  • tryAcquireShared
  1. 如果有其他线程拿有写锁,当前线程就调用doAcquireShared进AQS队列等待;
  2. 如果拿有写锁的线程是当前线程,当前线程继续尝试获取读锁;
  3. 当前线程尝试拿读锁的时候需要判断是否要排队,这里公平锁和非公平锁表现就有差异了:
  4. 公平锁:如果AQS当中有线程在排队,则当前线程也需要排队
  5. 非公平锁:如果AQS当中有线程排队且不是读线程时,当前线程需要排队
  6. 如果当前线程尝试获取锁成功了,则直接返回,否则进入fullTryAcquireShared接着尝试
  • doAcquireShared
  1. 将当前线程加到AQS的尾部
  2. 再次尝试一下tryAcquireShared(万一其他线程释放锁了呢)
  3. 如果成功则拿到锁,唤醒下一个在AQS中等待的读锁线程,返回
  4. 如果不成功,则标记AQS中前节点,告诉它等你释放锁的时间记得唤醒我,然后休眠等待被唤醒

读锁举例

下面通过例子来看下读锁的几种情况:

  • 当前只有读锁

结论:最简单的情况,所有线程都能获得读锁。

  • 有线程拿了写锁,并且不是当前线程

结论:当前线程进队列等待,等写锁释放了才有可能获取读锁

下面通过代码来验证下我们的结论。

/**
     * 测试当其他线程获取写锁时,当前线程不能同时获取读锁
     * 
     * @throws Exception
     */
    private static void testReadLockWhenAnotherThreadHasWriteLock() throws Exception {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        DemoThread readThread = new DemoThread("ReadThread", readLock);
        DemoThread writeThread = new DemoThread("WriteThread", writeLock);
        writeThread.start();
        Thread.sleep(1000);
        readThread.start();
}
    
static class DemoThread extends Thread {
        private DateFormat df = new SimpleDateFormat("HH:mm:ss---");
        private Lock lock;
        
        public DemoThread(String name, Lock lock) {
            super(name);
            this.lock = lock;
        }
        
        @Override
        public void run() {
            try {
                lock.lock();
                System.out.println(df.format(new Date()) + getName() + " is working.");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } finally {
                System.out.println(df.format(new Date()) + getName() + " finished work.");
                lock.unlock();
            }
            
        }
}

以上代码,我们创建了两个线程,写线程WriteThread先获取了写锁,5秒钟后释放,然后另一个读线程ReadThread需要等到写锁释放才能获取读锁。

12:56:53---WriteThread is working.
12:56:58---WriteThread finished work.
12:56:58---ReadThread is working.
12:57:03---ReadThread finished work.
  • 当前线程拿了写锁,再去获取读锁

结论1: 当AQS没有其他线程排队时,当前线程能立刻获取读锁

同样我们看下代码

    /**
     * 测试当其他线程获取写锁时,当前线程不能同时获取读锁
     * 
     * @throws Exception
     */
    private static void testReadLockWhenSameThreadHasWriteLock() throws Exception {
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        try {
            writeLock.lock();
            System.out.println("I got the write lock.");
            readLock.lock();
            System.out.println("I got the read lock.");
        } finally {
            readLock.unlock();
            writeLock.unlock();
        }
    }

控制台打出来的日志显示,当前线程获取写锁后,也获取到了读锁。

I got the write lock.
I got the read lock.

结论2: 当AQS有其他线程排队获取写锁时,当前线程需要排队等待

/**
     * 测试当前线程获取写锁后,如果AQS有其他线程要获取写锁,需要排队获取读锁
     * 
     * @throws Exception
     */
    private static void testReadLockWhenSameThreadHasWriteLockAndAQSHasWriteThread() throws Exception {
        DateFormat df = new SimpleDateFormat("HH:mm:ss---");
        ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
        DemoThread2 readWriteThread = new DemoThread2("ReadWriteThread", lock);
        readWriteThread.start();
        Thread.sleep(1000);
        System.out.println(df.format(new Date()) +"MainThread start to gain WriteLock");
        writeLock.lock();
        System.out.println(df.format(new Date()) +"MainThread got WriteLock");
        Thread.sleep(5000);
        writeLock.unlock();
        System.out.println(df.format(new Date()) +"MainThread released WriteLock");
}
    
    static class DemoThread2 extends Thread {
        private DateFormat df = new SimpleDateFormat("HH:mm:ss---");
        private ReentrantReadWriteLock lock;
        
        public DemoThread2(String name, ReentrantReadWriteLock lock) {
            super(name);
            this.lock = lock;
        }
        
        @Override
        public void run() {
            try {
                ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
                ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
                writeLock.lock();
                System.out.println(df.format(new Date()) + getName() + " got WriteLock.");
                Thread.sleep(5000);
                writeLock.unlock();
                System.out.println(df.format(new Date()) + getName() + " release WriteLock.");
                
                System.out.println(df.format(new Date()) + getName() + " try to get ReadLock.");
                readLock.lock();
                System.out.println(df.format(new Date()) + getName() + " got ReadLock.");
                readLock.unlock();
                System.out.println(df.format(new Date()) + getName() + " released ReadLock.");
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }            
        }
}

控制台显示当线程ReadWriteThread获取写锁后,主线程MainThread尝试获取写锁失败进入AQS排队,然后ReadWriteThread尝试获取读锁也失败进入了AQS排队。

13:28:49---ReadWriteThread got WriteLock.
13:28:50---MainThread start to gain WriteLock
13:28:54---ReadWriteThread release WriteLock.
13:28:54---MainThread got WriteLock
13:28:54---ReadWriteThread try to get ReadLock.
13:28:59---MainThread released WriteLock
13:28:59---ReadWriteThread got ReadLock.
13:28:59---ReadWriteThread released ReadLock.

结论3: 当读写锁是公平锁时,AQS若有其他线程排队获取读锁时,当前线程需要排队等待

结论4: 当读写锁是非公平锁时,AQS若有其他线程排队获取读锁时,当前线程不需要排队可立刻获取读锁

结论3和结论4由于篇幅原因这里就不贴代码了,感兴趣的朋友可以参考下面的gitee代码。

Demo代码位置


src/main/java/net/weichitech/juc/ReentrantReadWriteLockTest.java · 小西学编程/java-learning - Gitee.com