相信很多开发者都遇到过多线程并发访问共享资源的情况,那么为了防止在多线程访问的时候出现线程安全问题,通常我们可以使用加锁的方式来避免线程安全问题,可以使用synchronized关键字以及一些显式锁操作,当然在分布式场景中还可以使用一些分布式锁机制来保证共享资源线程安全访问。
对于共享资源的访问,可以分为两种操作,一种是对共享资源的读操作,一种是对共享资源的写操作。如果有多个线程在同一时刻对共享资源的操作都是读操作的话,虽然是存在资源竞争的情况,但是并不会影响到最终的访问结果,也不会引起数据不一致的情况发生。这样时候,如果还是使用排他锁的方式进行加锁,很明显就有点得不偿失了。如下图所示,共享资源被多个线程共同进行读操作的时候,是不会出现线程安全问题的。
所以,如果在某个场景中,对于共享资源的读操作要明显多于写操作的时候,这个时候,读操作是可以不用进行加锁的。这样对系统的性能的提升也会非常明显。
如图所示,在多个线程对共享资源进行写操作的时候,很明显,一定会造成资源的数据一致性问题出现。所以对于写操作来讲,一定要注意进行加锁的操作。
第一步、定义一个锁接口
无论是对于读锁还是写锁,都离不开加锁和解锁两个操作,所以在接口中需要定义两个基本的操作,加锁和解锁,代码如下。
public interface Lock {
/**
* 进行加锁操作
* @throws InterruptedException
*/
void lock() throws InterruptedException;
/**
* 进行解锁操作
*/
void unlock();
}
第二步、读写锁接口实现
在定义好锁接口之后,接下来就是需要去定义如何来操作锁。既然需要对读写锁进行判断,那么首先就需要有关于锁的判断条件的规范,下面实现的ReadWriteLock其实就是读写锁的使用规范。
public interface ReadWriteLock {
/**
* 创建读锁
* @return
*/
Lock readLock();
/**
* 创建写锁
* @return
*/
Lock writeLock();
/**
* 获取正在执行的写操作个数
* @return
*/
int getDoingWriters();
/**
* 获取正在等待执行的写操作个数
* @return
*/
int getWAItingWriters();
/**
* 获取正在执行的读操作个数
* @return
*/
int getDoingReader();
static ReadWriteLock readWriteLock(){
return new ReadWriteLockImpl();
}
static ReadWriteLock readWriteLock(boolean preferWriter){
return new ReadWriteLockImpl(preferWriter);
}
}
会看到,在上面这个接口中定义了如下的一些操作
为什么要有这些操作呢?其实这样写操作就是来规定如何去使用读写锁的,例如,读操作的个数大于0的时候,这个适合就意味着写操作的个数是等于0的。反之当写操作的个数大于0 的时候,就意味着读操作的个数是等于0的。因为在读写操作的过程中,读操作和写操作是冲突的过程,也就是说在写的时候不能读,在读的时候不能写。那么通过这样的一个数量关系我们就可以实现什么时候加锁什么时候解锁了。
第三步、按照规则实现读写锁。
既然有了上面的规则的定义,那么一定要有规则的实现,下面这段代码其实就是对上面的规则的实现。
public class ReadWriteLockImpl implements ReadWriteLock {
/**
* 定义锁对象
*/
private final Object MUTEX = new Object();
private int doingWriters = 0;
private int waitingWriters = 0;
private int doingReaders = 0;
/**
* 偏好设置
*/
private boolean preferWriter;
public ReadWriteLockImpl() {
this(true);
}
public ReadWriteLockImpl(boolean preferWriter) {
this.preferWriter = preferWriter;
}
@Override
public Lock readLock() {
return new ReadLock(this);
}
@Override
public Lock writeLock() {
return new WriteLock(this);
}
void addDoingWriters(){
this.doingWriters++;
}
void addWaitingWriters(){
this.waitingWriters++;
}
void addDoingReader(){
this.doingReaders++;
}
void minusDoingWriters(){
this.doingWriters--;
}
void minusWaitingWriters(){
this.waitingWriters--;
}
void minusDoingReader(){
this.doingReaders--;
}
@Override
public int getDoingWriters() {
return this.doingWriters;
}
@Override
public int getWaitingWriters() {
return this.waitingWriters;
}
@Override
public int getDoingReader() {
return this.doingReaders;
}
Object getMutex(){
return this.MUTEX;
}
boolean getPreferWriter(){
return this.preferWriter;
}
void changePrefer(boolean preferWriter){
this.preferWriter = preferWriter;
}
}
上述代码中包含了如下的一些内容
其他的操作可以先不做了解,这里重要的部分有两部分内容,第一是对于等待读写操作的增加和减少;第二则是对Object getMutex()操作的立即。
对于读写操作数量的判断主要是用来进行加锁和解锁操作的判断。那么Object getMutex()是用来干什么的?
Object getMutex()操作是用来获取一个锁对象,那么我们真正使用的ReadLock 和 WriterLock又是干嘛的?其实细心的读者可能发现了Object getMutex()锁操作其实是为了保证读写操作内部的一个线程安全,也就是为了保证我们对于加锁条件判断的一个线程安全性的保证,而真正我们通过readLock()方法和writeLock()方法进行的读写操作实现才是读写锁的重点。
根据上面的规则,读锁的条件是当前没有再执行的写操作的时候就可以进行加锁,当前没有再有执行的读操作的时候就可以释放锁。根据规则实现,代码如下。
public class ReadLock implements Lock {
private final ReadWriteLockImpl readWriteLock;
public ReadLock(ReadWriteLockImpl readWriteLock) {
this.readWriteLock = readWriteLock;
}
/***
* 进行加锁操作
*/
@Override
public void lock() throws InterruptedException {
synchronized (readWriteLock.getMutex()){
while (readWriteLock.getDoingWriters()>0
||(readWriteLock.getPreferWriter()
&&readWriteLock.getWaitingWriters()>0)){
readWriteLock.getMutex().wait();
}
readWriteLock.addDoingReader();
}
}
/**
* 进行解锁操作
*/
@Override
public void unlock() {
synchronized (readWriteLock.getMutex()){
readWriteLock.minusDoingReader();
readWriteLock.changePrefer(true);
readWriteLock.getMutex().notifyAll();
}
}
}
写锁实现的条件根据上面的描述可以知道,加锁的条件是当前没有读操作,解锁的条件是当前没有正在执行的写操作。代码实现如下。
public class WriteLock implements Lock {
private final ReadWriteLockImpl readWriteLock;
public WriteLock(ReadWriteLockImpl readWriteLock) {
this.readWriteLock = readWriteLock;
}
/**
* 进行加锁操作
* @throws InterruptedException
*/
@Override
public void lock() throws InterruptedException {
synchronized (readWriteLock.getMutex()){
try{
readWriteLock.addWaitingWriters();
while (readWriteLock.getDoingReader()>0
||readWriteLock.getDoingWriters()>0){
readWriteLock.getMutex().wait();
}
}finally {
this.readWriteLock.minusWaitingWriters();
}
readWriteLock.addDoingWriters();
}
}
/**
* 进行解锁操作
*/
@Override
public void unlock() {
synchronized (readWriteLock.getMutex()){
readWriteLock.minusDoingWriters();
readWriteLock.changePrefer(true);
readWriteLock.getMutex().notifyAll();
}
}
}
根据读写锁分别的实现来看,似乎底层锁定的就是在ReadWriteLockImpl规则中实现的final Object MUTEX = new Object()锁定对象,但是仔细想来,这个对象其实就是为了保证读写锁的安全性,真正能够行决定读写锁的其实是ReadWriteLockImpl对象中定义一些数量规则。通过这些数量规则的判断决定是读操作还是写操作。
另外在JDK并发包
JAVA.util.concurrent.locks中提供了一些读写锁的操作,如下图所示。
通过图中所提供的方法来看,基本的实现思路与上面我们的实现思路是一样。我们采取的是偏向设置,而JDK提供的是公平性相关的内容。而关于公平性相关的内容,这里我们不做过多的介绍,有兴趣的读者可以自己研究一下啊。