ReentrantLock 相对于 synchronized 它具备如下特点
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: TestReentrantLock
* @projectName hm-juc-codes
* @description: 测试可重入锁
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-22 17:23
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestReentrantLock")
public class TestReentrantLock {
/**
* 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
* 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
*/
static ReentrantLock lock = new ReentrantLock();
public static void method01() {
lock.lock();
try {
log.debug("execute method01");
method02();
} finally {
// 释放锁
lock.unlock();
}
}
private static void method02() {
lock.lock();
try {
log.debug("execute method02");
method03();
} finally {
// 释放锁
lock.unlock();
}
}
private static void method03() {
lock.lock();
try {
log.debug("execute method03");
} finally {
// 释放锁
lock.unlock();
}
}
public static void main(String[] args) {
method01();
}
}
可以看到执行结果, 当前线程在执行时多次获取锁, 并不会被锁挡住, 而是正常运行
可打断就是, 当前线程t1
在等待锁的时候, 可以被其他的线程t2
使用 t1.interrupt()
方法打断.
lock.lockInterruptibly()
尝试获取锁, 并且这个等待锁是可以被打断的
/**
* 除非当前线程被中断,否则获取锁。
* 如果没有被另一个线程持有,则获取锁并立即返回,将锁持有计数设置为 1。
* 如果当前线程已经持有这个锁,那么持有计数加一并且方法立即返回。
* 如果锁被另一个线程持有,那么当前线程将被禁用以用于线程调度目的并处于休眠状态,直到发生以下两种情况之一:
* 锁被当前线程获取;或者
* 其他一些线程中断当前线程。
* 如果当前线程获取了锁,则锁持有计数设置为 1。
* 如果当前线程:
* 在进入此方法时设置其中断状态;或者
* 在获取锁时被中断,
* 然后抛出InterruptedException并清除当前线程的中断状态。
* 在此实现中,由于此方法是显式中断点,因此优先响应中断而不是正常或可重入获取锁。
* 抛出:
* InterruptedException – 如果当前线程被中断
*/
lock.lockInterruptibly();
我们通过下面的代码去演示
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
/**
* @author 王天赐
* @title: TestLockInterruptibly
* @projectName hm-juc-codes
* @description: 测试可打断
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 14:01
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestLockInterruptibly")
public class TestLockInterruptibly {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 尝试获取锁, 并且这个锁是可以被打断的 如果被打断就会抛出异常
// 如果有竞争就进入阻塞队列
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("获取锁的过程中被打断");
return;
}
try {
log.debug("获得锁... ");
} finally {
lock.unlock();
}
}, "t1");
Thread t2 = new Thread(() -> {
try {
lock.lock();
log.debug("获取锁 ... ");
sleep(1);
log.debug("打断t1线程的等待锁的过程!");
t1.interrupt();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, "t2");
t2.start();
t1.start();
}
}
我们可以看到 t2 线程先获取锁, 然后t1线程在等待锁获取的过程中被t2线程打断
注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
修改下上面的代码
Thread t1 = new Thread(() -> {
try {
// 尝试获取锁, 并且这个锁是可以被打断的 如果被打断就会抛出异常
// 如果有竞争就进入阻塞队列
// lock.lockInterruptibly();
lock.lock();
} catch (Exception e) {
e.printStackTrace();
log.debug("没有获得锁, 返回");
return;
}
try {
log.debug("获得锁... ");
} finally {
lock.unlock();
}
}, "t1");
可以看到, 即使执行了 interrupt
但是实际上还是没有打断
锁超时就是, 如果无法获取锁, 不仅如此阻塞队列, 直接结束
// 仅当调用时没有被另一个线程持有时才获取锁。
// 如果没有被另一个线程持有,则获取锁,并立即返回值为true ,将锁持有计数设置为 1。
// 即使此锁已设置为使用公平排序策略,调用tryLock()也会立即获取该锁(如果可用),无论其他线程当前是否正在等待该锁。
// 这种“闯入”行为在某些情况下可能很有用,即使它破坏了公平性。
// 如果您想尊重此锁的公平设置,请使用几乎等效的tryLock(0, TimeUnit.SECONDS) (它也检测中断)。
// 如果当前线程已经持有这个锁,那么持有计数加一并且该方法返回true 。
// 如果锁被另一个线程持有,则此方法将立即返回值为false 。
// 返回:true锁是空闲的并且被当前线程获取,或者锁已经被当前线程持有,则返回 true;否则false
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: TestTimeOutLock
* @projectName hm-juc-codes
* @description: 测试锁超时
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 15:29
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestTimeOutLock")
public class TestTimeOutLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
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 {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
log.debug("释放锁...");
}
}
}
可以看到这里获取锁失败直接就退出了 , 我们也可以使用 tryLock(long, TimeUnit)
方法来设置尝试的时间
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: TestTimeOutLock
* @projectName hm-juc-codes
* @description: 测试锁超时
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 15:29
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestTimeOutLock")
public class TestTimeOutLock {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("尝试获取锁");
try {
if (!lock.tryLock(2, TimeUnit.SECONDS)) {
log.debug("获取锁失败!, 直接跑路");
return;
}
log.debug("获得了锁 !");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获取锁");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
log.debug("释放锁...");
}
}
}
可以看到, 主线程在获得锁后 1s
后释放锁, 而 t1
线程等待2s ,在t1线程释放锁后, 第一时间获取了锁
需要继承 ReentrantLock 类
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: Chopstick
* @projectName hm-juc-codes
* @description: 筷子类
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-22 14:37
*/
@Slf4j(topic = "c.Chopstick")
public class Chopstick extends ReentrantLock {
private String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" +
"name='" + name + '\'' +
'}';
}
}
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @author 王天赐
* @title: Philosopher
* @projectName hm-juc-codes
* @description: 哲学家类
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-22 14:37
*/
@Slf4j(topic = "c.Philosopher")
@SuppressWarnings("all")
public class Philosopher extends Thread {
private Chopstick left;
private Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
public void eat() {
log.debug("eat ... ");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
while (true) {
if (left.tryLock()) {
try {
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
}
可以看到, 我们需要使用 tryLock
方法去获取左筷子和右筷子, 如果获取失败直接结束, 另外在成功获取锁后.
要在 finally 里释放锁
package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
/**
* @author 王天赐
* @title: TestPhilosopher
* @projectName hm-juc-codes
* @description: 哲学家进餐问题
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-22 15:24
*/
@SuppressWarnings("all")
@Slf4j(topic = "TestPhilosopher")
public class TestPhilosopher {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
公平锁一般没有必要,会降低并发度
ReentrantLock 默认是不公平的 , 也就是说并不是按照阻塞队列中先来先得的顺序得到锁的, 随机分配锁的
package cn.knightzz.reentrantlock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author 王天赐
* @title: TestFairLock
* @projectName hm-juc-codes
* @description: 测试公平锁
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 16:31
*/
@SuppressWarnings("all")
public class TestFairLock {
public static void main(String[] args) throws InterruptedException {
// 构造参数设置公平锁
ReentrantLock lock = new ReentrantLock(false);
lock.lock();
for (int i = 0; i < 500; i++) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "t" + i).start();
}
// 1s 之后去争抢锁
Thread.sleep(1000);
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " start...");
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " running...");
} finally {
lock.unlock();
}
}, "强行插入").start();
lock.unlock();
}
}
注意:该实验不一定总能复现
如果是公平锁, 随机插入一定是在最后插入
synchronized
中也有条件变量,就是 waitSet
休息室,当条件不满足时进入 waitSet
等待ReentrantLock
的条件变量比 synchronized
强大之处在于,它是支持多个条件变量的,这就好比 synchronized
是那些不满足条件的线程都在一间休息室等消息,而 ReentrantLock
支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒conditionObject
等待package cn.knightzz.reentrantlock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import static java.lang.Thread.sleep;
/**
* @author 王天赐
* @title: TestConditionLock
* @projectName hm-juc-codes
* @description: 测试条件变量
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @create: 2022-07-23 16:45
*/
@SuppressWarnings("all")
@Slf4j(topic = "c.TestConditionLock")
public class TestConditionLock {
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitBreakfastQueue = lock.newCondition();
static boolean hasCigarette = false;
static boolean hasBreakfast = false;
private static void sendCigarette() {
lock.lock();
try {
log.debug("烟送来了 ...");
hasCigarette = true;
// 唤醒对应waitset阻塞的线程
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
log.debug("早餐送来了 ...");
hasBreakfast = true;
// 唤醒对应waitset阻塞的线程
waitBreakfastQueue.signal();
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
// 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行, 和 synchronized 类似
lock.lock();
while (!hasCigarette) {
// 不满足条件就到对应的 waitSet 等待
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("等到烟了");
} finally {
lock.unlock();
}
}, "小南").start();
new Thread(() -> {
try {
// 如果没有拿到锁的话, 线程就会阻塞在这, 不会向下执行, 和 synchronized 类似
lock.lock();
while (!hasBreakfast) {
// 不满足条件就到对应的 waitSet 等待
try {
waitBreakfastQueue.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("等到早餐了");
} finally {
lock.unlock();
}
}, "小白").start();
sleep(1000);
sendCigarette();
sleep(1000);
sendBreakfast();
}
}