多线程之StampedLock
多线程之StampedLock
StampedLock
是 JDK1.8 版本中在 J.U.C 并发包里新增的一个锁,StampedLock
是对读写锁ReentrantReadWriteLock
的增强,优化了读锁、写锁的访问,更细粒度控制并发。
StampedLock
为什么引入StampedLock
?
ReentrantReadWriteLock可能会导致写线程饥饿。
- 读写锁多应用在读多写少的场景
- 读锁是共享锁,当一个线程持有读锁时其他线程是可以获取到读锁的
- 读写锁不支持锁升级,当一个线程持有读锁时,该线程自己和其他线程都是不可以获取写锁的
当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景下读操作远远大于写的操作,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。
StampedLock
提供了解决这一问题的方案————乐观读锁 Optimistic reading,即一个线程获取的乐观读锁之后,不会阻塞线程获取写锁。
三种锁模式
写锁 writeLock
、悲观读锁 readLock
、乐观读锁 Optimistic reading
。
写锁 writeLock
类似ReentrantReadWriteLock
的写锁,独占锁,当一个线程获取该锁后,其它请求的线程必须等待。
获取:没有线程持有悲观读锁或者写锁的时候才可以获取到该锁。
释放:请求该锁成功后会返回一个 stamp
票据变量用来表示该锁的版本,当释放该锁时候需要将这个 stamp
作为参数传入解锁方法。
悲观读锁 readLock
类似ReentrantReadWriteLock
的读锁,共享锁,同时多个线程可以获取该锁。
获取:在没有线程获取独占写锁的情况下,同时多个线程可以获取该锁。
释放:请求该锁成功后会返回一个 stamp
票据变量用来表示该锁的版本,当释放该锁时候需要 unlockRead
并传递参数 stamp
。
乐观读锁 tryOptimisticRead
获取:不需要通过 CAS 设置锁的状态,如果当前没有线程持有写锁,直接简单的返回一个非 0 的 stamp 版本信息,表示获取锁成功。
释放:并没有使用 CAS 设置锁状态所以不需要显示的释放该锁。
乐观读锁在获取 stamp 时,会将需要的数据拷贝一份出来。在真正进行读取操作时,验证 stamp 是否可用。如何验证 stamp 是否可用呢?从获取 stamp 到真正进行读取操作这段时间内,如果有线程获取了写锁,stamp 就失效了。如果 stamp 可用就可以直接读取原来拷贝出来的数据,如果 stamp 不可用,就重新拷贝一份出来用。我们操作的是方法栈里面的数据,也就是一个快照,所以最多返回的不是最新的数据,但是一致性还是得到保障的。
乐观读锁在读多写少的情况下提供更好的性能,因为乐观读锁不需要进行 CAS 设置锁的状态而只是简单的测试状态。
乐观读锁的使用步骤:
long stamp = lock.tryOptimisticRead(); // 非阻塞获取版本信息 |
StampedLock
是不可重入的,如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁。StampedLock
支持读锁和写锁的相互转换。我们知道ReentrantReadWriteLock
中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。而StampedLock
提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
## 总结
读写锁在读线程非常多,写线程很少的情况下可能会导致写线程饥饿,JDK1.8 新增的StampedLock
通过乐观读锁来解决这一问题。
StampedLock
有三种访问模式:
①写锁writeLock:功能和读写锁的写锁类似 |
所有获取锁的方法,都返回一个票据 Stamp,Stamp 为 0 表示获取失败,其余都表示成功;所有释放锁的方法,都需要一个票据 Stamp,这个 Stamp 必须是和成功获取锁时得到的 Stamp 一致。
乐观读锁:乐观的认为在具体操作数据前其他线程不会对自己操作的数据进行修改,所以当前线程获取到乐观读锁的之后不会阻塞线程获取写锁。为了保证数据一致性,在具体操作数据前要检查一下自己操作的数据是否经过修改操作了,如果进行了修改操作,就重新读一次。因为乐观读锁不需要进行 CAS 设置锁的状态而只是简单的测试状态,所以在读多写少的情况下有更好的性能。