一、概述
ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。
相对于 synchronized 它具备如下特点:
- 可中断(等待获取锁的过程中可以被打断)
- synchronized锁加上去不能中断,a线程应用锁,b线程不能取消掉它
- 可以设置超时时间
- synchronized它去获取锁时,如果对方持有锁,那么它就会进入entryList一直等待下去。而ReentrantLock可以设置超时时间,规定时间内如果获取不到锁,就放弃锁。
- 可以设置为公平锁
- 防止线程饥饿的情况,即先到先得。如果争抢的人比较多,则可能会发生永远都得不到锁。
- 支持多个条件变量(相当于有多个EntryList)
- synchronized只支持同一个waitset。
- 与 synchronized 一样,都支持可重入
二、基本语法
步骤
(1)创建一个ReentrantLock
对象;
(2)调用ReentrantLock
对象的lock()
方法;
(3)将临界区的代码写在try代码块中;
(4)将ReentrantLock
对象的unlock()
方法写在finally代码块中。
1 2 3 4 5 6 7 8 9 10
| private Lock lock = new ReentrantLock();
lock.lock(); try { } finally { lock.unlock(); }
|
- synchronized是在关键字的级别来保护临界区,而reentrantLock是在对象的级别保护临界区。临界区即访问共享资源的那段代码。
finally
中表明不管将来是否出现异常,都会释放锁,释放锁即调用unlock方法。否则无法释放锁,其它线程就永远也获取不了锁。
注意:lock.lock();
与try代码块之间不要有空行或者其它逻辑,且lock.unlock();
要写在finally代码块的第一行。
三、可重入
3.1 概述
- 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁;
- 如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
3.2 代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j(topic = "c.Test03") public class Test03 {
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) { method1(); }
public static void method1(){ lock.lock();
try { log.debug("execute method1"); method2(); } finally { lock.unlock(); } }
public static void method2(){ lock.lock();
try { log.debug("execute method2"); method3(); }finally { lock.unlock(); } }
public static void method3(){ lock.lock();
try { log.debug("execute method3"); }finally { lock.unlock(); } } }
|
运行结果
1 2 3
| 21:58:48.257 c.Test03 [main] - execute method1 21:58:48.259 c.Test03 [main] - execute method2 21:58:48.259 c.Test03 [main] - execute method3
|
从运行结果可以看出,当前线程在执行时多次获取锁, 并不会被锁挡住, 而是正常运行
注意:加锁与解锁是必须匹配的,只有当解锁次数等于加锁次数时,锁才会被正确释放。
四、可打断
4.1 概述
可打断是指, 当前线程在等待锁的时候, 可以被其他的线程使用 interrupt()
方法打断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
方法说明
- **lock.lockInterruptibly()**:尝试获取锁,如果获取不到锁,进入等待;等待过程中可以被打断。
- lock.lock():等待锁的过程是不可以被打断的。
4.2 代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
@Slf4j(topic = "c.Test04") public class Test04 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("start...");
try { lock.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); log.debug("等锁的过程中被打断"); return; }
try { log.debug("获得了锁"); }finally { lock.unlock(); } },"t1");
lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(1); t1.interrupt(); log.debug("执行打断"); } finally { lock.unlock(); } } }
|
运行结果
1 2 3 4 5 6 7 8 9 10
| 22:21:02.967 c.Test04 [main] - 获得了锁 22:21:02.969 c.Test04 [t1] - start... 22:21:03.984 c.Test04 [main] - 执行打断 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.lilinchao.thread.demo06.Test04.lambda$main$0(Test04.java:22) at java.lang.Thread.run(Thread.java:748) 22:21:03.985 c.Test04 [t1] - 等锁的过程中被打断
|
说明
main线程首先获得锁,因此被创建出的线程t1启动后无法获得锁,之后,main线程打断线程t1,使得线程t1结束等待。
4.3 不可中断模式
注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
@Slf4j(topic = "c.Test05") public class Test05 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("start...");
lock.lock(); try { log.debug("获得了锁"); }finally { lock.unlock(); } },"t1");
lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(1); t1.interrupt(); log.debug("执行打断"); sleep(1); } finally { log.debug("释放了锁"); lock.unlock(); } } }
|
运行结果
1 2 3 4 5
| 22:24:52.756 c.Test05 [main] - 获得了锁 22:24:52.758 c.Test05 [t1] - start... 22:24:53.771 c.Test05 [main] - 执行打断 22:24:54.777 c.Test05 [main] - 释放了锁 22:24:54.777 c.Test05 [t1] - 获得了锁
|
五、锁超时
5.1 概述
可打断,是一种被动的打断,需要其他的线程来进行打断。
而锁超时可以通过主动方式,来解决线程无限制的等待下去。如果当前线程在等待了一段时间之后,还没有获取锁,将不在继续等待,继续向下执行。
通过设置获得锁的等待时间,当不能在等待时间内获得锁的时候释放锁,就能够避免死锁的问题。
5.2 设置超时时间API
ReetrantLock提供了两个获取锁并快速返回的方法,不会一直等待,无论成功失败都将立即返回。
5.3 代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
@Slf4j(topic = "c.Test06") public class Test06 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("start..."); if (!lock.tryLock()) { log.debug("获取立即失败,返回"); return; }
try { log.debug("获得了锁"); }finally { lock.unlock(); } },"t1");
lock.lock(); log.debug("获得了锁"); t1.start(); try { sleep(2); }finally { lock.unlock(); } } }
|
运行结果
1 2 3
| 22:40:21.990 c.Test06 [main] - 获得了锁 22:40:21.992 c.Test06 [t1] - start... 22:40:21.992 c.Test06 [t1] - 获取立即失败,返回
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.thread.utils.Sleeper.sleep;
@Slf4j(topic = "c.Test07") public class Test07 { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { log.debug("start..."); 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 { sleep(2); }finally { lock.unlock(); } } }
|
运行结果
1 2 3
| 22:42:41.537 c.Test07 [main] - 获得了锁 22:42:41.539 c.Test07 [t1] - start... 22:42:42.544 c.Test07 [t1] - 获取等待 1s 后失败,返回
|
说明
代码执行时,main线程先获得了锁,进入到2s的睡眠当中,此时t1线程启动,执行到lock.tryLock(1, TimeUnit.SECONDS)
,等待获取到lock锁后继续向下执行。1s后t1线程未获得所,将放弃继续获取锁,t1线程退出。
5.4 锁超时解决哲学家就餐问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| import site.weiyikai.thread.utils.Sleeper; import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.ReentrantLock;
public class Test08 { public static void main(String[] args) { Chopstick2 c1 = new Chopstick2("1"); Chopstick2 c2 = new Chopstick2("2"); Chopstick2 c3 = new Chopstick2("3"); Chopstick2 c4 = new Chopstick2("4"); Chopstick2 c5 = new Chopstick2("5"); new Philosopher2("苏格拉底", c1, c2).start(); new Philosopher2("柏拉图", c2, c3).start(); new Philosopher2("亚里士多德", c3, c4).start(); new Philosopher2("赫拉克利特", c4, c5).start(); new Philosopher2("阿基米德", c5, c1).start(); } }
@Slf4j(topic = "c.Philosopher2") class Philosopher2 extends Thread { Chopstick2 left; Chopstick2 right; public Philosopher2(String name, Chopstick2 left, Chopstick2 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(); } } } } private void eat() { log.debug("eating..."); Sleeper.sleep(1); } }
class Chopstick2 extends ReentrantLock { String name; public Chopstick2(String name) { this.name = name; } @Override public String toString() { return "筷子{" + name + '}'; } }
|
运行结果
1 2 3 4 5 6 7 8 9 10
| 22:56:17.080 c.Philosopher2 [亚里士多德] - eating... 22:56:17.080 c.Philosopher2 [苏格拉底] - eating... 22:56:18.093 c.Philosopher2 [柏拉图] - eating... 22:56:18.093 c.Philosopher2 [赫拉克利特] - eating... 22:56:19.093 c.Philosopher2 [苏格拉底] - eating... 22:56:19.093 c.Philosopher2 [亚里士多德] - eating... 22:56:20.101 c.Philosopher2 [亚里士多德] - eating... 22:56:20.101 c.Philosopher2 [阿基米德] - eating... ........... 程序会一直向下执行,不会产生死锁
|
说明
可以看到,需要使用 tryLock
方法去获取左筷子和右筷子, 如果获取失败直接结束, 另外在成功获取锁后,要在 finally 里释放锁。
六、公平锁
6.1 概念
- 公平锁:是指多个线程按照申请锁的顺序来获取锁,通过队列FIFO,先进先出,类似排队打饭,先来后到。
- 非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。*。*
6.2 语法
ReentrantLock默认是非公平锁
1 2 3 4 5 6 7
| ReentrantLock lock = new ReentrantLock(true); lock.lock(); try { } finally { lock.unlock(); }
|
说明
- 初始化构造函数入参,选择是否为初始化公平锁。
- 其实一般情况下并不需要公平锁,除非你的场景中需要保证顺序性。
- 使用 ReentrantLock 切记需要在 finally 中关闭,lock.unlock()。
公平锁和非公平锁的选择
一点源码
1 2 3
| public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
|
- 构造函数中选择公平锁(FairSync)、非公平锁(NonfairSync)。
6.3 代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo { 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(); } 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(); } }
|
强行插入,有机会在中间输出
运行结果
改为公平锁后
1
| ReentrantLock lock = new ReentrantLock(true);
|
强行插入,总是在最后输出
说明
开启公平锁后,所有的线程在entrylist中按照开始的时间顺序执行,不会出现插队现象,所以公平锁能够解决饥饿现象。不开启公平锁,当上一个线程结束后,随机从entrylist中执行一个线程。
6.4 总结
- 公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
- 非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
七、条件变量
7.1 概述
- 关键字synchronized中也有条件变量,就是waitSet,可以理解为条件不满足时进入waitSet等待,一个synchronized只能对应一个waitSet。
- ReentrantLock可以支持多个条件变量,因此可以将不同条件的线程放入等待集合中,以便于后续进行专门的唤醒。在ReentrantLock中使用条件变量需要使用await()方法。
ReentrantLock的条件变量比synchronized强大之处在于,它支持多个条件变量(对象)。
使用流程
- 使用
ReentrantLock
对象创建条件变量condition
;
- 执行
condition.await()
前需要先获取锁;
- 执行
condition.await()
后,线程会释放锁,并进入conditionObject
中等待;
- 其它线程执行
condition.signal()
或者condition.signalAll()
唤醒conditionObject
中等待的线程;
- 被唤醒后会重新竞争锁
- 竞争锁成功后,会从await()后的代码处开始执行
函数await()
调用方式
7.2 代码示例
- t1需要等待烟过来, 否则就一直等待
- t2需要等待早餐, 否则就一直等待
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock;
import static site.weiyikai.concurrent.utils.Sleeper.sleep;
@Slf4j(topic = "c.ConditionTest") public class ConditionTest { static ReentrantLock lock = new ReentrantLock(); static Condition waitCigaretteQueue = lock.newCondition(); static Condition waitbreakfastQueue = lock.newCondition(); static volatile boolean hasCigrette = false; static volatile boolean hasBreakfast = false; public static void main(String[] args) { new Thread(() -> { try { lock.lock(); while (!hasCigrette) { try { waitCigaretteQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("等到了它的烟"); } finally { lock.unlock(); } },"t1").start(); new Thread(() -> { try { lock.lock(); while (!hasBreakfast) { try { waitbreakfastQueue.await(); } catch (InterruptedException e) { e.printStackTrace(); } } log.debug("等到了它的早餐"); } finally { lock.unlock(); } },"t2").start(); sleep(1); sendBreakfast(); sleep(1); sendCigarette(); } private static void sendCigarette() { lock.lock(); try { log.debug("送烟来了"); hasCigrette = true; waitCigaretteQueue.signal(); } finally { lock.unlock(); } } private static void sendBreakfast() { lock.lock(); try { log.debug("送早餐来了"); hasBreakfast = true; waitbreakfastQueue.signal(); } finally { lock.unlock(); } } }
|
运行结果
1 2 3 4
| 23:52:23.927 c.ConditionTest [main] - 送早餐来了 23:52:23.930 c.ConditionTest [t2] - 等到了它的早餐 23:52:24.931 c.ConditionTest [main] - 送烟来了 23:52:24.931 c.ConditionTest [t1] - 等到了它的烟
|