锁—ReentrantLock(独占锁、可重入锁)
************ 如有侵权请提示删除 ***************
### 概念: java除了使用关键字synchronized外,还可以使用ReentrantLock实现独占锁的功能。而且ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。这篇文章主要是从使用的角度来分析一下ReentrantLock。
简介
ReentrantLock常常对比着synchronized来分析,我们先对比着来看然后再一点一点分析。
(1)synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活。ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。
(2)synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。
(3)synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以相应中断。
ReentrantLock好像比synchronized关键字没好太多,我们再去看看synchronized所没有的,一个最主要的就是ReentrantLock还可以实现公平锁机制。什么叫公平锁呢?也就是在锁上等待时间最长的线程将获得锁的使用权。通俗的理解就是谁排队时间最长谁先执行获取锁
使用
简单使用
我们先给出一个最基础的使用案例,也就是实现锁的功能。
public class TestReentrantLock {
//非公平锁 等价于new ReentrantLock(false)
private static final Lock lock = new ReentrantLock();
public static void main(String[] args) {
//测试-1 若果注释掉1、2处,未释放锁就可能有线程获取到锁
new Thread(() ->test(),"线程A").start();
new Thread(() ->test(),"线程B").start();
new Thread(() ->test(),"线程C").start();
}
public static void test(){
try {
lock.lock(); //1
System.out.println (Thread.currentThread().getName() + "获取了锁");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println (Thread.currentThread().getName() + "释放了锁");
lock.unlock();//2
}
}
}
输出效果:
//test1效果 非公平锁 cpu时间片轮到哪个线程,哪个线程就能获取锁
//线程A获取了锁
//线程A释放了锁
//线程C获取了锁
//线程C释放了锁
//线程B获取了锁
//线程B释放了锁
在这里我们定义了一个ReentrantLock,然后再test方法中分别lock和unlock,运行一边就可以实现我们的功能。这就是最简单的功能实现,代码很简单。我们再看看ReentrantLock和synchronized不一样的地方,那就是公平锁的实现。
公平锁实现
对于公平锁的实现,就要结合着我们的可重入性质了。公平锁的含义我们上面已经说了,就是谁等的时间最长,谁就先获取锁。
public class TestReentrantLock {
//实现公平锁机制
private static final ReentrantLock lock1 = new ReentrantLock(true);
public static void main(String[] args) {
//测试-2 公平锁实现
new Thread(() ->test2(),"线程A").start();
new Thread(() ->test2(),"线程B").start();
new Thread(() ->test2(),"线程C").start();
new Thread(() ->test2(),"线程D").start();
new Thread(() ->test2(),"线程E").start();
}
//公平锁实现
public static void test2(){
for (int i = 0; i < 2; i++) {
try {
lock1.lock();
System.out.println (Thread.currentThread().getName() + "获取了锁");
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock1.unlock();
}
}
}
}
首先new一个ReentrantLock的时候参数为true,表明实现公平锁机制。在这里我们多定义几个线程ABCDE,然后再test方法中循环执行了两次加锁和解锁的过程。
输出效果:
//test2效果 公平锁
//线程A获取了锁
//线程B获取了锁
//线程C获取了锁
//线程D获取了锁
//线程E获取了锁
//线程A获取了锁
//线程B获取了锁
//线程C获取了锁
//线程D获取了锁
//线程E获取了锁
非公平锁实现
非公平锁那就随机的获取,谁运气好,cpu时间片轮到哪个线程,哪个线程就能获取锁,和上面公平锁的区别很简单,就在于先new一个ReentrantLock的时候参数为false,当然我们也可以不写,默认就是false。直接测试一下
输出效果:
//test2效果 非公平锁 整个过程随机的,没有先后
//线程A获取了锁
//线程B获取了锁
//线程B获取了锁
//线程C获取了锁
//线程C获取了锁
//线程D获取了锁
//线程D获取了锁
//线程E获取了锁
//线程A获取了锁
//线程E获取了锁
响应中断
中断响应是锁申请等待过程中可以放弃等待转去处理中断
响应中断就是一个线程获取不到锁,不会一直等下去,ReentrantLock会给予一个中断回应。在这里我们举一个死锁的案例。
首先我们定义一个测试类ReentrantLockTest3。
public class TestReentrantLock {
public static void main(String[] args) {
Thread thread1 = new Thread(new ThreadDemo(1), "thread1");
Thread thread2 = new Thread(new ThreadDemo(2), "thread2");
thread1.start();
thread2.start();
thread2.interrupt();//中断第一个线程
}
static class ThreadDemo implements Runnable{
//实例化两把锁
public static ReentrantLock firstLock = new ReentrantLock();
public static ReentrantLock secondLock = new ReentrantLock();
int lock = 0;//标识先请求那把锁
//控制加锁顺序
public ThreadDemo(int lock) {
lock = lock;
}
@Override
public void run() {
try {
if (lock == 1) {
//lockInterruptibly()是对中断进行响应的申请动作
firstLock.lockInterruptibly();
//假设先获得firstLock锁,睡一秒,再请求secondLock锁
TimeUnit.MILLISECONDS.sleep(2000);
secondLock.lockInterruptibly();
System.out.println(Thread.currentThread().getName()
+ "执行完成");
} else {
//lockInterruptibly()是对中断进行响应的申请动作
secondLock.lockInterruptibly();
//假设先获得firstLock锁,睡一秒,再请求secondLock锁
TimeUnit.MILLISECONDS.sleep(2000);
firstLock.lockInterruptibly();
System.out.println(Thread.currentThread().getName()
+ "执行完成");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//查询当前线程是否保持此锁,此方法通常用于调试和测试
if (firstLock.isHeldByCurrentThread()) {
firstLock.unlock();
}
if(secondLock.isHeldByCurrentThread()){
secondLock.unlock();
}
System.out.println(Thread.currentThread().getName()
+ "线程退出");
}
}
}
}
代码要做的就是实例化两个线程,线程1先申请lock1锁,睡眠一秒后再申请lock2锁;线程2则相反,先申请lock2锁,完了睡眠一秒再申请lock1锁。当两个线程都执行后,会造成死锁,线程1占有lock1锁同时申请lock2锁,线程2占有lock2锁同时申请lock1锁, 此时两个线程产生死锁,不过在主线程中第70行我们对线程2做中断操作,线程2会放弃对锁lock1的申请,同时释放锁lock2,所以线程1会得到锁lock2,并成功退出。
输出结果如下:
thread1执行完成
thread1线程退出
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.mmz.tkp.controller.thread.locks.reentrantlock.TestReentrantLock$ThreadDemo.run(TestReentrantLock.java:128)
at java.lang.Thread.run(Thread.java:748)
thread2线程退出
由输出结果可以看出,线程2没有输出执行完成的语句,也就是说线程2并没有获得锁lock1,在线程2响应中断后,放弃了对锁lock1的申请而直接执行下面的语句,也就是输出退出语句。
线程1获得锁lock1后申请锁lock2,虽然产生了死锁,但线程2后来放弃了锁lock1申请,并释放了锁lock2,所以线程1可以得到锁lock2,输出执行完成的语句。
限时等待
等待限时是线程申请锁时给定一个等待时间,如果等待时间过后线程还没能拿到锁,那么线程就停止等待。
像短作业优先调度算法中有可能出现的饥饿现象,给定锁一个等待限时是很有用的。想要在申请锁时给定一个限时,就要用到重入锁中的方法tryLock(long time,TimeUnit unit),两个参数,一个表示时长,一个表示时间单位,看下面这段代码:
public class TestReentrantLock {
public static void main(String[] args) {
ThreadDemo1 threadDemo1 = new ThreadDemo1();
Thread thread1 = new Thread(threadDemo1,"thread1");
Thread thread2 = new Thread(threadDemo1,"thread2");
thread1.start();
thread2.start();
}
static class ThreadDemo1 implements Runnable {
//实例化一把重入锁
public static ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
try {
//尝试获取锁,拿不到3秒后再试
if (lock.tryLock(3, TimeUnit.SECONDS)) {
System.out.println("Thread " + Thread.currentThread().getId() + " get lock successful.");
//拿到锁后,睡5秒,也就是占有锁5秒
TimeUnit.SECONDS.sleep(5);
} else {
System.out.println("Thread " + Thread.currentThread().getId() + " get lock failed.");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
}
对于上面的代码,tryLock()方法中等待时长为3,时间单位是秒,也就是申请锁等待时间最长为3秒。线程1获得锁lock后会输出成功语句然后睡眠5秒,此时线程2申请锁lock2就会失败,在等待5秒后因为还得不到锁而输出失败语句:
Thread 13 get lock successful.
Thread 14 get lock failed.
ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间, 无参则表示立即返回锁申请的结果: true 表示获取锁成功, false 表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。