读读场景下不会出现线程安全问题,读写、写写场景下会出现线程安全问题。在没有写操作的时候,多个线程去读不会产生问题,但是当有一个线程想要去写操作时,就不应该有其他读线程和写线程。
读写锁顾名思义就是把一把锁分为读锁和写锁两部分,读锁允许多个线程共同获得,写锁只允许一个线程获得。
💡 问题:读操作为什么要加锁?
在读取数据的方法中如果要那这个数据做一些业务处理,此时没有加锁被其他线程给改变了,就会造成类似于“脏读”的现象。
(一)ReadWriteLock
内部维护了两个API规范,读锁和写锁。
(二)ReentrantReadWriteLock介绍
线程进入读锁的条件:
- 没有其他线程的写锁
线程进入写锁的条件:
- 没有其他线程的写锁
- 没有其他线程的读锁
读写锁有以下三个重要特性:
- 公平性选择:支持非公平锁和公平锁,默认非公平
- 可重入:读锁和写锁都支持线程的重入,以读锁为例:读线程获取到锁后,能够再次获取读锁。写线程在获取写锁以后能够再次获取写锁以及读锁。
- 可降级:遵循获取写锁,再获取读锁,最后再获取写锁的顺序,写锁能够降级为读锁。
总结:
当前线程如果获取到读锁,可以再次获取读锁。
当前线程如果获取到写锁,在写锁释放之前可以获取任意类型的锁。
2.1 内部结构
ReentrantReadWriteLock是可重入的读写搜实现类,内部维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。
2.2 如何使用
2.2.1 读锁的使用方式
注意:
- 读锁不支持条件变量
- 持有读锁的情况下不允许获取写锁(无法锁升级)
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
//读锁线程
new Thread(() -> {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到读锁");
ThreadUtil.sleep(3000);
}finally {
readLock.unlock();
countDownLatch.countDown();
}
},"【读】线程 " + i).start();
}
countDownLatch.await();
}
2.2.2 写锁的使用方式
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
CountDownLatch countDownLatch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
//写锁线程
new Thread(() -> {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到写锁");
ThreadUtil.sleep(3000);
}finally {
writeLock.unlock();
countDownLatch.countDown();
}
},"【写】线程 " + i).start();
}
countDownLatch.await();
}
2.3 锁降级
当前线程持有写锁的同时获取读锁,再把写锁给释放掉就成为锁的降级(又读锁转为写锁)
如果是当前线程持有写锁,又释放写锁后重新获取读锁则称为‘分段锁’
使用场景:
线程对同一个数据修改后立即进行读取,但是中间又不想被其他线程所干扰,就可以使用锁降级的特性。
例:方法中需要修改count变量的值,并且需要立即获取到修改后的值。
2.3.1 错误场景:使用分段锁
private static volatile int count;
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
CountDownLatch countDownLatch = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
//场景1:先写锁再释放再读锁
new Thread(() -> {
writeLock.lock();
try {
count ++;
}finally {
writeLock.unlock();
}
ThreadUtil.sleep(2000);
readLock.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到的值:" + count);
}finally {
readLock.unlock();
}
},"线程 " + i).start();
}
countDownLatch.await();
}
2.3.2 正确场景:使用锁降级
private static volatile int count;
public static void main(String[] args) throws InterruptedException {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
CountDownLatch countDownLatch = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
//场景2:使用锁降级的方式
new Thread(() -> {
writeLock.lock();
try {
count ++;
readLock.lock();
ThreadUtil.sleep(2000);
System.out.println(Thread.currentThread().getName() + "获取到的值:" + count);
}finally {
writeLock.unlock();
readLock.unlock();
}
},"线程 " + i).start();
}
countDownLatch.await();
}
(三)StampedLock介绍
它不是基于AQS的锁,并且是不可重入的、不支持条件变量 Conditon
ReentrantReadWriteLock的读锁我们可以认为是一种悲观锁,因为它在有读锁的时候是不允许其他线程进行获取写锁的。
StampedLock的读锁我们可以认为是一种乐观锁,因为它在读取资源的时候不会加读锁,如果有其他线程进行修改操作会在访问资源后进行一次版本比较,如果版本不同则就是被修改过,此时要么加悲观读要么重试。
3.1 访问模式
- 独占写锁:writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁模式,同一时刻有且只有一个写线程获取锁资源;
- 悲观读锁:writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁模式,同一时刻有且只有一个写线程获取锁资源;
- 乐观读锁:访问共享变量时不会进行加锁,
3.2 使用规范
- 获取当前版本号
- 读取资源
- 比对版本号
-
- 如果相同则没有被修改可以结束
- 如果不相同则资源被修改过要么加入悲观读锁重新获取,要么重试。
StampedLock lock = new StampedLock();
//获取版本号
long version = lock.tryOptimisticRead();
//比对版本号
if (!lock.validate(version)) {
version = lock.readLock();
try {
System.out.println("资源被修改过,实际值:" + count);
}finally {
lock.unlock(version);
}
}