前置知識
什么是行程,什么又是執行緒?咱不是講系統,簡單說下,知道個大概就好了,
行程:一個可執行檔案執行的程序,
執行緒:作業系統能夠進行運算調度的最小單位,它被包含在行程之中,是行程中的實際運作單位,一條執行緒指的是行程中一個單一順序的控制流,一個行程中可以并發多個執行緒,每條執行緒并行執行不同的任務
什么是并行,什么是并發?這個也簡單說下,
并行:cpu的兩個核心分別執行兩個執行緒,
并發:cpu的一個核心在兩個(或多個)執行緒上反復橫跳執行,
執行緒的創建
繼承Thread
// 宣告
class T extends Thread {
public void run() {
// do something
}
}
// 使用
new T().start();
實作Runnable
// 宣告
class T implements Runnable {
public void run() {
// do something
}
}
// 使用
new Thread(new T()).start();
為什么是start,而不是run,其實run只是個很普通的方法,我們來看看start的原始碼,
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0(); // 這個才是開啟執行緒
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
// start0的實作
private native void start0(); // 這是一個native方法,通常使用C/C++來實作
多執行緒機流程(從啟動到終止)
我們通過一個案例來說明,
順便說說sleep()
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
new Thread(new T0(), "T0").start();
int cnt = 0;
while (cnt < 50) {
cnt ++;
System.out.println(Thread.currentThread().getName());
Thread.sleep(200); // 讓當前執行緒停止200毫秒
}
}
}
class T0 implements Runnable {
int cnt;
@Override
public void run() {
while (cnt < 50) {
cnt ++;
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(200); // 讓當前執行緒停止200毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
執行流程:
- 啟動main方法(行程開啟)
- 啟動main執行緒
- 列印main/列印T0(根據執行緒調度執行)
- 其中一個執行緒結束
- 另一個執行緒結束
- 行程結束
執行緒常用方法
-
setName:設定執行緒名稱,不設定則使用默認執行緒名稱,
-
getName:獲取執行緒名稱,
-
start:開啟執行緒,實際開啟執行緒的方法為start0,
-
run:呼叫start后創建的新執行緒會呼叫run方法,單純呼叫run方法無法達到多執行緒的效果,run方法只是個普通的方法,
-
setPriority:更改執行緒優先級,
-
getPriority:獲取執行緒優先級,
執行緒的優先級: public static final int MIN_PRIORITY = 1; public static final int NORM_PRIORITY = 5; public static final int MAX_PRIORITY = 10; -
sleep:讓執行緒休眠指定時間,
執行緒終止
雖然Thread中提供了一個stop方法用來停止執行緒,但目前已經被廢棄,那么我們如何停止執行緒呢?我們來看看下面這個例子,
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
T0 t0 = new T0();
new Thread(t0, "T0").start();
int cnt = 0;
while (cnt < 50) {
cnt ++;
System.out.println(Thread.currentThread().getName());
}
t0.flag = false; // 設定為false用以跳出T0執行緒的回圈
/*
在main執行緒執行了50次后退出T0執行緒,接著退出main執行緒
*/
}
}
class T0 implements Runnable {
boolean flag = true; // 定義一個標記,用來控制執行緒是否停止
@Override
public void run() {
while (flag) {
System.out.println(Thread.currentThread().getName());
}
}
}
通過定義一個標記flag讓執行緒退出,
執行緒中斷
Thread中有一個interrupt方法,這個方法不是說中斷執行緒的運行,而是中斷執行緒當前執行的操作,我們來看下樣例,
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
T0 t0 = new T0();
Thread thread0 = new Thread(t0, "T0");
thread0.start();
System.out.println("張三在劃水,,,");
Thread.sleep(2000);
thread0.interrupt(); // 通過拋出一個InterruptedException例外來打斷當前操作(sleep)
}
}
class T0 implements Runnable {
boolean flag = true;
@Override
public void run() {
System.out.println("李四在打盹,,,");
while (flag) {
try {
Thread.sleep(20000); // 2秒后被interrupt中斷
} catch (InterruptedException e) {
flag = false;
System.out.println("老板來了,張三搖醒了李四,,,"); // 由于main執行緒呼叫了interrupt,實際過了2秒就輸出了
}
}
}
}
輸出結果:
張三在劃水,,,
李四在打盹,,,
老板來了,張三搖醒了李四,,,
執行緒讓步
Thread類中提供了yield方法,用來禮讓cpu資源,禮讓了資源就會執行其他執行緒嗎?我們看看這個例子
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
T0 t0 = new T0();
Thread thread0 = new Thread(t0, "T0");
thread0.start();
int cnt = 0;
while (cnt < 5) {
cnt ++;
System.out.println(Thread.currentThread().getName());
Thread.yield(); // 放棄當前的cpu資源,讓cpu重新分配資源,
}
}
}
class T0 implements Runnable {
int cnt;
@Override
public void run() {
int cur = 0; // 連續吃的包子數
while (cnt < 5) {
cnt ++;
System.out.println(Thread.currentThread().getName());
Thread.yield(); // 放棄當前的cpu資源,讓cpu重新分配資源,
}
}
}
輸出結果:
main
T0
main
main
main
main
T0
T0
T0
T0
可以看到兩個執行緒互相禮讓,如果yield方法會強制執行其他執行緒的話,那執行緒應該會交替執行,而不是有連續執行同一個執行緒的情況,所以證明了yield并不是強制禮讓,
執行緒插隊
Thread類提供了join方法,可以指定一個執行緒優先執行,
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread thread0 = new Thread(new T0(), "T0");
thread0.start();
Thread thread1 = new Thread(new T0(), "T1");
thread1.start();
int cnt = 0;
while (cnt < 2) {
cnt ++;
System.out.println(Thread.currentThread().getName());
thread0.join(); // 讓執行緒thread0插隊,執行完thread0的所有任務后回到當前執行緒,
}
}
}
class T0 implements Runnable {
int cnt;
@Override
public void run() {
int cur = 0;
while (cnt < 2) {
cnt ++;
System.out.println(Thread.currentThread().getName());
}
}
}
輸出結果:
main
T1
T1
T0
T0
main
總體上main執行緒確實被T0插隊了,但為啥T1在T0的前面被執行?因為當前是多核CPU的環境,其它的核心在執行剩下的執行緒,執行T1執行緒的核心比T0的快所以T1在T0之前被輸出,不止有并發,還有并行,在單純并發的條件下,就變成了T0的所有任務都執行完畢后,才會執行其他執行緒,當前的main與T0是并發的,與T1是并行,
守護執行緒
Thread類提供setDaemon方法,可以設定目標執行緒為當前執行緒的守護執行緒,當前執行緒終止時,目標(守護)執行緒也隨之終止,
public class ThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread thread0 = new Thread(new T0(), "T0");
thread0.setDaemon(true);
thread0.start();
System.out.println("張三 --> 新一天的作業開始了");
int time = 0;
while (time < 8) { // 張三每天作業八個小時,,,
time ++;
}
System.out.println("張三 --> 下班了,回家吃老婆做的飯咯");
System.out.println("小紅 --> 我老公張三下班了,今天就到這了");
System.out.println("小紅 --> 守護執行緒YYDS");
}
}
class T0 implements Runnable {
int cnt;
@Override
public void run() {
System.out.println("小紅 --> 李四來我家甜蜜雙排王者榮耀");
while (true);
}
}
輸出結果:
張三 --> 新一天的作業開始了
小紅 --> 李四來我家甜蜜雙排王者榮耀
張三 --> 下班了,回家吃老婆做的飯咯
小紅 --> 我老公張三下班了,今天就到這了
小紅 --> 守護執行緒YYDS
當main執行緒終止時,T0執行緒也終止,
執行緒的狀態
5種狀態是OS的執行緒狀態,而6種則說的是JVM的執行緒狀態,以下是狀態圖,

