前言:Java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。这篇文章主要是从使用的角度来分析一下ReentrantLock。
一、Lock接口简介
1.1、Lock接口概述
在JUC包下面有一个java.util.concurrent.locks 包,这个包提供了一系列基础的锁工具,对传统的synchronizd、wait和notify等同步机制进行补充和增强。下面先来介绍下这个Lock接口。
Lock 接口可以视为synchronized 的增强版,提供了更灵活的功能。相对于synchronized关键字 ,Lock 接口还提供了限时锁等待、锁中断和锁尝试等功能。
该接口的定义如下
public interface Lock {
// 尝试去获得锁
// 如果锁不可用,当前线程会变得不可用,直到获得锁为止。(中途会忽略中断)
void lock();
// 尝试去获取锁,如果锁获取不到,线程将不可用
// 直到获取锁,或者被其他线程中断
// 线程在获取锁操作中,被其他线程中断,则会抛出InterruptedException异常,并且将中断标识清除。
void lockInterruptibly() throws InterruptedException;
// 锁空闲时返回true,锁不空闲是返回false
// 该方法不会引起当前线程阻塞
boolean tryLock();
// 在unit时间内成功获取锁,返回true
// 在unit时间内未成功获取锁,返回false
// 如果当前线程在获取锁操作中,被其他线程中断,则会抛出InterruptedException异常,并且将中断标识清除。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 获取一个绑定到当前Lock对象的Condition对象
// 获取Condition对象的前提是当前线程持有Lock对象
Condition newCondition();
}
关于上面的lock()和lockInterruptibly()方法,有如下区别:
lock()方法类似于使用synchronized关键字加锁,如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。 lockInterruptibly()方法顾名思义,就是如果锁不可用,那么当前正在等待的线程是可以被中断的,这比synchronized关键字更加灵活。
1.2、Lock接口的经典用法
Lock lock = new ReentrantLock();
//尝试获取锁,如果当前该锁没有被其他线程持有,则当前线程获取该锁并返回true,否则返回false。
//该方法不会引起当前线程阻塞
if (lock.tryLock()) {
try {
// manipulate protected state
} finally {
lock.unlock();
}
} else {
// perform alternative actions
}
或者是
lock.lock()
try {
// manipulate protected state
} finally {
lock.unlock();
}
这边不要将获取锁的过程写在try块中,因为如果在获取锁(自定义锁的实现)时发生了异常,异常抛出的同时,也会导致锁无故释放。
二、ReentrantLock实现类
ReentrantLock是Lock接口的实现类。ReentrantLock字面意思为可重入的锁,顾名思义ReentrantLock 类是一个可重入的独占锁。除了具有和synchronized一样的功能外,还具有限时锁等待、锁中断和公平锁等功能。ReentrantLock 底层是通过继承AQS来实现独占锁功能的,AOS是并发编程的基石,后面我会单独抽一篇文章讲。
刚入行Java开发或者说在JDK1.5之前,遇到并发问题时,第一印象想到的都是synchronized 同步锁。我们前面已经讲过,关于synchronized 同步锁的使用,会存在以下几个问题:
于是,Doug Lea 大师就搞了个ReentrantLock ,ReentrantLock诞生于JDK1.5 ,位于java.util.concurrent (简称JUC )包目录下。
2.1、ReentrantLock类组成
进入java.util.concurrent.locks.ReentrantLock 中,发现ReentrantLock 有三个内部类:
对应UML类图结构如下:
看到了一个熟悉的身影java.util.concurrent.locks.AbstractQueuedSynchronizer (江湖人简称为AQS ),这就是传说中的AQS (同步队列器)。
我们都知道锁分为公平锁和非公平锁。你那么上面类图中NonfairSync 为非公平锁,FairSync 为公平锁。
2.2、ReentrantLock对比synchronized
ReentrantLock常常对比着synchronized来分析,我们先对比着来看然后再一点一点分析。
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized在发生异常的时候,会自动释放线程占有的锁,而ReentrantLock在发生异常时,如果没有通过unlock去释放锁,很有可能造成死锁,因此需要在finally块中释放锁
(3)ReentrantLock支持非阻塞的方式获取锁,能够响应中断,而synchronized不可响应中断,一个线程获取不到锁就一直等着。
? (4)ReentrantLock可以是公平锁或者非公平锁,而synchronized只能是非公平锁
2.3、公平锁和非公平锁
关于ReentrantLock ,有两个很重要的概念需要学习:公平锁和非公平锁。
查看ReentrantLock 的源代码,我们会看到两个构造函数,分为对应构造公平锁和非公平锁。
//默认构造非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//true构造公平锁,false构造非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁:是指线程在抢占锁失败后会进入一个等待队列,先进入队列的线程会先获得锁。公平性体现在先来先得。 非公平锁:是指线程抢占锁失败后会进入一个等待队列,但是这些等待线程谁能先获得锁不是按照先来先得的规则,而是随机的。不公平性体现在后来的线程可能先得到锁。
如果有很多线程竞争一把公平锁,系统的总体吞吐量(即速度很慢,常常极其慢)比较低,因为此时在线程调度上面的开销比较大。
原因是采用公平策略时,当一个线程释放锁时,需要先将等待队列中的线程唤醒。这个唤醒的调度过程是比较耗费时间的。如果使用非公平锁的话,当一个线程释放锁之后,可用的线程能立马获得锁,效率较高。
2.4、ReentrantLock代码实现
a. 非公平锁代码
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//如果没有线程占据锁,则占据锁,也就是将state从0设置为1
//这种抢占方式不要排队,有人释放了锁,你可以直接插到第一位
//去抢,只要你能抢到
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
//否则尝试抢占锁
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
AQS中抢占锁的时候会调用 tryAcquire 方法。非公平锁的这个方法直接调用了父类中的nonfairTryAcquire 。
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;
}
b. 公平锁代码
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;
}
}
三、ReentrantLock使用场景
规范的话,lock.lock()应该放在try之前。
lock.lock();
try {
doSomething();
} finally {
lock.unlock();
}
如果按照下边写法。当lock.lock()报错时也会进入finally释放锁,根据Lock.unlock()文档,当非锁持有线程调用该方法时会抛出unchecked异常或者IllegalMonitorStateException,这样会丢失掉导致加锁失败的异常(虽然结果都是加锁失败,但异常信息却会不一致)。
try {
lock.lock();
doSomething();
} finally {
lock.unlock();
}
下面场景图片来源于网上,我就不改了太麻烦.......
场景1:如果发现该操作已经在执行中则不再执行(有状态执行)
a、用在定时任务时,如果任务执行时间可能超过下次计划执行时间,确保该有状态任务只有一个正在执行,忽略重复触发。? b、用在界面交互时点击执行较长时间请求操作时,防止多次点击导致后台重复执行(忽略重复触发)。
以上两种情况多用于进行非重要任务防止重复执行,(如:清除无用临时文件,检查某些资源的可用性,数据备份操作等)
?场景2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似synchronized)
这种比较常见大家也都在用,主要是防止资源使用冲突,保证同一时间内只有一个操作可以使用该资源。?但与synchronized的明显区别是性能优势(伴随jvm的优化这个差距在减小)。同时Lock有更灵活的锁定方式,公平锁与不公平锁,而synchronized永远是公平的。
这种情况主要用于对资源的争抢(如:文件操作,同步消息发送,有状态的操作等)
ReentrantLock默认情况下为不公平锁
不公平锁与公平锁的区别:? 公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)? 不公平情况下,是无序状态允许插队,jvm会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)
场景3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)
这种其实属于场景2的改进,等待获得锁的操作有一个时间的限制,如果超时则放弃执行。? 用来防止由于资源处理不当长时间占用导致死锁情况(大家都在等待资源,导致线程队列溢出)。
场景4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作
synchronized与Lock在默认情况下是不会响应中断(interrupt)操作,会继续执行完。lockInterruptibly()提供了可中断锁来解决此问题。(场景2的另一种改进,没有超时,只能等待中断或执行完毕)
这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞)
下面是ReentrantLock的一个代码示例
//: concurrency/AttemptLocking.java ?
// 以下是ReentrantLock中断机制的一个代码实现、如果换成synchronized就会出现死锁 ?
import java.util.concurrent.*; ?
import java.util.concurrent.locks.*; ?
?
public class AttemptLocking { ?
? ? private ReentrantLock lock = new ReentrantLock(); ?
?
? ? public void untimed() { ?
? ? ? ? boolean captured = lock.tryLock(); ?
? ? ? ? try { ?
? ? ? ? ? ? System.out.println("tryLock(): " + captured); ?
? ? ? ? } finally { ?
? ? ? ? ? ? if (captured) ?
? ? ? ? ? ? ? ? lock.unlock(); ?
? ? ? ? } ?
? ? } ?
?
? ? public void timed() { ?
? ? ? ? boolean captured = false; ?
? ? ? ? try { ?
? ? ? ? ? ? captured = lock.tryLock(2, TimeUnit.SECONDS); ?
? ? ? ? } catch (InterruptedException e) { ?
? ? ? ? ? ? throw new RuntimeException(e); ?
? ? ? ? } ?
? ? ? ? try { ?
? ? ? ? ? ? System.out.println("tryLock(2, TimeUnit.SECONDS): " + captured); ?
? ? ? ? } finally { ?
? ? ? ? ? ? if (captured) ?
? ? ? ? ? ? ? ? lock.unlock(); ?
? ? ? ? } ?
? ? } ?
?
? ? public static void main(String[] args) throws InterruptedException { ?
? ? ? ? final AttemptLocking al = new AttemptLocking(); ?
? ? ? ? al.untimed(); // True -- 可以成功获得锁 ?
? ? ? ? al.timed(); // True --可以成功获得锁 ?
? ? ? ? //新创建一个线程获得锁并且不释放 ?
? ? ? ? new Thread() { ?
? ? ? ? ? ? { ?
? ? ? ? ? ? ? ? setDaemon(true); ?
? ? ? ? ? ? } ?
?
? ? ? ? ? ? public void run() { ?
? ? ? ? ? ? ? ? al.lock.lock(); ?
? ? ? ? ? ? ? ? System.out.println("acquired"); ?
? ? ? ? ? ? } ?
? ? ? ? }.start(); ?
? ? ? ? Thread.sleep(100);// 保证新线程能够先执行 ?
? ? ? ? al.untimed(); // False -- 马上中断放弃 ?
? ? ? ? al.timed(); // False -- 等两秒超时后中断放弃 ?
? ? } ?
} ?
参考链接:
同步锁——ReentrantLock
并发编程的基石——AQS类
ReentrantLock实现原理
深入剖析ReentrantLock实现原理
|