Java多線程
一、程式、程序、線程
1. 程式(program)
是為了完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜态的代碼,靜态對象。
2. 程序(process)
概念:在一個作業系統中,每個獨立執行的程式都可稱為一個程序,也就是“正在運作的程式”。
說明:程序作為資源配置設定的機關,系統在運作時會為每個程序配置設定不同的記憶體區域
3. 線程(thread)
概念:每個運作的程式都是一個程序,在一個程序中還可以有多個執行單元同時運作,這些執行單元可以看作是一個程式内部的一條條執行路徑,被稱為線程。
說明:線程是程式執行的最小機關,即程序的最小機關。作業系統中的每一個程序中都至少存在一個線程。每個線程擁獨立的運作棧和程式計數器(pc),線程切換的開銷小。
記憶體結構:
程序可以細化為多個線程。 每個線程,擁有自己獨立的:棧、程式計數器 。多個線程,共享同一個程序中的結構:方法區、堆。
二、單線程和多線程
1. 單線程:
就是程序隻有一個線程。單線程在程式執行時,所走的程式路徑都是按照調用順序依次往下執行的,前面的必須處理好,後面的才會執行,不會出現多段程式代碼交替運作的效果。
2. 多線程:
是指一個程序在執行過程中可以産生多個單線程,這些單線程程式在運作時是互相獨立的,并發執行。
三、并行與并發
1. 單核CPU與多核CPU
- 單核CPU,其實是一種假的多線程,因為在一個時間單元内,也隻能執行一個線程的任務。涉及到CPU處理線程的方式,CPU在機關時間(也就是說一個時間片内)内隻能處理一個線程,于是就将其他的線程設定為阻塞狀态,加入到阻塞隊列中,等到處理完成目前線程後從就緒隊列中取出新的線程進行處理,由于切換和處理時間很快使用者感覺不到,于是使用者便認為CPU在同一時間内處理多個線程。
- 多核CPU,才能更好的發揮多線程的效率。(現在的伺服器都是多核的)
- 一個Java應用程式java.exe,其實至少三個線程:main()主線程,gc()垃圾回收線程,異常處理線程。當然如果發生異常,會影響主線程。
2. 并行與并發的了解
(1) 并發:
并發(Concurrent),在作業系統中,是指一個時間段中有幾個程式都處于已啟動運作到運作完畢之間,且這幾個程式都是在同一個處理機上運作,但是任意一個時刻點上隻有一個程式在處理機上運作。
并發不是真正意義上的“同時進行”,隻是CPU把一個時間段劃分成幾個時間片段(時間區間),然後在這幾個時間區間之間來回切換,由于CPU處理的速度非常快,隻要時間間隔處理得當,即可讓使用者感覺是多個應用程式同時在進行。
(2) 并行:
并行(Parallel),當系統有一個以上CPU時,當一個CPU執行一個程序時,另一個CPU可以執行另一個程序,兩個程序互不搶占CPU資源,可以同時進行,這種方式我們稱之為并行(Parallel)。
其實決定并行的因素不是CPU的數量,而是CPU的核心數量,比如一個CPU多個核也可以并行。
(3) 并行和并發的差別:
并發:指的是多個事情,在同一時間段内同時發生了。 多個任務之間是互相搶占資源的。
并行:指的是多個事情,在同一時間點上同時發生了。 多個任務之間是不互相搶占資源的。
并發的關鍵是你有處理多個任務的能力,不一定要同時。
并行的關鍵是你有同時處理多個任務的能力。
并發是輪流處理多個任務,并行是同時處理多個任務
隻有在多CPU或者一個CPU多核的情況中,才會發生并行。否則,看似同時發生的事情,其實都是并發執行的。
四、 為什麼要使用多線程?
當我們在進行商品搶購的時候,在支付按鈕上總是有個計時器在進行倒計時,但是我們此時仍然可以進行商品資訊的檢視,這個計時器和我們浏覽商品資訊的線程是同時進行的,這樣也就實作了搶購場景,增加了使用者的體驗。
1. 多線程程式的優點:
- 提高應用程式的響應。對圖形化界面更有意義,可增強使用者體驗。
- 提高計算機系統CPU的使用率。
- 改善程式結構。将既長又複雜的程序分為多個線程,獨立運作,利于了解和修改。
2. 應用的場景
- 程式需要同時執行兩個或多個任務。
- 程式需要實作一些需要等待的任務時,如使用者輸入、檔案讀寫操作、網絡操作、搜尋等
- 需要一些背景運作的程式時
五、Thread類
Java語言的JVM允許程式運作多個線程,它通過 java. lang.Thread類來展現
1. Thread類的特性
每個線程都是通過某個特定 Thread對象的run()方法來完成操作的,經常把run()方法的主體稱為線程體 通過該 Thread對象的 start()方法來啟動這個線程,而非直接調用run
2. 構造器:
- Thread():建立新的 Thread對象
- Thread(String threadName):建立線程并指定線程執行個體名
- Thread(Runnable target):指定建立線程的目标對象,它實作了 Runnable接口中的run方法
- Thread(Runnable target, String name):建立新的 Thread對象
3. 建立多線程的兩種方式
3.1. 方式一繼承Thread類的方式:
- 建立一個繼承于Thread類的子類
- 重寫Thread類的run() --> 将此線程執行的操作聲明在run()中
- 建立Thread類的子類的對象
- 通過此對象調用start():①啟動目前線程 ② 調用目前線程的run()
注意點:
- 我們啟動一個線程,必須調用start(),不能調用run()的方式啟動線程。 如果再啟動一個線程,必須重新建立一個Thread子類的對象,調用此對象的start().(注意後面的點)
- 如果自己手動調用run()方法,那麼就隻是普通方法,沒有啟動多線程模式
- run()方法由JVM調用,什麼時候調用,執行的過程控制都有作業系統的CPU排程決定。
- 想要啟動多線程,必須調用 start方法。
- 一個線程對象隻能調用一次 start()方法啟動,如果重複調用了,則将抛出異常“lllegalThreadStateException”.
代碼示例
//1.繼承Thread類
class MyThread extends Thread {
public MyThread() {
}
//2.重run方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.建立Thread對象
MyThread myThread = new MyThread();
//4.調用start方法
myThread.start();
}
}
3.2. 方式二實作Runnable接口的方式:
- 建立一個實作了Runnable接口的類
- 實作類去實作Runnable中的抽象方法:run()
- 建立實作類的對象
- 将此對象作為參數傳遞到Thread類的構造器中,建立Thread類的對象
- 通過Thread類的對象調用start()
代碼示例:
//1. 建立一個實作了Runnable接口的類
public class RunnableTest implements Runnable {
// 2. 實作類去實作Runnable中的抽象方法:run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
class test {
public static void main(String[] args) {
//3. 建立實作類的對象
RunnableTest runnableTest = new RunnableTest();
//4. 将此對象作為參數傳遞到Thread類的構造器中,建立Thread類的對象
Thread thread = new Thread(runnableTest);
//5. 通過Thread類的對象調用start()
thread.start();
}
}
3.3 兩種方式的對比:
開發中優先選擇:實作Runnable接口的方式
原因:
- 實作的方式沒類的單繼承性的局限性
- 實作的方式更适合來處理多個線程共享資料的情況。
聯系:public class Thread implements Runnable
相同點:兩種方式都需要重寫run(),将線程要執行的邏輯聲明在run()中。 目前兩種方式,要想啟動線程,都是調用的Thread類中的start()。
也可以采用建立匿名類的方式
public class ThreadDemo {
public static void main(String[] args) {
//建立Thread類的匿名子類的方式
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
new Thread() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}.start();
}
}
4. Thread類的常用方法
4.1 常用方法:
- start():啟動目前線程;調用目前線程的run(),隻有Thread類和他的子類才能調用start()方法
- run(): 通常需要重寫Thread類中的此方法,将建立的線程要執行的操作聲明在此方法中
- currentThread():靜态方法,傳回執行目前代碼的線程
- getName():擷取目前線程的名字
- setName():設定目前線程的名字
- yield():釋放目前cpu的執行權
- join():線上程a中調用線程b的join(),此時線程a就進入阻塞狀态,直到線程b完全執行完以後,線程a才結束阻塞狀态。
- stop():已過時。當執行此方法時,強制結束目前線程。
- sleep(long millitime):讓目前線程“睡眠”指定的millitime毫秒。在指定的millitime毫秒時間内,目前線程是阻塞狀态。
- isAlive():判斷目前線程是否存活
4.2 線程的排程
Java虛拟機會按照特定的機制為程式中的每個線程配置設定CPU的使用權,這種機制被稱作線程的排程。
線程排程有兩種模型,分别是分時排程模型和搶占式排程模型。
分時排程模型:是指讓所有的線程輪流獲得CPU使用權,并且平均配置設定每個線程占用CPU的時間片。
搶占式排程模型:是指讓優先級高的線程優先占用CPU,而對于優先級相同的線程,随機選擇一個線程使其占用CPU,當它失去CPU 的使用權後,再随機選擇其他線程擷取CPU使用權。
Java虛拟機預設采用搶占式排程模型。
4.3 線程的優先級:
- MAX_PRIORITY:10
- MIN _PRIORITY:1
- NORM_PRIORITY:5 -->預設優先級
擷取和設定目前線程的優先級:
- getPriority():擷取線程的優先級
- setPriority(int p):設定線程的優先級
說明:高優先級的線程要搶占低優先級線程CPU的執行權。但是隻是從機率上講,高優先級的線程高機率的情況下被執行。并不意味着隻當高優先級的線程執行完以後,低優先級的線程才執行。
線程通信:wait() / notify() / notifyAll() :此三個方法定義在Object類中的。
5. Thread的生命周期
線程的五種狀态:
- 建立:當一個 Thread類或其子類的對象被聲明并建立時,新生的線程對象處于建立狀态
- 就緒:處于建立狀态的線程被star()後,将進入線程隊列等待CPU時間片,此時它已具備了運作的條件,隻是沒配置設定到CPU資源
- 運作:當就緒的線程被排程并獲得CPU資源時,便進入運作狀态,run()方法定義了線程的操作和功能
- 阻塞:在某種特殊情況下,被人為挂起或執行輸入輸出操作時,讓出CP∪并臨時中止自己的執行,進入阻塞狀态
- 死亡:線程完成了它的全部工作或線程被提前強制性地中止或出現異常導緻結束
說明:
- 生命周期關注兩個概念:狀态、相應的方法
- 關注:狀态a–>狀态b:哪些方法執行了(回調方法) 某個方法主動調用:狀态a–>狀态b
- 阻塞:臨時狀态,不可以作為最終狀态
- 死亡:最終狀态。
六、線程的分類
- 使用者線程,如:main方法的主線程
- 守護線程,如:垃圾回收線程。依賴于主線程而存在,即依賴于使用者線程而存在,當使用者線程執行結束,守護線程也将結束。
七、線程的同步機制
同步的方式,解決了線程的安全問題。
1.背景
例子:建立個視窗賣票,總票數為100張.使用實作Runnable接口的方式
- 問題:賣票過程中,出現了重票、錯票 -->出現了線程的安全問題
- 問題出現的原因:當某個線程操作車票的過程中,尚未操作完成時,其他線程參與進來,也操作車票。
- 如何解決:當一個線程a在操作ticket的時候,其他線程不能參與進來。直到線程a操作完ticket時,其他線程才可以開始操作ticket。這種情況即使線程a出現了阻塞,也不能被改變。
注意:<缺點> 操作同步代碼塊時,隻能有一個線程參與,其它線程等待。相當于是一個單線程的過程,效率低。
2. Java解決方案:同步機制
在Java中,我們通過同步機制,來解決線程的安全問題。
2.1 方式一:synchronized 同步代碼塊
synchronized (同步螢幕){ //同步螢幕就是需要同步線程的公共對象
//需要被同步的代碼
}
缺點:操作同步代碼塊時,隻能有一個線程參與,其它線程等待。相當于是一個單線程的過程,效率低。
說明:
- 操作共享資料的代碼,即為需要被同步的代碼。 -->不能包含代碼多了,也不能包含代碼少了。
- 共享資料:多個線程共同操作的變量。比如:ticket就是共享資料。
- 同步螢幕,俗稱:鎖。任何一個類的對象,都可以充當鎖。但是必須多個線程共用同一把鎖。
- 要求多個線程必須要共用同一把鎖。
- 考慮使用目前類對象作為同步螢幕,如下:
- 在實作Runnable接口建立多線程的方式中,我們可以考慮使用this充當同步螢幕。
-
在繼承Thread類建立多線程的方式中,慎用this充當同步螢幕,考慮使用目前類充當同步螢幕, 因為它是多個不同的對象。(可以通過反射調用目前類,格式: 目前類類名.class)
類也是對象,是Class的對象,如:Class c = Demo.class;
代碼示例:
(1) 使用同步代碼塊解決實作Runnable接口的線程安全問題
public class Ticket implements Runnable {
private int tick = 100;
@Override
public void run() {
while (true) {
synchronized (this) { //這裡this指的是目前類Ticket對象
if (tick > 0) {
System.out.println(Thread.currentThread().getName() + "号視窗買票,票号為:" + tick--);
} else {
break;
}
}
}
}
}
class TicketTest {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
thread1.setName("視窗1");
thread2.setName("視窗2");
thread3.setName("視窗3");
thread1.start();
thread2.start();
thread3.start();
}
}
(2) 使用同步代碼塊解決繼承Thread類的線程安全問題
public class Ticket2 extends Thread {
private static int tick = 100;
private static Object object = new Object();
public Ticket2() {
}
@Override
public void run() {
while (true) {
synchronized (object) {
//錯誤示範:synchronized (this),這裡this代表着ticket1,ticket2,ticket3三個對象
//正确示範:synchronized (Ticket2.class) {//通過反射調用目前類
if (tick > 0) {
System.out.println(Thread.currentThread().getName() + "号視窗買票,票号為" + tick--);
} else {
break;
}
}
}
}
}
class TicketTest2 {
public static void main(String[] args) {
Ticket2 ticket1 = new Ticket2();
Ticket2 ticket2 = new Ticket2();
Ticket2 ticket3 = new Ticket2();
ticket1.setName("視窗1");
ticket2.setName("視窗2");
ticket3.setName("視窗3");
ticket1.start();
ticket2.start();
ticket3.start();
}
}
2.2 方式二:synchronized 同步方法
把操作共享資料的代碼放到一個方法中,用synchronized修飾方法,預設鎖定的是目前對象,
(預設使用this或者目前類做為鎖, 靜态方法(類名.class)、非靜态方法(this))
public synchronized void show(){
....
}
代碼示例:
(1) 使用同步方法解決實作Runnable接口的線程安全問題
public class Ticket3 implements Runnable{
private int tick = 100;
private boolean isFlag = true;
@Override
public void run() {
while (isFlag) {
show();
}
}
public synchronized void show() {//同步show方法
if (tick > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "号視窗買票,票号為:" + tick--);
} else {
isFlag = false;
}
}
}
class TicketTest3 {
public static void main(String[] args) {
Ticket3 ticket = new Ticket3();
Thread thread1 = new Thread(ticket);
Thread thread2 = new Thread(ticket);
Thread thread3 = new Thread(ticket);
thread1.setName("視窗1");
thread2.setName("視窗2");
thread3.setName("視窗3");
thread1.start();
thread2.start();
thread3.start();
}
}
(2) 使用同步方法解決繼承Thread類的線程安全問題
public class Ticket2 extends Thread {
private int tick = 100;
private boolean isFlag = true;
@Override
public void run() {
while (isFlag) {
show();
}
}
public static synchronized void show() {//同步show方法,同時需要給方法加static關鍵字,確定show方法被所有執行個體對象共享
if (tick > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "号視窗買票,票号為:" + tick--);
} else {
isFlag = false;
}
}
}
class TicketTest3 {
public static void main(String[] args) {
Ticket2 ticket1 = new Ticket2();
Ticket2 ticket2 = new Ticket2();
Ticket2 ticket3 = new Ticket2();
ticket1.setName("視窗1");
ticket2.setName("視窗2");
ticket3.setName("視窗3");
ticket1.start();
ticket2.start();
ticket3.start();
}
}
2.3 方式三:Lock鎖 — JDK 5.0新增
- 從JDK 5.0開始,Java提供了更強大的線程同步機制–通過顯式定義同步鎖對象來實作同步。同步鎖使用Lock對象充當。
- jva.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行通路的工具。鎖提供了對共享資源的獨占通路,每次隻能有一個線程對Lock對象加鎖,線程開始通路共享資源之前應先獲得Lock對象。
- ReentrantLock類實作了Lock,它擁有與 synchronized相同的并發性和記憶體語義,在實作線程安全的控制中,比較常用的是 Reentrantlock,可以顯式加鎖、釋放鎖。
class A {
//1.執行個體化ReentrantLock對象
private final ReenTrantLock lock = new ReenTrantLook();
public void m (){
lock.lock()//2.先加鎖
try{
//保證線程同步的代碼
}finally{
lock.unlock();//3.後解鎖
}
}
}
//注意:如果同步代碼塊有異常,要将unlock()寫入finally語句塊中
注意:如果同步代碼塊有異常,要将unlock()寫入finally語句塊中
代碼示例:
class Window implements Runnable{
private int ticket = 100;
//1.執行個體化ReentrantLock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while(true){
try{
//2.調用鎖定方法lock()
lock.lock();
if(ticket > 0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":售票,票号為:" + ticket);
ticket--;
}else{
break;
}
}finally {
//3.調用解鎖方法:unlock()
lock.unlock();
}
}
}
}
public class LockTest {
public static void main(String[] args) {
Window w = new Window();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("視窗1");
t2.setName("視窗2");
t3.setName("視窗3");
t1.start();
t2.start();
t3.start();
}
}
3.同步機制的總結:
對于并發工作,你需要某種方式來防止兩個任務通路相同的資源(其實就是共享資源競争)。防止這種沖突的方法就是當資源被一個任務使用時,在其上加鎖。第一個通路某項資源的任務必須鎖定這項資源,使其他仼務在其被解鎖之前,就無法通路它了,而在其被解鎖之時,另一個任務就可以鎖定并使用它了。
synchronized的鎖是什麼:
- 任意對象都可以作為同步鎖。所有對象都自動含有單一的鎖(螢幕)
- 同步方法的鎖:靜态方法(類名.class)、非靜态方法(this)
- 同步代碼塊:自己指定,很多時候也是指定為this或類名.class
注意點:
- 必須確定使用同一個資源的多個線程共用一把鎖,這個非常重要,否則就無法保證共享資源的安全
- 一個線程類中的所有靜态方法共用同一把鎖(類名.class),所有非靜态方法共用同一把鎖(this),同步代碼塊(指定需謹慎)
- 同步方法仍然涉及到同步螢幕,隻是不需要我們顯式的聲明。
- 非靜态的同步方法,同步螢幕是:this
- 靜态的同步方法,同步螢幕是:目前類本身
4. 同步的範圍:
如何找問題,即代碼是否存線上程安全?(非常重要)
(1)明确哪些代碼是多線程運作的代碼
(2)明确多個線程是否有共享資料
(3)明确多線程運作代碼中是否有多條語句操作共享資料
如何解決呢?(非常重要)
對多條操作共享資料的語句,隻能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行。 即所有操作共享資料的這些語句都要放在同步範圍中
注意點:
範圍太小:沒鎖住所有有安全問題的代碼 範圍太大:沒發揮多線程的功能。
5. synchronized 與 Lock 面試題
1. synchronized 與 Lock的異同?
(1) 相同:二者都可以解決線程安全問題
(2) 不同:(synchronized自動,Lock手動)
- Lock是顯示鎖(手動開啟和關閉鎖,注意别忘記關閉鎖),synchronized是隐式鎖,出了作用域自動釋放。
- Lock隻有代碼塊鎖,synchronized有代碼塊鎖和方法鎖。
- 使用Lock鎖,JVM将花費較少的時間來排程線程,性能更好。并且具有更好的擴充性 (提供更多的子類)
(3) 使用的優先順序:
Lock—> 同步代碼塊(已經進入了方法體,配置設定了相應資源 ) —>同步方法(在方法體之外)
(4)利弊:
同步的方式,雖然解決了線程的安全問題。 但是操作同步代碼時,隻能一個線程參與,其他線程等待。相當于 是一個單線程的過程,效率低。
2. Java是如何解決線程安全問題的,有幾種方式?并對比幾種方式的不同
利用同步鎖的方式,有三種方式:同步代碼塊、同步方法、lock方法
3. synchronized和Lock方式解決線程安全問題的對比
- 相同:二者都可以解決線程安全問題
- 不同:synchronized機制在執行完相應的同步代碼以後,自動的釋放同步螢幕
- Lock需要手動的啟動同步 lock( ),同時結束同步也需要手動的實作 unlock( )
6. 線程安全的單例模式
使用同步機制将單例模式中的懶漢式改寫為線程安全的。
class Bank{
private Bank(){}
private static Bank instance = null;
public static Bank getInstance(){
//方式一:效率稍差
// synchronized (Bank.class) {
// if(instance == null){
//
// instance = new Bank();
// }
// return instance;
// }
//方式二:效率更高
if(instance == null){
synchronized (Bank.class) {
if(instance == null){
instance = new Bank();
}
}
}
return instance;
}
}
7. 死鎖問題
- 死鎖的了解: 不同的線程分别占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖
- 說明:
- 出現死鎖後,不會出現異常,不會出現提示,隻是所的線程都處于阻塞狀态,無法繼續
-
我們使用同步時,要避免出現死鎖。
死鎖舉例:
public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { synchronized (s1){ s1.append("a"); s2.append("1"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); }
八、線程通訊
為了解決線程的死鎖問題,引入線程通訊
1. 為什麼要線程通信?
- 多個線程并發執行時, 在預設情況下CPU是随機切換線程的,當我們需要多個線程來共同完成一件任務,并且我們希望他們有規律的執行, 那麼多線程之間需要一些協調通信,以此來幫我們達到多線程共同操作一 份資料。
- 當然如果我們沒有使用線程通信來使用多線程共同操作同一份資料的話,雖然可以實作,但是在很大程度會造成多線程之間對同一共享變量的争奪,那樣的話勢必為造成很多錯誤和損失!
- 是以,我們才引出了線程之間的通信,多線程之間的通信能夠避免對同一共享變量的争奪,避免死鎖。
2. 什麼是線程通信?
多個線程在處理同一個資源,通過線程通信來幫助解決線程之間對同一個變量的使用或操作。就是多個線程在操作同一份資料時, 避免對同一共享變量的争奪。
于是我們引出了等待喚醒機制:wait()、notify()、notifyAll()
就是在一個線程進行了規定操作後,就進入等待狀态(wait), 等待其他線程執行完他們的指定代碼過後 再将其喚醒(notify/notifyAll);
3. 線程通信涉及到的三個方法:
- wait(): 一旦執行此方法,目前線程就進入阻塞狀态,并且釋放同步螢幕(釋放鎖)。
- notify(): 一旦執行此方法,就會喚醒被wait的一個線程。如果有多個線程被wait,随機喚醒一個線程,至于是 哪個這與線程的優先級等有關。
- notifyAll(): 一旦執行此方法,就會喚醒所有被wait的線程。
4. 說明:
- wait(),notify(),notifyAll()三個方法必須使用在同步代碼塊或同步方法中。
-
wait(),notify(),notifyAll()三個方法的調用者必須是同步代碼塊或同步方法中的同步螢幕。
否則,會出現IllegalMonitorStateException異常
- wait(),notify(),notifyAll()三個方法是定義在java.lang.Object類中。
代碼示例:
使用兩個線程列印 1-100,線程1, 線程2 交替列印。
(1) synchronized同步代碼塊
class MyThread implements Runnable {
private int number = 1;
private Object object = new Object();
@Override
public void run() {
while (true) {
synchronized (object) {
object.notify();//調用notify()方法喚醒線程
if (number <= 100) {
//線程休眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + number);
number++;
try {
object.wait();//列印輸出一次後調用wait()方法将線程阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
public class CommunicationTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.setName("線程1:");
thread2.setName("線程2:");
thread1.start();
thread2.start();
}
}
(2) synchronized同步方法 (預設使用this或者目前類做為鎖, 靜态方法(類名.class)、非靜态方法(this))
class MyThread implements Runnable {
private int number = 1;
private boolean isFlag = true;
@Override
public void run() {
while (isFlag) {
show();
}
}
public synchronized void show(){
this.notify();//調用notify()方法喚醒線程
if (number <= 100) {
//線程休眠
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + number);
number++;
try {
this.wait();//列印輸出一次後調用wait()方法将線程阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
isFlag = false;
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
Thread thread2 = new Thread(myThread);
thread1.setName("線程1:");
thread2.setName("線程2:");
thread1.start();
thread2.start();
}
}
5. sleep() 和 wait()差別的面試題:
相同點:一旦執行方法,都可以使得目前的線程進入阻塞狀态。
不同點:
1)兩個方法聲明的位置不同:sleep()聲明在Thread類中,wait()方法聲明在Object類中
2)調用的要求不同:sleep()可以在任何需要的場景下調用。wait()隻能在同步代碼塊或同步方法中調用
3)關于是否釋放同步螢幕:sleep()不會釋放鎖,wait()會釋放鎖
6. 釋放鎖的操作:
- 目前線程的同步方法、同步代碼塊執行結束
- 目前線程在同步代碼塊、同步方法中遇到 break、 return終止了該代碼塊該方法的繼續執行。
- 目前線程在同步代碼塊、同步方法中出現了未處理的Error或 Exception,導緻異常結束。
- 目前線程在同步代碼塊、同步方法中執行了線程對象的 wait()方法,目前線程暫停,并釋放鎖
7. 不會釋放鎖的操作:
- 線程執行同步代碼塊或同步方法時,程式調用 Thread. sleep()、Thread yield()方法暫停目前線程的執行
- 線程執行同步代碼塊時,其他線程調用了該線程的 suspend()方法将該線程挂起,該線程不會釋放鎖(同步螢幕)
- 應盡量避免使用 suspend()和 resume()來控制線程
8. 經典例題:生産者/消費者問題
代碼示例:
/**
* 經典通信例題:生産者/消費者
* 多線程:生産者線程,消費者線程
* 共享資料:店員(商品)
* 線程安全機制:同步機制
* 涉及線程通訊:解決避免死鎖問題
*
*/
class Clerk{ //店員(商品)
private int productCount = 0;
//生産産品
public synchronized void produceProduct(){
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName()+"開始生産第"+productCount+"個産品");
notify();//喚醒
}else {
try {
wait();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消費商品
public synchronized void conumerProduct() {
if(productCount > 0){
System.out.println(Thread.currentThread().getName()+"開始生産第"+productCount+"個産品");
productCount--;
notify();//喚醒
}else {
try {
wait();//等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Producer extends Thread{ //生産者
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+":開始生産産品......");
while (true){
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
class Consumer extends Thread{//消費者
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
System.out.println(getName()+":開始消費産品......");
while (true){
try {
sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.conumerProduct();
}
}
}
public class DemoTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer p1 = new Producer(clerk);
p1.setName("生産者1");
Consumer c1 = new Consumer(clerk);
c1.setName("消費者1");
p1.start();
c1.start();
}
}
九、JDK 5.0新增線程建立方式
1. 新增方式一:實作Callable接口
1.1 Callable比Runnable更強大的原因:
- 相比run()方法,call() 方法可以有傳回值
- call() 方法可以抛出異常,擷取異常資訊
- call() 支援泛型的傳回值
- 需要借助FutureTask類,比如擷取傳回結果
Future接口:
- 可以對具體Runnable、Callable任務的執行結果進行取消、查詢是否完成、擷取結果等
- FutureTask是Future接口的唯一的實作類
- FutureTask同時實作了Runnable、Future接口。它既可以作為Runnable被線程執行,又可以作為Future得到Callable的傳回值
1.2實作方法:
- 建立一個實作Callable的實作類
- 實作call方法,将此線程需要執行的操作聲明在call()中
- 建立Callable接口實作類的對象
- 将此Callable接口實作類的對象作為傳遞到FutureTask構造器中,建立FutureTask的對象
- 将FutureTask的對象作為參數傳遞到Thread類的構造器中,建立Thread對象,并調用start()
- 擷取Callable中call方法的傳回值
代碼示例:
//1.建立一個實作Callable的實作類
class NumThread implements Callable{
//2.實作call方法,将此線程需要執行的操作聲明在call()中
@Override
public Object call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
//3.建立Callable接口實作類的對象
NumThread numThread = new NumThread();
//4.将此Callable接口實作類的對象作為傳遞到FutureTask構造器中,建立FutureTask的對象
FutureTask futureTask = new FutureTask(numThread);
//5.将FutureTask的對象作為參數傳遞到Thread類的構造器中,建立Thread對象,并調用start()
new Thread(futureTask).start();
try {
//6.擷取Callable中call方法的傳回值
//get()傳回值即為FutureTask構造器參數Callable實作類重寫的call()的傳回值。
Object sum = futureTask.get();
System.out.println("總和為:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
如何了解實作Callable接口的方式建立多線程比實作Runnable接口建立多線程方式強大?
- call()可以有傳回值。
- call()可以抛出異常,被外面的操作捕獲,擷取異常的資訊
- Callable是支援泛型的
2. 新增方式二:使用線程池
背景:經常建立和銷毀、使用量特别大的資源,比如并發情況下的線程對性能影響很大。
解決方案:
提前建立好多個線程,放入線程池中,使用時直接擷取,使用完放回池中。可以避免頻繁建立銷毀、實作重複利用。類似生活中的公共交通工具。
2.1 線程池的好處:
- 提高響應速度(減少了建立新線程的時間)
- 降低資源消耗(重複利用線程池中的線程,不需要每次都建立)
- 便于線程管理
2.2 線程池的參數說明:
corePoolSize:核心池的大小 (線程池中目前的線程數)
maximumPoolSize: 最大線程數 (核心線程 + 新建立的非核心線程)
keepAliveTime :線程沒任務時最多保持多長時間後會終止(空閑線程的存活時間)
unit :線程空閑存活時間機關
workQueue: 任務隊列(存放來不及處理的任務的隊列)
threadFactory:用于設定建立線程的工廠,可以給建立的線程設定有意義的名字,可友善排查問題。
handler :拒絕政策,當線程池中的線程數大于最大線程數,拒絕新任務的政策
拒絕政策,主要有四種類型,四種類型如下。
AbortPolicy(抛出一個異常,預設的)
DiscardPolicy(直接丢棄任務)
DiscardOldestPolicy(丢棄隊列裡最老的任務,将目前這個任務繼續送出給線程池)
CallerRunsPolicy(交給線程池調用所在的線程進行處理)
詳細處理過程:
- 送出多個任務,任務數量小于核心線程數時,線程池會建立核心線程去處理送出的任務。
- 如果送出的任務數已經等于核心線程數,下一個送出的新任務,會被放進任務隊列workQueue排隊等待執行。
- 當線程池裡面存活的線程數已經等于核心線程數了,并且任務隊列workQueue也滿,判斷線程數是否達到最大線程數,即最大線程數是否已滿,如果沒到達,建立一個非核心線程執行送出的任務。
- 如果核心線程數+新建立的非核心線程數達到了最大線程數,還有新的任務過來的話,直接采用4種拒絕政策處理。
2.3 實作方法:
- 提供指定線程數量的線程池
- 執行指定的線程的操作。需要提供實作Runnable接口或Callable接口實作類的對象
- 關閉連接配接池
2.4 相關API:
- JDK 5.0起提供了線程池相關API: ExecutorService 和 Executors
-
ExecutorService:真正的線程池接口。常見子類 ThreadPoolexecutor
(1) void execute(Runnable command):執行任務/指令,沒有傳回值,隻能用來執行Runnable類型的任務。
(2) Future submit(Callable task / Runnable task):執行任務,有傳回值,一般用來執行Callable類型的任務。當然也可以執行Runnable類型的任務,無傳回值。
(3) void shutdown():關閉連接配接池
-
Executors:工具類、線程池的工廠類,用于建立并傳回不同類型的線程池
(1) Executors. newCachedThreadPool():建立一個可根據需要建立新線程的線程池
(2) Executors. newFⅸedthreadPool(n);建立一個可重用固定線程數的線程池
(3) EXecutors. newSingleThreadEXecutor():建立一個隻有一個線程的線程池
(4) Executors. newScheduledThreadPool(n):建立一個線程池,它可安排在給定延遲後運作指令或者定期地執行。
代碼示例:
class NumberThread implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
class NumberThread1 implements Runnable{
@Override
public void run() {
for(int i = 0;i <= 100;i++){
if(i % 2 != 0){
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}
}
public class ThreadPool {
public static void main(String[] args) {
//1. 提供指定線程數量的線程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//設定線程池的屬性
// System.out.println(service.getClass());
// service1.setCorePoolSize(15);
// service1.setKeepAliveTime();
//2.執行指定的線程的操作。需要提供實作Runnable接口或Callable接口實作類的對象
service.execute(new NumberThread());//适合适用于Runnable
service.execute(new NumberThread1());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
//3.關閉連接配接池
service.shutdown();
}
}
2.5 面試題:Java中多線程的建立方式:四種
JDK 5.0以前:
- 即繼承Thread類,重寫run方法
- 實作Runnable接口,實作run方法
JDK 5.0以後:
- 實作Callable接口,實作call方法
- 利用線程池