关于java:JAVA并发编程ReentrantReadWriteLock锁降级和StampedLock

49次阅读

共计 3499 个字符,预计需要花费 9 分钟才能阅读完成。

1. 锁的一路演变

2.ReentrantReadWriteLock 锁降级

3. 比读写锁更快的锁————邮戳锁

4. 总结

1. 锁的一路演变
当咱们在学习 java 的锁的时候,经验了以下四个阶段的锁演变:无锁→独占锁→读写锁→邮戳锁

无锁:
咱们一开始学会编写代码的时候,必定写的都是无锁的代码。
长处:执行效率高
毛病:多线程无序争夺导致谬误数据

而后咱们发现其中的问题,就学会了 synchronized, reentrantlock
长处:串行化保障了数据一致性
毛病:所有操作互斥,执行效率低

接着咱们发现这样子效率太低,如果读线程占大多数,写线程占多数,就又去学习了 ReentrantReadWriteLock
长处:读读共享,读写互斥,晋升了大面积的共享性能
毛病:读线程还没完结永,写线程永远不可能会取得锁(造成锁饥饿)

而后其实还有比读写锁更快的 锁 StampedLock(咱们会在下文进行解说)
长处:读的过程中也容许获取写锁染指,效率更高
毛病:不反对重入,不反对 Condition,也不反对中断

这样就有了以下这样的表格:

无锁 synchronized, reentrantlock ReentrantReadWriteLock StampedLock
长处 执行效率高 串行化保障了数据一致性 读读共享,读写互斥,晋升了大面积的共享性能 读的过程中也容许获取写锁染指,效率更高
毛病 多线程无序争夺导致谬误数据 所有操作互斥,执行效率低 读线程还没完结永,写线程永远不可能会取得锁(造成锁饥饿) 不反对重入,不反对 Condition,也不反对中断

2.ReentrantReadWriteLock 锁降级

咱们后面刚学习了 JAVA 并发编程——Synchronized 与锁降级,明天咱们来学习一下锁降级。

咱们先来看一下 ReentrantReadWriteLock 锁的定义:
一个资源能别多个读线程拜访,或者被一个写线程拜访。
也就是说:
读读不互斥
写写互斥
读写互斥

只有在读多写少情境之下,读写锁才具备较高的性能体现。

而锁降级又是什么呢?
锁降级
将写入锁降级为读锁 (就像 linux 文件读写权限,写权限肯定会高于读权限)
换句话说,我 们在 lock.writeLock(); 的同时,能够再进行 lock.readLock(),这个时候读锁就会降级成写锁,反之则不行,程序会死锁

这样说咱们可能还是看不太懂,咱们间接用代码解释好了。


import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 锁降级:遵循获取写锁→再获取读锁→再开释写锁的秩序,写锁可能降级成为读锁。*
 * 如果一个线程占有了写锁,在不开释写锁的状况下,它还能占有读锁,即写锁降级为读锁。*/
public class LockDownGradingDemo
{public static void main(String[] args)
    {ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();


        writeLock.lock();
        System.out.println("------- 正在写入");


        readLock.lock();
        System.out.println("------- 正在读取");

        writeLock.unlock();}
}

读写锁降级的目标:
在高并发的状况下,咱们为了让程序感知到咱们批改了内容,就先用读锁锁住这个后果,不让其它写线程进来,因为读读是能够共享的,保障了该次变动的数据可见性。

以下摘自 ReentrantReadWriteLock 的源码:

1 代码中申明了一个 volatile 类型的 cacheValid 变量,保障其可见性。

2 首先获取读锁,如果 cache 不可用,则开释读锁,获取写锁,在更改数据之前,再查看一次 cacheValid 的值,而后批改数据,将 cacheValid 置为 true,而后在开释写锁前获取读锁;此时,cache 中数据可用,解决 cache 中数据,最初开释读锁。这个过程就是一个残缺的锁降级的过程,目标是保证数据可见性。

如果违反锁降级的步骤
如果以后的线程 C 在批改完 cache 中的数据后,没有获取读锁而是间接开释了写锁 ,那么 假如此时另一个线程 D 获取了写锁并批改了数据 ,那么 C 线程 无奈感知 到数据已被批改,则数据呈现谬误。
如果 遵循 锁降级的步骤
线程 C 在开释写锁之前获取读锁 ,那么线程 D 在获取写锁时将被 阻塞 ,直到线程 C 实现数据处理过程,开释读锁。这样能够保障返回的数据是这次更新的数据, 该机制是专门为了缓存设计的。

3. 比读写锁更快的锁————邮戳锁

因为读写锁有 锁饥饿 的问题。
锁饥饿:如果当初有 1000 个线程,999 个读,1 个写,那就是读线程长时间占据锁,而写线程长时间无奈获取锁。

那么如何缓解锁饥饿问题?咱们有上面这几个解决办法
1) 应用偏心锁策略 能够肯定水平上缓解这个问题,然而吞吐量不高
2) 应用邮戳锁

为了解决这个问题,咱们应用邮戳锁:
SteampedLock 有三种拜访模式
1)Reading(读模式):性能和 ReentrantReadWriteLock 读锁相似
2)Writing(写模式):性能和 ReentrantReadWriteLock 写锁相似
3)Optimistic reading(乐观读模式):无锁机制,相似于数据库中的乐观锁,反对读写并发,很乐观认为读取时没人批改,如果被批改再实现为乐观读模式。

    // 乐观读
    // 咱们通过刚开始取得的版本号开判断是不是有人动过这个数据
    // 而后如果有人批改过,再进行锁降级
    public void tryOptimisticRead() {
        // 先获取一个乐观读标记位
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        // 距离 4 秒钟,咱们很乐观的认为没有其余线程批改过 number 值,理论靠判断。System.out.println("4 秒前 stampedLock.validate 值(true 无批改,false 有批改)" + "\t" + stampedLock.validate(stamp));
        for (int i = 1; i < 4; i++) {
            try {TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t 正在读取中......" + i +
                    "秒后 stampedLock.validate 值(true 无批改,false 有批改)" + "\t"
                    + stampedLock.validate(stamp));
        }
        if (!stampedLock.validate(stamp)) {System.out.println("有人动过 -------- 存在写操作!");
            stamp = stampedLock.readLock();
            try {System.out.println("从乐观读 降级为 乐观读");
                result = number;
                System.out.println("从新乐观读锁通过获取到的成员变量值 result:" + result);
            } catch (Exception e) {e.printStackTrace();
            } finally {stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t finally value:" + result);
    }

4. 总结
这次咱们学习的锁的演变,每一种锁都有各自的优缺点,再上一下下面那个表格。

无锁 synchronized, reentrantlock ReentrantReadWriteLock StampedLock
长处 执行效率高 串行化保障了数据一致性 读读共享,读写互斥,晋升了大面积的共享性能 读的过程中也容许获取写锁染指,效率更高
毛病 多线程无序争夺导致谬误数据 所有操作互斥,执行效率低 读线程还没完结永,写线程永远不可能会取得锁(造成锁饥饿) 不反对重入,不反对 Condition,也不反对中断

正文完
 0