ReentrantLock继承了AbstractQueuedSynchronizer(简称AQS),AQS用到了模板模式这种设计模式,所以阅读AQS和ReentrantLock的源码时,最好对模板设计模式有一定了解,这里我就讲AQS了。
ReentrantLock是除了synchronized用得较多的一种锁。ReentrantLock也属于重入锁,后面接着就会提到它的重入锁实现原理。
ReentrantLock的功能要比内部锁synchronized更多,如指定锁等待时间的方法tryLock(long time,TimeUnit unit)、中断锁的方法lockInterruptibly()、没获取锁直接返回的方法tryLock()。
所以,什么时候选择ReentrantLock呢?
一般是synchronized 不满足需求时,才选择ReentrantLock,比如实现立即返回、可中断、conditon机制时
方法 | 说明 |
---|---|
lock() | 获取锁。如果锁已经被占用,则等待 |
tryLock() | 尝试获取锁,拿到锁返回true,没拿到返回false,并立即返回 |
tryLock(long time, TimeUnit unit) | 在指定时间内会等待获取锁,如果一直拿不到就返回false,并立即返回。在等待过程中可以进行中断 |
lockInterruptibley() | 获取锁。如果线程interrupted了,则跟着抛出异常中断 |
unLock() | 释放锁 |
newCondition() | 创建一个与此 Lock 实例一起使用的 Condition 实例。 |
private ReentrantLock lock = new ReentrantLock();
public void run() {
// 加锁
if(lock.tryLock()){
System.out.println(Thread.currentThread().getName()+" 拿到锁");
try {
Thread.sleep(3000);
// doSomething...
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();// 释放锁
}
} else{
System.out.println(Thread.currentThread().getName()+" 获取锁失败");
}
}
公平锁:公平锁讲究先来先到,线程在获取锁时,会先看这个锁的等待队列中是否有线程在等待,如果有,则当前线程就会直接进入等待队列中,而不是直接去抢占锁。
ReentrantLock fairLock = new ReentrantLock(true); // 初始化一个公平锁
非公平锁:不管是否有等待队列,先直接尝试获取锁,如果拿到锁,则立刻占有锁对象;如果未拿到,则自动排到队尾等待。
ReentrantLock fairLock = new ReentrantLock(); // 初始化一个非公平锁
ReentrantLock fairLock = new ReentrantLock(false); // 初始化一个非公平锁
非公平锁的性能比公平锁的性能好很多,所以ReentrantLock默认是非公平锁。
为什么非公平锁性能好?
//公平锁静态内部类
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) {
// hasQueuedPredecessors返回true,说明当前节点不是头节点或队列不为空,此时直接加在队列后面
// hasQueuedPredecessors前面有个非符号,此时不再走&&后面的CAS操作。
// hasQueuedPredecessors返回false,说明当前节点是头节点或队列为空,
// 说明只有当前线程在竞争锁,此时可以进行compareAndSetState操作。
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;
}
}
// 非公平锁静态内部类
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); // 是Sync的方法
}
}
// Sync的方法
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;
}
看了ReentrantLock的公平锁和非公平锁的源代码你就会发现,公平锁直接走的父类AbstractQueuedSynchronizer的acquire方法,而非公平锁是先作CAS操作。非公平锁这样做的优点是:
1.如果直接拿到了锁,就避免了维护node链表队列
2.如果直接拿到了锁,就避免了线程休眠和唤醒的上下文切换
ReentrantLock它是怎么实现重入锁的呢?
截取前面小节的部分代码:
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
从代码可以看出,来获取锁的线程如果是当前占有锁的线程,则直接将nextc+1。而且从if (nextc < 0)
知道,可重入的次数是int的最大值。刚入门的同学可能不知道为什么会是int的最大的值,这是因为一个int值在做不限制累加,到了最大值2147483647时会溢出变成负数,这个在大学计算机相关课程应该会讲到。
ReentrantLock还提供了Condition机制来进行复杂的线程控制功能。
Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition。
ArrayBlockingQueue的实现就依靠了Condition机制。如下核心代码。
下面代码中Condition机制的核心原理就是:当前线程被哪个condtion阻塞(调用await),就会加到当前condition的阻塞队列里。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 数组阻塞队列
*
* @param
*/
class ArrayBlockingQueueDemon {
final ReentrantLock lock;
/**
* put元素时被阻塞的条件
*/
final Condition putCondition;
/**
* take数据时被阻塞的条件
*/
final Condition takeCondition;
/**
* 放元素的队列
*/
final Object[] items;
/**
* take下一个元素的索引下标
*/
int takeIndex;
/**
* put下一个元素的索引下标
*/
int putIndex;
/**
* 队列中元素个数
*/
int count;
/**
* 构造方法
*
* @param capacity 允许队列
* @param fair 是否创建公平锁
*/
public ArrayBlockingQueueDemon(int capacity, boolean fair) {
if (capacity <= 0) {
throw new IllegalArgumentException();
}
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
takeCondition = lock.newCondition();
putCondition = lock.newCondition();
}
public void put(E e) throws InterruptedException {
if (e == null) {
throw new NullPointerException();
}
lock.lockInterruptibly();
try {
// 队列到达初始化上限时,不再允许向队列放数据,放数据的线程要等待
// 此刻,当前线程会被添加到putCondition的阻塞队列里
while (count == items.length) {
putCondition.await();
}
// 入队
enqueue(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
lock.lockInterruptibly();
try {
// 当队列没有数据时,拿数据的线程等待
// 此该,当前线程会被添加到takeCondition的阻塞队列里
while (count == 0) {
takeCondition.await();
}
// 出队并返回
return dequeue();
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
items[putIndex] = x;
++putIndex;
if (putIndex == items.length) {
putIndex = 0;
}
count++;
// 队列里增加元素了,可以唤醒取元素的线程了
takeCondition.signal();
}
private E dequeue() {
E x = (E) items[takeIndex];
items[takeIndex] = null;
++takeIndex;
if (takeIndex == items.length) {
takeIndex = 0;
}
count--;
// 队列元素减少了,腾出了位置,可唤醒放元素的线程了
putCondition.signal();
return x;
}
}
注意:
Condition在使用之前,一定要先获取监视器。即调用Condition的await()和signal()方法的代码,都必须在lock.lock()和lock.unlock之间。
Conditon中的await()对应Object的wait(),Condition中的signal()对应Object的notify(),Condition中的signalAll()对应Object的notifyAll()。
在资源竞争不激烈的情形下,ReentrantLock性能稍微比synchronized差一点。但是当同步非常激烈的时候,synchronized的性能会下降好几十倍。而ReentrantLock不会有太大的性能波动。
在写同步的时候,优先考虑synchronized,毕竟synchronized更简单,总得来说性能被优化得还不错。ReentrantLock比较复杂,写得不好,还可能会给程序性能带来大问题。