深夜!小胖问我什么是读写锁?插队策略?升降级?(下)

简介: 深夜!小胖问我什么是读写锁?插队策略?升降级?

「而我们的读写锁(非公平)就是采用第二种策略,只要等待队列的头结点是尝试获取写锁的线程,那么读锁依然是不能插队的,目的是避免 "饥饿"」


策略选择演示


口说无凭不写代码的行为简直不讲码德。还是得写代码演示下上述流程图的结论:


/**
 * 读写锁(非公平),读锁不插队
 */
public class ReadLockJumpQueue {
    private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
    private static void read() {
        readLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取");
            Thread.sleep(2000);
        } catch (InterruptedException e) { e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放读锁");
            readLock.unlock();
        }
    }
    private static void write() {
        writeLock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入");
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            System.out.println(Thread.currentThread().getName() + "释放写锁");
            writeLock.unlock();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(ReadLockJumpQueue::read, "帅比狗哥").start();
        Thread.sleep(100);
        new Thread(ReadLockJumpQueue::read, "渣男小钊").start();
        Thread.sleep(100);
        new Thread(ReadLockJumpQueue::write, "写线程1").start();
        Thread.sleep(100);
        new Thread(ReadLockJumpQueue::read, "海王小宝").start();
    }
}


从这个结果可以看出,不公平下的读锁选择了不允许插队的策略,从而很大程度上减小了发生 "饥饿" 的概率.


640.png


什么是读写锁的升降级?


先看段代码。代码演示的是在更新缓存的时候,如何使用读写锁的升降级。


/**
 * 更新缓存演示锁降级
 */
public class CachedData {
    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            //在获取写锁之前,必须首先释放读锁。
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                // 这里需要再次判断数据的有效性
                // 因为在我们释放读锁和获取写锁的空隙之内,可能有其他线程修改了数据。
                if (!cacheValid) {
                    data = new Object();
                    cacheValid = true;
                }
                //在不释放写锁的情况下,直接获取读锁,这就是读写锁的降级。
                rwl.readLock().lock();
            } finally {
                //释放了写锁,但是依然持有读锁
                rwl.writeLock().unlock();
            }
        }
        try {
            System.out.println(data);
        } finally {
            //释放读锁
            rwl.readLock().unlock();
        }
    }
}


先看看程序,注释写得很清楚了。大概的流程就是先获取读锁读缓存,再释放读锁获取写锁,写锁修改缓存。然后重点来了,「线程在不释放写锁的情况下,获取读锁(这就是锁的降级)」,然后释放写锁。读锁读取数据,最后释放读锁。


「PS:由于写锁是独占锁,当前线程获取写锁之后,其它线程就既不能获取写锁也不能获取读锁了,但是当前已经获取写锁的线程仍然可以获取读锁」


为什么需要锁的降级?


你可能会说,写锁既能修改、又能读取。那直接用写锁就好了呀。干嘛要降级?其实不然,仔细看刚刚的代码:


data = new Object();


只有这一句是写的操作。如果这个线程一直用写锁,那其他线程在这段时间就无法获取锁操作了。浪费资源、降低了效率。「所以针对读多,写非常少的任务,还是用锁的降级比较明智」


只支持降级,不支持升级


运行下面这段代码,「在不释放读锁的情况下直接尝试获取写锁,也就是锁的升级,会让线程直接阻塞,程序是无法运行的」


final static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main(String[] args) {
    upgrade();
}
public static void upgrade() {
    rwl.readLock().lock();
    System.out.println("获取到了读锁");
    rwl.writeLock().lock();
    System.out.println("成功升级");
}


为什么不支持锁的升级?


读写锁的特点是如果线程都申请读锁,是可以多个线程同时持有的,可是如果是写锁,只能有一个线程持有,并且不可能存在读锁和写锁同时持有的情况。


举个栗子:假设线程 A 和 B 都想升级到写锁,那么对于线程 A 而言,它需要等待其他所有线程,包括线程 B 在内释放读锁。而线程 B 也需要等待所有的线程,包括线程 A 释放读锁。这就是一种非常典型的死锁的情况。谁都愿不愿意率先释放掉自己手中的锁。


一句话总结就是「多个线程同时发生锁升级的时候,会发生死锁,因为发生锁升级的线程会等待其它线程释放读锁」


总结


1、定义


  • 写锁也叫独占锁,它既能读取数据也能修改数据,同一时间只能有一个线程持有,它是非线程安全的
  • 读锁也叫共享锁,它只能读取数据,允许多个线程同时持有,它是线程安全的


2、为什么?


  • 读写锁的出现可以提高程序的执行效率


3、加锁规则


读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)


4、插队策略


  • 公平策略下,只要队列里有线程已经在排队,就不允许插队。