OS的執行緒狀態為粗體
JVM的執行緒狀態為英文
執行緒同步機制
想看個經典問題,
多執行緒售票問題
我們來看看案例,
public class ThreadTest {
public static void main(String[] args) {
T0 t0 = new T0();
Thread thread0 = new Thread(t0, "張三");
Thread thread1 = new Thread(t0, "李四");
thread0.start();
thread1.start();
}
}
class T0 implements Runnable {
int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s買了一張票,還剩%d張%n", Thread.currentThread().getName(), -- ticket);
}
}
}
輸出結果:
...
張三買了一張票,還剩3張
張三買了一張票,還剩2張
李四買了一張票,還剩1張
張三買了一張票,還剩0張
李四買了一張票,還剩0張
出現了重復賣票,造成這種現象的原因是執行緒不安全,那如何讓執行緒安全呢,這就是接下來要介紹的互斥鎖,
互斥鎖
java提供了synchronized關鍵字用以開啟鎖,看看如何使用鎖解決上面的執行緒安全問題,
public class ThreadTest {
public static void main(String[] args) {
T0 t0 = new T0();
Thread thread0 = new Thread(t0, "張三");
Thread thread1 = new Thread(t0, "李四");
thread0.start();
thread1.start();
}
}
class T0 implements Runnable {
int ticket = 100;
@Override
public synchronized void run() { // 我們將鎖加在run方法上
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("%s買了一張票,還剩%d張%n", Thread.currentThread().getName(), -- ticket);
}
}
}
輸出結果:
...
張三買了一張票,還剩4張
張三買了一張票,還剩3張
張三買了一張票,還剩2張
張三買了一張票,還剩1張
張三買了一張票,還剩0張
所有的輸出都在執行緒張三上,這顯然不是我們想要的,
首先,為什么會有這種現象發生?其實,被synchronized修飾的方法或代碼塊會被上鎖,并發環境下先進入該方法或者代碼塊的執行緒將獲得鎖并執行這部分代碼,而其他執行緒則處于阻塞狀態直到獲得鎖的執行緒執行完被上鎖的所有代碼后,其他執行緒才有機會去爭奪鎖,
上述現象的原因是synchronized修飾了整個方法,所以當張三拿到鎖時會執行完所有的回圈后釋放鎖,這時李四就什么都輸出不了了,和單執行緒一樣,除了多了個一直阻塞的執行緒,性能低下,
那么如何保證執行緒安全的前提下,保證并發的性能呢?第一次嘗試解決,
public class ThreadTest {
public static void main(String[] args) {
T0 t0 = new T0();
Thread thread0 = new Thread(t0, "張三");
Thread thread1 = new Thread(t0, "李四");
thread0.start();
thread1.start();
}
}
class T0 implements Runnable {
int ticket = 100;
@Override
public /*synchronized*/ void run() { // 我們將鎖加在run方法上
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(this) { // 存在兩個執行緒執行時都滿足ticket>0,仍然有執行緒安全問題
System.out.printf("%s買了一張票,還剩%d張%n", Thread.currentThread().getName(), -- ticket);
}
}
}
}
輸出結果:
...
李四買了一張票,還剩3張
李四買了一張票,還剩2張
張三買了一張票,還剩1張
張三買了一張票,還剩0張
李四買了一張票,還剩-1張
雖然兩個執行緒恢復了并發,但執行緒安全問題也隨之出現,
第二次嘗試解決,
public class ThreadTest {
public static void main(String[] args) {
T0 t0 = new T0();
Thread thread0 = new Thread(t0, "張三");
Thread thread1 = new Thread(t0, "李四");
thread0.start();
thread1.start();
}
}
class T0 implements Runnable {
int ticket = 100;
@Override
public /*synchronized*/ void run() { // 我們將鎖加在run方法上
while (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(this) {
if (ticket <= 0) return; // 在同步代碼塊中再次判斷以確保執行緒安全
System.out.printf("%s買了一張票,還剩%d張%n", Thread.currentThread().getName(), -- ticket);
}
}
}
}
輸出結果:
...
李四買了一張票,還剩4張
李四買了一張票,還剩3張
張三買了一張票,還剩2張
張三買了一張票,還剩1張
李四買了一張票,還剩0張
通過在同步代碼塊中再次判斷以達到執行緒安全,
死鎖
并發編程中不只有執行緒安全問題,還有死鎖問題,
public class ThreadTest {
public static void main(String[] args) {
String lock1 = "A";
String lock2 = "B";
String lock3 = "C";
Thread t0 = new Thread(new T0(lock1, lock2));
Thread t1 = new Thread(new T0(lock2, lock3));
Thread t2 = new Thread(new T0(lock3, lock1));
t0.start();
t1.start();
t2.start();
}
}
class T0 implements Runnable {
String lock1;
String lock2;
T1(String lock1, String lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
synchronized(lock1) {
System.out.println("獲取鎖: " + lock1);
synchronized(lock2) {
System.out.println("獲取鎖: " + lock2);
}
System.out.println("釋放鎖: " + lock2);
}
System.out.println("釋放鎖: " + lock1);
}
}
輸出結果:
獲取鎖:B
獲取鎖:A
獲取鎖:C
可以看出三個執行緒分別持有一把鎖,相互鎖住不能釋放,形成死鎖,
為了不寫出死鎖的并發代碼,我們需要學習釋放鎖的時機,
釋放鎖
-
run執行完畢,釋放鎖,
-
wait執行,釋放鎖,
-
sleep執行,不會釋放鎖,
-
join執行,不會釋放鎖,而是掛起當前執行緒,
-
notify執行,不會釋放鎖,
public class ThreadTest { public static void main(String[] args) { String lock = "A"; Thread thread0 = new Thread(new T0(lock), "T0"); Thread thread1 = new Thread(new T1(lock), "T1"); thread0.start(); thread1.start(); } } class T0 implements Runnable { String lock; public T0(String lock) { this.lock = lock; } @Override public void run() { synchronized(lock) { System.out.println(Thread.currentThread().getName() + "獲取鎖"); try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName() + "釋放鎖"); } } } class T1 implements Runnable { String lock; public T1(String lock) { this.lock = lock; } @Override public void run() { try { Thread.sleep(5000); synchronized(lock) { System.out.println(Thread.currentThread().getName() + "獲取鎖"); lock.notify(); Thread.sleep(5000); System.out.println(Thread.currentThread().getName() + "釋放鎖"); } } catch (InterruptedException e) { throw new RuntimeException(e); } } }輸出結果:
T0獲取鎖 T1獲取鎖 T1釋放鎖 T0釋放鎖
? 證明notify并不會釋放鎖,只是通知一個wait的執行緒:Waiting → Runnable(Ready),接著在呼叫notify的執行緒執行完畢后釋放鎖,
本文來自博客園,作者:buzuweiqi,轉載請注明原文鏈接:https://www.cnblogs.com/buzuweiqi/p/16641509.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/504293.html
標籤:其他
上一篇:使用 Mypy 檢查 30 萬行 Python 代碼,總結出 3 大痛點與 6 個技巧!
下一篇:顯示圖示而不是整數,反應原生資料
