1.场景描述
对资源的访问一般包括两种类型的动作——读和写(更新、删除、增加等资源会发生变化的动作),如果多个线程在某个时刻都在进行资源的读操作,虽然有资源的竞争,但是这种竞争不足以引起数据不一致的情况发生,那么这个时候直接采用排他的方式加锁,就显得有些简单粗暴了。表1将两个线程对资源的访问动作进行了枚举,除了多线程在同一时间都进行读操作时不会引起冲突之外,其余的情况都会导致访问的冲突,需要对资源进行同步处理。
2.读写分离程序设计
2.1 接口定义
读写锁的类图
1.Lock接口定义
public interface Lock {
// 获取显示锁, 没有获得锁的线程将被堵塞
void lock() throws InterruptedException;
// 释放获取的锁
void unlock();
}
Lock接口定义了锁的基本操作, 加锁和解锁, 显式锁的操作强烈建议与try finally语句块一起使用,加锁和解锁说明如下。
- lock() :当前线程尝试获得锁的拥有权, 在此期间有可能进入阻塞。
- unlock() :释放锁, 其主要目的就是为了减少reader或者writer的数量。
2.ReadWriteLock接口定义
ReadWrite Lock虽然名字中有lock, 但是它并不是lock, 它主要是用于创建read lock和write lock的, 并且提供了查询功能用于查询当前有多少个reader和writer以及waiting中的writer, 根据我们在前文中的分析, 如果reader的个数大于0, 那就意味着writer的个数等于0, 反之writer的个数大于0(事实上writer最多只能为1) , 则reader的个数等于0,由于读和写,写和写之间都存在着冲突,因此这样的数字关系也就不奇怪了。
readLock() :该方法主要用来获得一个Read Lock。
writeLock() :同read Lock类似, 该方法用来获得Write Lock。
getWriting Writers) :获取当前有多少个线程正在进行写的操作, 最多是1个。
getWaiting Writers() :获取当前有多少个线程由于获得写锁而导致阻塞。
getReading Readers() :获取当前有多少个线程正在进行读的操作。
2.2程序实现
1.ReadWriteLockImpl
相对于Lock, ReadWrite Lock Impl更像是一个工厂类, 可以通过它创建不同类型的锁,我们将ReadWrite Lock Impl设计为包可见的类, 其主要目的是不想对外暴露更多的细节,在ReadWrite Lock Impl中还定义了非常多的包可见方法, 代码所示
public class ReadWriteLockImpl implements ReadWriteLock{
// 定义对象锁
private final Object MUTEX = new Object();
// 当前有多少个线程正在写入
private int writingWriters = 0;
// 当前有多少个线程正在等待写入
private int waitingWriters = 0;
// 当前有多少个线程正在read
private int readingReaders = 0;
// read 和 write 的偏好设置
private boolean preferWriter;
// 默认情况下preferWrite为true
public ReadWriteLockImpl() {
this(true);
}
// 构造ReadWriteLockImpl并且传入preferWriter
public ReadWriteLockImpl(boolean preferWriter) {
this.preferWriter = preferWriter;
}
public Object getMUTEX() {
return MUTEX;
}
public int getWaitingWriters() {
return waitingWriters;
}
public boolean getPreferWriter() {
return preferWriter;
}
// 创建读锁
@Override
public Lock readLock() {
return new ReadLock(this);
}
// 创建写锁
@Override
public Lock writeLock() {
return new WriteLock(this);
}
// 使写线程的数量增加
void incrementWritingWriters() {
this.writingWriters++;
}
// 使等待写入的线程数量增加
void incrementWaitingWriters() {
this.waitingWriters++;
}
// 使读线程的数量增加
void incrementReadingReaders() {
this.readingReaders++;
}
// 使写线程的数量减少
void decrementWritingWriters() {
this.writingWriters--;
}
// 使等待获取写入锁的数量减一
void decrementWaitingWriters() {
this.waitingWriters--;
}
// 使读取线程的数量减少
void descementReadingReaders() {
this.readingReaders--;
}
@Override
public int getWritingWriters() {
return this.writingWriters;
}
@Override
public int getReadingReaders() {
return this.readingReaders;
}
void changePrefer(boolean preferWriter) {
this.preferWriter = preferWriter;
}
}
虽然我们在开发一个读写锁,但是在实现的内部也需要一个锁进行数据同步以及线程之间的通信, 其中MUTEX的作用就在于此, 而prefer Writer的作用在于控制倾向性, 一般来说读写锁非常适用于读多写少的场景, 如果prefer Writer为false, 很多读线程都在读数据,那么写线程将会很难得到写的机会。
2.ReadLock
读锁是Lock的实现, 同样将其设计成包可见以透明其实现细节, 让使用者只用专注于对接口的调用,代码如所示
public class ReadLock implements Lock{
private final ReadWriteLockImpl readWriteLock;
ReadLock(ReadWriteLockImpl readWriteLock) {
this.readWriteLock = readWriteLock;
}
@Override
public void lock() throws InterruptedException {
// 使用Mutex 作为 锁
synchronized (readWriteLock.getMUTEX()) {
// 若此时有线程在进行写操作,或者有写线程在等待并且偏向写锁的标识为
// true时,就会无法获得读锁,只能被挂起
while(readWriteLock.getWritingWriters() > 0
|| (readWriteLock.getPreferWriter() && readWriteLock.getWritingWriters() > 0 )) {
readWriteLock.getMUTEX().wait();
}
readWriteLock.incrementReadingReaders();
}
}
@Override
public void unlock() {
// 使用Mutex作为锁,并且进行同步
synchronized (readWriteLock.getMUTEX()) {
// 释放锁的过程就是使得当前reading的数量减一
// 将perferWriter设置为true,可以使得writer线程获得更多的机会
// 通知唤醒与Mutex关联monitor waitset中的线程
readWriteLock.descementReadingReaders();
readWriteLock.changePrefer(true);
readWriteLock.getMUTEX().notifyAll();
}
}
}
- 当没有任何线程对数据进行写操作的时候,读线程才有可能获得锁的拥有权,当然除此之外,为了公平起见,如果当前有很多线程正在等待获得写锁的拥有权,同样读线程将会进入Mutex的wait set中, reading Reader的数量将增加。
- 读线程释放锁, 这意味着reader的数量将减少一个, 同时唤醒wait中的线程, reader唤醒的基本上都是由于获取写锁而进入阻塞的线程,为了提高写锁获得锁的机会,需要将prefer Writer修改为true
3.WriteLock
写锁是Lock的实现, 同样将其设计成包可见以透明其实现细节, 让使用者只用专注于对接口的调用,由于写-写冲突的存在,同一时间只能由一个线程获得锁的拥有权,代码所示。
public class WriteLock implements Lock{
private final ReadWriteLockImpl readWriteLock;
public WriteLock(ReadWriteLockImpl readWriteLock) {
this.readWriteLock = readWriteLock;
}
@Override
public void lock() throws InterruptedException {
synchronized (readWriteLock.getMUTEX()) {
try {
// 首先使等待获取写入锁的数字加一
readWriteLock.incrementWritingWriters();
// 如果此时有其他线程正在进行读操作,或者写操作,那么当前线程将被挂起
while(readWriteLock.getReadingReaders() > 0
|| readWriteLock.getWritingWriters() > 0) {
readWriteLock.getMUTEX().wait();
}
} finally {
// 成功获取到了写入锁,使得等待获取写入锁的计数减一
this.readWriteLock.decrementWaitingWriters();
}
// 将正在写入的线程数量加一
readWriteLock.incrementWritingWriters();
}
}
@Override
public void unlock() {
synchronized (readWriteLock.getMUTEX() ) {
// 减少正在写入锁的线程计数器
readWriteLock.decrementWritingWriters();
// 将偏好状态修改为false, 可以使得读锁被最快速的获得
readWriteLock.changePrefer(false);
// 通知唤醒其他在Mutex monitor waitset中的线程
readWriteLock.getMUTEX().notifyAll();
}
}
}
- 当有线程在进行读操作或者写操作的时候,若当前线程试图获得锁,则其将会进入MUTEX的wait set中而阻塞, 同时增加waiting Writer和writing Writer的数量, 但是当线程从wait set中被激活的时候waiting Writer将很快被减少。
- 写释放锁, 意味着writer的数量减少, 事实上变成了0, 同时唤醒wait中的线程,并将prefer Writer修改为false, 以提高读线程获得锁的机会。
3.读写锁的使用
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ShareData {
// 定义共享数据(资源)
private final List<Character> container = new ArrayList<>();
//构造ReadWriteLock
private final ReadWriteLock readWriteLock = ReadWriteLock.readWriteLock();
// 创建读取锁
private final Lock readLock = readWriteLock.readLock();
// 创建写入锁
private final Lock writeLock = readWriteLock.writeLock();
private final int length;
public ShareData(int length) {
this.length = length;
for(int i = 0; i < length; i++) {
container.add(i, 'c');
}
}
public char[] read() throws InterruptedException {
try {
// 首先使用读锁进行lock
readLock.lock();
char[] newBuffer = new char[length];
for(int i = 0; i < length; i++) {
newBuffer[i] = container.get(i);
}
slowly();
return newBuffer;
} finally {
// 当操作结束之后,将锁释放
readLock.unlock();
}
}
public void write(char c) throws InterruptedException {
try {
//使用写锁进行lock
writeLock.lock();
for(int i = 0; i < length; i++ ) {
this.container.add(i, c);
}
slowly();
}finally {
// 当所有的操作都完成之后,对写锁进行释放
writeLock.unlock();
}
}
// 简单模拟操作的耗时
private void slowly() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ShareData中涉及了对数据的读写操作, 因此它是需要进行线程同步控制的。首先, 创建一个ReadWrite Lock工厂类, 然后用该工厂分别创建ReadLock和WriteLock的实例, 在read方法中使用Read Lock对其进行加锁, 而在write方法中则使用WriteLock, 的程序则是关于对ShareData的使用。
public class ReadWriteLockTest {
//// this is the example for read write lock
private final static String text = "this";
public static void main(String[] args) {
// 定义共享数据
final ShareData shareData = new ShareData(50);
// 创建两个线程进行数据写操作
for(int i = 0; i < 2; i++) {
new Thread(
() -> {
for(int index = 0; index < text.length(); index++ ) {
try {
char c= text.charAt(index);
shareData.write(c);
System.out.println(Thread.currentThread() + " write " + c);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
).start();
// 创建10个线程进行数据读操作
for(int i1 = 0; i1 < 10; i1++ ) {
new Thread( ()-> {
while(true) {
try {
System.out.println(Thread.currentThread() + " read " + new String(shareData.read()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
}