JUC Lock 读写锁
文章目录
ReentrantReadWriteLock1.5+ 读写锁
对共享数据的查看、查询就是读,对共享数据的修改就是写,读时不会涉及共享数据的修改,不修改意味着多个线程读取的数据就不会变化,那么在此情况下多个线程在用锁来进行排它读取操作就会影响效率。反过来说,如果此共享数据需要修改(写),那么开始写到结束写这段时间就不能读。
以上总结起来就是,多个线程可以同时读,但一旦有某一个线程开始写,那么其余线程就不能读也不能写。再总结简单点:读读线程可以同时进行,读写不能同时进行,写写也不能同时进行。
ReentrantReadWriteLock 继承关系图
- ReentrantReadWriteLock 并不是直接实现 Lock 接口,而是实现 ReadWriteLock 接口
public interface ReadWriteLock {
/**
* 返回读锁,就是 ReadLock 对象
*
*/
Lock readLock();
/**
* 返回写锁,就是 WriteLock 对象
*/
Lock writeLock();
}
- ReentrantReadWriteLock 内部定义了 5 个内部类,Sync(以及对应的两个实现类:FairSync、NonfairSync)、WriteLock、ReadLock
- Sync 和 ReentrantLock 一样,继承自 AQS。
- WriteLock、ReadLock:都实现了 Lock 接口,他们的实例都包含一个 Sync 对象,该 Sync 对象和ReentrantReadWriteLock 实例化时的 Sync 对象是一致的。
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);// 传入当前的 ReentrantReadWriteLock 对象
writerLock = new WriteLock(this);// 传入当前的 ReentrantReadWriteLock 对象
}
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
示例 1
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
public class ReentrantReadWriteLockTest1 {
private static int x = 0;
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
for(int i = 0; i < 10; i++){
new Thread(()->{
rwLock.readLock().lock();
try {
log.debug("开始读取数据");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}", x);
log.debug("数据读取完毕");
}finally {
rwLock.readLock().unlock();
}
},"read-" + i).start();
}
for(int i = 0; i < 10; i++){
new Thread(()->{
rwLock.writeLock().lock();
try {
log.debug("开始写数据");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
x++;
log.debug("数据写入完毕");
}finally {
rwLock.writeLock().unlock();
}
},"write-" + i).start();
}
}
}
结果:
16:19:12.658 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:12.658 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:13.663 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.665 [read-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.663 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.665 [read-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.665 [read-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.663 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.663 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 0
16:19:13.666 [read-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.666 [read-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:13.666 [write-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:14.672 [write-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:14.672 [write-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:15.677 [write-0] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:15.677 [write-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:16.682 [write-1] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:16.682 [write-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:17.691 [write-4] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:17.691 [write-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:18.697 [write-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:18.697 [write-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:19.705 [write-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:19.705 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:19.705 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:19.705 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始读取数据
16:19:20.713 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 6
16:19:20.713 [read-5] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.713 [read-9] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.714 [read-8] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据读取完毕
16:19:20.714 [write-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:21.725 [write-2] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:21.725 [write-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:22.729 [write-3] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:22.729 [write-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:23.734 [write-6] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
16:19:23.735 [write-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 开始写数据
16:19:24.744 [write-7] DEBUG com.yyoo.thread.lock.ReentrantReadWriteLockTest1 - 数据写入完毕
从结果可以看出,不同的读线程可以同时进行(结果顺序不是以“1.开始读取数据;2. x的值;3. 数据读取完毕”这样的顺序完成),但读写、写写线程是互斥的(打印结果顺序一定是“1. 开始写数据;2. 数据写入完毕”)。
示例 2
示例 1 是读锁和写锁在不同的线程中,如果读锁和写锁都在同一个线程中,有什么要求呢?
在同一个线程中,同时使用读锁和写锁的情况有如下几种:
获取读锁 -> 释放读锁 -> 获取写锁 -> 释放写锁
获取写锁 -> 释放写锁 -> 获取读锁 -> 释放读锁
以上两种方式不存在嵌套,不会出现问题
获取读锁 -> 获取写锁 -> 释放读锁 -> 释放写锁 》》》先获取读锁再获取写锁
获取写锁 -> 获取读锁 -> 释放写锁 -> 释放读锁 》》》先获取写锁再获取读锁
其他 。。。。。。
先获取读锁再获取写锁
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
public class ReentrantReadWriteLockTest2 {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();
log.debug("获取读锁成功");
rwLock.writeLock().lock();
log.debug("获取写锁成功");
rwLock.readLock().unlock();
log.debug("释放读锁成功");
rwLock.writeLock().unlock();
log.debug("释放写锁成功");
}
}
结果:在获取写锁的地方阻塞了。因为写锁在开始写的时候,是不能读取的,与读锁互斥,这样的写法将导致写锁阻塞。
先获取写锁再获取读锁
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantReadWriteLock;
@Slf4j
public class ReentrantReadWriteLockTest2 {
public static void main(String[] args) {
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.writeLock().lock();
log.debug("获取写锁成功");
rwLock.readLock().lock();
log.debug("获取读锁成功");
rwLock.readLock().unlock();
log.debug("释放读锁成功");
// 这里不论是线解锁写还是先解锁读,都可以正常执行结束
rwLock.writeLock().unlock();
log.debug("释放写锁成功");
}
}
先获取写锁,再嵌套获取读锁,可以正常执行完毕。按照我们前面的说法,只要有写操作,就会互斥,但是这里怎么能执行完成呢?
锁降级
导致上面示例可以正常执行完成的机制,称作锁降级。就是写锁降级为了读锁。
- 因为我们在线程内先获取的是写锁,获取成功后,其他线程的读和写锁都互斥,进行阻塞等待该线程是释放锁。
- 当前线程再次获取读锁,因为其他线程不会获取到锁,所以,这里再次获取读锁是可以的(此时的读锁和写锁都是同一个线程,至于什么时候修改数据,什么时候对写操作对读操作可见,就是在该线程中自己定义了,这变成了一个单线程的问题,跟多线程无关)
- 最后释放锁的时候,如果写锁先释放,那么其他线程的读锁就可以访问了,当前线程读写锁都释放后,其他线程的写锁就可以尝试获取锁了。
总结
- ReentrantReadWriteLock 适合线程读多写少的情况(缓存)
- ReentrantReadWriteLock 容易导致写线程的饥饿情况(因为写线程少,在极端情况下可能一直争抢不到 CPU 资源)
StampedLock1.8+ (邮戳锁)
基本使用
public static void main(String[] args) {
// 只有一个无参构造
StampedLock sl = new StampedLock();
// 获取读锁
long stamp = sl.readLock();
try{
log.debug("获取读锁,{}",stamp);
}finally {
// 释放读锁
sl.unlockRead(stamp);
}
// 获取写锁
stamp = sl.writeLock();
try {
log.debug("获取写锁,{}",stamp);
}finally {
// 释放写锁
sl.unlockWrite(stamp);
}
}
示例1
public static void main(String[] args) {
// 只有一个无参构造
StampedLock sl = new StampedLock();
long stamp = sl.readLock();
log.debug("第一次获取读锁,{}",stamp);
stamp = sl.readLock();
log.debug("第二次次获取读锁,{}",stamp);
}
结果:
21:39:57.127 [main] DEBUG com.yyoo.thread.lock.StampedLockTest1 - 第一次获取读锁,257
21:39:57.132 [main] DEBUG com.yyoo.thread.lock.StampedLockTest1 - 第二次次获取读锁,258
此处两次的 stamp 值是不一样的。
示例2
public static void main(String[] args) {
// 只有一个无参构造
StampedLock sl = new StampedLock();
long stamp = sl.readLock();
log.debug("第一次获取读锁,{}",stamp);
sl.unlockRead(stamp);
stamp = sl.readLock();
log.debug("第二次次获取读锁,{}",stamp);
sl.unlockRead(stamp);
}
示例2 ,两次打印 stamp 的值是一样的。说明我们每次 readLock() 上锁时,都会打个戳(邮戳),在解锁时使用相同的戳才能正常解锁。如果解锁时的 stamp 与上锁时不一样,则会抛出 IllegalMonitorStateException 异常。
示例3
public static void main(String[] args) {
// 只有一个无参构造
StampedLock sl = new StampedLock();
long stamp = sl.readLock();
log.debug("第一次获取读锁,{}",stamp);
stamp = sl.writeLock();
log.debug("第二次次获取写锁,{}",stamp);
}
示例3 在获取读锁后,再次获取写锁就阻塞了(这里无论是先获取写锁还是读锁,都会阻塞),其表现和 ReentrantReadWriteLock 的读写锁一致,读读可重复获取(只是戳不一样),读写、写读、写写都互斥。如果和 ReentrantReadWriteLock 一样,那么 JDK1.8 就没必要再提供 StampedLock 了
StampedLock 提供了如下两种方式来实现乐观锁机制
- tryOptimisticRead 尝试获取读戳,通常和 validate 方法一起使用,validate 方法来验证是否同时存在写,如果存在就可以通过 readLock 升级为读锁的方式来互斥
- tryConvertToWriteLock 尝试转换为写戳,其返回 0 表示获取失败,在此之前需要先获取读锁或读戳。只要不是 0 就可以进行写操作,表示没有并发的写操作,如果返回0获取写戳失败,则进行锁升级,将当前的读锁升级为写锁来互斥
- StampedLock 就是对 ReentrantReadWriteLock 的优化,就是通过以上两个方法及其相关方法来实现乐观锁方式的写操作,以提高效率,减少写线程饥饿问题。
这里的描述不是特别清楚,请结合下面的示例来理解
StampedLock 源码文档上的示例
根据源码文档上的示例进行修改的示例
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;
@Slf4j
public class StampedLockTest3 {
public static void main(String[] args) throws InterruptedException {
Point p = new Point();
Random r = new Random();
for (int i = 0; i < 20; i++) {
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(r.nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("{}",p.distanceFromOrigin());
},"read-"+i).start();
}
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
TimeUnit.MILLISECONDS.sleep(r.nextInt(5000));
} catch (InterruptedException e) {
e.printStackTrace();
}
p.moveIfAtOrigin(r.nextInt(100),r.nextInt(100));
},"write-"+i).start();
}
TimeUnit.SECONDS.sleep(10);
}
static class Point{
// x和y轴
private double x, y;
private final StampedLock sl = new StampedLock();
// writeLock 方式(会阻塞)
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;
// 用 validate 方法检查当前戳是否可直接使用(没有写)
if (!sl.validate(stamp)) {
// 直接获取读锁
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
// 如果当前没有写,那么就可以直接计算,否则就得重新独占式的重新读取(if 里面的逻辑)
return currentX * currentX + currentY * currentY;
}
// 乐观锁进行写
void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
// 先获取读锁(示例上提示可以使用 tryOptimisticRead 来获取)
long stamp = sl.readLock();
try {
// 尝试获取写戳
// tryConvertToWriteLock 方法如果返回 0,则表示获取写戳失败
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {// ws != 0 表示写戳获取成功
// 设置 stamp 为写戳
stamp = ws;
// 修改数据
x = newX;
y = newY;
log.debug("使用写戳修改数据");
}
else {
// 获取写戳失败,则释放读锁
sl.unlockRead(stamp);
// 并切换为写锁(也可以说升级为写锁)
stamp = sl.writeLock();
// 修改数据
x = newX;
y = newY;
log.debug("升级为写锁修改数据");
}
} finally {
// 释放锁
sl.unlock(stamp);
}
}
}
}
执行后会发现大多数都是使用的写戳(如果将写操作的线程的sleep 去掉,会有写戳和写锁两种方式出现)
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!