Java使用读写锁替代同步锁

3,250 阅读3分钟

应用情景

前一阵有个做反抄袭检测的小伙伴问了我一个问题。

---
在多线程里就是有个变量,我需要读取它来判断是否给它写入一些信息。
打算加锁,但是如果读取时候加入readlock,写入时候加入writelock,
这样做可能读写不同步。但是如果一起加lock效果就跟synchronized一样,效率变差
---

其实他的问题就是下面的场景:

  • 高并发的读写请求
  • 读取请求明显大于写入的请求
  • 如果用synchronized同步锁会导致性能下降,本来读取是可以多线程同步进行的,同步锁就只能让他们一个一个排队读取。
  • 如果读取时加readlock,写入时候加writelock,会提升效率,因为读可以多线程并发,但是在线程A读完上锁的毫秒级时间里,有可能线程B也读完了,而且抢在了线程A之前修改了变量,导致程序出错。

处理方案

我去研究了下读写锁,后来发现其实java官方已经给了答案。使用读写锁加标志位解决读写不同步的问题。Java ReentrantReadWriteLock
在这先普及下读写锁。

  • 读锁:
    • 读锁拒绝其他线程获得写锁,读锁不拒绝其他线程获得读锁,多个上了读锁的线程可以并发读不会阻塞。
    • 多个读锁同时作用期间,其他想上写锁的线程都处在等待状态,当最后一个读锁释放后,才有可能上锁。
  • 写锁:
    • 写锁拒绝其他线程获取读锁和写锁。
    • 当一个线程获取写锁后,其他想要获取读写锁的线程都处于等待状态,直到写锁释放才有可能上锁。

##代码实例

直接拿Java官方示例解读了。

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
   void processCachedData() {
     rwl.readLock().lock();  //1. 上读锁
     if (!cacheValid) {      //2. 验证cacheValid
        // Must release read lock before acquiring write lock
        rwl.readLock().unlock(); //3. 解除读锁
        rwl.writeLock().lock(); //4. 上写锁
        try {
          // Recheck state because another thread might have
          // acquired write lock and changed state before we did.
          if (!cacheValid) {    //5. 验证cacheValid
            data = ...
            cacheValid = true;
          }
          // Downgrade by acquiring read lock before releasing write lock
          rwl.readLock().lock(); //6. 上读锁
        } finally {
          rwl.writeLock().unlock(); // Unlock write, still hold read //7. 解除写锁
        }
     }
     try {
       use(data);
     } finally {
       rwl.readLock().unlock();//8. 解除读锁
     }
   }
 }

比如现在有线程ABCDE五个线程,使用processCachedData()方法,接下来会发现如下步骤。

-> DE线程滞后,ABC同时进入到步骤1. 上读锁
-> ABC进入到步骤2. 验证cacheValid。(新实例中cacheValid初始为fasle,所以进入if条件句中)
-> ABC执行步骤3. 解除读锁。
-> 假设此时A率先完成步骤4. 上写锁。
-> BC无法获取写锁,处于等待状态,被阻塞在步骤4。 上写锁。此时D线程执行使用processCachedData()方法,被阻塞在步骤1. 上读锁。
-> A进入到步骤5. 验证cacheValid。
步骤五很关键,如果线程A写完后,解除了写锁,此时新的线程E获取到了写锁,就会写入新数据,此时就不是同步锁了,程序出错。
-> A修改数据,cacheValid置为true。步骤6和步骤7是写锁的降级操作,即写锁释放的时候,先降级为读锁,这样其他等待获取写锁的线程会继续等待,然后再释放写锁,保证同步性。
-> A执行完步骤8,BC阻塞结束,其中一位获取写锁。