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

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

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


策略选择演示


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


/**
 * 读写锁(非公平),读锁不插队
 */
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、升降级策略


  • 升降级策略:只能从写锁降级为读锁,不能从读锁升级为写锁。
相关文章
|
6月前
|
存储 算法 安全
【软件设计师备考 专题 】计算机管理(状态转换、共享与互斥、分时轮转、抢占、死锁)
【软件设计师备考 专题 】计算机管理(状态转换、共享与互斥、分时轮转、抢占、死锁)
70 0
|
3月前
|
Java 开发者
解锁Java并发编程的秘密武器!揭秘AQS,让你的代码从此告别‘锁’事烦恼,多线程同步不再是梦!
【8月更文挑战第25天】AbstractQueuedSynchronizer(AQS)是Java并发包中的核心组件,作为多种同步工具类(如ReentrantLock和CountDownLatch等)的基础。AQS通过维护一个表示同步状态的`state`变量和一个FIFO线程等待队列,提供了一种高效灵活的同步机制。它支持独占式和共享式两种资源访问模式。内部使用CLH锁队列管理等待线程,当线程尝试获取已持有的锁时,会被放入队列并阻塞,直至锁被释放。AQS的巧妙设计极大地丰富了Java并发编程的能力。
42 0
|
Java 开发者
解锁Java多线程编程中的死锁之谜
解锁Java多线程编程中的死锁之谜
56 0
|
6月前
|
Java 调度
没了解死锁怎么能行?进来看看,一文带你拿下死锁产生的原因、死锁的解决方案。
没了解死锁怎么能行?进来看看,一文带你拿下死锁产生的原因、死锁的解决方案。
48 0
|
存储 设计模式 缓存
线程池面试连环炮,你能抗住几题?
最近有朋友在面试时,刚好被问到了线程池相关的问题,于是我想就抽时间整理了一些关于线程池的面试题来分享给大家。
|
NoSQL Java Redis
得不到你的心,就用“分布式锁”锁住你的人 码农在囧途
朋友,如果喜欢,就去表白吧,不要因为害羞,更不要因为自卑,如果现在你都还不敢表白,那么多年后,再回头来看的时候,你可能会为曾经的胆小而后悔,也可能会为错过一个人而心中久久不能释怀,所以,大胆一点,即使失败也无所谓,至少我们曾经做过,做过了就无怨无悔,在人生这条道路上,时光稍纵即逝,我们应该把握好眼前的一切,爱是一种力量,更是一种内心的慰藉,冲吧!不要因为钱不够,不要因为容貌不出中国,更不要因为身世不显赫,你只要足够勇敢,这一切都是附加品!
109 0
|
算法 前端开发 JavaScript
不逼自己一把都不知道自己还能这么优秀(小鹅通学习记录大批量队列同步)
不逼自己一把都不知道自己还能这么优秀(小鹅通学习记录大批量队列同步)
152 0
【AQS我可以讲十分钟】
【AQS我可以讲十分钟】
134 0
【AQS我可以讲十分钟】
死锁的发生原因和怎么避免,写的明明白白
死锁的发生原因和怎么避免,写的明明白白
死锁的发生原因和怎么避免,写的明明白白
小白鼠排队
小白鼠排队
154 0
下一篇
无影云桌面