翻译成中文,ReentrantLock表示可重入锁,与synchronized一样,都是属于可重入锁。
与synchronized相比具有如下特点:
- 可中断:synchronized只能等待同步代码块执行结束,不可以中断,而reentrantlock可以调用线程的interrupt方法来中断等待,继续执行下面的代码。
- 可以设置超时时间:调用lock.trylock(),如果没有设置等待时间的话,没获取到锁,将返回false
- 可以设置为公平锁:公平锁其实是为了解决饥饿问题,当一个线程由于优先级太低的时候,就可能没有办法获取到时间片
- 可以支持多个变量:类似于调用wait方法时,不满足条件的线程进入waitset队列等待CPU随机调度,支持多个变量表示支持多个类似自定义waitset,这样就可以指定对象来唤醒了。
基本语法:
//获取锁
reentrantLock.lock();
try{
//临界区
}finally{
//释放锁
reentrantLock.unlock();
}
1. ReentrantLock可重入
同一个线程 如果首次获取到该锁资源,则它就有权力再次获取到该锁,这就是锁的重入!
public class LockInterruptibly {
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
m1();
}
public static void m1(){
reentrantLock.lock();
try {
log.debug("m1 lock");
m2();
}finally {
reentrantLock.unlock();
}
}
public static void m2(){
reentrantLock.lock();
try {
log.debug("m2 lock");
m3();
}finally {
reentrantLock.unlock();
}
}
public static void m3(){
reentrantLock.lock();
try {
log.debug("m3 lock");
}finally {
reentrantLock.unlock();
}
}
}
输出:
2. ReentrantLock可中断
可中断指的是在尝试获取锁的过程中,可以中断该过程,并且执行相关业务。
public class LockInterruptibly {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
log.debug("t1尝试获取锁");
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
log.debug("获取锁过程被打断");
return;
}
try {
log.debug("t1 获取到锁");
}finally {
reentrantLock.unlock();
}
}, "t1");
//优先t1上锁
reentrantLock.lock();
log.debug("main获取到锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
输出:
可见在获取锁的过中被打断后,程序return停止
注意我们这边使用的是可中断上锁lockInterruptibly
,如果使用了普通的lock()
public class LockInterruptibly {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
// try {
log.debug("t1尝试获取锁");
reentrantLock.lock();
// } catch (InterruptedException e) {
// log.debug("获取锁过程被打断");
// return;
// }
try {
log.debug("t1 获取到锁");
}finally {
reentrantLock.unlock();
}
}, "t1");
//优先t1上锁
reentrantLock.lock();
log.debug("main 获取到锁");
t1.start();
try {
Thread.sleep(1000);
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
程序阻塞,输出:
3. ReentrantLock超时时间
如果没有指定时间,则立即失败(没有获取到锁)
public class LockInterruptibly {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("t1尝试获取锁");
if(!reentrantLock.tryLock()){
log.debug("没有获取到锁");
return;
}
try {
log.debug("t1 获取到锁");
}finally {
reentrantLock.unlock();
}
}, "t1");
//主线程上锁
reentrantLock.lock();
log.debug("main 获取到锁");
t1.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
输出:
在主线程还没释放到锁,t1线程是无法获取到锁的,因此reentrantLock.tryLock()
返回false,执行没有锁的相关业务。
如果有指定时间,在指定最大时间内没有获取到锁则失败
@Slf4j(topic = "c.LockInterruptibly")
public class LockInterruptibly {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("t1尝试获取锁");
try {
if(!reentrantLock.tryLock(2,TimeUnit.SECONDS)){
log.debug("没有获取到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("t1 获取到锁");
}finally {
reentrantLock.unlock();
}
}, "t1");
//主线程上锁
reentrantLock.lock();
log.debug("main 获取到锁");
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
reentrantLock.unlock();
}
}
}
当等待获取锁的时间大于主线程释放锁的时间,则肯定可以获取到锁!!
用所学的tryLock知识,解决一个死锁问题:哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
如果筷子被身边的人拿着,自己就得等待
public class TestDeadLock {
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();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
// 尝试获得右手筷子
synchronized (right) {
eat();
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
程序运行后一会儿会卡住。
使用我们之前的synchronized,就会出现死锁,即每个人某一时刻都只拥有一只筷子,又想要另外一只,此刻都无法得到另外一只,彼此都又不放开自己拥有的筷子,这种现象就是死锁。
使用ReentrantLock解决死锁问题
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
private ReentrantLock reentrantLock = new ReentrantLock();
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
if(left.tryLock()){
try {
if(right.tryLock()){
try {
eat();
}finally {
right.unlock();
}
}
}finally {
left.unlock();
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
注意
//while循环中的一种错误写法
if(left.tryLock()){
if(right.tryLock()){
try {
eat();
}finally {
left.unlock();
right.unlock();
}
}
}
这种错误的写法会造成一些哲学家一直哪些一些筷子不放,也就是锁没有释放,又一直加锁,会导致上锁的次数超过预期的
4. ReentrantLock支持多个变量
支持多个变量类似于synchronized的waitset休息室,当条件不满足时,进入waitset队列等待,ReentrantLock的强大之处在于支持多个自定义的waitset,这样就可以灵活的应对和唤醒指定的线程。
使用要点:
- await前需要获取到锁
- await执行后会释放锁,进入conditionObject等待
- await线程被唤醒(超时或打断后)重新竞争ReentrantLock锁
- 竞争到ReentrantLock后,继续执行await后的代码
看一个例子:
public class TestWaitNotify {
final static Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
//小南
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
//小女等外卖
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
//送外卖
sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify();
}
}, "送外卖的").start();
}
}
输出:
synchronized 的弊端也很明显了,只能有一个休息室,所以唤醒的目标无法明确。
使用ReentrantLock 改进
public class Test17 {
private static ReentrantLock lock = new ReentrantLock();
//等烟休息室
static Condition cigaretteRoom = lock.newCondition();
//等外卖休息室
static Condition eattingRoom = lock.newCondition();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) throws InterruptedException {
//小南
new Thread(() -> {
lock.lock();
try {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
cigaretteRoom.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}finally {
lock.unlock();
}
}, "小南").start();
//小女等外卖
new Thread(() -> {
lock.lock();
try {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
eattingRoom.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}finally {
lock.unlock();
}
}, "小女").start();
//送烟的来了
Thread.sleep(1000);
new Thread(() -> {
lock.lock();
try {
hasCigarette = true;
cigaretteRoom.signal();
}finally {
lock.unlock();
}
}, "送烟的").start();
//送外卖的来了
Thread.sleep(1000);
new Thread(() -> {
lock.lock();
try {
hasTakeout = true;
eattingRoom.signal();
}finally {
lock.unlock();
}
}, "送外卖的").start();
}
}
输出:
需要注意的是
Thread.sleep(1000);
new Thread(() -> {
lock.lock();
try {
hasCigarette = true;
cigaretteRoom.signal();
}finally {
lock.unlock();
}
}, "送烟的").start();
其中
hasCigarette = true;
cigaretteRoom.signal();
必须在获取锁之后才能执行,同时也要注意unlock,这也是相比于synchronized 较为繁琐的。
多个线程的lock会出现堵塞,但是await会暂时让出锁资源,所以可以开断电调试便以观察程序的具体运行过程。
5. ReentrantLock公平性
多线程情况下部分线程无法获取到CPU的时间片,导致线程饥饿问题,ReentrantLock默认是不开启公平规则的,公平性就是为了解决该问题而产生的。
启动公平性
ReentrantLock unFair = new ReentrantLock(true);
看一个例子:
@Slf4j(topic = "c.TestFair")
public class TestFair implements Runnable {
private ReentrantLock reentrantLock; //锁
private static Integer num = 0;
public TestFair(ReentrantLock reentrantLock){
this.reentrantLock = reentrantLock;
}
@Override
public void run() {
while (true){
reentrantLock.lock();
try {
num ++;
log.debug(Thread.currentThread().getName() + num);
}finally {
reentrantLock.unlock();
}
}
}
public static void main(String[] args) {
//公平锁
ReentrantLock fair = new ReentrantLock(true);
new Thread(new TestFair(fair),"t1").start();
new Thread(new TestFair(fair),"t2").start();
//不公平锁
// ReentrantLock unFair = new ReentrantLock(false);
// new Thread(new TestFair(unFair),"t1").start();
// new Thread(new TestFair(unFair),"t2").start();
}
}
公平性下的输出:
线程之间都交替运行。
不公平下的输出:
可见,这就是饥饿问题。
学习资料:
https://www.bilibili.com/video/BV16J411h7Rd?p=127