Java并发编程——StampedLock

一、StampedLock类简介

StampedLock类,在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。

首先明确下,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。

1.1 StampedLock的引入

先来看下,为什么有了ReentrantReadWriteLock,还要引入StampedLock?

ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的。

但是,读写锁如果使用不当,很容易产生“饥饿”问题:

比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。(在ReentrantLock类的介绍章节中,介绍过这种情况)

1.2 StampedLock的特点

StampedLock的主要特点概括一下,有以下几点:

  • 1、所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;

  • 2、所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;

  • 3、StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

  • 4、StampedLock有三种访问模式:

    • ①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
    • ②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
    • ③Optimistic reading(乐观读模式):这是一种优化的读模式。
  • 5、StampedLock支持读锁和写锁的相互转换

    • 我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
    • StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
  • 6、无论写锁还是读锁,都不支持Conditon等待

我们知道,在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会阻塞。
但是,在Optimistic reading中,即使读线程获取到了读锁,写线程尝试获取写锁也不会阻塞,这相当于对读模式的优化,但是可能会导致数据不一致的问题。所以,当使用Optimistic reading获取到读锁时,必须对获取结果进行校验。

StampedLock 的性能之所以比 ReentrantReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReentrantReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。

注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。

1.3 理解乐观读

乐观读往往与版本号挂钩,在获取数据时会同时记录与此对应的版本,然后写数据时查看当前的版本号与当时读取数据的版本号是否对应,如果匹配则更新数据以及版本号,否则需要重新获取数据。

//读取数据X,同时拿到的版本号为10
x = get(x), version=10

//修改数据
x = 20

//写回数据,如果put失败,说明version已过时
put(x, version)

二、StampedLock使用

2.1 获取/释放悲观读锁示意代码

StampedLock lock = new StampedLock();

long stamp = lock.readLock();
try {
    //省略业务相关代码
} finally {
    lock.unlockRead(stamp);
}

2.2 获取/释放写锁示意代码

StampedLock lock = new StampedLock();

long stamp = lock.writeLock();
try {
    //省略业务相关代码
} finally {
    lock.unlockWrite(stamp);
}

2.3 StampedLock.tryOptimisticRead() 乐观读

StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。

StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead();
// 验戳
if(lock.validate(stamp)){
    //获取数据, 并返回
}

// 锁升级 - 读锁
stamp = lock.readLock();
try {
    //省略业务相关代码
} finally {
    lock.unlockRead(stamp);
}

2.4 StampedLock使用示例

@Slf4j
public class DataContainerStamped {

    private int data;

    private final StampedLock lock = new StampedLock();

    public DataContainerStamped(int data) {
        this.data = data;
    }

