前言
处理好线程之间的同步问题一直都是开发界的难题,我们最常用的就是synchronized关键字,synchronized常用在方法上或者使用synchronized块
.
而且synchronized在JDK1.6之后得到了很多性能上的改进,,当然平常我们也鼓励尽量多使用synchronized关键字,因为非常的简单,屏蔽了很多实现上和操作上的细节。
但是我不得不说作为开发人员的我们,如果想要最大化的优化或者灵活的进行控制,还是要对另外一种同步方式要有一定的了解。那就是我们今天所要讨论的重点对象lock
为什么会诞生lock,我想我们也是很容理解的,因为JDK1.6之前的synchronized关键字不够高效,而且synchronized不够灵活(比如无法使用尝试在规定时间内获取锁)等,所以就诞生了lock.
lock改善了很多同步上的性能问题,而且有非常灵活的API.
今天我们就ReentrantLock类(一种独占式的锁)开始我们的话题.
ReentrantLock
ReentrantLock是通过扩展AbstractQueuedSynchronizer进行锁的控制,并包含公平锁和非公平锁,此处说的公平锁和非公平锁的最大区别就体现在尝试获取锁的过程中,非公平锁立即就可以进行尝试获取锁,而公平锁不可以直接尝试获取锁,必须按顺序进入等待锁的队列
,下面详细通过代码详细介绍一下.
首先看一下我们的代码结构图

从上图我们可以看到ReentrantLock存在三个内部类,请记住这三个内部类,Sysc
继承了AbstractQueuedSynchronizer
这个类,而NonfairSync
和FairSync
又继承了Sync
类,这两个类分别是非公平和公平锁实现的关键.
下面看一下公平锁和非公平锁如何声明的
//非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认情况下我们的声明的都是非公平锁,如果需要声明为公平锁则可以自己通过fair变量来设置.
获取锁
下面我们看一下具体的差别
//NonfairSync获取lock
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
//fairSync获取lock
final void lock() {
acquire(1);
}
就像我们在上面说的那样,对于非公平锁首先会尝试获取一次锁,如果成功获取锁就设置当前的独占锁为当前线程,并改变状态值为1.否则都进行acquire
方法.
下面看一下acquire
方法的具体实现
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里主要做了几个事情:
1.尝试获取锁或者改变锁的数量,如果操作失败
2.如果没有获取到,则把当前线程加入到等待的链表
3.如果添加成功则设置线程中断状态为true
下面先看一下tryAcquire方法的主要内容,首先看一下非公平锁的实现
final boolean nonfairTryAcquire(int acquires) {
//获取当前你线程
final Thread current = Thread.currentThread();
//获取当前的状态值
int c = getState();
//0代表还未线程抢占没有锁或者说某个线程刚释放锁
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;
}
//否则就设置为false
return false;
}
上面介绍了非公平锁的方法,其实实现很简单,首先判断是否当前时刻没有线程获得锁,如果没有则自己尝试获取锁,如果当前已经有线程获得了锁,那么就需要判断获得锁的是不是就是当前线程,如果是的话代表是重入锁,将获得锁的数量加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;
}
}
...省略
return false;
}
其实公平锁差不多,只是在判断时多加了hasQueuedPredecessors方法
判断当前线程是否为列表头,不为列表头且也不是表头的下一个元素,则不能获得了锁。
入队自旋
当上面都没有成功的情况下,说明已经有线程获得了锁,那么我们需要把当前的线程加入到等待的队列中.具体的就不赘述了,已经在我的另外一篇文章并发 - AbstractQueuedSynchronizer分析中有讲到底是如何进入等待队列和进行自旋的,本文主要讲述ReentrantLock的相关部分.
unlock方法
unlock是当前线程释放锁的方法,是通过调用release方法实现的
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) { //尝试释放锁
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
尝试去释放锁,但是由于有重入锁的特效,所以很有可能一次无法释放所有的锁,这是非常重要和值得关注的点。
tryRelease是由sync类重写的,FairSync和NonfairSync类都没有重写,因为释放的原理都是一样的,下面看一下具体的实现
protected final boolean tryRelease(int releases) {
//状态值相减,因为有重入锁的情况
int c = getState() - releases;
//如果当前线程并没有持有锁,抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//等于0,说明完全释放,不然只是释放了一层锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
//否则只是更新状态值
setState(c);
return free;
}
道理很简单就是:判断当前线程获取是否获得了锁,如果是就减去releases个锁,如果状态值为0则表示已释放完,否则没有获得锁的线程调用释放锁的方法会抛出异常.
unparkSuccessor也是父类AQS中的方法,主要做的事情就是解锁离head最近的那个正在等待线程.为什么这么说是因为有可能head.next已经取消了。
tryLock
下面我们在看一下扩展的方法tryLock,此方法是为了线程漫长等待锁的问题,意在尝试性的获取锁,如果没获取到就退出获取,可以用来做做其他的事情,这样更能调高线程的效率(这里是以非公平锁为例)。
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
另外tryLock还有带有时间限制的方法,表示在指定时间内进行获取锁的尝试。
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
这里带时间尝试的方法是可以中断的,没有中断的情况下首先先去尝试性的获取一次锁,如果不成功,则进行规定时间的轮询政策.
private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {
//超时直接返回
if (nanosTimeout <= 0L) return false;
//当前时间+尝试的时间
final long deadline = System.nanoTime() + nanosTimeout;
//加入等待队列
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
//拿到锁了,设置信息,返回
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//超时返回
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
//失败后的状态处理,中断会抛出异常
if (shouldParkAfterFailedAcquire(p, node) && nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
首先是尝试去拿锁,如果拿到了直接返回,如果没拿到继续判断尝试的时间是否大于1000L,如果大于这么多就要把对象挂起,如果最终都没有拿到,在finnally中将此节点从同步队列中移除,以失败告终.
另外还有一个带中断获取锁方式lockInterruptibly和带时间的tryLock方法相似.
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
doAcquireInterruptibly
和doAcquireNanos
方法也几乎一样,只是doAcquireInterruptibly
方法没有时间的限制,发生中断都会抛出异常.
condition方法
通过newCondition方法就可以获得一个condition对象,此对象主要是用来点对点的去让一个线程暂停和启用,看一下具体的方法
public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
这里先不介绍里面的细节了,有兴趣的可以自己看一下,也可以等我下一篇文章介绍。
小结
上面我们介绍了lock和unlock以及lock的扩展方法的实现,本文并没有详细介绍AbstractQueuedSynchronizer相关的内容,因为当初笔者在看的时候就是混在一起看的,还牵涉到condition相关的内容,导致整个人的都看的很晕,分不清楚什么是什么,因此在写本文就将其内容全部分开了,上文中也给出了AbstractQueuedSynchronizer相关的内容的链接地址。我们先了解每一个部分的内容,最终在看ReentrantLock的内容,这样更容易梳理和理解.
再次总结一下本文的内容,
1.首先根据构造函数选择合适的锁方式(公平锁和非公平锁),
2.然后我们使用lock方法尝试获取锁,如果获取到锁返回
3.假如没有获取到锁,就会被加入到一个等待的同步队列中,等待获取锁
4.当其他持有锁的方法调用unlock方法,则就会从队列头部拿到下一个等待的线程,让他持有锁.
5.循环直到所有等待的线程都获得锁.
另外本文还讲述了可中断方式获取锁和指定时间内尝试获取锁,使用这些方法可以防止线程一直等待而不一定获得锁的情况,特别是非公平锁存在插队的情况。
结语
本文均出自个人的观点,如果有什么表述错误或者纰漏的地方,欢迎指正。
与君共勉!!!