一、简介
相对于 synchronized 来说它具有可中断、可以设置超时时间、可以设置公平锁、支持多个条件变量等特点。
并且和 synchronized 一样,都支持可重入。
可中断的意思是:ReentrantLock 可以被其他线程中断,而synchronized 加上锁之后是不能中断的,即不能使用其他的线程或者语法给这锁给中断掉。
可以设置超时时间的意思是:ReentrantLock 支持等待一段时间后就放弃锁的争抢,而 synchronized 获取锁时如果对方持有锁,那它就会到 entryList 里面等待去了。
可以设置公平锁的意思是:ReentrantLock 支持先到先得,而 synchronized 不支持先到先得。
可以支持多个条件变量的意思是:条件变量就相当于 synchronized 的 Monitor 中的 waitSet,即当条件不满足时线程需要到 waitSet 中等待,即条件不满足时线程呆的一个地方。ReentrantLock 支持多个条件变量的意思就是有好多个 waitSet,即不满足条件一的线程到 waitSet1 中等待,不满足条件二的到线程到 waitSet2 中等待。而 synchronized 只支持一个条件变量。
二、语法
ReentrantLock 的语法如下:
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
三、特性演示
3.1 可重入
可重入是指同一个线程如果首次获得了这把锁,那么它就是这把锁的拥有者,它就有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。如下代码:
public class MyServlet {
static Lock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
System.out.println("execute main");
m1();
} finally {
lock.unlock();
}
}
public static void m1() {
lock.lock();
try {
System.out.println("execute m1");
m2();
} finally {
lock.unlock();
}
}
public static void m2() {
lock.lock();
try {
System.out.println("execute m2");
} finally {
lock.unlock();
}
}
}
3.2 可打断
线程在等待锁释放的过程中,可以被其他的线程打断,这种可打断锁存在的意义就是防止线程无限制的等待下去,也是一种避免死锁的做法。测试代码如下:
public class MyServlet {
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
try{
// 可打断需要调用 lock 的 lockInterruptibly() 方法
// 如果没有竞争那么此方法会获取 lock 对象
// 如果有竞争就进入阻塞队列,可以被其他线程调用 interrupt() 方法打断
System.out.println("t1 线程尝试获取锁...");
lock.lockInterruptibly();
}catch (InterruptedException e){
e.printStackTrace();
System.out.println("t1 线程没有获取到锁,返回");
return;
}
try{
System.out.println("t1线程获取到锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
System.out.println("主线程获得了锁");
t1.start();
try {
Thread.sleep(1);
t1.interrupt();
System.out.println("执行打断");
} finally {
lock.unlock();
}
}
}
3.3 超时释放
上面讲的可打断特性是被动的,是由其他线程调用 interrupt() 方法让线程不要死等下去了。而锁超时是以主动的方式让线程不再死等下去。
第一种使用方式,只尝试一次,失败立刻返回,代码如下:
@Slf4j(topic = "c.test")
public class Main {
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("启动...");
if (!lock.tryLock()) {
log.debug("获取立刻失败,返回");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
Thread.sleep(2);
} finally {
lock.unlock();
}
}
}
第二种方式,可以设置等待的时间,当超过等待的时间没有获取到锁时,也是失败返回。代码如下:
@Slf4j(topic = "c.test")
public class Main {
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
Thread.sleep(2000);
} finally {
lock.unlock();
}
}
}
3.4 公平锁
ReentrantLock 默认是不公平的,其实没有必要设置公平锁,因为设置成公平锁会降低并发度,设置成公平锁的语法如下:
Lock lock = new ReentrantLock(true);
四、条件变量
synchronized 中也有条件变量,就是我们以前说过的 Monitor 中的 waitSet,当条件不满足时进入到 waitSet 等待,它相当于一个休息室。而 ReentrantLock 比 synchronized 强的地方在于,它可以支持多个条件变量,即拥有多个休息室。
举例来说,那些不满足条件的线程在 synchronized 中只有一间吸烟的休息室。而在 ReentrantLock 中有吸烟的休息室还有洗澡的休息室。
创建条件变量的语法如下,需要注意的是调用 await() 时需要先获取到锁,调用完 await() 方法后会释放锁,当前线程进入到休息室去等待,当处于休息室的线程被唤醒后会重新竞争 lock 锁。竞争成功之后,会从 await() 后继续执行。
@Slf4j(topic = "c.test")
public class Main {
static Lock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
// 创建一个香烟条件变量(休息室 waitSet)
Condition cigarette_condition = lock.newCondition();
// 创建一个洗澡条件变量(休息室 waitSet)
Condition bathe_condition = lock.newCondition();
lock.lock();
// 进入到香烟休息室等候
cigarette_condition.await();
// 随机唤醒休息室里面的一条线程
cigarette_condition.signal();
// 唤醒休息室里面的所有线程
cigarette_condition.signalAll();
}
}
五、Lock 类常用方法
# 查询当前线程保持此锁定的个数,也就是调用 lock() 方法的次数。
int getHoldCount()
# 返回正等待获取此锁定的线程估计数。
int getQueueLength()
# 返回等待与此锁定相关的给定条件 Condition 的线程估计数。
int getWaitQueueLength(Condition condition)
# 查询指定的线程是否在等待获取此锁定。
boolean hasQueuedThread(Thread thread)
# 查询是否有线程正在等在与此锁有关的 condition 条件。
boolean hasWaiters(Condition condition)
# 判断是不是公平锁,默认情况下 ReentrantLock 是非公平锁。
boolean isFair()
# 查询当前线程是否保持此锁定。
boolean isHeldByCurrentThread()
# 查询此锁是是否由任意线程保持。
boolean isLocked()
# :如果当前线程未被中断,则获取锁定,如果已经被中断则出现异常。
void lockInterruptibly()
# 仅在调用时锁定未被另一个线程保持的情况下,才获取该锁定。
boolean tryLock()
# 如果锁定在给定等待时间内没有被另外一个线程保持,且当当前线程未被中断,则获取该锁定。
boolean tryLock(longtimeout,TimeUnit unit)
六、实现生产者和消费者
接下来我们使用 Lock 和 Condition 来实现生产者和消费者的模型。
// 线程通信的前提一定是保证线程安全,否则线程通信没有任何意义
// 锁是可以夸方法的,底下的 put 和 get 方法用的就是同一把锁(桌子对象),这个锁可以锁住底下的 5 个线程
// 每次只有一个线程可以抢到桌子对象,让一个线程访问 put 或者 get 方法,这样就控制了 5 个线程访问桌子对象是线程安全的
public class Desk {
// 创建的五个线程都会抢同一个桌子对象
private List<String> list = new ArrayList<>();
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
// 放一个包子的方法
// 厨师1、厨师、厨师3都会来竞争的做包子
public void put() {
try {
String name = Thread.currentThread().getName();
lock.lock();
if(list.size() == 0){
list.add(name+"做的肉包子");
System.out.println(name+"做了一个肉包子");
Thread.sleep(2000);
// 唤醒别人,自己等待
condition.signalAll();
}else{
// 有包子,不做了
condition.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
// 取一个包子的方法
// 吃货1、吃货2都会来竞争的吃包子
public synchronized void get(){
try {
String name = Thread.currentThread().getName();
lock.lock();
if(list.size() ==1){
// 有包子,吃了
System.out.println(name+"吃了:"+list.get(0));
list.clear();
Thread.sleep(1000);
// 唤醒别人,自己等待
condition.signalAll();
}else{
// 没有包子,自己等待
condition.signalAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Desk desk = new Desk();
// 创建三个生产者线程(3个厨师)
new Thread(() ->{
while (true){
desk.put();
}
},"厨师1").start();
new Thread(() ->{
while (true){
desk.put();
}
},"厨师2").start();
new Thread(() ->{
while (true){
desk.put();
}
},"厨师3").start();
// 创建两个消费者线程(2个吃货)
new Thread(() ->{
while (true){
desk.get();
}
},"吃货1").start();
new Thread(() ->{
while (true){
desk.get();
}
},"吃货2").start();
}
}