1、同步模式保护性暂停
用一个线程等待另一个线程的执行结果
- 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个中间类。
- 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)。
- JDK 中,join 的实现、Future 的实现,采用的就是此模式。
- 因为要等待另一方的结果,因此归类到同步模式。
@Slf4j(topic = "c.Demo1")
public class Demo1 {
public static void main(String[] args) {
ObjectResponse response = new ObjectResponse();
Thread t1 = new Thread(() -> {
try {
Thread.sleep(5000);
response.notifyWaitResult("运行结果");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "生产者");
Thread t2 = new Thread(() -> {
Object res = response.waitResult(2000);
log.info("获取结果:{}", res);
}, "消费者");
//生产者和消费者线程同时启动,当消费者接收到消息后会唤醒生产者并且给response赋值,生产者返回结果
log.info("生产者启动");
t1.start();
log.info("消费者启动");
t2.start();
}
}
class ObjectResponse {
private static final Logger log = LoggerFactory.getLogger(ObjectResponse.class);
Object response;
public Object waitResult(long waitTime) {
synchronized (this) {
//记录开始时间
long startTime = System.currentTimeMillis();
//记录一次循环执行了多久
long workTime = 0L;
while (response == null) {
if (workTime > waitTime){
log.error("已超时");
break;
}
try {
//避免虚假唤醒的问题。
//假设等待时间为2s,然后wait了1秒被虚假唤醒,下次还需要等2s,实际上只需要等1s
this.wait(waitTime - workTime);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//本次循环的执行时间
workTime = System.currentTimeMillis() - startTime;
}
return response;
}
}
public void notifyWaitResult(Object response) {
synchronized (this) {
this.response = response;
this.notifyAll();
}
}
}
2、join原理
public final synchronized void join(long millis)
throws InterruptedException {
//记录当前时间
long base = System.currentTimeMillis();
long now = 0;
//传递参数的时间小于0抛出异常
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
//调用join时未传递参数
if (millis == 0) {
//当线程存活时一直等待
while (isAlive()) {
wait(0);
}
//调用join时传入了参数
} else {
//线程存活时
while (isAlive()) {
//计算剩余等待时间
long delay = millis - now;
//剩余等待时间为0时结束循环
if (delay <= 0) {
break;
}
//等待剩余时间
wait(delay);
//记录本轮消耗的时间
now = System.currentTimeMillis() - base;
}
}
}
3、多任务版同步模式保护性暂停
下图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右 侧的t1,t3,t5 就好比邮递员。
如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类, 这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理。
消息中间类需要增加一个ID进行不同消息的区分。
class GuardedObject {
// 标识 Guarded Object
private int id;
public GuardedObject(int id) {
this.id = id;
}
public int getId() {
return id;
}
// 结果
private Object response;
// 获取结果
// timeout 表示要等待多久 2000
public Object get(long timeout) {
synchronized (this) {
// 开始时间 15:00:00
long begin = System.currentTimeMillis();
// 经历的时间
long passedTime = 0;
while (response == null) {
// 这一轮循环应该等待的时间
long waitTime = timeout - passedTime;
// 经历的时间超过了最大等待时间时,退出循环
if (timeout - passedTime <= 0) {
break;
}
try {
this.wait(waitTime); // 虚假唤醒 15:00:01
} catch (InterruptedException e) {
e.printStackTrace();
}
// 求得经历时间
passedTime = System.currentTimeMillis() - begin; // 15:00:02 1s
}
return response;
}
}
// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}
创建信箱类
这里的信箱使用线程安全的HashTable集合。
class mailBoxes{
private static Map<Integer,GuardedObject> boxes = new Hashtable<>();
private static int id = 0;
public synchronized static int getId(){
return id++;//获取自增id
}
public static GuardedObject getBox(int id){
return boxes.remove(id);//取出id后删除对应信息
}
/**
* 创建GuardedObject对象,并且放入boxes管理
* @return
*/
public static GuardedObject createGuardedObject(){
GuardedObject guardedObject = new GuardedObject(getId());
boxes.put(guardedObject.getId(),guardedObject);
return guardedObject;
}
public static Set<Integer> getIds(){
return boxes.keySet();
}
}
创建居民类,模拟接受信件
居民首先要在邮箱内创建一个属于自己的格子,放一个信件进去
/**
* 居民,投递信件
*/
@Slf4j(topic = "c.People")
class People extends Thread{
@Override
public void run() {
//创建GuardedObject对象,并且放入boxes管理,相当于创建了一个信箱格子(GuardedObject),并且放了一封信进去(id,mail)
GuardedObject guardedObject = mailBoxes.createGuardedObject();
log.info("放一封信进入信箱格子,信件id:{}", guardedObject.getId());
Object mail = guardedObject.get(2000);
log.info("接收到邮递员送信完成反馈,id:{},内容:{}",guardedObject.getId(),mail);
}
}
创建邮递员类,模拟发送信件
从格子中拿走信件并且派送,给予居民反馈
/**
* 投递员,发送信件
*/
@Slf4j(topic = "c.postMan")
class postMan extends Thread{
private int id;
private String mail;
public postMan(int id, String mail) {
this.id = id;
this.mail = mail;
}
@Override
public void run() {
//从信箱中取信
GuardedObject box = mailBoxes.getBox(id);
log.info("送信:id:{},内容:{}",id,mail);
//送信,并且给居民反馈。
box.complete(mail);
}
}
模拟三个居民在信箱中创建了三个格子,三个邮递员取出信件送信,然后给予居民反馈。
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Thread.sleep(1000);
for (Integer id : mailBoxes.getIds()) {
new postMan(id, "内容" + id).start();
}
}
}
可以看到,线程一的信件由线程三去邮递,并且给予了线程一反馈。
4、异步模式生产者/消费者
- 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应。
- 消费队列可以用来平衡生产和消费的线程资源。
- 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据。
- 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据。
- JDK 中各种阻塞队列,采用的就是这种模式。
首先封装一个消息类
/**
* 封装消息类
*/
class Message{
private Integer id;
private Object message;
public Message() {
}
public Message(Integer id, Object message) {
this.id = id;
this.message = message;
}
/**
* 获取
* @return id
*/
public Integer getId() {
return id;
}
/**
* 设置
* @param id
*/
public void setId(Integer id) {
this.id = id;
}
/**
* 获取
* @return message
*/
public Object getMessage() {
return message;
}
/**
* 设置
* @param message
*/
public void setMessage(Object message) {
this.message = message;
}
public String toString() {
return "Message{id = " + id + ", message = " + message + "}";
}
}
创建消息队列中间类
当队列中消息数量达到队列最大长度时,生产者陷入阻塞,消费者消费完消息后将队列头部的消息移除,唤醒生产者。
当队列中消息数量为0时,消费者陷入阻塞,生产者向队列的尾部存入一条消息,唤醒消费者。
**
* 线程消息队列通信
*/
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
//模拟消息队列
private LinkedList<Message> queue = new LinkedList<>();
//最大消息数量
private Integer maxMessageSize;
public MessageQueue(Integer maxMessageSize) {
this.maxMessageSize = maxMessageSize;
}
/**
* 向队列写入消息
* @param message
*/
public void setMessage(Message message) {
synchronized (queue){
while (queue.size() >= maxMessageSize) {
try {
log.error("Message queue is full");
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
queue.addLast(message);
log.info("add message: {}", message);
queue.notifyAll();
}
}
/**
* 读取消息
* @return
*/
public Message getMessage() {
synchronized (queue){
while (queue.size() == 0) {
log.error("Message queue is empty");
try {
queue.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
Message message = queue.removeFirst();
log.info("remove message: {}", message);
queue.notifyAll();
return message;
}
}
}
public class Demo1 {
public static void main(String[] args) {
//创建容量为2的消息队列
MessageQueue messageQueue = new MessageQueue(2);
//创建三个生产者
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(()->{
messageQueue.setMessage(new Message(id,"消息"+id));
},"生产者"+i).start();
}
new Thread(()->{
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
Message message = messageQueue.getMessage();
}
},"消费者").start();
}
}
此时队列的长度为2,有三个生产者,当发现队列已满,唤起消费者,消费者将消息消费后,此时队列中还有1个消息,唤起生产者。
5、park&unpark
5.1、基本概念
park:当一个线程被挂起时,它暂时停止执行,并进入一种等待或睡眠状态。线程可以被挂起因为等待某些条件的发生,例如等待某个资源可用或者等待某个条件变为真。
unpark:当一个线程被解除挂起时,它会从挂起状态恢复到可运行状态,可以继续执行。解除挂起的操作通常由其他线程触发,例如当某个条件满足时唤醒等待的线程。
5.2、和wait¬ify的区别
park和unpark是Thread类的方法,用于线程的挂起和解除挂起;而wait和notify是Object类的方法,用于对象之间的线程协作。
park方法不需要持有对象锁,可以直接使用;而wait方法需要在同步块或同步方法中调用,会释放对象锁。
unpark可以指定唤醒具体的线程,而notify只是唤醒等待队列中的一个线程,notifyAll则唤醒所有等待的线程。unpark唤醒线程更加精确,并且可以先unpark再park,此时线程不会陷入阻塞。
先unpark再park:
@Slf4j(topic = "c.Demo1")
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
//park案例
//wait,notify和notifyAll必须配合Object Monitor一起使用,park unpark不用
//park unpark可以精准到线程单位进行唤醒
//park unpark可以先unpark
Thread t1 = new Thread(() -> {
log.info("start");
try {
// Thread.sleep(1000);
//可以先unpark再park
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("park");
LockSupport.park();
log.info("resume");
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("unpark");
LockSupport.unpark(t1);
}
}
可以看到此时t1线程没有阻塞,而是继续打印出了resume。
5.3、原理
park:
- 当线程调用park方法时,它会进入一个挂起状态,暂停执行。此时,线程的状态会被设置为WAITING或者TIMED_WAITING。
- park方法会将线程放入一个等待队列中,并暂时释放掉线程所持有的所有锁。
- 等待队列中的线程会等待被其他线程调用unpark方法来唤醒。
unpark:
- 当线程调用unpark方法时,它会尝试唤醒指定的线程,使其从等待队列中移出,并转换为可运行状态。
- 如果目标线程还没有进入挂起状态,那么调用unpark方法后,目标线程在下一次调用park时将不会被挂起。
为什么先unpark再park线程不会阻塞的原因在于线程的状态管理和unpark的作用:
当线程调用unpark方法时,即使目标线程还未进入挂起状态,也会记录一个许可证,表示目标线程在未来调用park时可以立即返回而不会被挂起。
如果线程先调用了unpark,然后再调用park,那么park方法会立即返回,不会阻塞,因为已经有一个许可证允许它不被挂起。
6、重新理解线程状态转换
假设有一个线程t
- new->runnable:当调用t线程的.start()方法时,状态由new转变成runnable。
- runnable->blocked:当t线程与其他线程争抢synchronized锁失败时,进入blocked状态。当其他线程执行完同步代码块中的代码后,会释放锁,线程间重新竞争锁,未竞争到锁的线程同样会进入blocked状态。
- runnable->waiting:
- 情况一:t线程获取到synchronized锁后,调用对象的wait方法。如果再次调用对象的notify/notifyAll方法,竞争锁成功,会进入runnable状态。竞争锁失败,会进入blocked状态。
- 情况二:当前线程调用t线程的join方法时,当前线程进入waiting状态。(当前线程在t线程对象的监视器上等待)。t线程运行结束,或者调用当前线程的interrput方法时,当前线程重新回到runnable状态。
- 情况三:当前线程调用park方法进入waiting状态。当前线程调用unpark(目标线程),或调用了interrupt方法,会让目标线程重新回到runnable状态。
- runnable->timed_waiting:与runnable->waiting相似,此时调用的是有时间参数的wait,join,park方法。
- runnable->terminated:所有方法运行结束,转变为terminated状态。
7、线程活跃性
7.1、多把锁
在多个操作不会互相影响,一个操作不必等待另一个操作的结果时,降低锁的粒度可以提高并发度。
public class Demo4 {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(bigRoom::study,"t1").start();
new Thread(bigRoom::sleep,"t2").start();
}
}
@Slf4j(topic = "c.BigRoom")
class BigRoom{
final Object study = new Object();
final Object sleep = new Object();
public void study(){
synchronized (study){
try {
log.info("学习一小时");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public void sleep(){
synchronized (sleep){
try {
log.info("休息两小时");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
7.2、死锁
7.2.1、基本概念
在多线程或多进程环境中,两个或多个线程或进程互相等待对方释放资源而无法继续执行的情况。这种情况下,所有涉及的线程或进程都被阻塞,无法继续向前执行,导致整个系统陷入僵局。
死锁发生的原因通常包括以下四个必要条件:
- 互斥条件:资源只能被一个线程或进程独占,如果另一个线程或进程想要使用该资源,就必须等待。
- 持有和等待条件:线程或进程持有至少一个资源,并且在等待获取另一个资源时保持对已有资源的持有。
- 非抢占条件:资源不能被抢占,即已经被一个线程或进程持有时,其他线程或进程无法直接抢占,只能等待。
- 循环等待条件:存在一个循环等待序列,即线程 A 等待线程 B 持有的资源,线程 B 等待线程 C 持有的资源,而线程 C 又在等待线程 A 持有的资源,形成一个闭环等待。
@Slf4j(topic = "c.Demo1")
public class Demo1 {
public static void main(String[] args) {
test1();
}
public static void test1(){
Object A = new Object();
Object B = new Object();
new Thread(()->{
synchronized (A){
log.info(Thread.currentThread().getName()+"获取到了A锁");
try {
Thread.sleep(1000);
synchronized (B){
log.info(Thread.currentThread().getName()+"获取到了B锁");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"线程一").start();
new Thread(()->{
synchronized (B){
log.info(Thread.currentThread().getName()+"获取到了B锁");
try {
Thread.sleep(500);
synchronized (A){
log.info(Thread.currentThread().getName()+"获取到了A锁");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"线程二").start();
}
}
在这个案例中
线程一尝试获取A锁,获取到锁后睡眠1s,然后尝试获取B锁。
线程二尝试获取B锁,在获取到锁后睡眠0.5s,然后尝试获取A锁。
由于线程一获取到A锁后没有释放锁,所以B线程获取到B锁后无法获取到A锁,由于线程二已经获取到了B锁,所以A也无法获取到B锁,反之也相同。
7.2.2、定位死锁
终端输入jconsole
7.2.3、哲学家就餐死锁问题
假设有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待。
筷子类
/**
* 筷子类
*/
class Chopsticks extends ReentrantLock {
private String name;
public Chopsticks() {
}
public Chopsticks(String name) {
this.name = name;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
public String toString() {
return "Chopsticks{name = " + name + "}";
}
}
哲学家类
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread{
//左手筷子
private Chopsticks left;
//右手筷子
private Chopsticks right;
public Philosopher() {
}
public Philosopher(Chopsticks left, Chopsticks right,String name) {
super(name);
this.left = left;
this.right = right;
}
public void eat() {
log.info(getName()+"思考");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void run() {
while (true) {
// 获得左手筷子
synchronized (left) {
// 获得右手筷子
synchronized (right) {
// 吃饭
eat();
}
// 放下右手筷子
}
// 放下左手筷子
}
}
/**
* 获取
* @return left
*/
public Chopsticks getLeft() {
return left;
}
/**
* 设置
* @param left
*/
public void setLeft(Chopsticks left) {
this.left = left;
}
/**
* 获取
* @return right
*/
public Chopsticks getRight() {
return right;
}
/**
* 设置
* @param right
*/
public void setRight(Chopsticks right) {
this.right = right;
}
public String toString() {
return "Philosopher{left = " + left + ", right = " + right + ", log = " + log + "}";
}
}
主类测试
/**
* 哲学家就餐问题,模拟死锁
*/
@Slf4j(topic = "c.Dining")
public class Dining {
public static void main(String[] args) {
Chopsticks t1 = new Chopsticks("t1");
Chopsticks t2 = new Chopsticks("t2");
Chopsticks t3 = new Chopsticks("t3");
Chopsticks t4 = new Chopsticks("t4");
Chopsticks t5 = new Chopsticks("t5");
new Philosopher(t1,t2,"苏格拉底").start();
new Philosopher(t2,t3,"柏拉图").start();
new Philosopher(t3,t4,"亚里士多德").start();
new Philosopher(t4,t5,"赫拉克利特").start();
new Philosopher(t5,t1,"阿基米德").start();
}
}
运行一段时间就会发生死锁问题。后续我们会通过ReentranLock进行解决
7.3、活锁
7.3.1、基本概念
活锁(Livelock)是另一种并发编程中的问题,类似于死锁,但有所不同。在活锁情况下,线程或进程不是被阻塞在等待对方释放资源的状态,而是在尝试解决冲突时一直互相让步,导致无法继续向前执行。
活锁通常发生在需要互相等待对方执行某个操作的情况下,例如两个线程都在等待对方放弃某个资源或者执行某个操作,但双方都不愿意先让步。这种情况下,线程不会被阻塞,它们会不断尝试解决冲突,但最终导致系统无法进展。
7.3.2、与死锁的区别
- 活锁:线程或进程不断尝试解决冲突,但始终没有进展,系统一直处于忙碌但无法完成任务的状态。
- 死锁:线程或进程因为互相等待对方释放资源而被阻塞,导致系统陷入僵局,无法继续执行。
@Slf4j(topic = "c.Demo1")
public class Demo1 {
static volatile int counter = 10;
public static void main(String[] args) {
//模拟活锁
new Thread(() -> {
while (counter < 20) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
counter++;
log.debug("counter = {}", counter);
}
},"线程一").start();
new Thread(() -> {
while (counter > 0) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
counter--;
log.debug("counter = {}", counter);
}
},"线程二").start();
}
}
此时两个线程对共享变量counter进行并发访问。两个线程互相改变对方的结束条件,最后谁也无法结束。
7.4、锁饥饿
锁饥饿(Lock Starvation)是指在并发编程中,某些线程由于竞争锁资源失败而长时间无法获取所需的锁,导致它们无法继续执行或者执行效率低下的情况。
锁饥饿通常发生在以下情况下:
- 优先级倾斜:如果某些线程的优先级较低,而且高优先级的线程频繁地获取锁资源并且长时间不释放,低优先级线程可能由于竞争失败而长时间无法获取锁,导致饥饿现象。
- 饥饿阻塞:当多个线程竞争同一个锁资源时,如果某个线程总是被其他线程抢先获取锁,那么它可能会长时间无法获取锁,导致饥饿现象。(此前提到的非公平锁synchronized就可能导致这样的问题)
解决锁饥饿的方法通常包括:
- 公平性策略:使用公平锁来确保锁资源的公平分配,按照线程请求锁的顺序分配锁资源,避免某些线程长时间无法获取锁。
- 优先级调整:合理设置线程的优先级,避免高优先级线程长时间独占锁资源,导致低优先级线程无法获取锁。
- 超时等待:设置获取锁资源的超时时间,在超时后放弃获取锁并进行适当的处理,避免线程长时间阻塞。
8、ReentrantLock
相比较于synchronized具有以下的特点
-
可重入性:ReentrantLock是可重入的,也就是说同一个线程可以多次获取同一个对象而不会被阻塞。这意味着线程可以在持有锁的情况下多次进入同步代码块,而不会因为自己已经持有锁而阻塞。
-
公平性:ReentrantLock支持公平性和非公平性两种方式。在公平模式下,锁会按照线程请求锁的顺序进行分配,而非公平模式下则可能会将锁分配给一个刚刚释放锁的线程,以提高整体的吞吐量。
-
可中断性:ReentrantLock支持可中断的锁获取方式。线程可以在等待获取锁的过程中被中断,并且可以通过响应中断来处理中断请求,这在处理死锁等情况下非常有用。
-
锁的条件变量:ReentrantLock提供了Condition对象,可以通过该对象实现线程间的等待/通知机制。这使得ReentrantLock可以更加灵活地控制线程的等待和唤醒。
-
手动加锁和解锁:与synchronized关键字相比,ReentrantLock需要手动显式地加锁和解锁。可以更加精细地控制锁的范围和持有时间,从而避免死锁等问题。
8.1、可重入
@Slf4j(topic = "c.Demo1")
public class Demo1 {
static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
//演示reentrantLock的可重入
reentrantLock.lock();
try {
log.info("开始执行主方法");
method1();
} finally {
reentrantLock.unlock();
}
}
public static void method1(){
reentrantLock.lock();
try {
log.info("开始执行m1方法");
method2();
} finally {
reentrantLock.unlock();
}
}
public static void method2(){
reentrantLock.lock();
try {
log.info("开始执行m2方法");
} finally {
reentrantLock.unlock();
}
}
}
主方法获取到了锁,并且要执行method1方法
method1方法获取到了同一把锁
8.2、可打断
@Slf4j(topic = "c.Demo2")
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
//如果没有竞争,那么这个方法可以获取到lock锁
//如果有竞争就进入阻塞队列,可以被其他线程通过interrput打断
log.info("开始获取锁");
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.info("被打断");
return;
}
try {
log.info("获取到锁,执行其他操作...");
} finally {
reentrantLock.unlock();
}
}, "t1");
log.info("开始获取锁");
reentrantLock.lock();
t1.start();
Thread.sleep(1000);
log.info("执行打断操作");
t1.interrupt();
}
}
通过reentrantLock.lockInterruptibly();设置锁为可打断。
8.3、有时限等待
@Slf4j(topic = "c.Demo3")
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock();
Thread t1 = new Thread(() -> {
//有时限的等待
try {
//会等待2s,如果2s没有获得锁就执行下面的代码
if (!reentrantLock.tryLock(2, TimeUnit.SECONDS)){
log.info("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.info("获取锁被打断!");
return;
}
try {
log.info("获取到锁,执行其他操作");
} finally {
reentrantLock.unlock();
}
},"t1");
reentrantLock.lock();
log.info(Thread.currentThread().getName()+"获取到了锁");
t1.start();
//主线程获取到锁1s之后释放锁
Thread.sleep(1000);
log.info(Thread.currentThread().getName()+"释放了锁");
reentrantLock.unlock();
}
}
8.4、解决哲学家就餐死锁问题
改写Philosopher中的run方法。
此时假设某位哲学家获取到了左手的筷子,要去尝试获取右手筷子,结果失败了,会将左手筷子也一起放下,相当于破坏了死锁中的持有和等待条件。
@Override
public void run() {
while (true){
// synchronized (left){
// synchronized (right){
// log.info(getName() + "eat");
// }
// }
if (left.tryLock()){
try {
//获取右手筷子,如果失败了就放下左手筷子
if (right.tryLock()){
try {
eat();
}finally {
right.unlock();
}
}
}finally {
left.unlock();
}
}
}
}
tryLock和Lock的区别:
tryLock
是非阻塞的尝试获取锁的方法。如果当前锁没有被其他线程持有,tryLock
会立即获取锁并返回true,表示获取成功;如果锁被其他线程持有,会立即返回false,表示获取失败。常用于实现一种非阻塞的获取锁机制,可以用来避免线程因为等待锁而被阻塞,从而提高程序的响应性。还支持超时参数,可以指定获取锁的最长等待时间,在超时后如果还未获取到锁,则返回false。
lock
是阻塞的获取锁的方法。如果当前锁已经被其他线程持有,会使当前线程进入等待状态,直到获取到锁为止。在获取到锁之前会一直阻塞当前线程。
8.5、条件变量
在synchronized中也有条件变量(图中的waitSet):
可以把它理解成一个房间。而 ReentrantLock的条件变量,支持创建不同的房间。
使用要点:
- await 前需要获得锁 。
- await 执行后,会释放锁,进入 conditionObject 等待 。
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁 。
- 竞争 lock 锁成功后,从 await 后继续执行。
@Slf4j(topic = "c.Demo5")
public class Demo5 {
static ReentrantLock lock = new ReentrantLock();
static Boolean hasCigarette = false;
static Boolean hasTakeOut = false;
//创建单独的休息室
static Condition cigaretteSet = lock.newCondition();
static Condition takeOutSet = lock.newCondition();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
lock.lock();//上锁
try {
log.debug("是否有烟:{}", hasCigarette);
if (!hasCigarette) {
log.debug("等待香烟");
try {
cigaretteSet.await();//进入休息室等烟
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.debug("开始干活");
} finally {
lock.unlock();
}
}, "壹号").start();
new Thread(() -> {
lock.lock();
try {
log.info("是否有外卖:{}", hasTakeOut);
if (!hasTakeOut) {
log.debug("等待外卖");
takeOutSet.await();
}
log.info("开始干活");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}, "贰号").start();
Thread.sleep(1000);
new Thread(() -> {
lock.lock();
try {
hasTakeOut = true;
log.info("送外卖:{}", hasTakeOut);
takeOutSet.signal();
} finally {
lock.unlock();
}
}, "外卖员").start();
new Thread(() -> {
lock.lock();
try {
hasCigarette = true;
log.info("送烟:{}",hasCigarette);
cigaretteSet.signal();
} finally {
lock.unlock();
}
}, "送烟").start();
}
}
在上面的案例中,首先创建了两个休息室,分别用于等烟和等外卖。如果没有烟或者外卖,则陷入等待。送烟或送外卖线程在执行完送烟/送外卖操作后,会唤醒相应休息室的线程继续执行。
9、固定顺序执行
现在有两个线程t1和t2,要求t2完成后再执行t1
9.1、解法一:wait¬ify
思路:定义一个t2是否执行完成的标记,t1在执行前先判断,如果为false证明t2没有执行就陷入阻塞,等待t2执行完成后唤醒t1。
@Slf4j(topic = "c.Demo6")
public class Demo6 {
static final Object lock = new Object();
static Boolean t2Runned = false;
public static void main(String[] args) {
//要求t2运行完成后再运行t1
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!t2Runned) {
try {
lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
log.info("t1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.info("t2");
t2Runned = true;
lock.notify();
}
},"t2");
t1.start();
t2.start();
}
}
9.2、解法二:park&unpark
同样是设置标记位的思路,但是区别在于使用park&unpark无需关联对象的锁。
@Slf4j(topic = "c.Demo6")
public class Demo7 {
static Boolean t2HasRun = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
if (!t2HasRun){
LockSupport.park();
}
log.info("t1");
}, "t1");
Thread t2 = new Thread(() -> {
log.info("t2");
t2HasRun = true;
LockSupport.unpark(t1);
},"t2");
t1.start();
t2.start();
}
}
10、交替执行
要求交替打印abc五次。
10.1、解法一:wait¬ify
思路:使用标记位记录执行线程,以及本轮标记和下一轮标记
线程 本轮标记 下轮标记
A 1 2
B 2 3
C 3 1
创建WaitNotify对象时,设置循环五次,初始标记为1。
假设此时线程二争抢到了锁的执行权,发现本轮标记2和初始标记1不相同,就会陷入阻塞,让出锁的执行权。
然后线程一争抢到了锁的执行权,发现本轮标记1和初始标记1一致,就打印a,并且将初始标记设置成为下一轮的标记2。执行完成后解锁。
最后线程二又争抢到了锁的执行权,发现本轮标记2和初始标记2一致,就打印b,并且将初始标记设置成为下一轮的标记3。执行完成后解锁。
@Slf4j(topic = "c.Demo8")
public class Demo8 {
public static void main(String[] args) {
//交替打印abc五次
WaitNotify waitNotify = new WaitNotify(5, 1);
new Thread(()->{
waitNotify.print("a",1,2);
},"线程一").start();
new Thread(()->{
waitNotify.print("b",2,3);
},"线程二").start();
new Thread(()->{
waitNotify.print("c",3,1);
},"线程三").start();
}
}
class WaitNotify{
private int loopNumber;
private int flag;
/**
* 打印
* @param msg 打印信息
* @param waitFlag 本轮标记
* @param nextFlag 下一轮标记
*/
public void print(String msg,int waitFlag,int nextFlag){
for (int i = 0; i < loopNumber; i++) {
synchronized (this) {
while (flag != waitFlag) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.print(msg);
flag = nextFlag;
this.notifyAll();
}
}
}
public WaitNotify() {
}
public WaitNotify(int loopNumber, int flag) {
this.loopNumber = loopNumber;
this.flag = flag;
}
/**
* 获取
* @return loopNumber
*/
public int getLoopNumber() {
return loopNumber;
}
/**
* 设置
* @param loopNumber
*/
public void setLoopNumber(int loopNumber) {
this.loopNumber = loopNumber;
}
/**
* 获取
* @return flag
*/
public int getFlag() {
return flag;
}
/**
* 设置
* @param flag
*/
public void setFlag(int flag) {
this.flag = flag;
}
public String toString() {
return "WaitNotify{loopNumber = " + loopNumber + ", flag = " + flag + "}";
}
}
10.2、解法二:await&signal
此时将标记替换成为了本间休息室和下一间休息室的对象。
主线程会先唤醒线程一,线程一被唤醒后打印a,然后唤醒下一个处于休息室的线程b...
@Slf4j(topic = "c.Demo9")
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition aSet = awaitSignal.newCondition();
Condition bSet = awaitSignal.newCondition();
Condition cSet = awaitSignal.newCondition();
new Thread(() -> {
awaitSignal.print("a", aSet, bSet);
}, "线程一").start();
new Thread(() -> {
awaitSignal.print("b", bSet, cSet);
}, "线程二").start();
new Thread(() -> {
awaitSignal.print("c", cSet, aSet);
}, "线程三").start();
Thread.sleep(1000);
awaitSignal.lock();
try {
//唤醒a休息室
aSet.signal();
}finally {
//解锁
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock {
//循环次数
private int loopNumber;
public AwaitSignal() {
}
public AwaitSignal(int loopNumber) {
this.loopNumber = loopNumber;
}
/**
* 打印方法
* @param msg 信息
* @param current 现在的休息室
* @param next 下一个休息室
*/
public void print(String msg, Condition current, Condition next) {
for (int i = 0; i < 5; i++) {
//获取锁
this.lock();
try {
//自己进入休息室
current.await();
//被唤醒后执行
System.out.print(msg);
//唤醒下一个
next.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//释放锁
this.unlock();
}
}
}
/**
* 获取
* @return loopNumber
*/
public int getLoopNumber() {
return loopNumber;
}
/**
* 设置
* @param loopNumber
*/
public void setLoopNumber(int loopNumber) {
this.loopNumber = loopNumber;
}
public String toString() {
return "AwaitSignal{loopNumber = " + loopNumber + "}";
}
}
10.3、解法三:park&unpark
@Slf4j(topic = "c.Demo10")
public class Demo10 {
static Thread t1;
static Thread t2;
static Thread t3;
public static void main(String[] args) throws InterruptedException {
ParkUnpark parkUnpark = new ParkUnpark(5);
t1 = new Thread(() -> {
parkUnpark.print("a",t2);
},"线程一");
t2 = new Thread(() -> {
parkUnpark.print("b",t3);
},"线程二");
t3 = new Thread(() -> {
parkUnpark.print("c",t1);
},"线程三");
t1.start();
t2.start();
t3.start();
Thread.sleep(1000);
LockSupport.unpark(t1);
}
}
class ParkUnpark{
//循环次数
private int loopNumber;
public ParkUnpark() {
}
public ParkUnpark(int loopNumber) {
this.loopNumber = loopNumber;
}
public void print(String msg,Thread unpark){
for (int i = 0; i < 5; i++) {
LockSupport.park();
System.out.print(msg);
LockSupport.unpark(unpark);
}
}
/**
* 获取
* @return loopNumber
*/
public int getLoopNumber() {
return loopNumber;
}
/**
* 设置
* @param loopNumber
*/
public void setLoopNumber(int loopNumber) {
this.loopNumber = loopNumber;
}
public String toString() {
return "ParkUnpark{loopNumber = " + loopNumber + "}";
}
}