非公平策略下:


  • 为了防止 “饥饿”,在等待队列的头结点是尝试获取写锁的线程的时候,不允许读锁插队。
  • 写锁可以随时插队,因为写锁并不容易插队成功,写锁只有在当前没有任何其他线程持有读锁和写锁的时候,才能插队成功,同时写锁一旦插队失败就会进入等待队列,所以很难造成 “饥饿” 的情况,允许写锁插队是为了提高效率。


5、升降级策略


  • 升降级策略:只能从写锁降级为读锁,不能从读锁升级为写锁。
相关文章
|
Web App开发 JavaScript 前端开发
网页VUE纯前端在线预览编辑Office,支持doc/docx、xls/xlsx、ppt/pptx、pdf等格式
随着互联网技术的不断发展,越来越多的企业开始采用在线办公模式,微软Office Word 是最好用的文档编辑工具,然而doc、docx、xls、xlsx、ppt、pptx等格式的Office文档是无法直接在浏览器中直接打开的,如果可以实现Web在线预览编辑OffIce,肯定会还带来了更高效、便捷的办公体验,为我们的工作带来了更多可能性。
3334 0
|
3月前
|
传感器 人工智能 监控
【免费开源】基于STM32的智能宠物喂食系统设计与实现(全流程技术详解)附源码
本项目基于STM32F103C8T6设计实现智能宠物喂食系统,支持定时喂食、远程控制、余粮检测、语音提示等功能,结合传感器与物联网技术,提升宠物喂养智能化水平,适用于家庭及嵌入式课程实践。源码开源,具备良好扩展性。
【免费开源】基于STM32的智能宠物喂食系统设计与实现(全流程技术详解)附源码
|
存储 Kubernetes NoSQL
k8s学习--资源控制器StatefulSet详细解释与应用
StatefulSet 是 Kubernetes 中用于管理有状态应用的控制器,提供稳定的网络标识符和持久化存储能力。相较于 Deployment、DaemonSet 等无状态服务控制器,StatefulSet 支持有状态服务如 MySQL、Redis 等集群的部署和管理。本文详细介绍 StatefulSet 的概念、特点及部署方法,并通过具体示例展示了如何配置 NFS 存储及 StatefulSet 应用,确保每个 Pod 拥有独立且持久的数据存储空间。
435 0
什么锁比读写锁性能更高?
Java并发编程中,ReentrantReadWriteLock是高效的锁机制,但在高并发环境下,乐观锁(如CAS)和JDK 8引入的StampedLock可提供更高性能。StampedLock支持读锁、写锁和乐观读锁,其乐观读锁在读多写少的场景下能提升并发性能,通过tryOptimisticRead方法实现。当乐观读锁无效时,可无缝切换至悲观读锁。
248 1
|
机器学习/深度学习 算法 Python
在Python中,独热编码(One-Hot Encoding)
在Python中,独热编码(One-Hot Encoding)
1451 8
|
存储 弹性计算 大数据
阿里云8核16G云服务器价格多少钱?2024年阿里云8核16G云服务器性能测评
2024年阿里云8核16G云服务器的价格为199元一年。这是阿里云为新用户提供的优惠价格,用户可以根据自己的需求选择合适的带宽和云盘配置。关于阿里云8核16G云服务器的性能测评,该服务器配备了16GB的内存和8核的CPU,具有强大的计算能力和处理速度,能够轻松应对大型应用、复杂计算和高并发场景。同时,服务器提供了多种带宽选择,最高可达5M,确保了网络连接的快速性和稳定性。40GB ESSD云盘提供了充足的存储空间,能够满足用户的数据存储需求。总体来说,阿里云8核16G云服务器在性能和价格方面都表现出色,适合大型网站、企业级应用和高并发场景。用户可以根据自己的实际需求选择合适的带宽和云盘配置。
1163 0
阿里云8核16G云服务器价格多少钱?2024年阿里云8核16G云服务器性能测评
|
存储 分布式计算 数据管理
HDFS中的数据一致性是如何保证的?请解释数据一致性的概念和实现方式。
HDFS中的数据一致性是如何保证的?请解释数据一致性的概念和实现方式。
540 0
|
网络协议 安全 网络安全
Ngrok免费实现内网穿透
Ngrok免费实现内网穿透
1060 0
Ngrok免费实现内网穿透
|
JavaScript 前端开发 UED
目前最流行的 5 大 Vue 动画库,使用后太炫酷了
动画在交互方式中发挥着重要作用,它们可通过添加一些视觉风格来增强用户体验。 在本文中,我们将研究和比较目前最流行的 Vue.js 动画库。
2260 0
目前最流行的 5 大 Vue 动画库,使用后太炫酷了