并发-Java中的锁(三)---读写锁ReentrantReadWriteLock

读写锁ReentrantReadWriteLock

  • 排它锁:Mutex 和 ReentrantLock基本都是排它锁,在同一时刻只允许一个线程进行访问
  • 读写锁,同一时刻允许多个读线程访问,但在写线程访问时,所有的读线程和其他线程均被阻塞。
  • 读写锁维护了一对锁,通过分离读锁和写锁,使并发性相比一般的排它锁有很大提升
  • ReentrantReadWriteLock 特性
    • 公平性选择:支持非公平和公平的锁获取方式,吞吐量非公平优于公平
    • 重进入:支持重进入
    • 锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级为读锁

读写锁接口示例

  • ReadWriteLock进定义了获取读锁和写锁的方法,readLock()和writeLock() 其实现了ReentrantReadWriteLock

  • 除了接口方法外还提供了一些便于监控其内部工作状态的方法

    • int getReadLockCount:返回当前读锁被获取的次数
    • int getReadHoldCount:返回当前现场获取读锁的次数
    • boolean isWriteLocked:判断写锁是否被获取
    • int getWriteHoldCount():返回当前写锁被获取的次数
  • 示例:使用读锁和写锁来保证cache是线程安全的

  • import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantReadWriteLock;
    
    public class Cache {
        static Map<String, Object> map = new HashMap<>();
        static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
        static Lock r = rwl.readLock();
        static Lock w = rwl.writeLock();
    
        //获取一个key对应的value
        public static final Object get(String key){
            r.lock();
            try {
                return map.get(key);;
            }finally {
                r.unlock();
            }
        }
    
        public static final Object put(String key, Object value){
            w.lock();
            try {
                return map.put(key,value);
            }finally {
                w.unlock();
            }
        }
    
        public static final void clear(){
            w.lock();
            try {
                map.clear();
            }finally {
                w.unlock();
            }
        }
    }
    

读写锁实现分析

  • 读写状态设计:就是同步器的同步状态

    • 同步状态表示锁被一个线程重复获取的次数,读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程状态
    • 整型变量,需要按位切割使用这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写。
    • 根据状态划分:S不等于0时,当写状态等于0时,则读状态大于0,即读锁已被获取
  • 写锁的获取与释放

    • 写锁:tryAcquire支持重进入的排它锁,
      • 如果当前线程已经获取了写锁,则增加写状态
      • 如果当前线程在获取写锁时,读锁已经被获取或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。
      • 该方法除了重入条件,增加了一个读锁是否存在的判断,如果存在,则写锁不能获取
    • 写锁释放:每次释放均减少写状态,当写状态为0时,表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
  • 读锁的获取与释放

    • 读锁获取tryAcquireShared:支持重进入的共享锁,能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功的获取,只是增加读状态。
      • 如果当前线程已经获取了读锁,增加读状态
      • 如果当时在获取读锁时,写锁已被其他线程获取,则进入等待状态。
      • 新增功能,例如getReadHoldCount,作用是返回当前线程获取读锁的次数
      • 读状态是所有线程获取读锁次数的总和,每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,使获取读锁的实现变得复杂
      • 如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态
    • 每次释放均减少读状态,减少的值是1<<16
  • 锁降级:写锁降级为读锁,是指把持住(当前拥有的)写锁,再次获取到读锁,所有释放(先前拥有的)写锁的过程

    • 为了保证数据可见性,锁降级中读锁的获取是必要的, ReentrantReadWriteLock不支持锁升级

    • 示例:当数据发生变更后,update变量被设置为false,此时所有访问processData方法的线程都能感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁he写锁的lock()方法上,当前线程获取写锁完成数据准备之后,在获取读锁,随后释放写锁,完成锁降级

    • public void processData(){
          readLock.lock();
          if (!update){
              //必须先释放读锁
              readLock.unlock;
              //锁降级从写锁获取到开始
              writeLock.lock();
              try {
                  if(!update){
                      //准备数据的流程
                      update = true;
                  }
                  readLock.lock();
              }finally {
                  writeLock.unlock();
              }
              //锁降级完成,写锁降级为读锁
          }
          try {
              //使用数据
          }finally {
              readLock.lock();
          }
      }
      

你可能感兴趣的:(#,java并发编程,java,开发语言,并发)