全網最詳細併發編程(3)—入門篇
本文主要講解wait/notify的正確使用姿勢、park/unpark、join()的原理、模式之生產者-消費者模式(異步)、保護性暫停模式(同步)、線程狀態轉換的流程、死鎖和活鎖以及如何檢查死鎖等。
一、 wait notify
演示wait/notify
運行結果
二、wait/notify的正確使用姿勢
step 1
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
//思考下面的解決方案好不好,爲什麼?
new Thread(() -> {
synchronized (room) {
log.debug("有煙沒?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("沒煙,先歇會!");
sleep(2);
}
log.debug("有煙沒?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以開始幹活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以開始幹活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
// 這裏能不能加 synchronized (room)?
hasCigarette = true;
log.debug("煙到了噢!");
}, "送煙的").start();
輸出
20:49:49.883 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:49:49.887 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:49:50.882 [送煙的] c.TestCorrectPosture - 煙到了噢!
20:49:51.887 [小南] c.TestCorrectPosture - 有煙沒?[true]
20:49:51.887 [小南] c.TestCorrectPosture - 可以開始幹活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:49:51.887 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:49:51.888 [其它人] c.TestCorrectPosture - 可以開始幹活了
step 2
思考下面的實現行嗎,爲什麼?
new Thread(() -> {
synchronized (room) {
log.debug("有煙沒?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("沒煙,先歇會!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有煙沒?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以開始幹活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以開始幹活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("煙到了噢!");
room.notify();
}
}, "送煙的").start();
輸出
20:51:42.489 [小南] c.TestCorrectPosture - 有煙沒?[false]
20:51:42.493 [小南] c.TestCorrectPosture - 沒煙,先歇會!
20:51:42.493 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:42.493 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:42.494 [其它人] c.TestCorrectPosture - 可以開始幹活了
20:51:43.490 [送煙的] c.TestCorrectPosture - 煙到了噢!
20:51:43.490 [小南] c.TestCorrectPosture - 有煙沒?[true]
20:51:43.490 [小南] c.TestCorrectPosture - 可以開始幹活了
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("可以開始幹活了");
} else {
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();
step4
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外賣到了噢!");
room.notifyAll();
}
}, "送外賣的").start();
三、模式篇
1、保護性暫停
class GuardedObject {
private Object response;
private final Object lock = new Object();
public Object get() {
synchronized (lock) {
// 條件不滿足則等待
while (response == null) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
} }
return response; }
}
public void complete(Object response) {
synchronized (lock) {
// 條件滿足,通知等待線程
this.response = response;
lock.notifyAll();
}
}
}
應用:一方等待另一方的返回結果(帶超時版 GuardedObject)
@Slf4j(topic = "c.TestGurad")
public class TestGuard {
public static void main(String[] args) {
GuardObject guardObject = new GuardObject();
new Thread(() ->{
//等待結果
log.debug("等待結果");
List<String> results = (List<String>)guardObject.get(2000);
// log.debug("獲取結果完畢,結果大小爲:{}",results.size());
log.debug("結果:{}",results);
},"t1").start();
new Thread(() -> {
//執行下載
log.debug("執行下載");
List<String> downloaderData = null;
try {
downloaderData = Downloader.download();
guardObject.produce(null);
// log.debug("下載結束");
} catch (IOException e) {
e.printStackTrace();
}
},"t2").start();
}
}
class GuardObject{
//結果
private Object response;
//獲取結果
public Object get(long timeout){
synchronized (this){
//記錄開始時間
long start = System.currentTimeMillis();
//記錄經歷的時間
long passTime = 0;
//沒有結果
while (response == null) {
//waitTime爲這一輪循環應該等待的時間
long watiTime = timeout - passTime ;
//經歷的時間超過超時時間就退出循環不再等待
if(watiTime<0){
break;
}
try {
this.wait(watiTime); //避免虛假喚醒的情況,下一輪就不用等待timeout這麼多時間了
} catch (InterruptedException e) {
e.printStackTrace();
}
passTime = System.currentTimeMillis() - start;
}
return response;
}
}
//產生結果
public void produce(Object response){
synchronized (this){
//給結果成員變量賦值
this.response = response;
this.notifyAll();
}
}
}
1.1、join()的原理
1.2多任務版GuardObject(一個線程對應一個GuardObject)
@Slf4j(topic = "c.Test20")
public class Test20 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new People().start();
}
Sleeper.sleep(1);
for (Integer id : Mailboxes.getIds()) {
new Postman(id, "內容" + id).start();
}
}
}
@Slf4j(topic = "c.People")
class People extends Thread{
@Override
public void run() {
// 收信
GuardedObject guardedObject = Mailboxes.createGuardedObject();
log.debug("開始收信 id:{}", guardedObject.getId());
Object mail = guardedObject.get(5000);
log.debug("收到信 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 guardedObject = Mailboxes.getGuardedObject(id);
log.debug("送信 id:{}, 內容:{}", id, mail);
guardedObject.complete(mail);
}
}
class Mailboxes {
private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
private static int id = 1;
// 產生唯一 id
private static synchronized int generateId() {
return id++;
}
public static GuardedObject getGuardedObject(int id) {
//根據id獲取到box並刪除對應的key和value,避免堆內存爆了
return boxes.remove(id);
}
public static GuardedObject createGuardedObject() {
GuardedObject go = new GuardedObject(generateId());
boxes.put(go.getId(), go);
return go;
}
public static Set<Integer> getIds() {
return boxes.keySet();
}
}
// 增加超時效果
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();
}
}
}
2、生產者-消費者模式(異步模式)
@Slf4j(topic = "c.Test21")
public class Test21 {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(2);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
queue.put(new Message(id , "值"+id));
}, "生產者" + i).start();
}
new Thread(() -> {
while(true) {
sleep(1);
Message message = queue.take();
}
}, "消費者").start();
}
}
// 消息隊列類 , java 線程之間通信
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
// 消息的隊列集合
private LinkedList<Message> list = new LinkedList<>();
// 隊列容量
private int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
// 獲取消息
public Message take() {
// 檢查隊列是否爲空
synchronized (list) {
while(list.isEmpty()) {
try {
log.debug("隊列爲空, 消費者線程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 從隊列頭部獲取消息並返回
Message message = list.removeFirst();
log.debug("已消費消息 {}", message);
list.notifyAll();
return message;
}
}
// 存入消息
public void put(Message message) {
synchronized (list) {
// 檢查對象是否已滿
while(list.size() == capcity) {
try {
log.debug("隊列已滿, 生產者線程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 將消息加入隊列尾部
list.addLast(message);
log.debug("已生產消息 {}", message);
list.notifyAll();
}
}
}
final class Message {
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
輸出結果:
2.1 park & unpark
輸出結果:
2.1.1 park/unpark與wait/notify的區別
2.1.2 park & unpark的原理
兩種情況:
① 先調用park,再調用unpark
② 先調用unpark,再調用park
2.1.3 重新理解線程之間的六種轉換狀態
2.1.4 多把鎖
改進
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"小南").start();
new Thread(() -> {
bigRoom.sleep();
},"小女").start();
}
}
@Slf4j(topic = "c.BigRoom")
class BigRoom {
private final Object studyRoom = new Object();
private final Object bedRoom = new Object();
public void sleep() {
synchronized (bedRoom) {
log.debug("sleeping 2 小時");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (studyRoom) {
log.debug("study 1 小時");
Sleeper.sleep(1);
}
}
}
2.1.5 活躍性
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
結果:
2.1.6 哲學家就餐問題
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("阿基米德", c1, c5).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(1);
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
2.1.7 活鎖
活鎖出現在兩個線程互相改變對方的結束條件,最後誰也無法結束,例如
import lombok.extern.slf4j.Slf4j;
import static cn.itcast.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望減到 0 退出循環
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超過 20 退出循環
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
2.1.8 飢餓
很多教程中把飢餓定義爲,一個線程由於優先級太低,始終得不到 CPU 調度執行,也不能夠結束,飢餓的情況不易演示,講讀寫鎖時會涉及飢餓問題
下面我講一下我遇到的一個線程飢餓的例子,先來看看使用順序加鎖的方式解決之前的死鎖問題:
順序加鎖的解決方案