【JUC】12.读写锁与StampedLock[完结]

1. 什么是读写锁

读写锁是指ReentrantReadWriteLock类

该类能被多个读线程访问或者一个写线程访问,但是不能同时存在读写线程

其特点主要是:一体两面、读写互斥、读读共享

有线程在写的时候,其他线程不能读;有线程在读的时候,其他线程不能写


2. 锁的演化历程

第一阶段:无锁

在无锁的阶段,会导致数据大乱,多个线程写入数据,导致出现脏数据

第二阶段:synchronized 与 Lock接口(ReentrantLock类)

这两个的出现,使得线程有序,且能保持数据一致性

无论多少个线程过来,不管读还是写,每次都是一个

每次线程都是只有一个,所以会导致多个线程想读也只能一个个线程读,效率比较慢,因为读之间应该是可以共享的

第三阶段:ReadWriteLock接口(ReentrantReadWriteLock)

这个类不仅可以读写互斥,并实现了读读共享,多个线程并发可以访问,大面积可以容许多个线程来读取

在读多写少的时候,可以使用读写锁

但是也有以下缺点:

  1. 写锁饥饿问题:比如有10W个线程是读,只有一个线程是写的时候,会导致写的线程一直抢占不到资源,出现写锁饥饿问题
  2. 会出现锁降级问题

第四阶段:邮戳锁StampedLock

读的时候也允许写锁的接入(读写两个操作也让你“共享”),这样会导致我们读的数据就可能不一致,所以需要额外的方法来判断写的操作是否有写入,这是一种乐观锁

虽然乐观锁并发效率更高,但是一栏有小概率的写入导致读取的数据不一致,需要能检测出来,再读一次即可


3. 锁降级

什么是锁降级

锁降级就是ReentrantReadWriteLock将写入锁降级为读锁(就像Linux一样,写权限高于读权限),锁的严苛程度变强叫做升级,反之叫做降级

在这里插入图片描述

  1. 如果同一个线程有了写锁,在没有释放写锁的情况下,它还可以继续获取读锁。这就是写锁的降级,降级成了读锁
  2. 规则惯例,先获取写锁,然后获取读锁,再释放写锁的次序
  3. 如果释放了写锁,那么就完全转换为读锁
public class LockDownGradingDemo {
    public static void main(String[] args) {
        ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
        ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();

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

        System.out.println("正在读.....");
        writeLock.unlock();
        readLock.unlock();

    }
}

在这里插入图片描述

但是注意的是,锁可以降级,但是不可以升级,且写锁和读锁是互斥的

锁降级:遵循获取写锁–》再获取读锁–》再释放写锁的次序,写锁能够降级成为读锁

在这里插入图片描述

锁降级是为了让当前线程感知到数据的变化,目的是保证数据的可见性

当读锁被使用时,如果有线程尝试获取写锁,该写线程会被阻塞。所以需要释放所有读锁,才能获取写锁

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写钱锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

因此,分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:
读锁结束,写锁有望;写锁独占,读写全堵
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁

即ReentrantReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁,o(一_-)o,人家还在读着那,你不先别去写,省的数据乱。


4. 锁降级的策略

为什么要锁降级呢?

在这里插入图片描述

cacheValid是一个布尔值,默认为false,如果其他线程修改过这里的数据,那么会将cacheValid改为true

上面案例是首先先读取一次数据,然后接着获取写锁,获取写锁后,判断cacheValid是否有被修改过,如果没有那将data修改为某个值,然后将cacheValid设置为true。

接着锁降级获取读锁,获取读锁之后释放写锁

对数据进行读操作

这样做的好处有以下几点:

  • 在写锁释放之前锁降级成读锁,防止其他线程竞争
  • 这样使得写完之后立马读,防止了其他线程对data进行修改从而出现脏读

5. StampedLock简介

StampedLock优化的主要是ReentrantReadWriteLock出现的锁饥饿问题

