JavaSE第18篇:多執行緒上篇
核心內容:在實際開發中,若程式需要同時處理多個任務時,我們該如何實現?此時多執行緒就可幫助我們實現。使用多執行緒可以提高CPU的利用率及程式的處理效率。本篇將會學習多執行緒相關概念、建立和使用、執行緒安全問題及執行緒狀態的瞭解。
目錄
第一章:多執行緒基礎
本章主要了解和多執行緒相關的一些概念。
想要設計一個程式,邊打遊戲邊聽歌,怎麼設計?
要解決上述問題,得使用多程序或者多執行緒來解決.
1.1-併發和並行(瞭解)
併發
併發簡而言之就是:指兩個或多個事件在同一個時間段內發生 (交替執行)。
在作業系統中,安裝了多個程式,併發指的是在一段時間內巨集觀上有多個程式同時執行,這在單 CPU 系統中,每 一時刻只能有一道程式執行,即微觀上這些程式是分時的交替執行,只不過是給人的感覺是同時執行,那是因為分時交替執行的時間是非常短的。
並行
簡而言之,並行:是指兩個或多個事件在同一時刻發生(同時發生)。
而在多個 CPU 系統中,則這些可以併發執行的程式便可以分配到多個處理器上(CPU),實現多工並行執行,即利用每個處理器來處理一個可以併發執行的程式,這樣多個程式便可以同時執行。目前電腦市場上說的多核 CPU,便是多核處理器,核越多,並行處理的程式越多,能大大的提高電腦執行的效率。
1.2-程序與執行緒 (瞭解)
程序
是指一個記憶體中執行的應用程式,每個程序都有一個獨立的記憶體空間,一個應用程式可以同時執行多個程序;程序也是程式的一次執行過程,是系統執行程式的基本單位;系統執行一個程式即是一個程序從建立、執行到消亡的過程。
執行緒
執行緒是程序中的一個執行單元,負責當前程序中程式的執行,一個程序中至少有一個執行緒。一個程序
中是可以有多個執行緒的,這個應用程式也可以稱之為多執行緒程式。
注意
一個程式執行後至少有一個程序,一個程序中可以包含多個執行緒。
由於建立一個執行緒的開銷比建立一個程序的開銷小的多,那麼我們在開發多工執行的時候,通常考慮建立多執行緒,而不是建立多程序。
多執行緒可以提高cpu利用率
大部分作業系統都支援多程序併發執行,現在的作業系統幾乎都支援同時執行多個程式。在同時執行的程式,”感覺這些軟體好像在同一時刻執行著“。
實際上,CPU(中央處理器)使用搶佔式排程模式在多個執行緒間進行著高速的切換。對於CPU的一個核而言,某個時刻,只能執行一個執行緒,而 CPU的在多個執行緒間切換速度相對我們的感覺要快,看上去就是在同一時刻執行。 其實,多執行緒程式並不能提高程式的執行速度,但能夠提高程式執行效率,讓CPU的使用率更高。
1.3-執行緒排程(瞭解)
分時排程
所有執行緒輪流使用 CPU 的使用權,平均分配每個執行緒佔用 CPU 的時間。
搶佔式排程
優先讓優先順序高的執行緒使用 CPU,如果執行緒的優先順序相同,那麼會隨機選擇一個(執行緒隨機性),Java使用的為搶佔式排程。
第二章:Java中建立和使用多執行緒
2.1-繼承Thread類方式建立執行緒(重要)
Java使用 java.lang.Thread 類代表執行緒,所有的執行緒物件都必須是Thread類或其子類的例項。每個執行緒的作用是 完成一定的任務,實際上就是執行一段程式流即一段順序執行的程式碼。Java使用執行緒執行體來代表這段程流。
Java中通過繼承Thread類來建立並啟動多執行緒的步驟如下:
- 定義Thread類的子類,並重寫該類的run()方法,該run()方法的方法體就代表了執行緒需要完成的任務,因此把 run()方法稱為執行緒執行體。
- 建立Thread子類的例項,即建立了執行緒物件 。
- 呼叫執行緒物件的start()方法來啟動該執行緒 。
Thread類構造方法
- public Thread() :分配一個新的執行緒物件。
- public Thread(String name) :分配一個指定名字的新的執行緒物件。
示例程式碼
/*測試類中的程式碼*/
public class DemoThread {
public static void main(String[] args) {
MyThread mt = new MyThread("執行緒1");
mt.start(); // 啟動執行緒1的任務
MyThread mt2 = new MyThread("執行緒2");
mt2.start(); // 啟動執行緒2的任務
}
}
/*定義的執行緒類程式碼*/
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(getName() + "執行緒執行" + i);
}
}
}
2.2-多執行緒原理(瞭解)
多個執行緒之間的程式不會影響彼此(比如一個執行緒崩潰了並不會影響另一個執行緒)。
在Java中,main方法是程式執行的入口,也是Java程式的主執行緒。當在程式中開闢新的執行緒時,執行過程是這樣的。
執行過程
-
首先main方法作為主程式先壓棧執行。
-
在主程式的執行過程中,若建立了新的執行緒,則記憶體中會另開闢一個新的棧來執行新的執行緒。
-
每一個新的執行緒都會有一個新的棧來存放新的執行緒任務。
-
棧與棧之間的任務不會互相影響。
-
CPU會隨機切換執行不同棧中的任務。
圖解執行過程(以上述程式碼為例)
2.3-Thread類常用方法(重要)
常用方法
- public String getName() :獲取當前執行緒名稱。
- public void start() :導致此執行緒開始執行; Java虛擬機器呼叫此執行緒的run方法。
- public void run() :此執行緒要執行的任務在此處定義程式碼。
- public static void sleep(long millis) :使當前正在執行的執行緒以指定的毫秒數暫停(暫時停止執行)。
- public static Thread currentThread() :返回對當前正在執行的執行緒物件的引用。
示例程式碼
//【程式碼測試類】
public class Main01 {
public static void main(String[] args) {
MyThread mt = new MyThread("執行緒1");
mt.start();
// 列印執行緒名稱
System.out.println(mt.getName());
System.out.println("當前執行緒是" + Thread.currentThread().getName());
// 每間隔一秒鐘列印一個數字
for (int i = 0; i < 60; i++) {
System.out.println(i);
try {
// sleep丟擲了異常,需要處理異常
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//【MyThread類】
public class MyThread extends Thread{
public MyThread(){
super();
}
// 建構函式中呼叫父類建構函式傳入執行緒名稱
public MyThread(String name) {
super(name);
}
@Override
public void run() {
// 列印執行緒名稱
System.out.println(this.getName());
System.out.println("當前執行緒是" + Thread.currentThread().getName());
}
}
run方法和start方法
run()方法,是執行緒執行的任務方法,每個執行緒都會呼叫run()方法執行,我們將執行緒要執行的任務程式碼都寫在run()方法中就可以被執行緒呼叫執行。
start()方法,開啟執行緒,執行緒呼叫run()方法。start()方法原始碼中會呼叫本地方法start0()來啟動執行緒:private native void start0()
,本地方法都是和作業系統互動的,因此可以看出每次開啟一個執行緒的執行緒都會和作業系統進行互動。
注意:一個執行緒只能被啟動一次!
關於執行緒的名字
執行緒是有預設名字的,如果我們不設定執行緒的名字,JVM會賦予執行緒預設名字Thread-0,Thread-1。
2.4-實現Runnable介面方式建立執行緒(重要)
翻閱API後得知建立執行緒的方式總共有兩種,一種是繼承Thread類方式,一種是實現Runnable介面方式 。
Runnable使用步驟
- 定義Runnable介面的實現類,並重寫該介面的run()方法,該run()方法的方法體同樣是該執行緒的執行緒執行體。
- 建立Runnable實現類的例項,並以此例項作為Thread的target來建立Thread物件,該Thread物件才是真正的執行緒物件。
- 呼叫執行緒物件的start()方法來啟動執行緒。
Thread類建構函式
- public Thread(Runnable target) :分配一個帶有指定目標新的執行緒物件。
- public Thread(Runnable target,String name) :分配一個帶有指定目標新的執行緒物件並指定名字。
示例程式碼
// 測試類
public class Main01 {
public static void main(String[] args) {
// 建立Runnable物件
RunnableImpl ra = new RunnableImpl();
// 建立執行緒物件並傳入Runnable物件
Thread th = new Thread(ra);
// 啟動並執行執行緒任務
th.start();
}
}
// 【Runnable實現類】
public class RunnableImpl implements Runnable {
@Override
public void run() {
System.out.println("執行緒任務1");
}
}
總結
- 通過實現Runnable介面,使得該類有了多執行緒類的特徵。run()方法是多執行緒程式的一個執行目標。所有的多執行緒程式碼都在run方法裡面。Thread類實際上也是實現了Runnable介面的類。
- 在啟動的多執行緒的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出物件,然後呼叫Thread物件的start()方法來執行多執行緒程式碼。
- 實際上所有的多執行緒程式碼都是通過執行Thread的start()方法來執行的。因此,不管是繼承Thread類還是實現Runnable介面來實現多執行緒,最終還是通過Thread的物件的API來控制執行緒的,熟悉Thread類的API是進行多執行緒程式設計的基礎。
- Runnable物件僅僅作為Thread物件的target,Runnable實現類裡包含的run()方法僅作為執行緒執行體。而實際的執行緒物件依然是Thread例項,只是該Thread執行緒負責執行其target的run()方法。
2.5-Runnable和Thread的關係(瞭解)
建立執行緒方式2好像比建立執行緒方式1操作要麻煩一些,為何要多此一舉呢?
因為如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable介面的話,則很容易的實現資源共享。
實現Runnable介面比繼承Thread類所具有的優勢:
- 適合多個相同的程式程式碼的執行緒去共享同一個資源。
- 可以避免java中的單繼承的侷限性。
- 增加程式的健壯性,實現解耦操作,程式碼可以被多個執行緒共享,程式碼和執行緒獨立。
- 執行緒池只能放入實現Runable或Callable類執行緒,不能直接放入繼承Thread的類。(後面篇幅介紹)
擴充套件瞭解
在java中,每次程式執行至少啟動2個執行緒。一個是main執行緒,一個是垃圾收集執行緒。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實在就是在作業系統中啟動了一個程序。
2.6-匿名內部類方式實現執行緒建立(重要)
使用執行緒的內匿名內部類方式,可以方便的實現每個執行緒執行不同的執行緒任務操作。
簡而言之,使用匿名內部類可以簡化程式碼。
// 匿名內部類建立執行緒方式1
new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}.start();
// 匿名內部類建立執行緒方式2
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
第三章:執行緒安全問題
3.1-執行緒安全概述(理解)
多個執行緒執行同一個任務並操作同一個資料時,就會造成資料的安全問題。我們通過以下案例來看執行緒安全問題。
案例需求
電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “皮卡丘大戰葫蘆娃”,本次電影的座位共100個 (本場電影只能賣100張票)。
我們來模擬電影院的售票視窗,實現多個視窗同時賣 “皮卡丘大戰葫蘆娃”這場電影票(多個視窗一起賣這100張票) 需要視窗,採用執行緒物件來模擬;需要票,Runnable介面子類來模擬 。
案例程式碼
//【操作票的任務程式碼類】
public class RunnableImpl implements Runnable {
// 執行緒任務要操作的資料(100張電影票)
private int ticket = 100;
// 執行緒要執行的任務
@Override
public void run() {
while (true){
if(ticket>0){
System.out.println(Thread.currentThread().getName() + "正在賣第" + ticket+"張票");
ticket--;
}else {
break;
}
}
}
}
//【測試類】
public class Main01 {
public static void main(String[] args) {
// 建立執行緒任務
RunnableImpl ra = new RunnableImpl();
// 建立第一個執行緒執行執行緒任務
new Thread(ra).start();
// 建立第二執行緒執行執行緒任務
new Thread(ra).start();
// 建立第三個執行緒執行執行緒任務
new Thread(ra).start();
}
}
執行結果及問題
問題原因
搶奪cpu執行權和執行緒執行時間是不確定的,比如執行緒0搶到了cpu執行權並執行到了列印程式碼處,此時cpu又被執行緒1搶奪,其他執行緒處於等待執行緒1頁執行到了列印程式碼處,沒等ticket--,兩個執行緒都列印了售票資訊。
這種問題,幾個視窗(執行緒)票數不同步了,這種問題稱為執行緒不安全。
執行緒安全問題都是由全域性變數及靜態變數引起的。若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫 操作,一般來說,這個全域性變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步, 否則的話就可能影響執行緒安全。
3.2-執行緒安全解決方案(重要)
上述我們知道,執行緒安全問題是因為執行緒在操作資料時不同步造成的,所以只要能夠實現操作資料同步,就可以解決執行緒安全問題。
同步指的就是,當一個執行緒執行指定同步的程式碼任務時,其他執行緒必須等該執行緒操作完畢後再執行。
根據案例描述:視窗1執行緒進入操作的時候,視窗2和視窗3執行緒只能在外等著,視窗1操作結束,視窗1和視窗2和視窗3才有機會進入程式碼 去執行。也就是說在某個執行緒修改共享資源的時候,其他執行緒不能修改該資源,等待修改完畢同步之後,才能去搶奪CPU 資源,完成對應的操作,保證了資料的同步性,解決了執行緒不安全的現象。
為了保證每個執行緒都能正常執行原子操作,Java引入了執行緒同步機制(synchronize)。
那麼怎麼去使用呢?有三種方式完成同步操作:
- 同步程式碼塊
- 同步方法
- 同步鎖
3.3-同步程式碼塊(重要)
概述
同步程式碼塊: synchronized
關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。
格式
synchronized(同步鎖){ 需要同步操作的程式碼 }
- 同步鎖:物件的同步鎖只是一個概念,可以想象為在物件上標記了一個鎖。
- 鎖物件可以是任意型別。
- 多個執行緒物件 要使用同一把鎖。
- 注意:在任何時候,最多允許一個執行緒擁有同步鎖,誰拿到鎖就進入程式碼塊,其他的執行緒只能在外等著 。
示例程式碼
//【測試類】
public class Main01 {
public static void main(String[] args) {
// 建立執行緒任務
RunnableImpl ra = new RunnableImpl();
// 建立第一個執行緒執行執行緒任務
new Thread(ra).start();
// 建立第二執行緒執行執行緒任務
new Thread(ra).start();
// 建立第三個執行緒執行執行緒任務
new Thread(ra).start();
}
}
//【執行緒任務類】
public class RunnableImpl implements Runnable {
// 執行緒任務要操作的資料
private int ticket = 100;
// 定義執行緒鎖物件(任意物件)
Object obj = new Object();
// 執行緒任務
@Override
public void run() {
while (true){
synchronized (obj){
if(ticket>0){
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"在售賣第" + ticket + "張票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
3.4-同步方法(重要)
概述
同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A執行緒執行該方法的時候,其他執行緒只能在方法外等著
格式
public synchronized void method(){ // 可能會產生執行緒安全問題的程式碼 }
同步鎖是誰?
- 對於非static方法,同步鎖就是this
- 對於static方法,我們使用當前方法所在類的位元組碼物件(類名.class)。
示例程式碼
//【測試類】
public class Main01 {
public static void main(String[] args) {
// 建立執行緒任務
RunnableImpl ra = new RunnableImpl();
// 建立第一個執行緒執行執行緒任務
new Thread(ra).start();
// 建立第二執行緒執行執行緒任務
new Thread(ra).start();
// 建立第三個執行緒執行執行緒任務
new Thread(ra).start();
}
}
//【執行緒任務類】
public class RunnableImpl implements Runnable {
// 執行緒任務要操作的資料
private int ticket = 100;
@Override
public void run() {
while (true) {
int flag = func();
if(flag==0) {
break;
}
}
}
public synchronized int func() {
if (ticket > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "在售賣第" + ticket + "張票");
ticket--;
return 1;
}else {
return 0;
}
}
}
3.5-同步鎖(重要)
概述
Lock:java.util.concurrent.locks.Lock 機制提供了比synchronized程式碼塊和synchronized方法更廣泛的鎖定操作, 同步程式碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向物件。
格式
Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了
public void lock()
:加同步鎖。public void unlock()
:釋放同步鎖。
示例程式碼
//【測試類】
public class Main01 {
public static void main(String[] args) {
// 建立執行緒任務
RunnableImpl ra = new RunnableImpl();
// 建立第一個執行緒執行執行緒任務
new Thread(ra).start();
// 建立第二執行緒執行執行緒任務
new Thread(ra).start();
// 建立第三個執行緒執行執行緒任務
new Thread(ra).start();
}
}
//【執行緒任務類】
public class RunnableImpl implements Runnable {
// 執行緒任務要操作的資料
private int ticket = 100;
// 建立鎖物件
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 開啟同步鎖
lock.lock();
if (ticket > 0) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + "在售賣第" + ticket + "張票");
ticket--;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
// 釋放同步鎖
lock.unlock();
}
}else {
break;
}
}
}
}
第四章:執行緒狀態
4.1-執行緒狀態介紹(瞭解)
當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。線上程的生命週期中, 有幾種狀態呢?在API中 java.lang.Thread.State 這個列舉中給出了六種執行緒狀態:
我們不需要去研究這幾種狀態的實現原理,我們只需知道在做執行緒操作中存在這樣的狀態。那我們怎麼去理解這幾 個狀態呢,新建與被終止還是很容易理解的,我們就研究一下執行緒從Runnable(可執行)狀態與非執行狀態之間 的轉換問題。
4.2-TimedWaiting計時等待(瞭解)
概述
Timed Waiting在API中的描述為:一個正在限時等待另一個執行緒執行一個(喚醒)動作的執行緒處於這一狀態。
單獨 的去理解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀態了,在哪裡呢? 在我們寫賣票的案例中,為了減少執行緒執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就 強制當前正在執行的執行緒休眠(暫停執行),以“減慢執行緒”。
其實當我們呼叫了sleep方法之後,當前執行的執行緒就進入到“休眠狀態”,其實就是所謂的Timed Waiting(計時等 待),那麼我們通過一個案例加深對該狀態的一個理解。
示例
需求:實現一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字輸出一個字串 。
public class MyThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
if ((i) % 10 == 0) {
System.out.println("‐‐‐‐‐‐‐" + i);
}
System.out.print(i);
try {
Thread.sleep(1000);
System.out.print(" 執行緒睡眠1秒!\n");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new MyThread().start();
}
}
通過案例可以發現,sleep方法的使用還是很簡單的。我們需要記住下面幾點:
- 進入 TIMED_WAITING 狀態的一種常見情形是呼叫的 sleep 方法,單獨的執行緒也可以呼叫,不一定非要有協 作關係。
- 為了讓其他執行緒有機會執行,可以將Thread.sleep()的呼叫放執行緒run()之內。這樣才能保證該執行緒執行過程 中會睡眠 。
- sleep與鎖無關,執行緒睡眠到期自動甦醒,並返回到Runnable(可執行)狀態 。
注意:sleep()中指定的時間是執行緒不會執行的最短時間。因此,sleep()方法不能保證該執行緒睡眠到期後就 開始立刻執行。
圖解
4.3-Blocked鎖阻塞(瞭解)
概述
Blocked狀態在API中的介紹為:一個正在阻塞等待一個監視器鎖(鎖物件)的執行緒處於這一狀態 。
我們已經學完同步機制,那麼這個狀態是非常好理解的了。比如,執行緒A與執行緒B程式碼中使用同一鎖,如果執行緒A獲 取到鎖,執行緒A進入到Runnable狀態,那麼執行緒B就進入到Blocked鎖阻塞狀態。
這是由Runnable狀態進入Blocked狀態。除此Waiting以及Time Waiting狀態也會在某種情況下進入阻塞狀態。
圖解
4.4-Waiting 無限等待(瞭解)
概述
Wating狀態在API中介紹為:一個正在無限期等待另一個執行緒執行一個特別的(喚醒)動作的執行緒處於這一狀態。
示例
我們通過一段程式碼來 學習一下:需求如下,消費者吃包子。過程如下:
- 消費者問:包子好了嗎? 處於等待...
- 3秒鐘後....
- 老闆答:包子好了
- 消費者:可以吃包子了
示例程式碼:
public class Test04 {
// 鎖物件
public static Object obj = new Object();
public static void main(String[] args) {
// 【消費者執行緒】
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
synchronized (obj) {
System.out.println(Thread.currentThread().getName() + "-顧客1:老闆包子好了嗎?");
try {
// 等待,釋放鎖,處於阻塞狀態
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 喚醒之後要執行的程式碼
System.out.println(Thread.currentThread().getName() + "顧客1:可以吃包子了。");
System.out.println("--------------------------------------");
}
}
}
}).start();
// 【生產者執行緒】
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
System.out.println("等待3秒後...");
System.out.println(Thread.currentThread().getName() + "老闆說:包子好了!");
// 喚醒,喚醒其他被阻塞的執行緒
obj.notify();
}
}
}
}).start();
}
}
分析
通過上述案例我們會發現,一個呼叫了某個物件的 Object.wait 方法的執行緒會等待另一個執行緒呼叫此物件的 Object.notify()方法 或 Object.notifyAll()方法 。
其實waiting狀態並不是一個執行緒的操作,它體現的是多個執行緒間的通訊,可以理解為多個執行緒之間的協作關係, 多個執行緒會爭取鎖,同時相互之間又存在協作關係。就好比在公司裡你和你的同事們,你們可能存在晉升時的競 爭,但更多時候你們更多是一起合作以完成某些任務。
當多個執行緒協作時,比如A,B執行緒,如果A執行緒在Runnable(可執行)狀態中呼叫了wait()方法那麼A執行緒就進入 了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B執行緒獲取到了同步鎖,在執行狀態中呼叫了 notify()方法,那麼就會將無限等待的A執行緒喚醒。注意是喚醒,如果獲取到鎖物件,那麼A執行緒喚醒後就進入 Runnable(可執行)狀態;如果沒有獲取鎖物件,那麼就進入到Blocked(鎖阻塞狀態)。
圖解