前置知識 什麼是進程,什麼又是線程?咱不是講系統,簡單說下,知道個大概就好了。 進程:一個可執行文件執行的過程。 線程:操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務 什 ...
前置知識
什麼是進程,什麼又是線程?咱不是講系統,簡單說下,知道個大概就好了。
進程:一個可執行文件執行的過程。
線程:操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務
什麼是並行,什麼是併發?這個也簡單說下。
並行: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