ReentrantLock概述
ReentrantLock(重入锁),顾名思义是可以重新进入的锁,也就是说当一个线程获取了这个锁之后还能在释放锁之前再次获取这个锁,同时还支持公平所和非公平锁的选择。
ReentrantLock的重入
重入例子
synchronized就很好的支持了重入:
例:
public class ReentrantLockDemo {
public static void main(String[] args) {
new Thread(new Reentrant()).start();
}
static class Reentrant implements Runnable {
@Override
public void run() {
System.out.println(sum(1));
}
public synchronized int sum(int start) {
start = start + 1;
if(start < 100)
return sum(start);
return start;
}
}
}
上面的代码用递归的方式做了一个累加的操作,当每次递归的时候都将重新获得当前的class对象的锁(ReentrantLockDemo.class)。
同样对于ReentantLock来说也实现了类似的功能,很遗憾不能像synchronized一样隐式的重进入,因为需要程序员自己写锁的获取和释放的操作。
例:
public class ReentrantLockDemo {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
new Thread(new Reentrant(), "Thread1").start();
new Thread(new Reentrant(), "Thread2").start();
}
static class Reentrant implements Runnable {
@Override
public void run() {
System.out.println(sum());
}
public int sum() {
lock.lock();
lock.lock();
System.out.println(Thread.currentThread().getName() + " run");
lock.unlock();
//lock.unlock();
return 1;
}
}
}
上面的代码开启了两个线程Thread1和Thread2,但是线程中连续两次获取了对象的锁,但是只释放了一次。不难想象,这个demo运行之后只能有一个线程输出,而另一个线程将会被阻塞,因为第一个获取锁的线程没有把占用的锁全部释放,用AQS中的方法来说的话就是连续调用了两个aquire(1),但是却只调用了一次release(1),资源没有全部释放。实际运行结果:
输出:
Thread2 run
1
并且JVM虚拟机并没有退出,还在继续执行。
重入锁的实现分析
根据AQS的原理可以知道,获取锁的行为链中一个很关键的需要子类去实现的方法就是tryAquire方法(以独占式获取为例,实际上ReentrantLock也只实现了独占式获取同步状态)。由于ReentrantLock的AQS实现分为公平锁和非公平锁,这里以非公平锁为例(常用非公平锁)。
首先在ReentrantLock里面有一个内部类Syn继承了AbstractQueueSynchronizer类,同时这个Syn类只实现了部分方法,同时新增了一个lock方法用于子类公平锁和非公平锁去实现:
bstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
//.....
}
再看看非公平锁的实现:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
公平锁的实现:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
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;
}
}
思路很明确,父类Syn实现部分公共的方法,并新增了一个lock方法供外部调用,同时公平锁和非公平锁各自使用各自的tryAquire的实现。
那么实际获取锁的调用顺序则是:
- 外部调用lock()
- 调用AQS的aquire(1)
- AQS的aquire方法调用子类实现的tryAquire
- 根据实现不同调用nonfairTryAcquire或者公平锁直接实现的tryAquire
看非公平锁的获取同步状态实现:
final void lock() {
//通过CAS尝试直接获取同步状态(一般并发环境下锁的竞争激烈这里失败的可能性很高)
//失败后进入acquire(1)方法
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//AQS的aquire方法会调用tryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取到当前的state(同步资源)
int c = getState();
//如果state == 0说明还没有任何线程获取到同步状态,那么直接尝试CAS获取同步状态
//成功则设置当前线程为同步队列的头节点,并返回true
//失败则return false
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果state大于0 说明已经有线程获取到了同步状态,但是由于要实现重入,所以判断是否为当前线程获取到的同步状态
//如果是当前线程获取到了同步状态,那么将state更新(一般就是加1)并返回true
//失败返回false
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
可以看出这里实现重入效果的思路是:如果已经有线程获取到了同步状态那么先判断是否为当前线程,如果是则继续获取同步状态,不是则返回false、相比一般的互斥锁来看,互斥索一般只会单纯的判断c==0,所以当拥有互斥锁的线程重入的时候就会由于这里的tryAquire的失败而进入同步队列等待。
锁的释放:
protected final boolean tryRelease(int releases) {
//获取当前的state并减去需要release的资源
int c = getState() - releases;
//如果当前线程不是已经获取到同步状态的线程则抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//是否完全释放的标志
//只有当同步状态被完全释放也就是等于0的时候free才会等于true
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//修改同步状态
setState(c);
return free;
}
这里可以看出,锁的释放是要等到state等于0的时候才会返回true,就是说当一个线程重入了n次,那么在释放n-1的时候都会返回false,直到第n次释放同步状态之后state变成了0 然后返回true成功释放然后去唤醒等待队列中的线程。
重入锁的公平锁和非公平锁的实现与区别
- 公平锁与非公平锁:在绝对时间上,先对锁获取的请求一定先被满足,那么这个锁是公平的。反之是不公平的。
也就是说等待时间最长的线程应该最优先获取到锁,公平锁能够减少“饥饿”发生的概率,但是事实上一般非公平锁的效率要高一些。因为在非公平锁的实现中的逻辑是当一个线程如果直接能获取到同步状态(设置state成功)那么就获取到了锁,这就意味着如果当前已经获取锁的线程在释放锁之后立马又去请求获取锁,那么也有可能获取到同步状态,这样其他线程就只能继续等待。而实际中这种情况也是很多的(比如循环中有获取锁的操作),但是非公平锁的好处在于避免了很多线程之间的切换,减少了开销。
看看公平锁的实现:
final void lock() {
//直接请求获取同步资源
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
//获取同步状态前先检查是否有前驱节点
//有:说明有别的线程已经获取了同步状态,那么不去获取同步状态直接返回false
//没有:说明自己可以去获取同步状态,获取到就返回true
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;
}
可以看到非公平锁的实现和公平锁的实现的区别在于,非公平锁是请求一来就先尝试获取同步状态,如果失败那么再进入同步队列,而公平锁则是请求来了之后先判断是否有前驱节点,有的话说明有别的线程正在排队就不去获取,没有那就直接获取。
公平锁和非公平锁的使用是通过构造器传入一个布尔类型来决定:
//true为公平锁,false为非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//默认为非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
ReentrantLock中还有许多好用的方法,包括lockInterruptibly等,这里做过多赘述,在理解了AQS原理的情况下,其他的都很容易理解了。
同时很实用的方法还有
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
其中tryLock顾名思义尝试获取锁,成功则返回true,失败返回false,同时还可以指定超时的tryLock,这两个方法的实现原理都很简单,但是却很富有实用的价值,因为这意味着我们不需要一直阻塞到获取到锁为止可以有更多的策略。