StampedLock读写锁

63 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第22天,点击查看活动详情

ReadWriteLock出现的问题

1、深入分析ReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写线程去抢锁,这是一种悲观的读锁,会出现写饥饿。

2、有100个线程访问某个资源,如果有99线程个需要读锁,1个线程需要写锁,此时,写的线程很难得到执行。

StampedLock改进

3、StampedLock和ReadWriteLock相比,改进之处在于:读的过程中也允许获取写锁后写入 。这样一来,我们读的数据就可能不一致,所以,需要一点额外的代码来判断读的过程中是否有写入,这种读锁是一种乐观锁

4、乐观锁的意思就是乐观地估计读的过程中大概率不会有写入,因此被称为乐观锁。反过来,悲观锁则是读的过程中拒绝有写入,也就是写入必须等待。显然乐观锁的并发效率更高,但一旦有小概率的写入导致读取的数据不一致,需要能检测出来,再读一遍就行。

用StampedLock去悲观的读

StampedLock可以完全实现ReadWriteLock的功能。

public class StampedLockDemo1 {


    private static final StampedLock stampedLock = new StampedLock();

    private static Integer DATA = 0;

    public static void write() {
        long stamp = -1;
        try {
            stamp = stampedLock.writeLock();// 获取写锁
            DATA++;
            System.out.println("写-->" + DATA);
        } finally {
            stampedLock.unlockWrite(stamp); // 释放写锁
        }
    }

    public static void read() {
        long stamp = -1;

        try {
            stamp = stampedLock.readLock();// 获取悲观读锁
            System.out.println("读-->" + DATA);
        } finally {
            stampedLock.unlockRead(stamp); // 释放悲观读锁
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        //写任务
        Runnable writeTask = () -> {
            for (; ; ) {
                write();
            }
        };
        //读任务
        Runnable readTask = () -> {
            for (; ; ) {
                read();
            }
        };

        //一个线程写,9个线程读
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(readTask);
        executor.submit(writeTask);//写线程要写最后


    }
}

输出结果如下图 image.png

使用StampedLock改造,只需要变更read方法

1、在这块可能会有写锁抢锁,修改数据,所以用validate检查乐观读锁后是否有其他写锁发生 判断执行读操作期间,是否存在写操作,如果存在则validate返回false

2、如果有写锁抢锁,修改了数据,那么就要获取悲观锁。因为写锁在修改数据的过程中,你不能直接 去读,只能老老实实拿到读锁再去读,才不会发生线程安全问题

    public static void read() {
        long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁

        //在这块可能会有写锁抢锁,修改数据,所以用validate检查乐观读锁后是否有其他写锁发生
        if (!stampedLock.validate(stamp)) {//检查乐观读锁后是否有其他写锁发生
            try {
                stamp = stampedLock.readLock();// 获取悲观读锁
                System.out.println("悲观读-->" + DATA);
                return;
            } finally {
                stampedLock.unlockRead(stamp); // 释放悲观读锁
            }
        }
        System.out.println("乐观读-->" + DATA);

    }