Bootstrap

Java并发编程——互斥3——StampedLock锁——一种比ReadWriteLock更快的锁

🌈hello,你好鸭,我是Ethan,西安电子科技大学大三在读,很高兴你能来阅读。

✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。
🏃人生之义,在于追求,不在成败,勤通大道。加油呀!

🔥个人主页:Ethan Yankang
🔥推荐:史上最强八股文||一分钟看完我的几百篇博客

🔥温馨提示:划到文末发现专栏彩蛋   点击这里直接传送

🔥本篇概览:详细讲解了Java并发编程——互斥3——StampedLock锁——一种比ReadWriteLock更快的锁。🌈⭕🔥


【计算机领域一切迷惑的源头都是基本概念的模糊,算法除外】


🌈序言:

JAVA并发编程一直是难点,痛点,但又是进阶之重点,此关必过。今日得《冰河技术》之良品辅助,应按本系列学之习之,时时复习,长此以往必能穿魂入脉,习得大功。

记住——别违背科学发展的客观规律。别一味地赶进度以满足自己学的都么快的虚荣心,自欺欺人,要老老实实的走好每一步。

 【并发编程全貌】


🔥Java并发编程全集

🔥 所有JAVA基础一键查阅(含习题集)-CSDN博客


🌈引出

        ReadWriteLock锁允许多个线程同时读取共享变量,但是在读取共享变量的时候,不允许另外的线程多共享变量进行写操作,更多的适合于读多写少的环境中。那么,在读多写少的环境中,有没有一种比ReadWriteLock更快的锁呢?答案当然是有!——JDK1.8中新增的StampedLock!


  • StampedLock
    • 引出
      • 比ReadWriteLock更快的锁
        • ReadWriteLock锁允许多个线程同时读取共享变量,但是在读取共享变量的时候,不允许另外的线程多共享变量进行写操作,更多的适合于读多写少的环境中。那么,在读多写少的环境中,有没有一种比ReadWriteLock更快的锁呢?答案当然是有!——JDK1.8中新增的StampedLock!
      • 读写锁不互斥
        • StampedLock与ReadWriteLock相比,在读的过程中也允许后面的一个线程获取写锁对共享变量进行写操作
          为了避免读取的数据不一致,使用StampedLock读取共享变量时,需要对共享变量进行是否有写入的检验操作,并且这种读是一种乐观读。
    • 标准定义
      • 总之,StampedLock是一种在读取共享变量的过程中,允许后面的一个线程获取写锁对共享变量进行写操作,
        使用乐观读避免数据不一致的问题
        ,并且在读多写少的高并发环境下,比ReadWriteLock更快的一种锁
    • StampedLock三种锁模式
      • ReadWriteLock
        • 仅支持两种锁
          • 读锁
          • 写锁
        • 读共享、写排他
        • 读写互斥
      • StampedLock
        • 支持两种三种锁
          • 读锁
          • 写锁
          • 乐观锁
            • StampedLock支持乐观读,这是它比
              ReadWriteLock性能要好的关键所在。
            • ReadWriteLock在读取共享变量时,所有对共享变量的写操作都会被阻塞。
              1. 而StampedLock提供的乐观读,在多个线程读取共享变量时,允许一个线程对共享变量进行写操作
        • 读共享、写排他
        • 读写互斥
      • StampedLock支持乐观锁
        • StampedLock在获取读锁或者写锁成功后,都会返回一个Long类型的变量,
          之后在释放锁时,需要传入这个Long类型的变量。
        • 伪代码演示
          • public class StampedLockDemo{
                //创建StampedLock锁对象
                public StampedLock stampedLock = new StampedLock();
               
                //获取、释放读锁
                public void testGetAndReleaseReadLock(){
                    long stamp = stampedLock.readLock();
                    try{
                        //执行获取读锁后的业务逻辑
                    }finally{
                        //释放锁
                        stampedLock.unlockRead(stamp);
                    }
                }
               
                //获取、释放写锁
                public void testGetAndReleaseWriteLock(){
                    long stamp = stampedLock.writeLock();
                    try{
                        //执行获取写锁后的业务逻辑。
                    }finally{
                        //释放锁
                        stampedLock.unlockWrite(stamp);
                    }
                }
            }
        • 官方演示
          • class Point {
                private double x, y;
                private final StampedLock sl = new StampedLock();

                void move(double deltaX, double deltaY) { // an exclusively locked method
                    long stamp = sl.writeLock();
                    try {
                        x += deltaX;
                        y += deltaY;
                    } finally {
                        sl.unlockWrite(stamp);
                    }
                }

                double distanceFromOrigin() { // A read-only method
                    long stamp = sl.tryOptimisticRead();
                    double currentX = x, currentY = y;
            //如果其他线程修改了变量,变为悲观锁
                    if (!sl.validate(stamp)) {
                        stamp = sl.readLock();
                        try {
                            currentX = x;
                            currentY = y;
                        } finally {
                            sl.unlockRead(stamp);
                        }
                    }
                    return Math.sqrt(currentX * currentX + currentY * currentY);
                }

                void moveIfAtOrigin(double newX, double newY) { // upgrade
                    // Could instead start with optimistic, not read mode
                    long stamp = sl.readLock();
                    try {
                        while (x == 0.0 && y == 0.0) {
                            long ws = sl.tryConvertToWriteLock(stamp);
                            if (ws != 0L) {
                                stamp = ws;
                                x = newX;
                                y = newY;
                                break;
                            }
                            else {
                                sl.unlockRead(stamp);
                                stamp = sl.writeLock();
                            }
                        }
                    } finally {
                        sl.unlock(stamp);
                    }
                }
            }
        • 解释
          • 在上述代码中,如果在执行乐观读操作时,另外的线程对共享变量进行了写操作,则会把乐观读升级为悲观读锁,如下代码片段所示。
          • double distanceFromOrigin() { // A read-only method
                //乐观读
                long stamp = sl.tryOptimisticRead();
                double currentX = x, currentY = y;
                //判断是否有线程对变量进行了写操作
                //如果有线程对共享变量进行了写操作
                //则sl.validate(stamp)会返回false

                if (!sl.validate(stamp)) {
                    //将乐观读升级为悲观读锁
                    stamp = sl.readLock();
                    try {
                        currentX = x;
                        currentY = y;
                    } finally {
                        //释放悲观锁
                        sl.unlockRead(stamp);
                    }
                }
                return Math.sqrt(currentX * currentX + currentY * currentY);
            }
        • 优点
          • 这种将乐观读升级为悲观读锁的方式相比一直使用乐观读的方式更加合理,如果不升级为悲观读锁,则程序会在一个循环中反复执行乐观读操作,
            直到乐观读操作期间没有线程执行写操作,而在循环中不断的执行乐观读会消耗大量的CPU资源,升级为悲观读锁是更加合理的一种方式
    • StampedLock实现思想
      • 原理
        •  CLH锁
          • StampedLock内部是基于CLH锁实现的,CLH是一种自旋锁,能够保证没有“饥饿现象”的发生,并且能够保证FIFO(先进先出)的服务顺序。
        • 详细原理
          • 等待队列
            • 在CLH中,锁维护一个等待线程队列,所有申请锁,但是没有成功的线程都会存入这个队列中每一个节点代表一个线程,保存一个标记位(locked),用于判断当前线程是否已经释放锁,当locked标记位为true时, 表示获取到锁,当locked标记位为false时,表示成功释放了锁。
          • 获取锁
            • 当一个线程试图获得锁时,取得等待队列的尾部节点[只要队尾的锁释放了,那么就一定自己可以获得]
              作为其前序节点
              ,并使用类似如下代码判断前序节点是否已经成功释放锁:
              • while (pred.locked) {
                    //省略操作 
                }
            • 只要前序节点(pred)没有释放锁,则表示当前线程还不能继续执行,因此会自旋等待
              反之,如果前序线程已经释放锁,则当前线程可以继续执行。
          • 释放锁
            • 释放锁时,也遵循这个逻辑,线程会将自身节点的locked位置标记为false
              后续等待的线程就能继续执行了,也就是已经释放了锁。
    • StampedLock的注意事项
      • 与ReadWriteLock的选择
        • 在读多写少的高并发环境下,StampedLock的性能确实不错,但是它不能够完全取代ReadWriteLock。在使用的时候,也需要特别注意以下几个方面。
      • StampedLock不支持重入
        • 在使用StampedLock时,不能嵌套使用,这点在使用时要特别注意。
      • StampedLock不支持条件变量
        • 无论是读锁还是写锁,都不支持条件变量。
      • StampedLock使用不当会导致CPU飙升
        • 如果某个线程阻塞在StampedLock的readLock()或者writeLock()方法上时,
          此时调用阻塞线程的interrupt()方法中断线程,会导致CPU飙升到100%
        • 示例代码
          • public void testStampedLock() throws Exception{
                final StampedLock lock = new StampedLock();
                Thread thread01 = new Thread(()->{
                    // 获取写锁
                    lock.writeLock();
                    // 永远阻塞在此处,并且不释放写锁
                    LockSupport.park();
                });
                thread01.start();
                // 保证thread01获取写锁[启动线程执行之]
                Thread.sleep(100);

                Thread thread02 = new Thread(()->
                                       //阻塞在悲观读锁[因为读写锁互斥]
                                       lock.readLock()

                                      );
                thread02.start();
                // 保证T2阻塞在读锁
                Thread.sleep(100);
                //中断线程thread02
                //会导致线程thread02所在CPU飙升

                thread02.interrupt();
                //表示先把线程02执行完了在执行主线程
                thread02.join();
            }
            • LockSupport.park();
              • 表示让当前线程阻塞,java.util.concurrent.locks.LockSupport
        • 问题
          • 当对 thread02 调用中断后,thread02 会继续尝试获取读锁而不停止,持续占用 CPU 资源,导致 CPU 使用率飙升。

            原因是代码中在获取读锁处没有正确处理中断异常,使得线程在被中断后没有按照预期停止获取锁的操作,|
            而是继续不断尝试获取锁,进入了一个无限循环,从而消耗大量 CPU 资源。
        • 解释


          • 运行上面的程序,会导致thread02线程所在的CPU飙升到100%。

            这里,有很多小伙伴不太明白为啥LockSupport.park();会导致thread01会永远阻塞。
            这里,冰河为你画了一张线程的生命周期图,如下所示。
          • 看完这个线程的生命周期图,知道为啥调用LockSupport.park();会使thread01阻塞了吧?
        • 解决方案 
          • 如何避免StampedLock时
            线程所在的CPU飙升?
            • 使用StampedLock的readLock()方法或者读锁和使用writeLock()方法获取写锁时,一定不要使用传统的线程的中断方法
            • 一定要中断的话,要获取可中断的StampedLock锁
              • readLockInterruptibly()方法
              • writeLockInterruptibly()方法


💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖

热门专栏推荐

🌈🌈计算机科学入门系列                     关注走一波💕💕

🌈🌈CSAPP深入理解计算机原理        关注走一波💕💕

🌈🌈微服务项目之黑马头条                 关注走一波💕💕

🌈🌈redis深度项目之黑马点评            关注走一波💕💕

🌈🌈JAVA面试八股文系列专栏           关注走一波💕💕

🌈🌈JAVA基础试题集精讲                  关注走一波💕💕   

🌈🌈代码随想录精讲200题                  关注走一波💕💕


总栏

🌈🌈JAVA基础要夯牢                         关注走一波💕💕  

🌈🌈​​​​​​JAVA后端技术栈                          关注走一波💕💕  

🌈🌈JAVA面试八股文​​​​​​                          关注走一波💕💕  

🌈🌈JAVA项目(含源码深度剖析)    关注走一波💕💕  

🌈🌈计算机四件套                               关注走一波💕💕  

🌈🌈数据结构与算法                           ​关注走一波💕💕  

🌈🌈必知必会工具集                           关注走一波💕💕

🌈🌈书籍网课笔记汇总                       关注走一波💕💕         



📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤收藏✅ 评论💬,大佬三连必回哦!thanks!!!
📚愿大家都能学有所得,功不唐捐!

;