JUC中的 StampedLock

Posted XeonYu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JUC中的 StampedLock相关的知识,希望对你有一定的参考价值。

上一篇:

JUC中的读写锁(ReentrantReadWriteLock)

之前我们了解了 ReentrantReadWriteLock,ReentrantReadWriteLock有一个缺点就是读锁会阻塞住写锁,这样会导致当读的线程非常多时,写锁可能会被一直阻塞,无法及时完成写入操作。
所以,在Java8中有引入了StampedLock

StampedLock

Stamped 中文是盖上邮戳的的意思,这里可以理解为给锁加了个标记,这个标记我们可以叫做版本号。

那顾名思义 StampedLock就是带戳(标记)的一把锁

StampedLock同样支持读写锁,但是比ReentrantReadWriteLock中的读锁多了一个特性是StampedLock中的读锁可以是乐观锁

乐观锁实际上不加锁,只是判断一下版本号,不会阻塞住写锁,乐观的认为读的时候没有写的操作。所以,乐观锁在读的过程中,写锁是可以执行的。

我们先来看看StampedLock中的方法
在这里插入图片描述

其中很多方法都是跟ReentrantReadWriteLock中的方法类似,这里就不多说了

我们先来验证第一个点:

乐观读锁读的时候不会阻塞写操作

先来看看悲观读锁是什么样的

class StampedLockDemo {
    private final StampedLock stampedLock = new StampedLock();


    private int num = 0;

    public void write() {

        long stamp = stampedLock.writeLock();

        try {
            TimeUnit.MILLISECONDS.sleep(10);
            num++;
            System.out.println(Thread.currentThread().getName() + "=========写锁执行后的num========:" + num);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            stampedLock.unlockWrite(stamp);
        }


    }


    public int read() {

        long readStamp = stampedLock.readLock();
        String threadName = Thread.currentThread().getName();
        int readNum = 0;
        try {
            TimeUnit.MILLISECONDS.sleep(100);
            readNum = num;
            System.out.println(threadName + "读取的 readNum:" + readNum);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            stampedLock.unlockRead(readStamp);
        }
        return readNum;
    }

}

main方法如下:

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


        for (int i = 0; i < 10; i++) {
            new Thread(stampedLockDemo::read).start();
        }

        for (int i = 0; i < 10; i++) {
            new Thread(stampedLockDemo::write).start();
        }

    }


运行结果如下
在这里插入图片描述

可以看到,写线程只有在读锁都释放后才会执行。

下面改成乐观锁试一下。
tryOptimisticRead就表示获取的是乐观锁。

class StampedLockDemo {
    private final StampedLock stampedLock = new StampedLock();


    private int num = 0;

    public void write() {

        long stamp = stampedLock.writeLock();

        try {
            TimeUnit.MILLISECONDS.sleep(10);
            num++;
            System.out.println(Thread.currentThread().getName() + "=========写锁执行后的num========:" + num);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            stampedLock.unlockWrite(stamp);
        }


    }


    public int read() {

        long readStamp = stampedLock.tryOptimisticRead();
        String threadName = Thread.currentThread().getName();
        int readNum = 0;
        try {
            TimeUnit.MILLISECONDS.sleep(100);
            readNum = num;
            System.out.println(threadName + "读取的 readNum:" + readNum);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }
        return readNum;
    }

}


再来运行一下:

在这里插入图片描述

可以看到,读锁更改为乐观锁后,在读的期间,写操作是可以执行的。
由于 乐观读锁不会加锁,所以,我们无需做解锁操作

我们知道了乐观读锁读的时候,不会阻塞写操作,那么问题就来了,假如在读的过程中,恰好有其他线程执行了写操作,且写入完毕了,那么,本次读操作的数据就不对了。

我们先来看个示例:

代码如下:

class StampedLockDemo {
    private final StampedLock stampedLock = new StampedLock();


    private int num = 0;

    public void write() {

        long stamp = stampedLock.writeLock();

        try {
            TimeUnit.MILLISECONDS.sleep(10);
            num++;
            System.out.println(Thread.currentThread().getName() + "=========写锁执行后的num========:" + num);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            stampedLock.unlockWrite(stamp);
        }


    }


    public int read() {

        long readStamp = stampedLock.tryOptimisticRead();
        int readNum = 0;

        /*模拟一个线程执行写操作*/
        new Thread(this::write).start();

        try {
            readNum = num;

            /*模拟等待写线程执行完毕*/
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }


        return readNum;
    }

}

