ReentrantLock (简称RS)是区别于 synchronized 的一种锁技术,特性有 等价于 synchronized 的 lock、可重入锁、公平锁与非公平锁、读锁与写锁、Condition对象等等......
RS应该在同步性能上与 synchronized 到底孰优孰劣,未知。
不过由于 RS基于 volatile 变量 state 实现锁的机制,而 synchronized 是基于JVM的 mointer 对象实现的锁机制(偏向锁、轻锁不再考虑范围内)。各位可以自己推敲或试验下。
RS任然是排它锁,即在同一个单位时间内只能有一个线程持有锁(一个线程执行同步代码块)。
ReentrantReadWriteLock则是读写锁,读共享,读与写互斥,区分了读与写比RS更加细粒化。
Lock lock = new ReentrantLock(true);
Condition condition = lock.newCondition();
通常学习一个类,要从它的结构和静态变量,静态方法开始,这个对象也不例外。
看结构图得知,它实现了Lock接口,内部有个抽象的静态类 Sync(简称 S),S是实现各种特性的关键类,S下面有2个具体的实现子类,FairSync、NonFairSync,公平锁与非公平锁。
S 继承了 AbstractQueueSynchronizer (简称AQS),AQS是一个基类,里面有各种基础方法提供同步,同样也是很重要的类。
默认构造的RS是非公平锁,非公平锁比公平锁更高效一点,synchronized 关键字本身也是非公平的模式。
特性1:
等价于 synchronized 的 lock。RS的 lock 有2种,一种是 lock(),如果不成功就会阻塞,成功则返回true。另外一种是 tryLock(),不成功不会阻塞,返回false。
NonFairSync(简称NFS),NFS是先尝试CAS将state置为1,即当前已加锁1次,然后再将当前线程置为持有排它锁的线程。否则会调用AQS的 acquire()。
FairSync (简称FS),则是直接调用AQS.acquire()。
AQS的 acquire 可看到是调用 tryAcquire 尝试获取锁,实际上是由子类 RS中的 FS 和 NFS 复写了 tryAcquire 方法。即 RS 的 lock 直接获取锁的动作实际上也是 tryAcquire 方法。只不过加入了失败阻塞的操作。
如果失败,会将当前线程包装成Node 放入队列的尾部,然后在 acquireQueued() 中死循环获取
锁,此时线程在排队过程中是被公平唤醒和外面的线程竞争锁。只有线程变成了头结点时才被唤醒
再次调用tryAcquire方法获取锁,成功了则把自己设置成头结点并唤醒下一个节点。否则挂起自己。
观摩unlock()操作,实际是调用tryRelease()释放锁:
tryRelease 在 Sync 中被复写,逻辑很简单。还是巧妙运用 volatile 修饰的 state,如果为0了,代表锁被释放完全了,将持有锁的线程置空,再CAS设置state。
思考:此时若线程在 setState() 停止了,其它线程去获取锁,能获取到吗?
答案是不能,因为获取锁的逻辑会判断 c 是否为 0 ,此时c还没被置为0,同样获取锁的线程也不会等于null,所以获取不到锁。
使用lock()时应注意始终将同步代码块加finally unlock(),如:
lock.lock();
try{
some codes...
}
finally{
lock.unlock();
}
因为难保try中的代码异常,此时就要将锁释放掉。否则就造成当前的执行线程一直持有锁,其它线程会被阻塞,造成大问题。
特性2:
可重入性,是指当前持有锁的线程可再次进入lock的代码块,即再次获取锁。同时state会被自增1,当解锁时state也需被减到0,才代表锁已被完全释放,否则其他线程是进入不了lock代码块的。
它是靠 state 变量实现的。state变量是被 volatile 修饰的,保证了内存可见性。
NFS:可看到不会管队列中的等待线程,直接CAS获取锁,抢占模式。
值得注意的是 hasQueuedPredecessors() 函数是判断等待队列中有无线程。
这个等待队列是AQS维护的,所有尝试获取锁的线程失败后都会被包装成Node,加入AQS等待队列的尾部。
特性3:
公平锁和非公平锁,分别指严格按照加锁的时间顺序获得锁与抢占模式获得锁。
非公平锁会比公平锁更加高效一点。非公平锁有可能刚放弃完锁,再一次进入lock代码块时又立即获得了锁,有可能造成 “饥饿” 现象。公平锁模式下,刚放弃完锁的线程不会立即获得锁,除非队列中没有其它线程。
synchronized 从这点来看是非公平的,因为A线程刚执行完 synchronized ,再次进入
synchronized 时有可能立即再一次获得锁。
特性4:
condition对象,Condition是一个接口,ReentrantLock获得的是ConditionObject对象,ConditionObject 实现了 Condition接口。
Condition可以理解为一个 等待和通知组件。等价于 Object 锁的 wait() notify() notifyAll()。
Condition是与相关的Lock被绑定使用的,Condition必须配合Lock一起使用,调用await()、singal()等方法必须获得与此Condition对象相关的Lock。否则会抛出 IllegalMonitorStateException。
首先要明白一点,AQS的等待队列和 ConditionObject 的等待队列是2个不同的队列,
ConditionObject 的队列是线程主动调用 await() 方法放弃锁,进入等待状态的队列。(简称等待队列)
AQS 等待队列与 Condition 队列是两个相互独立的队列
- #await() 就是在当前线程持有锁的基础上释放锁资源,并新建 Condition 节点加入到 Condition 的队列尾部,阻塞当前线程 。
- #signal() 就是将 Condition 的头节点移动到 AQS 等待节点尾部,让其等待再次获取锁。
以下是 AQS 队列和 Condition 队列的出入结点的示意图,可以通过这几张图看出线程结点在两个队列中的出入关系和条件。
I.初始化状态:AQS等待队列有 3 个Node,Condition 队列有 1 个Node(也有可能 1 个都没有)
III.节点 2 执行 Condition.signal() 操作
public final void await() throws InterruptedException {
// 当前线程中断
if (Thread.interrupted())
throw new InterruptedException();
// 当前线程加入等待队列
Node node = addConditionWaiter();
// 释放锁
long savedState = fullyRelease(node);
int interruptMode = 0;
/**
* 检测此节点的线程是否在竞争队列上,如果不在,则说明该线程还不具备竞争锁的资格,则继续等待
* 直到检测到此节点在竞争队列上
*/
while (!isOnSyncQueue(node)) {
// 线程挂起
LockSupport.park(this);
// 如果已经中断了,则退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 竞争锁
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清理下条件队列中的不是在等待条件的节点
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
1,调用await()的线程包装成Node,加入了 ConditionObject 的等待队列,且是队列尾部。
3,将线程挂起,这里的 LockSupport.park(this),是本地方法实现,是由操作系统实现的。
4,死循环,期间一直会有线程不断唤醒等待队列中的线程,先移出等待队列再将它们加入到竞争队列中。这个过程持续到当前线程变成等待队列的头部的时候,然后由某个线程 signal 唤醒它,同样地它也被移除等待队列加入到竞争队列中,此时循环终止。
6,直到线程恢复自由时,即回到业务代码中的时候,线程此时肯定是重新获取到了锁的。
AQS的 这个等待超时方法和 上面的 await() 差不多,不过差别就在等待时间上,可看到被加入等待队列尾部后,也是循环判断线程是否在竞争队列中。
不在就由 OS 暂时挂起线程,时间是精确到纳秒级的,当指定的等待时间耗尽后,仍然没有加入竞争队列时,就会放弃等待。进入 transferAfterCancellWait() 方法,直接加入竞争队列,然后跳出循环,开始竞争锁。
若当前节点有下一个线程在等待,因为当前节点已经被标记为放弃等待了,所以进入 unlinkCancelledWaiters() 这个方法会清除等待队列中所有已放弃等待的节点。
所以这个方法比起 await() 相当于走捷径的,等待一定时间后直接进入竞争队列,而不用辛苦排队到等待队列的头部才被加入竞争队列。
学习signal()方法:
final boolean transferForSignal(Node node) {
//将该节点从状态CONDITION改变为初始状态0,
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//将节点加入到syn队列中去,返回的是syn队列中node节点前面的一个节点
Node p = enq(node);
int ws = p.waitStatus;
//如果结点p的状态为cancel 或者修改waitStatus失败,则直接唤醒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
- 判断当前线程是否已经获取了锁,如果没有获取则直接抛出异常,因为获取锁为通知的前置条件。
- 如果线程已经获取了锁,则将唤醒条件队列的首节点
- 唤醒首节点是先将条件队列中的头节点移出,然后调用 AQS 的 #enq(Node node) 方法将其安全地移到 CLH 同步队列中
- 最后判断如果该节点的同步状态是否为 Node.CANCEL ,或者修改状态为 Node.SIGNAL 失败时,则直接调用 LockSupport 唤醒该节点的线程。
最后是 Condition 的生产者、消费者的经典场景使用:
通过代码看到,同一个锁衍生出2个条件。分别代表2种情况:有资源可消费,无资源可消费。
当生产者往容器加东西,当触发到某个条件如容器满了,就进入 有资源状态 的等待队列。等待资源被其它线程消费。
当消费者消费资源,触发某个条件,如资源不足就进入 缺乏资源状态 的等待队列,等待资源被生产。
顺利的情况下,生产者生产资源后未触发条件,此时有资源可消费,则唤醒一个 有资源状态 中等待的线程,让消费者竞争锁,去消费资源。
同理,消费者也是如此,在每一次消费一个资源后,都会唤醒一个生产者竞争锁,让它生产资源。
public class ConditionTest {
private LinkedList<String> buffer; //容器
private int maxSize ; //容器最大
private Lock lock;
private Condition fullCondition;
private Condition notFullCondition;
ConditionTest(int maxSize){
this.maxSize = maxSize;
buffer = new LinkedList<String>();
lock = new ReentrantLock();
fullCondition = lock.newCondition();
notFullCondition = lock.newCondition();
}
public void set(String string) throws InterruptedException {
lock.lock(); //获取锁
try {
while (maxSize == buffer.size()){
notFullCondition.await(); //满了,添加的线程进入等待状态
}
buffer.add(string);
fullCondition.signal();
} finally {
lock.unlock(); //记得释放锁
}
}
public String get() throws InterruptedException {
String string;
lock.lock();
try {
while (buffer.size() == 0){
fullCondition.await();
}
string = buffer.poll();
notFullCondition.signal();
} finally {
lock.unlock();
}
return string;
}
}