锁主要用来控制多线程访问的行为,对于同一个线程,如果连续两次对同一把锁进行lock,那么这个线程会被卡死在那里,这样的特性很不好,在实际的开发中,方法之间的调用方式错综复杂,如果不小心可能在多个不同的方法中,反复调用 lock(),这样就会把自己卡死。
所以,重入锁就是用来解决这个问题的,重入锁使同一个线程可以对同一把锁在不释放的前提下,反复的加锁不会导致线程的卡死,唯一的一点就是需要保证 unlock() 的次数和 lock()一样的多。
Java中的锁都来自与 Lock 接口,而 ReadWriteLoc k实现的 lock 接口,本文主要分析这两个接口的几个子类的实现细节。
而重入锁最重要的方法就是lock()。
重入锁实现的主要类如下图:
重入锁的核心功能委托给内部类 Sync 实现,并且根据是否是公平锁有 FairSync 和 NonfairSync 两种实现。这是一种典型的策略模式。
实现重入锁的方法很简单,就是基于一个状态变量 state。这个变量保存在AbstractQueuedSynchronizer对象中
private volatile int state;
当 stat e== 0 时,表示锁是空闲的,大于零表示锁已经被占用, 它的数值表示当前线程重复占用这个锁的次数。因此,lock() 的最简单的实现是:
final void lock() {
// CAS设置共享状态,返回true表示成功获取共享状态
if (compareAndSetState(0, 1))
// 设置当前线程为共享状态的持有线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 否则调用AQS中的acquire(int arg)尝试获取同步状态,失败则加入等待队列,自旋获取共享状态
acquire(1);
}
下面是acquire() 的实现:
public final void acquire(int arg) {
//tryAcquire() 再次尝试获取锁,
//如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,
//同时宣布获得所成功,这正是重入的关键所在
if (!tryAcquire(arg) &&
// 如果获取失败,那么就在这里入队等待
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//如果在等待过程中 被中断了,那么重新把中断标志位设置上
selfInterrupt();
}
下面我们说一下公平锁 和 非公平锁
初始化时, state = 0,表示无人抢占了锁。这时候,这时线程 A 请求锁,获得了锁,把 state + 1,如下所示:
线程 A 取得了锁,把 state +1,这时候 state 改为 1,线程 A 继续执行其他任务,此时线程B请求锁,线程 B 无法获取锁,生成节点进行排队,如下图所示:
初始化的时候,会生成一个空的头节点,然后才是线程 B 节点,这时候,如果线程 A 又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当 A 再次请求锁,这时候的状态如下图所示:
到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程 A 释放了一次锁,如下图所示:
仅仅是把状态值减了,只有线程 A 把此锁全部释放了,状态值减到 0 了,其他线程才有机会获取锁。当线程 A 把锁完全释放后,state 恢复为 0,然后会通知队列唤醒线程 B 节点,使B可以再次竞争锁。当然,如果线程 B 后面还有线程 C,线程 C 继续休眠,除非 B 执行完了,通知了线程 C。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。
理解了公平锁的话,那非公平锁就容易理解了,当线程 A 执行完之后,要唤醒线程 B 是需要时间的,而且线程 B 醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程 C,那么线程 C 是有可能获取到锁的,如果线程 C 获取到了锁,线程 B 就只能继续等待休眠了。
那公平锁和非公平锁实现的核心区别在哪里呢?如下所示:
//非公平锁
final void lock() {
//直接抢了再说
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//抢不到,就进队列慢慢等着
acquire(1);
}
//公平锁
final void lock() {
//直接进队列等着
acquire(1);
}
我们从代码中可以看到,非公平锁如果第一次争抢失败,后面的处理和公平锁是一样的,都是进入等待队列慢慢等。
而对应tryLock()方法也非常类似的:
//非公平锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 如果当前共享状态未被其他线程占用
if (c == 0) {
// 尝试通过CAS占有当前共享状态
if (compareAndSetState(0, acquires)) {
// 设置共享状态持有线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果共享状态已被占用,则判断当前占用共享状态的线程是否就是当前线程
else if (current == getExclusiveOwnerThread()) {
// 如果是则自增获取次数,设值state
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
//非公平锁
protected final boolean tryAcquire(int acquires) {
// 获取到当前线程
final Thread current = Thread.currentThread();
// 获取当前同步状态
int c = getState();
// 如果同步状态为0,则说明当前同步状态已完全释放
if (c == 0) {
// 1、hasQueuedPredecessors判断当前节点是否存在前驱节点
// 2、如果不存在则CAS设置state的值
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;
}
Condition 的作用与 Object.wait() 和 Object.notify() 的作用大致是相同的。但是 wait() 和 notify() 方法是与synchronized 关键字合作使用的,而 Condition 是与重入锁相关联的。通过 Lock 接口(重入锁实现了这一接口)的new Condition() 方法可以生成一个与当前重入锁绑定的 Condition 实例。利用 Condition 对象,可以让线程在合适的时间等待,或者在某一个特定的时间得到通知。
Condition 接口提供的方法
/**
* 使当前线程进入等待状态直到被通知(signal)或中断
* 当其他线程调用 singal() 或 singalAll() 方法时,该线程将被唤醒
* 当其他线程调用 interrupt() 方法中断当前线程
* await() 相当于 synchronized 等待唤醒机制中的 wait() 方法
*/
void await() throws InterruptedException;
//当前线程进入等待状态,直到被唤醒,该方法不响应中断要求
void awaitUninterrruptibly();
//调用该方法,当前线程进入等待状态,直到被唤醒或被中断或超时
//其中 nanosTimeout 指的等待超时时间,单位纳秒
long awaitNanos(long nanosTimeout) throws InterruptedException;
//同 awaitNanos,但可以指明时间单位
boolean await(long time, TmeUnit unit) throws InterruptedException;
//调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时
//间期限(deadline),如果没到指定时间就被唤醒,返回 true,其他情况返回 false
boolean await(Date deadline) throws InterruptedException;
//唤醒一个等待在 Condition 上的线程,该线程从等待方法返回前必须
//获取与 Condition 相关联的锁,功能与notify()相同
void signal();
//唤醒所有等待在 Condition 上的线程,该线程从等待方法返回前必须
//获取与 Condition 相关联的锁,功能与 notifyAll() 相同
void signalAll();
代码演示一下 Condition 的使用:
public class ReentrantLockCondition implements Runnable{
public static ReentrantLock lock = new ReentrantLock();
//通过 ReentrantLock 创建 Condition 实例,并与之关联
public static Condition condition = lock.newCondition();
@Override
public void run()
{
try
{
lock.lock();
condition.await();
System.out.println("Thread is going on");
}
catch (InterruptedException e)
{
e.printStackTrace();
}
finally
{
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
ReentrantLockCondition condition1 = new ReentrantLockCondition();
Thread thread= new Thread(condition1 );
thread.start();
Thread.sleep(2000);
lock.lock();
condition.signal();
lock.unlock();
}
}
与 Object.wait() 和 Object.notify() 方法类似,当前线程使用 Condition.await() 时,要求线程持有相关的重入锁,在Condition.await() 调用后,这个线程会释放这把锁。同理,在 Condition.signal() 方法调用时,也要求线程先获得相关的锁。在 signal() 方法调用后,系统会从当前 Condtion 对象的等待队列中,唤醒一个线程。一旦线程被唤醒,它会重新尝试获得与之绑定的重入锁,一旦成功获取,就可以继续执行了。因此,在 signal() 方法调用之后,一般需要释放相关的锁,谦让给被唤醒的线程,让它可以继续执行。
对于重入锁,这里我们需要知道几点: