前言
在没有读写锁之前,ReentrantLock
和 Synchronized
虽然可以保证线程安全,但是也浪费了一定的资源
- 因为如果多个读操作同时进行,其实并没有线程安全问题,我们可以允许让多个读操作并行,以便提高程序效率
- 但是写操作不是线程安全的,如果多个线程同时写,或者在写的同时进行读操作,便会造成线程安全问题
我们的读写锁就解决了这样的问题,它设定了一套规则,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。整体思路是它有两把锁:第 1
把锁是写锁,获得写锁之后,既可以读数据又可以修改数据,而第 2
把锁是读锁,获得读锁之后,只能查看数据,不能修改数据。读锁可以被多个线程同时持有,所以多个线程可以同时查看数据。在读的地方合理使用读锁,在写的地方合理使用写锁,灵活控制,可以提高程序的执行效率
ReadWriteLock
接口简介及源码
ReadWriteLock
是 java.util.concurrent.locks
包下的接口。ReadWriteLock
管理一组锁,一个是读锁,一个是写锁。适用于读多写少的并发情况
- 读锁是共享的
- 写锁是独占的
- 理论上,读写锁比互斥锁(
Synchronized
和ReentrantLock
)有更好的性能体现的 - 读写锁更适用于读多写少的情景
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
所有 ReadWriteLock
接口的实现类必须保证内存同步效果:所有写锁 writeLock
的相关操作都对只读锁 readLock
可见。也就是说,如果一个线程成功的获取了只读锁 readLock
,那么这个线程可以看到上个写锁writeLock
所做的所有修改
ReetrantReadWriteLock
类
ReentrantReadWriteLock
类是 ReadWriteLock
接口的实现。该类具有以下特性:
获取顺序
这个类不会强行指定访问锁的读写顺序,但是它支持一个可选的公平策略:
非公平模式(默认)
当以非公平模式初始化时,读锁和写锁的获取的顺序是不确定的。非公平锁主张竞争获取,可能会延缓一个或多个读或写线程,但是会比公平锁有更高的吞吐量
公平模式
- 当以公平模式初始化时,线程将会以队列的顺序获取锁。当前线程释放锁后,等待时间最长的写锁线程就会被分配写锁;或者有一组读线程组等待时间比写线程长,那么这组读线程组将会被分配读锁
- 当有写线程持有写锁或者有等待的写线程时,一个尝试获取公平的读锁(非重入)的线程就会阻塞。这个线程直到等待时间最长的写锁获得锁后并释放掉锁后才能获取到读锁
可重入性
ReentrantReadWriteLock
类定义的锁,允许读线程和写线程以 ReentrantLock
的形式去获取读锁和写锁。直到所有持有锁的写线程释放锁,不可重入的读线程才会被允许获取锁
此外,写线程可以获取读锁,反过来,读线程不可以获取写锁
可降级性
可重入性也允许写锁降级成为读锁:首先获取写锁,然后获取读锁,然后释放写锁。但是,从读锁升级为写锁是不可能的
支持 Condition
就像 ReentrantLock
一样,写锁支持 Condition
操作。当然,这种 Condition
操作,只能被应用在写锁上。读锁不支持 Condition
操作,readLock().newCondition()
会抛出一个 UnsupportedOperationException
异常
ReentrantReadWriteLock
类的源码
ReentrantReadWriteLock
的构造器
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
}
可以看到,默认的构造方法使用的是非公平模式,创建的 Sync
是 NonfairSync
对象,然后初始化读锁和写锁。一旦初始化后,ReadWriteLock
接口中的两个方法就有返回值了,如下:
public ReentrantReadWriteLock.WriteLock writeLock() {
return writerLock;
}
public ReentrantReadWriteLock.ReadLock readLock() {
return readerLock;
}
ReentrantReadWriteLock
获取与释放锁
读锁的获取
当需要使用读锁时,首先调用 lock
方法,如下:
public static class ReadLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquireShared(1);
}
}
读锁的释放
public void unlock() {
sync.releaseShared(1);
}
写锁的获取
当需要使用写锁时,首先调用 lock
方法,如下:
public static class WriteLock implements Lock, java.io.Serializable {
public void lock() {
sync.acquire(1);
}
}
写锁的释放
public void unlock() {
sync.release(1);
}
读写锁的获取规则
我们在使用读写锁时遵守下面的获取规则:
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请读锁,可以申请成功
- 如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,因为读写不能同时操作
- 如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,都必须等待之前的线程释放写锁,同样也因为读写不能同时,并且两个线程不应该同时写
一句话总结:要么是一个或多个线程同时有读锁,要么是一个线程有写锁,但是两者不会同时出现
也可以总结为:读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
ReentrantReadWriteLock
总结
获取琐时
- 如果当前没有写锁或读锁时,第一个获取锁的线程都会成功,无论该锁是写锁还是读锁
- 如果当前已经有了读锁,那么这时获取写锁将失败,获取读锁有可能成功也有可能失败
- 如果当前已经有了写锁,那么这时获取读锁或写锁,如果线程相同(可重入),那么成功;否则失败
释放琐时
- 如果当前是写锁被占有了,只有当写锁的数据降为
0
时才认为释放成功;否则失败。因为只要有写锁,那么除了占有写锁的那个线程,其他线程即不可以获得读锁,也不能获得写锁 - 如果当前是读锁被占有了,那么只有在写锁的个数为
0
时才认为释放成功。因为一旦有写锁,别的任何线程都不应该再获得读锁了,除了获得写锁的那个线程