在共享资源同步器AQS详解中讲了AQS的底层实现原理,现在来看一下它的具体运用,我们知道AQS定义了几个空的抛异常的方法让用户自定义实现。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
这里面其实用到了模板方法设计模式,公共逻辑在AQS中封装好了,那些个具体要用AQS的类只要重写上面的方法即可,我们来看一下JUC中运用了AQS的类:
首先来看一下ReentrantLock的运用,我们要保证一个成员变量的线程安全,可以使用synchronized关键字实现:
private int count =0;
public synchronized int calculateCount(){
return count++;
}
我们也可以使用ReentrantLock来实现线程安全:
private int count =0;
private ReentrantLock lock = new ReentrantLock();
public int calculateCount(){
try{
lock.lock();
return count++;
}finally {
lock.unlock();
}
}
接下来我们深入ReentrantLock的源代码,一起来看看它是怎么结合AQS来保证线程安全的,首先从构造方法入手,ReentrantLock有一个同步的类Sync,提供两个构造方法,一个是默认的构造方法,一个是需要传入一个bool类型的参数,这个参数是表示锁的公平与否,true代表公平锁,false代表非公平锁,默认的构造方法提供的是非公平锁
private final Sync sync;
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock中的提供了一个同步的内部类:Sync,它继承自AQS,NonfairSync和FairSync都是基于Sync来实现的,它里面提供了对非公平锁上锁的实现,即非公平锁对于AQS中tryAcquire方法的实现,也提供了释放锁的实现,即AQS中tryRelease方法的实现,具体的实现就是对AQS中的state变量进行操作,设置独占线程,有了AQS这个同步框架,会发现我们对于自定义锁的实现不是一般的简单:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
// 提供一个抽象的lock方法,供NonfairSync和FairSync实现
abstract void lock();
// 父类Sync提供非公平锁对于tryAcquire方法实现
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取AQS中的state的值
int c = getState();
// 没有线程上锁,通过自旋操作将state加1,并设置该线程为当前独占线程
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 有线程上锁,判断当前独占线程是否为自己,这里的判断主要是让当前线程可以获取到可重入锁,就是已经加锁的线程可以重复获取锁
else if (current == getExclusiveOwnerThread()) {
// 将state继续加1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 设置AQS中state的值
setState(nextc);
return true;
}
return false;
}
// 提供AQS中tryRelease方法的实现
protected final boolean tryRelease(int releases) {
// 每次释放掉锁,将当前AQS中的state的值减1
int c = getState() - releases;
// 如果当前的独占线程不是自己,那调用释放锁的方法会抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 判断当前线程是否已完全释放锁(有可能线程重复获取锁)
if (c == 0) {
free = true;
// 完全释放锁,将独占线程设置为空
setExclusiveOwnerThread(null);
}
// 设置AQS中的state为最新的值
setState(c);
return free;
}
protected final boolean isHeldExclusively() {
// 该方法保证获取独占锁的线程为当前线程
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
定义好了父类的Sync方法,我们首先来看非公平锁NonefairSync的实现,那就是很简单的几行代码搞定:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 首先通过CAS自旋操作将state的值设置为1,调用AQS中的compareAndSetState方法
if (compareAndSetState(0, 1))
// 设置成功将当前线程设置为独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
// CAS自旋失败,就调用AQS中的acquire方法入等待队列
acquire(1);
}
// 实现AQS中的tryAcquire方法,在Sync父类中具体实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
我们看公平锁FairSync对于Sync的实现:
static final class FairSync extends Sync {
// 直接调用AQS的acquire方法
final void lock() {
// 相比非公平锁,没有compareAndSetState操作
acquire(1);
}
// 实现AQS中的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取当前state的值
int c = getState();
// 如果此时没有线程加锁,则判断等待队列中是否有等待的线程
// 如果没有等待的线程,则自旋将state加1,成功则设置独占线程为当前线程
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果当前线程就是独占线程,则可获取可重入锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
我们会发现,公平锁和非公平锁的区别就是非公平锁在判断state=0的时候会通过CAS操作尝试去加一次锁,加锁失败再去调用AQS中的acquire方法,相当于先插一次队,插队不成功,再乖乖去后面排队,而公平锁则不会,因为它需要看等待队列里面有没有线程已经在排队了,所以会直接调用AQS中的acquire方法,流程图如下:
到这里,是不是感觉有了AQS这么个玩意,我们自定义实现锁的操作很easy,在获取锁的时候就是获取当前state,给state自增操作,然后通过CAS自旋设置state,拿不到就去排队,而且入队和出队的实现AQS都帮你干好了,释放锁的时候就是获取当前state,给state自减操作,然后通过CAS设置state,当state为0的时候,表示已经完全是否掉锁了,所以我们在使用ReentrantLock的时候,在调用lock后,一定要在finally中调用unlock操作,如果不这样,那么state状态无法一致,就会引起很多问题,所以这个一定要注意:调用lock方法一定要调用unlock方法,成对出现
我们在使用ReentrantLock的时候,会发现它里面还提供两个方法,一个是tryLock,一个是带参数的tryLock方法:
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
其实上面两个方法的底层实现和lock方法差不多,但是也有区别,还是用刚开始的例子,当我们在调用ReentrantLock中的lock方法而没有调用unlock方法的时候,会发现后面的线程会一直阻塞,所以lock方法是属于阻塞方法,我们调用的tryLock方法是一个有返回值的方法,当调用tryLock()方法的时候,它会获取锁,没有获取到锁,就返回false,获取到返回true,而tryLock(long,TimeUnit)方法表示在规定时间内阻塞,如果规定的时间内获取到锁,就返回true,否则返回false,注意:如果在当前线程中以将中断表示位设为true,例如Thread.currentThread().interrupt();执行lock.lock(long time,TimeUnit unit)就会报java.lang.InterruptedException异常
private int count =0;
private ReentrantLock lock = new ReentrantLock();
public int calculateCount(){
try{
lock.lock();
return count++;
}finally {
//lock.unlock();
}
}
借用别人的写的,下面对lock和tryLock方法做一个对比:
lock()
当锁可用,并且当前线程没有持有该锁,直接获取锁并把state set为1.
当锁可用,并且当前线程已经持有该锁,直接获取锁并把state增加1.
当锁不可用,那么当前线程被阻塞,休眠一直到该锁可以获取,然后把持有state设置为1.
tryLock()
当获取锁时,只有当该锁资源没有被其他线程持有才可以获取到,并且返回true,同时设置持有state为1;
当获取锁时,当前线程已持有该锁,那么锁可用时,返回true,同时设置持有state加1;
当获取锁时,如果其他线程持有该锁,无可用锁资源,直接返回false,这时候线程不用阻塞等待,可以先去做其他事情;
即使该锁是公平锁fairLock,使用tryLock()的方式获取锁也会是非公平的方式,只要获取锁时该锁可用那么就会直接获取并返回true。这种直接插入的特性在一些特定场景是很有用的。但是如果就是想使用公平的方式的话,可以试一试tryLock(0, TimeUnit.SECONDS),几乎跟公平锁没区别,只是会监测中断事件。
tryLock(long timeout, TimeUnit unit)
从上面代码中可以看出,获取锁成功或者超时之后返回。而且在公平锁和非公平锁的场景下都可以使用,只是会增加对中断事件的监测。
当获取锁时,锁资源在超时时间之内变为可用,并且在等待时没有被中断,那么当前线程成功获取锁,返回true,同时当前线程持有锁的state设置为1.
当获取锁时,在超时时间之内没有锁资源可用,那么当前线程获取失败,不再继续等待,返回false.
当获取锁时,在超时等待时间之内,被中断了,那么抛出InterruptedException,不再继续等待.
当获取锁时,在超时时间之内锁可用,并且当前线程之前已持有该锁,那么成功获取锁,同时持有state加1.