main方法:

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

        new Thread(() -> {
            int readNum = stampedLockDemo.read();
            System.out.println("readNum = " + readNum);
        }).start();


    }

运行结果:

在这里插入图片描述

可以看到,我们读的过程中,数据被写操作改成1了,但是,返回的依旧是0. 这个结果就不符合预期了。
所以,乐观锁读到数据后,返回数据之前一定要加个校验,判断一下读的过程中是否有写入的操作,如果有,则使用悲观读锁重新读一遍数据,以保证数据的正确性。

我们可以通过 validate 方法来校验版本号是否发生变化。

代码如下:

为了方便理解,加了很多打印以及注释

class StampedLockDemo {
    private final StampedLock stampedLock = new StampedLock();


    private int num = 0;

    public void write() {

        long stamp = stampedLock.writeLock();
        System.out.println("写操作的版本号stamp:" + stamp);


        try {
            num++;
            System.out.println(Thread.currentThread().getName() + "=========写锁执行后的num========:" + num);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {

            stampedLock.unlockWrite(stamp);
        }


    }


    public int read() {

        long readStamp = stampedLock.tryOptimisticRead();

        System.out.println("第一次获取的readStamp:" + readStamp);
        int readNum = 0;


        try {
            /*模拟一个线程执行写操作*/
            Thread writeThread = new Thread(() -> {
                this.write();

            });
            writeThread.start();

            /*获取读到的值*/
            readNum = num;

            /*模拟等待写线程执行完毕*/
            TimeUnit.MILLISECONDS.sleep(5);
            /*查看线程的状态 此时线程是TERMINATED  因为乐观读锁不会阻塞住谢先成,而写线程1毫秒就执行完了*/
            System.out.println("writeThread.getState() = " + writeThread.getState());

            System.out.println("乐观读锁读取的数据:" + readNum);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {

        }




        /*返回数据之前*/
        if (!stampedLock.validate(readStamp)) {

            System.out.println("版本号对不上,重新获取的版本号:" + readStamp);
            /*版本号对不上  表示读期间有写入操作  用悲观锁重新读一遍*/
            readStamp = stampedLock.readLock();//加锁,此时会写锁会被阻塞住,等读锁完全执行完毕释放后,写操作才能执行

            try {
                /*再次模拟一个线程执行写操作*/
                Thread writeThread = new Thread(() -> {
                    this.write();
                });
                writeThread.start();

                /*重新读数据*/
                readNum = num;

                /*给写线程留充足的时间去执行*/
                TimeUnit.MILLISECONDS.sleep(10);

                /*此时写线程处于等待状态  因为悲观读锁会阻塞住写锁,写线程必须等到悲观读锁释放锁之后,才会执行*/
                System.out.println("writeThread.getState() = " + writeThread.getState());

                System.out.println("悲观读锁读取的数据:" + readNum);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                /*释放锁*/
                stampedLock.unlockRead(readStamp);
                System.out.println("悲观读锁释放锁了,写线程可以执行了" );
            }

        }


        return readNum;
    }

}



运行结果如下:

在这里插入图片描述

根据运行结果看到,本次读的数据是符合预期的,在读的期间值被修改成了1,最终返回的也是1。

注意:
保证数据的正确性范围是在 本次 读操作开始到结束,本次读操作结束后你数据再更改,那就跟本次读没什么关系了,这一点不要搞混淆了。

总结

StampedLock 是ReentrantReadWriteLock的升级版,支持乐观读锁,但他是不可重入锁,主要区别在于乐观读锁实际上不上锁,不会阻塞写锁,理论上在读操作的性能上也会比ReentrantReadWriteLock的readLock性能要好(省去了加锁解锁的过程)。


如果你觉得本文对你有帮助,麻烦动动手指顶一下,可以帮助到更多的开发者,如果文中有什么错误的地方,还望指正,转载请注明转自喻志强的博客 ,谢谢!

以上是关于JUC中的 StampedLock的主要内容,如果未能解决你的问题,请参考以下文章

JUC中的 StampedLock

通俗易懂的JUC源码剖析-StampedLock

JUC并发编程 共享模式之工具 JUC 读写锁 StampedLock -- 介绍 & 使用

JUC之StampedLock读写锁增强辅助类

❤ 爆肝JUC 包中的 API 解读

❤ 爆肝JUC 包中的 API 解读