    public int read(int readTime) {
        long stamp = lock.tryOptimisticRead();
        log.info("optimistic read locking...{}", stamp);
        try {
            TimeUnit.SECONDS.sleep(readTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (lock.validate(stamp)) {
            log.info("read finish...{}, data:{}", stamp, data);
            return data;
        }
        // 锁升级 - 读锁
        log.info("updating to read lock... {}", stamp);
        try {
            stamp = lock.readLock();
            log.info("read lock {}", stamp);
            TimeUnit.SECONDS.sleep(readTime);
            log.info("read finish...{}, data:{}", stamp, data);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            log.info("read unlock {}", stamp);
            lock.unlockRead(stamp);
        }
        return data;
    }

    public void write(int newData) {
        long stamp = lock.writeLock();
        log.info("write lock {}", stamp);
        try {
            TimeUnit.SECONDS.sleep(2);
            this.data = newData;
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            log.info("write unlock {}", stamp);
            lock.unlockWrite(stamp);
        }
    }
}

2.4.1 测试 读-读 可以优化

public static void main(String[] args) throws InterruptedException {
    DataContainerStamped dataContainer = new DataContainerStamped(1);
    new Thread(() -> dataContainer.read(1), "t1").start();

    TimeUnit.SECONDS.sleep((long) 0.5);

    new Thread(() -> dataContainer.read(0), "t2").start();
}
  • 输出结果,可以看到实际没有加读锁
15:04:21.780 [t1] INFO com.yibo.lock.DataContainerStamped - optimistic read locking...256
15:04:21.785 [t2] INFO com.yibo.lock.DataContainerStamped - optimistic read locking...256
15:04:21.785 [t2] INFO com.yibo.lock.DataContainerStamped - read finish...256, data:1
15:04:22.796 [t1] INFO com.yibo.lock.DataContainerStamped - read finish...256, data:1

2.4.2 测试 读-写 时优化读补加读锁

public static void main(String[] args) throws InterruptedException {
    DataContainerStamped dataContainer = new DataContainerStamped(1);
    new Thread(() -> dataContainer.read(1), "t1").start();

    TimeUnit.SECONDS.sleep((long) 0.5);

    new Thread(() -> dataContainer.write(0), "t2").start();
}

输出结果:

15:06:46.747 [t1] INFO com.yibo.lock.DataContainerStamped - optimistic read locking...256
15:06:46.747 [t2] INFO com.yibo.lock.DataContainerStamped - write lock 384
15:06:47.764 [t1] INFO com.yibo.lock.DataContainerStamped - updating to read lock... 256
15:06:48.754 [t2] INFO com.yibo.lock.DataContainerStamped - write unlock 384
15:06:48.755 [t1] INFO com.yibo.lock.DataContainerStamped - read lock 513
15:06:49.761 [t1] INFO com.yibo.lock.DataContainerStamped - read finish...513, data:0
15:06:49.761 [t1] INFO com.yibo.lock.DataContainerStamped - read unlock 513

三、StampedLock 使用注意事项

对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是 StampedLock 的功能仅仅是ReentrantReadWriteLock的子集,并不能替代ReentrantReadWriteLock,在使用的时候,还是有几个地方需要注意一下。

  • 1、StampedLock不支持重入( Reentrant)
  • 2、StampedLock的悲观读锁,写锁不支持条件变量
  • 3、如果线程阻塞在StampedLock的readLock()或writeLock()上时,此时调用该阻塞线程的interrupt()会导致CPU飙升。

3.1 总结

可以看到相比直接用读锁,乐观读模式可以:

  • 1、进入悲观读锁前先看下有没有进入写模式(说白了就是有没有已经获取了悲观写锁)

  • 2、如果其他线程已经获取了悲观写锁,那么就只能老老实实的获取悲观读锁(这种情况相当于退化成了读写锁)

  • 3、如果其他线程没有获取悲观写锁,那么就不用获取悲观读锁了,减少了一次获取悲观读锁的消耗和避免了因为读锁导致写锁阻塞的问题,直接返回读的数据即可(必须再tryOptimisticRead和validate之间获取好数据,否则数据可能会不一致了,试想如果过了validate再获取数据,这时数据可能被修改并且读操作也没有任何保护措施)。

四、StampedLock的实现思想

在StampedLock中使用了CLH自旋锁,如果发生了读失败,不立刻把读线程挂起,锁当中维护了一个等待线程队列。所有申请锁但是没有成功的线程都会记录到这个队列中,每一个节点(一个节点表示一个线程)保存一个标记位(locked),用于判断当前线程是否已经释放锁。新加入的节点会加入到队列的末尾,当前等待队列尾部的节点作为其前序节点,并使用类似如下代码(一个空的死循环)判断前序节点是否已经成功的释放了锁: while(pred.locked){ }

解释:pred表示当前试图获取锁的线程的前序节点,如果前序节点没有释放锁,则当前线程就执行该空循环并不断判断前序节点的锁释放,即类似一个自旋锁的效果,避免被系统挂起。当循环一定次数后,前序节点还没有释放锁,则当前线程就被挂起而不再自旋。

StampedLock原理:https://segmentfault.com/a/1190000015808032

参考:
https://segmentfault.com/a/1190000015808032

https://www.cnblogs.com/moris5013/p/11882894.html

https://www.cnblogs.com/xidongyu/articles/12241190.html

https://www.cnblogs.com/zxporz/p/11642176.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容