一. 基础
- ReentrantReadWriteLock 可以看为读读共享,读写,写写依然互斥,总结一句话: 读写互斥,读读共享,既一个资源可以被多个读操作或一个写操作访问
-
ReentrantReadWriteLock 降级策略: 首先同一时间内允许多个读,但是读锁过程中,要获取写锁,当前正持有读锁的线程必须释放,否则无法获取,写锁时,允许插入读锁进来读取
-
先总结ReentrantReadWriteLock 的优点: 一. 允许多个读同时执行,在读多写少的需求中效率更高,二.根据该锁的降级策略,在获取写锁后再不释放写锁情况下允许获取读锁,增加数据可见性(也就是获取写锁修改数据成功后,防止写锁不释放,读请求进来不能及时拿到修改完成的数据)
- 使用示例
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockDemo {
//1.声明读写锁
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
//2.共享数据
private String data = "";
//3.读方法
public void read() {
//1.获取读锁
readWriteLock.readLock().lock();
System.out.println("读取数据 data:" + data);
//2.释放读锁
readWriteLock.readLock().unlock();
}
//4.写方法
private void write(String value) {
//1.获取写锁
readWriteLock.writeLock().lock();
this.data = value;
//2.释放写锁
readWriteLock.writeLock().unlock();
}
//运行测试,开启两个线程,一个线程写,一下线程读
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLockDemo demo = new ReentrantReadWriteLockDemo();
new Thread(() -> {
demo.write("aaaa");
}, "t1").start();
TimeUnit.SECONDS.sleep(2);
new Thread(() -> {
demo.read();
}, "t1").start();
TimeUnit.SECONDS.sleep(5);
System.out.println("主线程最终读取数据 data:" + demo.data);
}
}
二. ReentrantReadWriteLock 的锁降级
- 此处的锁降级是指: 将写锁可以降级为读锁,也就是一个线程通过ReentrantReadWriteLock 获取到了写锁,在没有释放写锁的情况下,还可以获取到读锁
- 提供锁降级的原因是: 让线程感知到数据的变化,保证数据可见性
- 示例代码,在下方test方法中先获取写锁,然后再不释放写锁的情况下是可以获取读锁的
public void test(){
//1.声明读写两把锁
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
//2.获取写锁
writeLock.lock();
System.out.println("获取到写锁");
//3.在不释放写锁的情况下,获取读锁
readLock.lock();
System.out.println("获取到读锁");
//4.释放锁
writeLock.unlock();
readLock.unlock();
}
- 总结:由ReentrantReadWriteLock 锁降级中可以理解到该锁除了读读共存以外,在不释放写锁的情况下也可以获取读锁继续执行,写读也是共存,但是在读锁没有释放时,无法获取写锁,所以写写互斥,读写也互斥
三. StampedLock 邮戳票据锁
- 思考一下 ReentrantReadWriteLock 存在的问题:
- 锁饥饿问题,假设当前有99个读请求进来,后续又进来一个写请求,但是需求上要求,只要有修改,马上生效,在使用 ReentrantReadWriteLock,极端情况下可能要等到前面99个读请求执行完毕,这个写请求才会有机会执行,例如一个购物系统,站在商户角度,修改完价格就想马上生效,但是由于高并发前面拍着多个购买的读请求,就会造成这些阻塞购买读请求执行完后,这个修改价格的写请求才能生效
- 虽然 ReentrantReadWriteLock 提供了降级策略,获取写锁,在未释放的情况下允许获取读锁,可以提高数据可见性,但是当先获取读锁,在读锁不能释放的情况下不能获取写锁,必须要等到前面的读锁先释放才行
- 根据上面两个问题出现了StampedLock 邮戳票据锁
- StampedLock 邮戳票据锁是什么: 是java8中根据 ReentrantReadWriteLock 优化升级新增的锁,内部通过一个long类型的stamp标记实现的比ReentrantReadWriteLock 性能更高的锁(stamp表示锁的状态,为0时表示线程获取锁失败,并且释放转换锁类型时需要传递最初获取的stamp的值)
- 先说一下 StampedLock 邮戳票据锁的特点:
- 在所有调用获取锁方法都会返回一个戳记stamp,如果为0表示获取锁失败,其它表示获取锁成功
- 在所有调用释放锁方法都需要传入一个戳记stamp,并且传入的这个戳记要与获取锁时的戳记保持一致
- 该锁是不可重入锁,如果写锁已经被获取,再去获取写锁会出现死锁问题
- StampedLock 支持三种访问模式
- Reading 读模式: 功能与 ReentrantReadWriteLock 类似获取一个读锁
- Writing 写模式: 功能与 ReentrantReadWriteLock 类似获取一个写锁
- Optimistic reading 乐观读模式: 无锁机制,类似于数据库的乐观锁,支持读写并发,根据stamp判断,如果发现被修改回升级为悲观读
- 代码示例(重点关注乐观读,通过stamp验证乐观读锁获取未释放前中间是否有写锁被获取,解决了ReentrantReadWriteLock 中读锁必须被释放才能获取读锁的问题,读写并不会阻塞解决了线饥饿问题,读请求可以马上感知到)
import java.util.concurrent.locks.StampedLock;
public class StampedLockDemo {
//1.声明StampedLock锁
private StampedLock stampedLock = new StampedLock();
//2.共享数据
private String data = "";
//写方法
public void write(String value) {
//1.获取写锁.返回 stamp 值,如果为0说明获取失败
long stamp = stampedLock.writeLock();
try {
//2.执行业务逻辑
this.data = value;
} catch (Exception e) {
} finally {
//3.释放写锁,需要传入前面获取锁时返回的stamp戳记,
stampedLock.unlockWrite(stamp);
}
}
//3.读方法,StampedLock中通过 readLock()获取的读锁与 ReentrantReadWriteLock
//相同,在当前读锁未释放情况下不可获取写锁
public void read() {
//1.获取读锁,返回stamp值,如果为0说明获取锁失败
long stamp = stampedLock.readLock();
System.out.println(this.data);
//2.释放读锁,需要传入获取锁时返回的stamp
stampedLock.unlockRead(stamp);
}
//4.乐观读,通过tryOptimisticRead()获取到读锁时,如果当前读锁未释放
//允许获取写锁,可能出现数据不安全问题,可以通过stamp作为版本号验证
//当前读时是否有写锁被获取,如果有被获取防止数据被篡改拿到脏数据
//在重新读一次数据
public void tryOptimisticRead() {
//1.获取乐观读锁,并返回戳记
long stamp = stampedLock.tryOptimisticRead();
System.out.println("第一次读取数据 data:" + data);
//2.通过stamp验证是否有写锁被获取,防止写请求进来修改数据,没有返回true
boolean flag = stampedLock.validate(stamp);
if (!flag) {
//3.当使用乐观读锁,获取读锁未释放时,中间有
//写锁被获取,当前乐观读锁要切换到普通读锁模式
//重新获取一个普通读锁,并返回新的戳记stamp
stamp = stampedLock.readLock();
System.out.println("有写锁被获取,重新读取 data:" + data);
//4.释放锁(乐观读锁不需要释放?)
stampedLock.unlockRead(stamp);
}
}
//运行测试
public static void main(String[] args) {
StampedLockDemo demo = new StampedLockDemo();
//1.写数据
demo.write("aaaaa");
//2.读数据
demo.read();
//3.乐观读
demo.tryOptimisticRead();
}
}
- StampedLock邮戳标记锁的缺点: (如果感觉处理不好不推荐使用)
- 不支持重入,假设一个线程第一次获取到锁,再次执行该方法,会造成死锁
- 普通读锁与普通写锁模式不支持条件变量
- 使用该锁时一定不能中断线程操作,例如执行interrupt(),会出现异常