ReadWriteLock读写锁

本文介绍了读写锁在并发编程中的作用,它提高了读取共享数据的效率,尤其适合读多写少的场景。读写锁允许多个线程同时读取,但只允许一个线程写入,确保数据一致性。通过一个简单的缓存工具类示例,展示了读写锁的使用,包括初始化、锁的升级与降级问题。在锁升级不被支持的情况下,通过锁降级确保数据正确性。最后,讨论了读写锁的特性,如响应中断和非阻塞获取锁等。
摘要由CSDN通过智能技术生成

ReadWriteLock读写锁

前言

现在我们知道了并发原语信号量和管程可以解决所有的并发问题,但是SDK并发包中却包含其它工具类这是重复造火箭吗?当然不是,工具包突出的是分场景优化性能,提升易用性

例如实际开发中一般存在如下业务场景,为了提升性能将一些基础数据、元数据等加入缓存提升读写效率,而且一般加入缓存后的数据改动少,读取次数多,针对这类业务场景SDK工具包就提供读写锁ReadWriteLock的解决方案,读写锁的效率高于一般互斥锁。

什么是读写锁

读写锁并不是JAVA所特有的,这是一个通用的技术,在Redis、Mysql等技术栈中都有应用。

一般读写锁遵循如下原则:

  • 允许多个线程同时读共享变量。
  • 只允许一个线程写共享变量。
  • 如果一个线程正在进行写操作,那么其它线程(不包含当前线程)不可以读共享变量。

总结就是:读读不互斥、写写互斥、读写互斥。

除上述的特点外,读写锁和互斥锁的不同点主要在于,读写锁允许多个线程访问同一个临界资源,有细心的朋友肯定发现了信号量和互斥锁的区别不也是这样吗?

是的一点毛病没有,但是信号量限制同时访问临界资源的线程数量,而读写锁是不限制的。

读写锁的简单应用-缓存

读写锁的应用场景就是读多写少,刚好可以用读写锁来实现一个简单的通用缓存工具类。

工具类中容器采用Map结构存储,互斥锁采用读写锁来保证线程安全,使用方式和Lock类似,代码结构如下

/**
 * 缓存工具类
 * @param <K>  map的key类型
 * @param <V>  map的value类型
 */
class Catch<K,V>{
    final Map<K,V> map = new HashMap<>();
    final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 定义读写锁
    final Lock readLock = readWriteLock.readLock();
    final Lock writeLock = readWriteLock.writeLock();

    /**
     * 缓存get方法
     * @param k
     * @return
     */
    public V get(K k){
        try {
            readLock.lock();
            return map.get(k);
        }finally {
            readLock.unlock();
        }
    }

    /**
     * 缓存set方法
     * @param k
     * @param v
     */
    public void set(K k,V v){
        try {
            writeLock.lock();
            map.put(k,v);
        }finally {
            writeLock.unlock();
        }
    }
}

缓存数据初始化

上面对读写锁进行了简单的使用,不过现实生产开发中除了要求缓存工具类有get、set方法还需要解决缓存的初始化问题,一般根据数据量的大小分为两种思路

  • 一次性加载,适用于加载的数据量小,在项目启动时加载进缓存。
  • 按需加载,也称为懒加载,适用于数据量大,当应用查询缓存发现数据不在缓存中时,去数据库中读取然后加载进入缓存。

这里以按需加载为例,在Catch工具类中加上缓存数据初始化代码。

/**
  * 缓存get方法
  * @param k
  * @return
  */
public V get(K k){
    V v = null;
    try {
        readLock.lock();
        v = map.get(k);
    }finally {
        readLock.unlock();
    }
    if (v != null){
        return v;
    }
    writeLock.lock();
    try {
        // 重复验证是否存在其它线程从数据库中读取值写入map中
        // 如果已经写入那么不需要再次从数据库查询
        v = map.get(k);
        if (v != null){
            return v;
        }
        v = "从数据库读取";
        // 写入缓存
        map.put(k,v);
    }finally {
        writeLock.unlock();
    }
    return v;
}

这里需要特别注意,在21行反复验证v在缓存中是否为空,有没有必要呢?当然有必要的,在高并发下存在多线程访问缓存容器Map,假设线程T1,T2,T3同时用相同的key获取值T1,T2,T3同时执行到17行都没有获取到v的值,这时只能有一个线程获取写锁,假设为T1,T1从数据库中读取值后写入缓存中,同时释放读锁,后续T2,T3竞争写锁其中有一个竞争成功,如果不校验缓存中是否存在v的值,T2,T3都将会从数据库读取值写入缓存这是无用的操作,浪费性能。

锁的升级与降级

get方法每次获取完v值,需要先释放读锁,如果v值为空,需要再去获写锁来更新缓存,那是否可以直接在读锁里面就更新缓存呢?代码如下

public V get(K k){
    V v = null;
    try {
        readLock.lock();
        v = map.get(k);
        if (v == null){
            writeLock.lock();
            try {
                // 验证并且更新缓存 代码省略
            }finally {
                writeLock.unlock();
            }
        }
    }finally {
        readLock.unlock();
    }
    return v;
}

上诉代码从语法和逻辑上看着都是没问题的,先获取读锁,还未释放就获取写锁,这个操作就是锁的升级,但遗憾的是ReadWriteLock不支持锁的升级,所以在线程获取读锁还未释放,马上获取写锁,会造成写锁永远获取不到,永久阻塞。

不支持锁升级的原因就是,读锁是允许多个线程方法访问同一个临界区的,如果读锁升级为写锁,写锁修改共享变量,那么其余拥有读锁的线程就有可能获取到脏数据。

虽然锁的升级不支持,但是却支持锁的降级,也就是写锁可以降级的写锁,如下示例。

class Catch<K,V>{
    final Map<K,V> map = new HashMap<>();
    volatile boolean cacheValid;
    final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 定义读写锁
    final Lock readLock = readWriteLock.readLock();
    final Lock writeLock = readWriteLock.writeLock();

    public void processCachedData(){
        readLock.lock();
        // 检查缓存值是否存在
        if (!cacheValid){
            // 释放读锁 不支持锁升级 读锁升级写锁
            readLock.unlock();
            // 获取写锁
            writeLock.lock();
            try {
                // 再次检查缓存值是否存在
                if (!cacheValid){
                    // 省略 更新缓存map的值
                    cacheValid = true;
                }
                // 释放写锁前 降级为读锁
                readLock.lock();
            }finally {
                writeLock.unlock();
            }
        }
        // 此处还有读锁
        try {
            // 省略 使用map的值
        }finally {
            readLock.unlock();
        }
    }
}

可能有人觉得奇怪,为什么要在写锁释放前加读锁呢?这样的锁降级有什么应用场景呢?

假设线程T1持有写锁后,修改map中的数据,没有获取读锁就将写锁释放了,这时存在线程T2马上获取到写锁修改了数据,那么线程T1是无法感知数据变化的,就会造成上诉代码中31行使用map的值错误,如果按照代码逻辑在释放写锁时将写锁降级为读锁,那么线程T2在获取写锁时就会阻塞,在31行使用map的值就不会错误。

总结

读写锁类似于ReentrantLock,所以支持它的所有特性,响应中断,非阻塞地获取锁,支持超时等,但是有一点需要注意的是读写锁是分为读锁、写锁,只有写锁支持条件变量,读锁不支持条件变量,因为读锁是支持并发的,如果有条件变量就会将线程挂起,并不符合读锁设计初衷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值