读书笔记《JAVA并发编程的艺术》 第五章 Java中的锁 5.3 重入锁 5.4 读写锁

5.3 重入锁

重入锁(ReentrantLock)就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。

5.3.1 实现重进入

  1. 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取。
  2. 锁的最终释放:线程重复N次获取锁,随后需要第N次释放锁后,才算释放,这期间需要维护一个计数器。

5.3.2 公平与非公平获取锁的区别

公平锁保证了锁的获取按照FIFO的原则,而代价是进行大量的线程切换。非公平性锁虽然造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。

5.4 读写锁

读写锁允许在同一个时刻多个线程同时访问。
J.U.C中提供读写锁的实现是ReentrantReadWriteLock,它的特性如下:

特性 说明
公平性选择 支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
重进入 支持重进入
锁降级 遵循获取写锁、获取读锁再获取写锁的次序,写锁可以降级成为读锁

5.4.1 读写锁的接口与示例

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();
    }
}

//设置key对应的value,并返回旧的value
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();
    }
}
}

上述示例中,Cache组合一个非线程安全的hashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。
在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞
写操作put(String key,Object value)方法和clear()方法,在更新HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放后,其他读操作才可以继续。

5.4.2 读写锁的实现分析

  1. 读写状态的设计

读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态
在一个整型变量上维护多种状态,需要“按位切割使用”,高16位表示读,低16位表示写。
这里写图片描述

读写锁是如何迅速确定读和写各自的状态?通过位运算。

假设当前同步状态值为S,写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。
当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000
推论:S不等于0时,当写状态(S & 0x0000FFFF)等于0时,则读状态(S >>>16)大于0,即读锁已被获取。

  1. 写锁的获取与释放
    写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者线程不是已经获取写锁的线程,则当前线程进入等待状态。
  2. 读锁的获取与释放
    读锁是一个支持重进入的共享锁。
  3. 锁降级
    锁降级指的是写锁降级成为读锁。锁降级指的是把持住(当前拥有的)写锁,再获取到读锁,随后释放先前拥有的写锁的过程。
    示例:数据不变化时,多个线程可以并发的进行数据处理,当数据变化时,如果当前线程感知了数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。

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

update的变量用volatile修饰,保证线程可见。

猜你喜欢

转载自blog.csdn.net/maohoo/article/details/81449610