目录
一、概述
ReentrantLock字面意思就是可重入锁(re表示重新,entrant表示进入,lock表示锁),它其实是属于JUC包(java,util.concurrent)下的一个类。在多线程环境下我们为了避免并发引发的一些问题常常会选择给可能引起并发问题的代码(临界区)加锁,说到加锁大家最熟悉的可能就是synchronized关键字,除此以外的另一个就是ReentrantLock类。ReentrantLock相比于synchronized有如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
以上四点是ReentrantLock相比于synchronized的主要区别,除了以上四个区别外他们之间还有一共同的特点就是“都支持可重入锁”。所谓可重入锁就是指“同一个线程对同一个对象可以反复的进行加锁操作”。以上概念性的东西听着比较懵逼,下面我们就以上概念具体展开讲解一下。
二、ReentrantLock基本语法
synchronized加锁只需写上synchronized关键字即可,它是在关键字层面的加锁,而想要使用ReentrantLock对临界区加锁必须先把ReentrantLock的对象new出来才行,它是在对象级别对临界区进行的加锁,看一下ReentrantLock使用的基本语法:
//创建ReentrantLock对象
ReentrantLock reentrantLock = new ReentrantLock();
//获取锁
reentrantLock.lock();
try{
//临界区
} finally{
//释放锁
reentrantLock.unlock();
}
以上代码需要注意两点:
- lock()和unlock()方法一定要成对出现,加了锁一定要释放;
- 用ReentrantLock加锁时,临界区的代码一般放到try块中,释放锁的操作放在finally块中,这样可以保证即使临界区的代码出现了异常也能正确的把锁释放掉。
作为对比,我们放一段同样的逻辑用synchronized编写是什么样子:
Object obj = new Object();
synchronized (obj){
//临界区
}
虽然看上synchronized比ReentrantLock要简单很多,但ReentrantLock比synchronized有很多其他的特性,我们接下来就这二者的异同展开讲讲。
三、ReentrantLock的特性
3.1可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。这个特点是synchronized和ReentrantLock都具备的,他们都是可重入的。今天讲解ReentrantLock,我们就以ReentrantLock为例看一下它的可重入特性:
public class ReentrantLockDemo {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.out.println("enter main");
m1();
}finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try {
System.out.println("enter m1");
m2();
}finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try {
System.out.println("enter m2");
}finally {
lock.unlock();
}
}
}
上面的代码只有一个主线程在运行,我们在main方法中通过调用lock.lock()方法加锁,之后在main中调用m1(),在m1()中又加锁,m1()中调用了m2(),在m2()中再次加锁,同一个线程加了三次锁,但在第2、3次加锁的时候并没有因为之前加过锁而发生阻塞,这就是可重入的特性体现,看一下输出结果(三个方法都执行了输出语句,证明没有阻塞):
enter main
enter m1
enter m2
3.2可打断
所谓可打断就是指某个线程在等待锁的过程中,其他线程可以通过interrupt()方法中止其等待,这个特征是ReentrantLock特有的,synchronized并没有。也就是说如果通过synchronized进行加锁,如果这个线程一直没有等到其他线程释放掉锁那么它会一直等下去,而ReentrantLock如果等的时间过长还没有发应,它是可以被中止等待的。直接看代码吧:
public class ThreadLocalBDemo02 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
//开启一个线程t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
//如果没有竞争那么此方法就会获取lock锁对象
//如果有竞争就进入阻塞队列,可以被其他线程用interrupt方法打断
System.out.println("尝试获取锁");
lock.lockInterruptibly();
}catch (InterruptedException e){
e.printStackTrace();
System.out.println("被打断,没有获得锁,返回");
return;
}
try {
System.out.println("获取到锁");
}finally {
lock.unlock();
}
}
},"t1");
//主线程先获取锁,这个时候t1线程就无法获取锁了,它会在哪里等待直到主线程把锁释放掉
lock.lock();
t1.start();
//主线程睡1秒,没有释放锁,之后打断t1,让他不要再等了,我是不会释放的。。。。
Thread.sleep(1000);
t1.interrupt();
}
}
输出日志:
尝试获取锁
打断t1,让他不要再等了
java.lang.InterruptedException
被打断,没有获得锁,返回
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:896)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1221)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:340)
at com.shhxbean.test.ThreadLocalBDemo02$1.run(ThreadLocalBDemo02.java:16)
at java.lang.Thread.run(Thread.java:745)
通过输出日志我们可以看到当t1长时间没有获得锁的时候,别的线程是可以通过调用等待线程的interrupt()方法将其打断的,避免无限制的等待下去。和上面演示可重入特性不同的是想要具备被打断的特性在获取锁的时候需要调用ReentrantLock的lockInterruptibly()方法而不是lock()方法。
3.3锁超时
上面3.3中讲的如果线程长时间没有获得锁,可以通过在其他线程中调用interrupt方法来打断他,进而避免死等的发生。只不过这种避免死等的方式是一种被动的方式,必须在其他线程中调用了interrupt方法才可以。而这小节讲的“锁超时”也是一种避免死等情况发生的特性,只不过相比“可打断”的被动,“锁超时”是一种更主动的方式。当某个线程尝试去获得锁的时候发现锁被其他线程占用了,那么它会等一段时间,如果过了一段时间还是没有获取到锁那么它就会主动放弃等待。代码如下:
public class ReentrantLockDemo03 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//尝试获取锁,获取不到立马放弃
System.out.println(Thread.currentThread().getName() + "尝试获取锁");
if(! lock.tryLock()){
System.out.println(Thread.currentThread().getName() + "获取不到锁");
return;
}
try {
System.out.println(Thread.currentThread().getName() + "获得到了锁");
}finally {
lock.unlock();
}
}
},"t1");
//主线程先获取到锁,那t1就获取不到了,获取不到它就会放弃
System.out.println(Thread.currentThread().getName() + "先获取到锁");
lock.lock();
t1.start();
}
}
输出日志:
main先获取到锁
t1尝试获取锁
t1获取不到锁
首先想要启用ReentrantLock的锁超时方法需调用其tryLock()方法,这个方法有个重载就是可以带参数tryLock(时间长度,时间单位),没带参数的就表示如果当前没有获得到锁那么就立马放弃,如果是带参数的它就会等待设置的参数时常,如果过了这个时常还是没有获取锁那就主动放弃。这个方法的返回值是一个布尔类型的,返回false表示没有获取到锁,反之,返回true表示获取到了锁。
3.4公平锁
所谓锁的公平性我们不妨先拿synchronized做个简单对比,synchronized其实就是一种不公平的锁,因为当一个线程持有了锁以后其他线程就会被阻塞,当锁的持有者释放了锁以后,这些被阻塞的线程就会一拥而上的去抢锁,睡抢到了谁就是锁的新主人,这就是一种不公平性,因为当线程很多的时候很有可能某个线程会一直抢不到进而一直处在阻塞状态。而所谓公平,这里指的就是一种先到先得的特征,根据线程的先后顺序来让其拥有锁,可以简单理解为先进先出。ReentrantLock其实默认也是不公平锁,但是它的某些构造方法可以让其实现公平锁。
//默认非公平锁,传入false也是非公平锁
ReentrantLock unfairLock = new ReentrantLock();
ReentrantLock unfairLock02 = new ReentrantLock(false);
//在构造方法中传入true即可构造出公平锁
ReentrantLock faiLock = new ReentrantLock(true);
3.5条件变量
条件变量这个概念可能你不太理解,我们在使用synchronized的时候有时候会调用线程的两个方法wait()和notify(),当一个线程获得了锁对象但是由于它缺少一些条件不能正常的执行的时候我们就会调用其wait()方法让其把锁让出来,而线程本身进入到一个叫waitSet的队列中等待,直到条件满足了通过调用notity()方法将其唤醒。这里的这个waitSet就可以简单的理解为是一个条件变量(可以类比理解为一个休息室)。不过synchronized中不管你线程当前是因为什么原因、缺少什么样的条件,他都会不加区分的将线程统一放到一个waitSet中,与synchronized不同的是ReentrantLock支持按线程缺少条件的不同,把线程放到不同的waitSet中。我们通过一个例子简单理解一下条件变量:假设有两个线程,其中一个必须抽着烟才能干活,而另一个线程必须吃了外卖才能干活,如果没满足有烟或有外卖的条件,那么线程一或二就必须进入休息室等待条件满足才可执行。我们用代码描述一下:
public class ReentrantLockDemo04 {
static ReentrantLock ROOM = new ReentrantLock();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
//等烟的休息室
static Condition waitCigaretteSet = ROOM.newCondition();
//等外卖的休息室
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) throws InterruptedException {
//线程1,模拟等烟的线程
new Thread(new Runnable() {
@Override
public void run() {
ROOM.lock();
try{
System.out.println(Thread.currentThread().getName()+"问有烟吗? " + hasCigarette);
//利用循环不断的去检测是否满足了条件:烟到了
while (!hasCigarette){
System.out.println(Thread.currentThread().getName()+"没有获取到烟,先歇会儿");
//进入专门等烟的休息室去等待
waitCigaretteSet.await();
}
//跳出了while循环,说明有烟了,可以开始干活了
if(hasCigarette){
System.out.println(Thread.currentThread().getName()+"开始干活儿...");
}else {
System.out.println(Thread.currentThread().getName()+"没干成活儿...");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ROOM.unlock();
}
}
},"小男").start();
//线程2,模拟等外卖的线程
new Thread(new Runnable() {
@Override
public void run() {
ROOM.lock();
try{
System.out.println(Thread.currentThread().getName()+"问外卖送到了吗? " + hasTakeout);
//利用循环不断的去检测是否满足了条件:外卖到了
while (!hasTakeout){
System.out.println(Thread.currentThread().getName()+"没有饭,先歇会儿");
//进入专门等饭的休息室去等待
waitTakeoutSet.await();
}
//跳出了while循环,说明外卖到了,可以开始干活了
if(hasTakeout){
System.out.println(Thread.currentThread().getName()+"开始干活儿...");
}else {
System.out.println(Thread.currentThread().getName()+"没干成活儿...");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ROOM.unlock();
}
}
},"小女").start();
//1s以后,送外卖的来了
Thread.sleep(1000);
//线程3,模拟送外卖的线程
new Thread(new Runnable() {
@Override
public void run() {
ROOM.lock();
try{
//把外卖送到专门等饭的休息室
hasTakeout = true;
waitTakeoutSet.signal();
}finally {
ROOM.unlock();
}
}
},"送外卖的").start();
//再等1s以后,送烟的来了
Thread.sleep(1000);
//线程3,模拟送外卖的线程
new Thread(new Runnable() {
@Override
public void run() {
ROOM.lock();
try{
//把烟送到专门等烟的休息室
hasCigarette = true;
waitCigaretteSet.signal();
}finally {
ROOM.unlock();
}
}
},"送烟的").start();
}
}
输出结果:
小男问有烟吗? false
小男没有获取到烟,先歇会儿
小女问外卖送到了吗? false
小女没有饭,先歇会儿
小女开始干活儿...
小男开始干活儿...
代码的详细含义我已在注释中做了比较详细的解析,又看不懂的就留言吧,这就是关于ReentrantLock最基本的用法和一些特性,以后有时间在深入其内部写一些关于其源码层面的和实现原理方面的文章。