Java高并发系列——ReentrantLock
ReentrantLock重入锁
synchronized的局限性
synchronized是java内置的关键字,它提供了一种独占的加锁方式。synchronized的获取和释放锁由jvm实现,用户不需要显示的释放锁,非常方便,然而synchronized也有一定的局限性,例如:
- 当线程尝试获取锁的时候,如果获取不到锁会一直阻塞,这个阻塞的过程,用户无法控制。(Synchronized不可中断的说法:只有获取到锁之后才能中断,等待锁时不可中断。)
- 如果获取锁的线程进入休眠或者阻塞,除非当前线程异常,否则其他线程尝试获取锁必须一直等待。(synchronized不能响应中断?)
ReentrantLock
ReentrantLock是Lock的默认实现,在聊ReentranLock之前,我们需要先弄清楚一些概念:
- 可重入锁:可重入锁是指同一个线程可以多次获得同一把锁;ReentrantLock和关键字Synchronized都是可重入锁
- 可中断锁:可中断锁是指线程在获取锁的过程中,是否可以响应线程中断操作。synchronized是不可中断的,ReentrantLock是可中断的
- 公平锁和非公平锁:公平锁是指多个线程尝试获取同一把锁的时候,获取锁的顺序按照线程到达的先后顺序获取,而不是随机插队的方式获取。synchronized是非公平锁,而ReentrantLock是两种都可以实现,不过默认是非公平锁。
ReentrantLock基本使用
ReentrantLock的使用过程:
- 创建锁:ReentrantLock lock = new ReentrantLock();
- 获取锁:lock.lock()
- 释放锁:lock.unlock();
对比上面的代码,与关键字synchronized相比,ReentrantLock锁有明显的操作过程,开发人员必须手动的指定何时加锁,何时释放锁,正是因为这样手动控制,ReentrantLock对逻辑控制的灵活度要远远胜于关键字synchronized,上面代码需要注意lock.unlock()一定要放在finally中,否则若程序出现了异常,锁没有释放,那么其他线程就再也没有机会获取这个锁了。
ReentrantLock是可重入锁
假如ReentrantLock是不可重入的锁,那么同一个线程第2次获取锁的时候由于前面的锁还未释放而导致死锁,程序是无法正常结束的。
- lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁了;可以将add中的unlock删除一个事实,上面代码运行将无法结束
- unlock()方法放在finally中执行,保证不管程序是否有异常,锁必定会释放
示例:
public class ReentrantLockTest {
private static int num = 0;
private static Lock lock = new ReentrantLock();
public static void add() {
lock.lock();
lock.lock();
try {
num++;
} finally {
//lock()方法和unlock()方法需要成对出现,锁了几次,也要释放几次,否则后面的线程无法获取锁
lock.unlock();
lock.unlock();
}
}
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
ReentrantLockTest.add();
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("get num =" + num);
}
}
//输出: get num =3000
ReentrantLock实现公平锁
在大多数情况下,锁的申请都是非公平的。这就好比买票不排队,上厕所不排队。最终导致的结果是,有些人可能一直买不到票。而公平锁,它会按照到达的先后顺序获得资源。公平锁的一大特点是不会产生饥饿现象,只要你排队,最终还是可以等到资源的;synchronized关键字默认是有jvm内部实现控制的,是非公平锁。而ReentrantLock运行开发者自己设置锁的公平性,可以实现公平和非公平锁。
看一下jdk中ReentrantLock的源码,2个构造方法:
public ReentrantLock() { sync = new NonfairSync();}
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
默认构造方法创建的是非公平锁。
第2个构造方法,有个fair参数,当fair为true的时候创建的是公平锁,公平锁看起来很不错,不过要实现公平锁,系统内部肯定需要维护一个有序队列,因此公平锁的实现成本比较高,性能相对于非公平锁来说相对低一些。因此,在默认情况下,锁是非公平的,如果没有特别要求,则不建议使用公平锁。
示例:
public class ReentrantLockFairTest {
private static int num = 0;
//private static Lock lock = new ReentrantLock(false);
private static Lock lock = new ReentrantLock(true);
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+" got lock");
} finally {
lock.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
T t3 = new T("t3");
t1.start();
t2.start();
t3.start();
}
}
输出:
公平锁:
t1 got lock
t1 got lock
t2 got lock
t2 got lock
t3 got lock
t3 got lock
非公平锁:
t1 got lock
t3 got lock
t3 got lock
t2 got lock
t2 got lock
t1 got lock
ReentrantLock获取锁的过程是可中断的——使用lockInterruptibly()和tryLock(long time, TimeUnit unit)有参方法时。
对于synchronized关键字,如果一个线程在等待获取锁,最终只有2种结果:
- 要么获取到锁然后继续后面的操作
- 要么一直等待,直到其他线程释放锁为止
而ReentrantLock提供了另外一种可能,就是在等的获取锁的过程中(发起获取锁请求到还未获取到锁这段时间内)是可以被中断的,也就是说在等待锁的过程中,程序可以根据需要取消获取锁的请求。拿李云龙平安县围点打援来说,当平安县城被拿下后,鬼子救援的部队再尝试救援已经没有意义了,这时候要请求中断操作。
关于获取锁的过程中被中断,注意几点:
- ReentrankLock中必须使用实例方法 lockInterruptibly()获取锁时,在线程调用interrupt()方法之后,才会引发 InterruptedException异常
- 线程调用interrupt()之后,线程的中断标志会被置为true
- 触发InterruptedException异常之后,线程的中断标志有会被清空,即置为false
- 所以当线程调用interrupt()引发InterruptedException异常,中断标志的变化是:false->true->false
实例:
public class InterruptTest2 {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
public static class T1 extends Thread {
int lock;
public T1(String name, Integer lock) {
super(name);
this.lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
lock1.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock2.lockInterruptibly();
} else {
lock2.lockInterruptibly();
TimeUnit.SECONDS.sleep(1);
lock1.lockInterruptibly();
}
} catch (InterruptedException e) {
//线程发送中断信号触发InterruptedException异常之后,中断标志将被清空。
System.out.println(this.getName() + "中断标志:" + this.isInterrupted());
e.printStackTrace();
} finally {
//ReentrantLock自有的方法,多态实现的Lock不能用
if (lock1.isHeldByCurrentThread()) {
lock1.unlock();
}
if (lock2.isHeldByCurrentThread()) {
lock2.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T1 t1 = new T1("thread1", 1);
T1 t2 = new T1("thread2", 2);
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(1000);
//不加interrupt()通过jstack查看线程堆栈信息,发现2个线程死锁了
//"thread2":
// waiting for ownable synchronizer 0x000000076b782028, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
// which is held by "thread1"
//"thread1":
// waiting for ownable synchronizer 0x000000076b782058, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
// which is held by "thread2"
t1.interrupt();
}
}
ReentrantLock锁申请等待限时
ReentrantLock刚好提供了这样功能,给我们提供了获取锁限时等待的方法 tryLock()
,可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。
tryLock无参方法——tryLock()是立即响应的,中间不会有阻塞。
看一下源码中tryLock方法:
public boolean tryLock()
tryLock有参方法
该方法在指定的时间内不管是否可以获取锁,都会返回结果,返回true,表示获取锁成功,返回false表示获取失败。 此方法在执行的过程中,如果调用了线程的中断interrupt()方法,会触发InterruptedException异常。
可以明确设置获取锁的超时时间:
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
关于tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,说明一下:
- 都会返回boolean值,结果表示获取锁是否成功。
- tryLock()方法,不管是否获取成功,都会立即返回;而有参的tryLock方法会尝试在指定的时间内去获取锁,中间会阻塞的现象,在指定的时间之后会不管是否能够获取锁都会返回结果。
- tryLock()方法不会响应线程的中断方法;而有参的tryLock方法会响应线程的中断方法,而出发
InterruptedException
异常,这个从2个方法的声明上可以可以看出来。
ReentrantLock其他常用的方法
- isHeldByCurrentThread:实例方法,判断当前线程是否持有ReentrantLock的锁,上面代码中有使用过。
获取锁的4种方法对比
获取锁的方法 | 是否立即响应(不会阻塞) | 是否响应中断 |
---|---|---|
lock() | × | × |
lockInterruptibly() | × | √ |
tryLock() | √ | × |
tryLock(long timeout, TimeUnit unit) | × | √ |
实例:
public class ReentrantLockTest1 {
private static ReentrantLock lock1 = new ReentrantLock();
public static class T extends Thread {
public T(String name) {
super(name);
}
@Override
public void run() {
try {
System.out.println(this.getName()+"尝试获取锁");
if (lock1.tryLock(2,TimeUnit.SECONDS)){
System.out.println(this.getName()+"获取锁成功");
TimeUnit.SECONDS.sleep(3);
}else {
System.out.println(this.getName()+"获取锁失败");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if(lock1.isHeldByCurrentThread()){
lock1.unlock();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
T t1 = new T("t1");
T t2 = new T("t2");
t1.start();
t2.start();
}
}
输出:
lock1.tryLock()
t1尝试获取锁
t1获取锁成功
t2尝试获取锁
t2获取锁失败
lock1.tryLock(2,TimeUnit.SECONDS)
t1尝试获取锁
t2尝试获取锁
t1获取锁成功
t2获取锁失败
总结
- ReentrantLock可以实现公平锁和非公平锁
- ReentrantLock默认实现的是非公平锁
- ReentrantLock的获取锁和释放锁必须成对出现,锁了几次,也要释放几次
- 释放锁的操作必须放在finally中执行
- lockInterruptibly()实例方法可以响应线程的中断方法,调用线程的interrupt()方法时,lockInterruptibly()方法会触发
InterruptedException
异常 - 关于
InterruptedException
异常说一下,看到方法声明上带有throwsInterruptedException
,表示该方法可以相应线程中断,调用线程的interrupt()方法时,这些方法会触发InterruptedException
异常,触发InterruptedException时,线程的中断中断状态会被清除。所以如果程序由于调用interrupt()
方法而触发InterruptedException
异常,线程的标志由默认的false变为ture,然后又变为false - 实例方法tryLock()获会尝试获取锁,会立即返回,返回值表示是否获取成功
- 实例方法tryLock(long timeout, TimeUnit unit)会在指定的时间内尝试获取锁,指定的时间内是否能够获取锁,都会返回,返回值表示是否获取锁成功,该方法会响应线程的中断
疑问
Q:可中断锁:可中断锁时线程在获取锁的过程中,是否可以相应线程中断操作。为什么synchronized是不可中断的,ReentrantLock是可中断的?
首先,只有获取到锁之后才能中断,等待锁时不可中断。
查看Thread.interrupt()
源码发现,这里面的操作只是做了修改一个中断状态值为true,并没有显式声明抛出InterruptedException
异常。因此:
- 若线程被中断前,如果该线程处于非阻塞状态(未调用过
wait
,sleep
,join
方法),那么该线程的中断状态将被设为true, 除此之外,不会发生任何事。 - 若线程被中断前,该线程处于阻塞状态(调用了
wait
,sleep
,join
方法),那么该线程将会立即从阻塞状态中退出,并抛出一个InterruptedException
异常,同时,该线程的中断状态被设为false, 除此之外,不会发生任何事。
所以说,Synchronized
锁此时为轻量级锁或重量级锁,此时等待线程是在自旋运行或者已经是重量级锁导致的阻塞状态了(非调用了wait
,sleep
,join
等方法的阻塞),只把中断状态设为true,没有抛出异常真正中断。
而ReentrantLock.lockInterruptibly()
首次尝试获取锁之前就会判断是否应该中断,如果没有获取到锁,在自旋等待的时候也会继续判断中断状态。(代码里会判断中断状态,所有会响应中断。)
JUC中的Condition对象
Condition使用简介——实现等待/通知机制
注意:在使用使用Condition.await()方法时,需要先获取Condition对象关联的ReentrantLock的锁;就像使用Object.wait()时必须在synchronized同步代码块内。
从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:
- Condition能够支持不响应中断,而通过使用Object方式不支持
- Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个
- Condition能够支持超时时间的设置,而Object不支持
Condition由ReentrantLock对象创建,并且可以同时创建多个,Condition接口在使用前必须先调用ReentrantLock的lock()方法获得锁,之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Condition的signal()方法唤醒线程,使用方式和wait()、notify()类似。
需要注意的时,当一个线程被signal()方法唤醒线程时,它第一个动作是去获取同步锁,注意这一点,而这把锁目前在调用signal()方法唤醒他的线程上,必须等其释放锁后才能得到争抢锁的机会。
实例:
public class ConditionTest {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
public static void main(String[] args) {
T1 t1 = new T1("TT1");
T2 t2 = new T2("TT2");
t1.start();
t2.start();
}
static class T1 extends Thread {
public T1(String name) {
super(name);
}
@Override
public void run() {
lock.lock();
System.out.println(this.getName() + " start");
try {
System.out.println(this.getName() + " wait");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
System.out.println(this.getName() + " end");
}
}
static class T2 extends Thread {
public T2(String name) {
super(name);
}
@Override
public void run() {
lock.lock();
System.out.println(this.getName() + " start");
System.out.println(this.getName() + " signal");
condition.signal();
System.out.println(this.getName() + " end");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName() + " end,2 second later");
}
}
}
输出:
TT1 start
TT1 wait
TT2 start
TT2 signal
TT2 end
TT2 end,2 second later
Condition常用方法
和Object中wait类似的方法
- void await() throws InterruptedException:当前线程进入等待状态,如果在等待状态中被中断会抛出被中断异常;
- long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;
- boolean await(long time, TimeUnit unit) throws InterruptedException:同第二种,支持自定义时间单位,false:表示方法超时之后自动返回的,true:表示等待还未超时时,await方法就返回了(超时之前,被其他线程唤醒了)
- boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间
- void awaitUninterruptibly();:当前线程进入等待状态,不会响应线程中断操作,只能通过唤醒的方式让线程继续
和Object的notify/notifyAll类似的方法
- void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。
- void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程
Condition.await()过程中被打断
调用condition.await()之后,线程进入阻塞中,调用t1.interrupt(),给t1线程发送中断信号,await()方法内部会检测到线程中断信号,然后触发 InterruptedException
异常,线程中断标志被清除。从输出结果中可以看出,线程t1中断标志的变换过程:false->true->false
await(long time, TimeUnit unit)超时之后自动返回
t1线程等待2秒之后,自动返回继续执行,最后await方法返回false,await返回false表示超时之后自动返回
await(long time, TimeUnit unit)超时之前被唤醒
t1线程中调用 condition.await(5,TimeUnit.SECONDS);
方法会释放锁,等待5秒,主线程休眠1秒,然后获取锁,之后调用signal()方法唤醒t1,输出结果中发现await后过了1秒(1、3行输出结果的时间差),await方法就返回了,并且返回值是true。true表示await方法超时之前被其他线程唤醒了。
long awaitNanos(long nanosTimeout)超时返回
t1调用await方法等待5秒超时返回,返回结果为负数,表示超时之后返回的。
//awaitNanos参数为纳秒,可以调用TimeUnit中的一些方法将时间转换为纳秒。
long nanos = TimeUnit.SECONDS.toNanos(2);
waitNanos(long nanosTimeout)超时之前被唤醒
t1中调用await休眠5秒,主线程休眠1秒之后,调用signal()唤醒线程t1,await方法返回正数,表示返回时距离超时时间还有多久,将近4秒,返回正数表示,线程在超时之前被唤醒了。
其他几个有参的await方法和无参的await方法一样,线程调用interrupt()方法时,这些方法都会触发InterruptedException异常,并且线程的中断标志会被清除。
同一个锁支持创建多个Condition
使用两个Condition来实现一个阻塞队列的例子:
public class MyBlockingQueue<E> {
//阻塞队列最大容量
private int size;
//队列底层实现
private LinkedList<E> list = new LinkedList<>();
private static Lock lock = new ReentrantLock();
//队列满时的等待条件
private static Condition fullFlag = lock.newCondition();
//队列空时的等待条件
private static Condition emptyFlag = lock.newCondition();
public MyBlockingQueue(int size) {
this.size = size;
}
public void enqueue(E e) throws InterruptedException {
lock.lock();
try {
//队列已满,在fullFlag条件上等待
while (list.size() == size) {
fullFlag.await();
}
//入队:加入链表末尾
list.add(e);
System.out.println("生产了" + e);
//通知在emptyFlag条件上等待的线程
emptyFlag.signal();
} finally {
lock.unlock();
}
}
public E dequeue() throws InterruptedException {
lock.lock();
try {
while (list.size() == 0) {
emptyFlag.await();
}
E e = list.removeFirst();
System.out.println("消费了" + e);
//通知在fullFlag条件上等待的线程
fullFlag.signal();
return e;
} finally {
lock.unlock();
}
}
/**
* 创建了一个阻塞队列,大小为3,队列满的时候,会被阻塞,等待其他线程去消费,队列中的元素被消费之后,会唤醒生产者,生产数据进入队列。上面代码将队列大小置为1,可以实现同步阻塞队列,生产1个元素之后,生产者会被阻塞,待消费者消费队列中的元素之后,生产者才能继续工作。
* @param args
*/
public static void main(String[] args) {
MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(1);
for (int i = 0; i < 10; i++) {
int finalI = i;
Thread producer = new Thread(() -> {
try {
queue.enqueue(finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
}
for (int i = 0; i < 10; i++) {
Thread consumer = new Thread(() -> {
try {
queue.dequeue();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
consumer.start();
}
}
}
输出:
生产了0
消费了0
生产了1
消费了1
。。。。
生产了9
消费了9
Object的监视器方法与Condition接口的对比
注意同步队列和等待队列的区别,同步队列表示在竞争一把锁的队列中,是处于阻塞或运行状态的队列。
而等待队列是指被置为等待、超时等待状态的线程,这些是没有竞争锁的权限的,处于等待被唤醒的状态中。
对比项 | Object 监视器方法 | Condition |
---|---|---|
前置条件 | 获取对象的锁 | 调用Lock.lock获取锁,调用Lock.newCondition()获取Condition对象 |
调用方式 | 直接调用,如:object.wait() | 直接调用,如:condition.await() |
等待队列个数 | 一个 | 多个,使用多个condition实现 |
当前线程释放锁并进入等待状态 | 支持 | 支持 |
当前线程释放锁进入等待状态中不响应中断 | 不支持 | 支持 |
当前线程释放锁并进入超时等待状态 | 支持 | 支持 |
当前线程释放锁并进入等待状态到将来某个时间 | 不支持 | 支持 |
唤醒等待队列中的一个线程 | 支持 | 支持 |
唤醒等待队列中的全部线程 | 支持 | 支持 |
总结
- 使用condition的步骤:创建condition对象,获取锁,然后调用condition的方法
- 一个ReentrantLock支持创建多个condition对象
- void await() throws InterruptedException;方法会释放锁,让当前线程等待,支持唤醒,支持线程中断
- void awaitUninterruptibly();方法会释放锁,让当前线程等待,支持唤醒,不支持线程中断
- long awaitNanos(longnanosTimeout)throws InterruptedException;参数为纳秒,此方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为负数;超时之前被唤醒返回的,结果为正数(表示返回时距离超时时间相差的纳秒数)
- boolean await (longtime,TimeUnitunit)throws InterruptedException;方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为false;超时之前被唤醒返回的,结果为true
- boolean awaitUntil(Datedeadline)throws InterruptedException;参数表示超时的截止时间点,方法会释放锁,让当前线程等待,支持唤醒,支持中断。超时之后返回的,结果为false;超时之前被唤醒返回的,结果为true
- void signal();会唤醒一个等待中的线程,然后被唤醒的线程会被加入同步队列,去尝试获取锁
- void signalAll();会唤醒所有等待中的线程,将所有等待中的线程加入同步队列,然后去尝试获取锁
疑问:
Q:Condition能够支持超时时间的设置,而Object不支持。Object不是有wait(long timeout)超时时间设置么?
网友评论