zoukankan      html  css  js  c++  java
  • 并发库应用之五 & ReadWriteLock场景应用

      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实例代码案例:
     1 class CachedData {
     2   Object data;
     3   volatile boolean cacheValid;
     4   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
     5 
     6   public void processCachedData() {
     7     rwl.readLock().lock();
     8     if (!cacheValid) {
     9       // Must release read lock before acquiring write lock
    10       rwl.readLock().unlock();
    11       rwl.writeLock().lock();
    12       try {
    13         // Recheck state because another thread might have,acquired write lock and changed state before we did.
    14         if (!cacheValid) {
    15           data = ...
    16           cacheValid = true;
    17         }
    18         // 在释放写锁之前通过获取读锁降级写锁(注意此时还没有释放写锁)
    19         rwl.readLock().lock();
    20       } finally {
    21         rwl.writeLock().unlock(); // 释放写锁而此时已经持有读锁
    22       }
    23     }
    24 
    25     try {
    26       use(data);
    27     } finally {
    28       rwl.readLock().unlock();
    29     }
    30   }
    31 }

    以上代码加锁的顺序为:

      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过程中,可能会有其他线程竞争到写锁 或者是更新数据 则获得的数据是其他线程更新的数据,可能会造成数据的污染,即产生脏读的问题。

    下面,让我们来实现真正趋于实际生产环境中的缓存案例:

     1 import java.util.HashMap;
     2 import java.util.Map;
     3 import java.util.concurrent.locks.ReadWriteLock;
     4 import java.util.concurrent.locks.ReentrantReadWriteLock;
     5 
     6 public class CacheDemo {
     7     /**
     8      * 缓存器,这里假设需要存储1000左右个缓存对象,按照默认的负载因子0.75,则容量=750,大概估计每一个节点链表长度为5个
     9      * 那么数组长度大概为:150,又有雨设置map大小一般为2的指数,则最近的数字为:128
    10      */
    11     private Map<String, Object> map = new HashMap<>(128);
    12     private ReadWriteLock rwl = new ReentrantReadWriteLock();
    13     public static void main(String[] args) {
    14 
    15     }
    16     public Object get(String id){
    17         Object value = null;
    18         rwl.readLock().lock();//首先开启读锁,从缓存中去取
    19         try{
    if(map.get(id) == null){ //如果缓存中没有释放读锁,上写锁 22 rwl.readLock().unlock(); 23 rwl.writeLock().lock(); 24 try{ 25 if(value == null){ //防止多写线程重复查询赋值 26 value = "redis-value"; //此时可以去数据库中查找,这里简单的模拟一下 27 } 28 rwl.readLock().lock(); //加读锁降级写锁,不明白的可以查看上面锁降级的原理与保持读取数据原子性的讲解 29 }finally{ 30 rwl.writeLock().unlock(); //释放写锁 31 } 32 } 33 }finally{ 34 rwl.readLock().unlock(); //最后释放读锁 35 } 36 return value; 37 } 38 }

     提示:读写锁之后有一个与它配合使用的有条件的阻塞,可以实现线程间的通信,它就是Condition。具体详情请查看我的博客:并发库应用之六 & 有条件阻塞Condition应用

  • 相关阅读:
    在ubuntu下复制文件出现权限不够的解决方法
    Ubuntu安装ROS Melodic
    gedit文件操作
    Linux下强制删除文件和权限操作
    VMware Tools 继续运行脚本未能在虚拟机中成功运行 解决方式
    Linux解压命令
    Ubuntu 18.04.4 LTS(Bionic Beaver)安装
    Socket层实现系列 — send()类发送函数的实现
    iOS7 CookBook精彩瞬间(三)UIActivityViewController的基本使用及自定义Activity
    iOS7 CookBook精彩瞬间(二)NSSet、通过Subscript访问类成员等
  • 原文地址:https://www.cnblogs.com/liang1101/p/6475555.html
Copyright © 2011-2022 走看看