欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

ReadWriteLock场景应用解析

程序员文章站 2022-06-19 21:15:41
Lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个Lock对象。 ......

本人免费整理了java高级资料,涵盖了java、redis、mongodb、mysql、zookeeper、spring cloud、dubbo高并发分布式等教程,一共30g,需要自己领取。
传送门:https://mp.weixin.qq.com/s/jzddfh-7ynudmkjt0irl8q

 

lock比传统线程模型中的synchronized方式更加面向对象,与生活中的锁类似,锁本身也应该是一个对象。两个线程执行的代码片段要实现同步互斥的效果,它们必须用同一个lock对象。

读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,我们只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!

 

读写锁接口:readwritelock,它的具体实现类为:reentrantreadwritelock

在多线程的环境下,对同一份数据进行读写,会涉及到线程安全的问题。比如在一个线程读取数据的时候,另外一个线程在写数据,而导致前后数据的不一致性;一个线程在写数据的时候,另一个线程也在写,同样也会导致线程前后看到的数据的不一致性。

这时候可以在读写方法中加入互斥锁,任何时候只能允许一个线程的一个读或写操作,而不允许其他线程的读或写操作,这样是可以解决这样以上的问题,但是效率却大打折扣了。因为在真实的业务场景中,一份数据,读取数据的操作次数通常高于写入数据的操作,而线程与线程间的读读操作是不涉及到线程安全的问题,没有必要加入互斥锁,只要在读-写,写-写期间上锁就行了。

 

对于以上这种情况,读写锁是最好的解决方案!其中它的实现类:reentrantreadwritelock--顾名思义是可重入的读写锁,允许多个读线程获得readlock,但只允许一个写线程获得writelock

读写锁的机制:

"读-读" 不互斥

"读-写" 互斥

"写-写" 互斥

reentrantreadwritelock会使用两把锁来解决问题,一个读锁,一个写锁。

线程进入读锁的前提条件:

   1. 没有其他线程的写锁

    2. 没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程

进入写锁的前提条件:

    1. 没有其他线程的读锁

    2. 没有其他线程的写锁

需要提前了解的概念:

  锁降级:从写锁变成读锁;
  锁升级:从读锁变成写锁。
  读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。
  如下代码会产生死锁,因为同一个线程中,在没有释放读锁的情况下,就去申请写锁,这属于锁升级,reentrantreadwritelock是不支持的。

 readwritelock rtlock = new reentrantreadwritelock();
 rtlock.readlock().lock();
 system.out.println("get readlock.");
 rtlock.writelock().lock();
 system.out.println("blocking");

 


  reentrantreadwritelock支持锁降级,如下代码不会产生死锁。

readwritelock rtlock = new reentrantreadwritelock();
rtlock.writelock().lock();
system.out.println("writelock");

rtlock.readlock().lock();
system.out.println("get read lock");

 


  以上这段代码虽然不会导致死锁,但没有正确的释放锁。从写锁降级成读锁,并不会自动释放当前线程获取的写锁,仍然需要显示的释放,否则别的线程永远也获取不到写锁。

============以下我会通过一个真实场景下的缓存机制来讲解 reentrantreadwritelock 实际应用============

首先来看看reentrantreadwritelock的javaodoc文档中提供给我们的一个很好的cache实例代码案例:

class cacheddata {
  object data;
  volatile boolean cachevalid;
  final reentrantreadwritelock rwl = new reentrantreadwritelock();

  public 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 = ...
          cachevalid = true;
        }
        // 在释放写锁之前通过获取读锁降级写锁(注意此时还没有释放写锁)
        rwl.readlock().lock();
      } finally {
        rwl.writelock().unlock(); // 释放写锁而此时已经持有读锁
      }
    }

    try {
      use(data);
    } finally {
      rwl.readlock().unlock();
    }
  }
}

 


以上代码加锁的顺序为:
1. rwl.readlock().lock();
2. rwl.readlock().unlock();
3. rwl.writelock().lock();
4. rwl.readlock().lock();
5. rwl.writelock().unlock();
6. rwl.readlock().unlock();
以上过程整体讲解:
1. 多个线程同时访问该缓存对象时,都加上当前对象的读锁,之后其中某个线程优先查看data数据是否为空。【加锁顺序序号:1 】

2. 当前查看的线程发现没有值则释放读锁立即加上写锁,准备写入缓存数据。(不明白为什么释放读锁的话可以查看上面讲解进入写锁的前提条件)【加锁顺序序号:2和3 】
3. 为什么还会再次判断是否为空值(!cachevalid)是因为第二个、第三个线程获得读的权利时也是需要判断是否为空,否则会重复写入数据。
4. 写入数据后先进行读锁的降级后再释放写锁。【加锁顺序序号:4和5 】
5. 最后数据数据返回前释放最终的读锁。【加锁顺序序号:6 】


如果不使用锁降级功能,如先释放写锁,然后获得读锁,在这个get过程中,可能会有其他线程竞争到写锁 或者是更新数据 则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题。
下面,让我们来实现真正趋于实际生产环境中的缓存案例:

import java.util.hashmap;
import java.util.map;
import java.util.concurrent.locks.readwritelock;
import java.util.concurrent.locks.reentrantreadwritelock;

public class cachedemo {
    /**
     * 缓存器,这里假设需要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每一个节点链表长度为5个
     * 那么数组长度大概为:150,又有雨设置map大小一般为2的指数,则最近的数字为:128
     */
    private map<string, object> map = new hashmap<>(128);
    private readwritelock rwl = new reentrantreadwritelock();
    public static void main(string[] args) {

    }
    public object get(string id){
        object value = null;
        rwl.readlock().lock();//首先开启读锁,从缓存中去取
        try{
               if(map.get(id) == null){  //如果缓存中没有释放读锁,上写锁
                rwl.readlock().unlock();
                rwl.writelock().lock();
                try{
                    if(value == null){ //防止多写线程重复查询赋值
                        value = "redis-value";  //此时可以去数据库中查找,这里简单的模拟一下
                    }
                    rwl.readlock().lock(); //加读锁降级写锁,不明白的可以查看上面锁降级的原理与保持读取数据原子性的讲解
                }finally{
                    rwl.writelock().unlock(); //释放写锁
                }
            }
        }finally{
            rwl.readlock().unlock(); //最后释放读锁
        }
        return value;
    }
}