读写锁
一、引言
锁(Lock)是java一个很重要的同步组件,Lock提供了跟 synchronized 关键字一样的功能,相比 synchronized 更加灵活,但是实现也更加复杂。
锁的分类:
锁主要分为排他锁和读写锁。
- 排他锁:在同一时刻只允许一个线程进行访问,其他线程等待;
- 读写锁:在同一时刻允许多个读线程访问,但是当写线程访问,所有的写线程和读线程均被阻塞。读写锁维护了一个读锁加一个写锁,通过读写锁分离的模式来保证线程安全,性能高于一般的排他锁。
java并发包提供了读写锁 ReadWriteLock 的具体实现 ReentranReadWriteLock,它主要提供了一下特性:
- 公平性选择:支持公平和非公平(默认)两种获取锁的方式,非公平锁的吞吐量优于公平锁;
- 可重入:支持可重入,读线程在获取读锁之后能够再次获取读锁,写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁;
- 锁降级:线程获取锁的顺序遵循获取写锁,获取读锁,释放写锁,写锁可以降级成为读锁。
二、读写锁的分析和实现
(1)示例:
情景:一个家庭中有张三、爸爸、妈妈、大姐、二姐和弟弟,有一个家庭账号,每个家庭成员都可以存钱取钱(写操作)和查询(读操作)。我们使用程序模拟以上情景:
账户类:
package basis.StuReadWriteSync;
public class HomeCount {
private String oid;
private int cash;
public HomeCount(String oid, int cash) {
this.oid = oid;
this.cash = cash;
}
public HomeCount() { }
public String getOid() {
return oid;
}
public void setOid(String oid) {
this.oid = oid;
}
public int getCash() {
return cash;
}
public void setCash(int cash) {
this.cash = cash;
}
@Override
public String toString() {
return "HomeCount{" +
"oid='" + oid + '\'' +
", cash=" + cash +
'}';
}
}
家庭成员类:
package basis.StuReadWriteSync;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
public class FamilyMember implements Runnable{
private String name;
private HomeCount homeCount;
private int iocash;
private ReadWriteLock mylock;
private boolean ischeck;
public FamilyMember(String name, HomeCount homeCount, int iocash, ReadWriteLock mylock, boolean ischeck) {
this.name = name;
this.homeCount = homeCount;
this.iocash = iocash;
this.mylock = mylock;
this.ischeck = ischeck;
}
@Override
public void run() {
if(ischeck){
mylock.readLock().lock();
try {
TimeUnit.SECONDS.sleep(2);
System.out.println("读:"+name+"正在查询"+homeCount.getOid()+"账户,余额:"+homeCount.getCash());
}catch (Exception e){
e.printStackTrace();
}
finally {
mylock.readLock().unlock();
}
}else{
mylock.writeLock().lock();
try {
//线程休眠两秒
TimeUnit.SECONDS.sleep(2);
System.out.println("写:"+name+"正在操作"+homeCount.getOid()+"账户,金额"+ iocash+",余额:"+homeCount.getCash());
homeCount.setCash(homeCount.getCash()+iocash);
System.out.println("写:"+name+"操作"+homeCount.getOid()+"账户成功,金额"+ iocash+",余额:"+homeCount.getCash());
}catch (InterruptedException e) {
e.printStackTrace();
}
finally {
mylock.writeLock().unlock();
}
}
}
}
测试类:
package basis.StuReadWriteSync;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestHomeCount {
public static void main(String[] args) {
HomeCount homeCount = new HomeCount("001",10000);
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
ExecutorService pool = Executors.newFixedThreadPool(2);
FamilyMember me = new FamilyMember("张三",homeCount,-2000,lock,false);
FamilyMember brother = new FamilyMember("弟弟",homeCount,-3000,lock,false);
FamilyMember father = new FamilyMember("爸爸",homeCount,0,lock,true);
FamilyMember sister_2 = new FamilyMember("二姐",homeCount,0,lock,true);
FamilyMember sister_1 = new FamilyMember("大姐",homeCount,0,lock,true);
FamilyMember mom = new FamilyMember("妈妈",homeCount,3000,lock,false);
pool.execute(me);
pool.execute(brother);
pool.execute(father);
pool.execute(sister_2);
pool.execute(sister_1);
pool.execute(mom);
pool.shutdown();
}
}
测试结果:
写:张三正在操作001账户,金额-2000,余额:10000
写:张三操作001账户成功,金额-2000,余额:8000
写:弟弟正在操作001账户,金额-3000,余额:8000
写:弟弟操作001账户成功,金额-3000,余额:5000
读:二姐正在查询001账户,余额:5000
读:爸爸正在查询001账户,余额:5000
读:大姐正在查询001账户,余额:5000
写:妈妈正在操作001账户,金额3000,余额:5000
写:妈妈操作001账户成功,金额3000,余额:8000
(2)源码分析
ReadWriteLock:
package java.util.concurrent.locks;
/**
* @see ReentrantReadWriteLock
* @see Lock
* @see ReentrantLock
*
* @since 1.5
* @author Doug Lea
*/
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReadWriteLock 接口 在java.util.concurrent.locks包中,该接口只有两个方法,返回值均是Lock 类型的,一个获取读锁,一个获取写锁。
ReentrantReadWriteLock:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
}
ReentrantReadWriteLock 类实现了 ReadWriteLock 接口和Serializable接口。
类中有一个ReadLock 类型的读锁对象( readLock),和一个WriterLock类型的写锁对象(WriteLock)。ReadLock和WriteLock 是 ReentrantReadWriteLock 的内部类,用于提供读锁和写锁的功能。这两个内部类都是 Lock 接口的实现类,同时还实现了Serializable 接口:
ReentrantReadWriteLock.ReadLock:
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
}
ReentrantReadWriteLock.WriteLock:
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
}
ReentrantReadWriteLock类中还有一个 Sync 类型的对象(sync),Sync 是 ReentrantReadWriteLock 中的一个抽象内部类,继承了 AbstractQueuedSynchronizer 接口。
abstract static class Sync extends AbstractQueuedSynchronizer {}
而在锁框架中,AbstractQueuedSynchronizer抽象类可以毫不夸张的说,占据着核心地位,它提供了一个基于FIFO队列,可以用于构建锁或者其他相关同步装置的基础框架。
ReentrantReadWriteLock的构造方法:
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
ReentrantReadWriteLock中有两个构造方法,一个无参的构造器,默认使用非公平锁(NonfairSync),一个接受一个boolean类型的参数,为true时使用公平锁(FairSync),为 false 时使用非公平锁。
NonfairSync和FairSync都是 ReentrantReadWriteLock 类中的内部类,都继承了抽象类 Sync。
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
fairSync和NonfairSync主要区别为:
- 如果当前线程不是锁的占有者,则NonfairSync并不判断是否有等待队列,直接使用compareAndSwap去进行锁的占用;
- 如果当前线程不是锁的占有者,则FairSync则会判断当前是否有等待队列,如果有则将自己加到等待队列尾;