StampedLock使用方式

    简述:在上篇文章中介绍和分析了ReentranReadWriteLock,我们发现是可以将读写分别进行加锁,需要注意的一点就是,当读锁获取到资源的时候,如果要进行写入,不管是否是当前同一个线程都不可以,在开发中我们大部分场景其实都是读多写少,显然,如果要进行写入,需要等待所有的读操作都执行完,显然性能上就会有所降低,因此,在JDK1.8里面提供了StampedLock这个类,他里面提供了一种乐观锁读,很大程度上满足了,读多写少的场景。

StampedLock介绍:在每次加锁之后,都会返回一个邮戳,来表示锁的版本,在释放的时候,需要把这个锁的版本传递进去

官方示例代码:

下面是他的一个基础类,包含两个元素,x和y, 及一个stampedLock锁

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);
        }
    }

在同一时刻只能有一个线程进行move操作,其他的读线程和写线程都会进行阻塞,获取锁之后返回一个邮戳,释放的时候,也基于邮戳进行释放。

乐观读锁:

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);
            }
        }
        // 返回计算结果(7)
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

1、获取乐观读锁

2、将变量拷贝到方法栈内

3、检查所是否失效,即在这个过程中是否有其他写线程修改了数据,否则执行 6

4、如果失效,获取悲观读锁,然后更新方法栈内的变量

5、释放掉悲观读锁,

6、进行计算

Q: 是否可以在获取到乐观锁之后,判断锁是否失效,如果没有失效再进行变量拷贝?

A: 不可以。因为在获取到乐观锁之后,判断乐观锁并未失效,但是在变量拷贝的过程中,其他写线程持有锁,对x和y其中的一个进行修改,最终计算的数据并不能保证一致性。

悲观读锁:

 // 使用悲观锁获取读锁,并尝试转换为写锁
    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);
        }
    }

1、先获取到悲观读锁,

2、判断当前数据是否在原点,如果是,尝试将锁转为写锁,

3、如果获取写锁成功,即返回的邮戳不等于0 ,对数据进行赋值,然后跳出循环

4、如果尝试升级写锁失败,先释放掉读锁,然后再转为获取独享写锁,等待其他线程执行结束

5、最终释放掉锁

对于乐观锁的使用步骤:

1、获取到乐观锁

2、将变量拷贝至方法栈内

3、判断乐观锁是否失效,如果失效执行4,否则执行7

4、获取悲观读取锁,再次拷贝变量

5、释放掉悲观读锁

7、执行计算结果

关于StampedLock中的一个bug:

StampedLock其内部是通过死循环+CAS操作的方式来修改状态位,在挂起线程时,是通过unsafe.park的方式,而对于中断的线程,unsafe.park会直接返回,而在StampedLock的死循环逻辑中,没有处理中断的逻辑,就会导致阻塞在park上的线程中断后,再次进入循环,直到当前死循环满足退出条件,因此整个过程会使cpu暴涨。

解决办法:

acquireRead(boolean interruptible, long deadline) 和acquireWrite(boolean interruptible, long deadline)中添加 保存/复原中断状态的机制。

以下是github上大神对StampedLock.java类的修改 点击打开链接

参考:JDK8中StampedLock原理探究

        《Java高并发程序设计》学习 --6.6 读写锁的改进:StampedLock

        【Java并发】- StampedLock实现浅析

         Bug:StampedLock的中断问题导致CPU爆满

你可能感兴趣的:(多线程)