stamp代表的是锁的状态。当stamp返回零时,表示线程获取锁失败。

并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值

回顾锁饥饿问题

ReentrantReadWriteLock实现读写分离,一旦读操作比较多的时候,想要获取写锁就会变得比较困难了,假如有1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那么1个写线程就悲剧了,因此当前有可能会一直存在读锁,而无法获得写锁

ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。
但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阳塞,这其实是对读锁的优化,
所以,在获取乐观读锁后,还需要对结果进行校验。


6. StampedLock的特点

  1. 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功
  2. 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
  3. StampedLock是不可重入锁,因为邮戳只有一个,危险(如果一个线程已经持有写锁,再去获取写的话就会导致死锁)
  4. StampedLock有三种访问模式
    1. Reading(读模式悲观),功能和ReentrantReadWriteLock的写锁类似
    2. Writing(写模式),功能和ReentrantReadWriteLock的写锁类似
    3. Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取的时候没人修改。假如被修改再实现升级为悲观读模式

7. StampedLock之传统读写

/**
 * @Author: lrk
 * @Date: 2022/10/25 下午 8:52
 * @Description:
 */
public class StampedLockDemo {

    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number += 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备结束");
    }

    //悲观读,都没有完成的时候写锁无法获取锁
    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "准备读,请等待.....");
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中");
        }

        try {
            int result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + "获取成员变量值result: " + result);
            System.out.println("写线程没有修改成功,读锁的时候写锁无法接入,传统读写互斥");
        } finally {
            stampedLock.unlockRead(stamp);
            System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备结束");
        }



    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
            resource.read();
        }, "readThread").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(() -> {
            resource.write();
        }, "writeThread").start();

    }
}

上面案例使用的Reading和Writing两种访问模式,实现的效果与ReentrantReadWriteLock的读写锁效果一样

在这里插入图片描述


8. StampedLock之乐观锁

public class StampedLockDemo {

    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write() {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备修改");
        try {
            number += 13;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "写线程准备结束");
    }

    public void read() {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName() + "\t" + "准备读,请等待.....");
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            System.out.println(Thread.currentThread().getName() + "\t" + "正在读取中");
        }

        try {
            int result = number;
            System.out.println(Thread.currentThread().getName() + "\t" + "获取成员变量值result: " + result);
            System.out.println("写线程没有修改成功,读锁的时候写锁无法接入,传统读写互斥");
        } finally {
            stampedLock.unlockRead(stamp);
            System.out.println(Thread.currentThread().getName() + "\t" + "读线程准备结束");
        }


    }

    public void tryOptimisticRead() {
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        System.out.println("4s前stampedLock.validate方法值(true无修改,false有修改)" + "\t" + stampedLock.validate(stamp));
        for (int i = 0; i < 4; i++) {
            try {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(Thread.currentThread().getName() + "\t" + "正在读取...." + i + "秒" +
                        stampedLock.validate(stamp) + "后stampedLock.validate方法值(true无修改,false有修改)" + "\t" + stampedLock.validate(stamp));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

        if (!stampedLock.validate(stamp)) {
            System.out.println("有人修改过--------有写操作");
            stamp = stampedLock.readLock();
            try {
                System.out.println("从乐观读升级为悲观读");
                result = number;
                System.out.println("重新悲观读result: " + result);
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "finally value:" + result);
    }

    public static void main(String[] args) {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
            resource.tryOptimisticRead();
        }, "readThread").start();

        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(() -> {
            resource.write();
        }, "writeThread").start();
    }

    private static void extracted(StampedLockDemo resource) {

        new Thread(() -> {
            resource.read();
        }, "readThread").start();

        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(() -> {
            resource.write();
        }, "writeThread").start();
    }
}

在这里插入图片描述


9. StampedLock缺点

  1. StampedLock不支持重入,没有Re开头
  2. StampedLock的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意
  3. 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法

来源:
尚硅谷2022版JUC并发编程(对标阿里P6-P7)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

起名方面没有灵感

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值