政府網(wǎng)站建設(shè)培訓講話惠州優(yōu)化怎么做seo
第六章:多線程
6.1:程序、進程、線程基本概念
-
程序
程序
program
是為了完成特定任務(wù)、用某種語言編寫的一組指令的集合。即指一段靜態(tài)的代碼,靜態(tài)對象。 -
進程
? 進程
process
是程序的一次執(zhí)行過程,或是正在運行的一個程序。是一個動態(tài)的過程:有它自身的產(chǎn)生、存在和消亡的過程?!旧芷凇?/p>- 程序是靜態(tài)的,進程是動態(tài)的。
- 進程作為資源分配的單位,系統(tǒng)在運行時會為每個進程分配不同的內(nèi)存區(qū)域。
-
線程
進程可進一步細化為線程,是一個程序內(nèi)部的一條執(zhí)行路徑。
- 若一個進行同一時間并行執(zhí)行多個線程,就是支持多線程的。
- 線程作為調(diào)度和執(zhí)行的單位,每個線程擁有獨立的運行棧和程序計數(shù)器(pc),線程切換的開銷小。
- 一個進程中的多個線程共享相同的內(nèi)存單元/內(nèi)存地址空間。他們從同一堆中分配對象,可以訪問相同的變量和對象。這就使得線程間通信更簡便、高效。但是多個線程操作共享的系統(tǒng)資源可能就會帶來安全的隱患。
-
單核CPU和多核CPU的理解
- 單核CPU,其實是一種假的多線程,因為在一個時間單元內(nèi),也只能執(zhí)行一個線程的任務(wù)。
- 如果是多核的話,才能更好的發(fā)揮多線程的效率。
- 一個
Java
應(yīng)用程序java.exe
,其實至少有三個線程。main()主線程
、gc()垃圾回收線程
、異常處理線程
。
-
并行與并發(fā)
-
并行:多個CPU同時執(zhí)行多個任務(wù)。
-
并發(fā):一個CPU(采用時間片)同時執(zhí)行多個任務(wù)。
-
-
使用多線程的優(yōu)點
- 提高應(yīng)用程序的響應(yīng)。對圖形化界面更有意義,可增強用戶體驗。
- 提供計算機系統(tǒng)CPU的利用率。
- 改善程序結(jié)構(gòu)。將既長又復雜的進程分為多個線程,獨立運行,利于理解和修改。
-
何時需要多線程
- 程序需要同時執(zhí)行兩個或多個任務(wù)。
- 程序需要實現(xiàn)一些需要等待的任務(wù)時,如用戶輸入、文件讀寫操作、網(wǎng)絡(luò)操作、搜索等。
- 需要一些后臺運行的程序時。
6.2:線程的創(chuàng)建和使用
? Java
語言的JVM
允許程序運行多個線程,它通過java.lang.Thread
類來體現(xiàn)。
-
Thread
類的特性- 每個線程都是通過某個特定
Thread
對象的run()
方法來完成操作的,經(jīng)常把run()
方法的主體稱為線程體。 - 通過該
Thread
對象的start()
方法來啟動這個線程,而非直接調(diào)用run()
。
- 每個線程都是通過某個特定
-
Thread
類的構(gòu)造器Thread()
:創(chuàng)建新的Thread
對象Thread(String threadname)
:創(chuàng)建線程并指定線程實例名。Thread(Runnable target)
:指定創(chuàng)建線程的目標對象,它實現(xiàn)了Runnable
接口中的run
方法。Thread(Runnable target, String name)
:創(chuàng)建新的Thread
對象。
-
多線程的創(chuàng)建一:繼承
Thread
類- 創(chuàng)建一個繼承與
Thread
類的子類。 - 重寫
Thread
類的run()
----> 將此線程執(zhí)行的操作聲明在run()
中。 - 創(chuàng)建
Thread
類的子類的對象。 - 通過此對象調(diào)用
start()
。
// 1.創(chuàng)建一個繼承與Thread類的子類。 class MyThread extends Thread{// 2.重寫Thread類的run() public void run() {for(int i = 0; i < 100; i++) {if(i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}} }public class ThreadTest {public static void main(String[] args) {// 1.創(chuàng)建Thread類的子類的對象MyThread t1 = new MyThread();// 2.通過此對象調(diào)用start()t1.start();} }
- 創(chuàng)建一個繼承與
-
多線程的創(chuàng)建二:實現(xiàn)
Runnable
接口- 創(chuàng)建一個實現(xiàn)了
Runnable
接口的類。 - 實現(xiàn)類去實現(xiàn)
Runnable
中的抽象方法:run()
- 創(chuàng)建實現(xiàn)類的對象。
- 將此對象作為參數(shù)傳遞到
Thread
類的構(gòu)造器中,創(chuàng)建Thread
類的對象。 - 通過
Thread
類的對象調(diào)用start()
。
// 1.創(chuàng)建一個實現(xiàn)了Runnable接口的類 class MThread implements Runnable {// 2.實現(xiàn)類去實現(xiàn)`Runnable`中的抽象方法:run()public void run() {for(int i = 0; i < 100; i++) {if(i % 2 == 0) {System.out.println(Thread.currentThread().getName() + ":" + i);}}} }public class ThreadTest1 {public static void main(String[] args) {// 3.創(chuàng)建實現(xiàn)類的對象MThread mThread = new MThread();// 4.將此對象作為參數(shù)傳遞到Thread類的構(gòu)造器中,創(chuàng)建Thread類的對象Thread t1 = new Thread(mThread);// 5.通過Thread類的對象調(diào)用start()t1.start();} }
- 創(chuàng)建一個實現(xiàn)了
-
比較創(chuàng)建線程的兩種方式
- 開發(fā)中:優(yōu)先選擇實現(xiàn)
Runnable
接口的方式。 - 原因:
- 實現(xiàn)的方式?jīng)]有類的單繼承性的局限性。
- 實現(xiàn)的方式更適合來處理多個線程有共享數(shù)據(jù)的情況。
- 聯(lián)系:
public class Thread implememts Runnable
- 相同點:兩種方式都需要重寫
run()
,將線程要執(zhí)行的邏輯聲明在run()
中。
- 開發(fā)中:優(yōu)先選擇實現(xiàn)
-
Thread
中的常用方法start()
:啟動當前線程,調(diào)用當前線程的run()
。run()
:通常需要重寫Thread
類中的此方法,將創(chuàng)建的線程要執(zhí)行的操作聲明在此方法中。currentThread()
:靜態(tài)方法,返回執(zhí)行當前代碼的線程。getName()
:獲取當前線程的名字。setName()
:設(shè)置當前線程的名字。yield()
:釋放當前cpu
執(zhí)行權(quán)。join()
:在線程a
中調(diào)用線程b
的join()
,此時線程a
就進入阻塞狀態(tài),知道線程b
完全執(zhí)行完以后,線程a
才結(jié)束阻塞狀態(tài)。stop()
:已過時。當執(zhí)行此方法時,強制結(jié)束當前線程。isAlive()
:判斷當前線程是否存活。
-
線程的優(yōu)先級
MAX_PRIORITY: 10 MIN_PRIORITY: 1 NORM_PRIORITY: 5 --> 默認優(yōu)先級
-
如何獲取和設(shè)置當前線程的優(yōu)先級
getPriority()
:獲取線程的優(yōu)先級。setPriority(int p)
:設(shè)置線程的優(yōu)先級。
-
說明
? 高優(yōu)先級的線程要搶占低優(yōu)先級線程
cpu
的執(zhí)行權(quán)。但是只是從概率上講,高優(yōu)先級的線程高概率的情況下被執(zhí)行。并不意味著只有當高優(yōu)先級的線程執(zhí)行完以后,低優(yōu)先級的線程才執(zhí)行。
-
-
線程的分類
Java
中的線程分為兩類:一種是守護線程,一種是用戶線程。- 它們在幾乎每個方面都是相同的,唯一的區(qū)別是判斷
JVM
何時離開。 - 守護是用來服務(wù)用戶線程的,通過在
start()
方法前調(diào)用thread.setDaemon(true)
可以把一個用戶線程變成一個守護線程。 Java
垃圾回收就是一個典型的守護線程。- 若
JVM
中都是守護線程,當前JVM
將退出。
- 它們在幾乎每個方面都是相同的,唯一的區(qū)別是判斷
6.3:線程的生命周期
-
JDK
中用Thread.State
類定義了線程的幾種狀態(tài)? 想要實現(xiàn)多線程,必須在主線程中創(chuàng)建新的線程對象。
Java
語言使用Thread
類及其子類的對象來表示線程,在它的一個完整的生命周期中通常要經(jīng)歷如下的五種狀態(tài)。-
新建
當一個
Thread
或其子類的對象被聲明并創(chuàng)建時,新生的線程對象處于新建狀態(tài)。 -
就緒
? 處于新建狀態(tài)的線程被
start()
后,將進入線程隊列等待CPU
時間片,此時它已具備了運行的條件,只是沒分配到CPU
資源。 -
運行
當就緒的線程被調(diào)度并獲得CPU資源時,便進入運行狀態(tài),
run()
方法定義了線程的操作和功能。 -
阻塞
在某種特殊情況下,被人為掛起或執(zhí)行輸入輸出操作時,讓出
CPU
并臨時中止自己的執(zhí)行,進入阻塞狀態(tài)。 -
死亡
線程完成了它的全部工作或線程被提前強制性地中止或出現(xiàn)異常導致結(jié)束。
-
-
線程的生命周期圖解
6.4:線程的同步
-
多線程出現(xiàn)了安全問題
-
問題的原因
? 當多條語句在操作同一個線程共享數(shù)據(jù)時,一個線程對多條語句只執(zhí)行了一部分,還沒有執(zhí)行完,另一個線程參與進來執(zhí)行,導致共享數(shù)據(jù)的錯誤。
-
解決辦法
? 對多條操作共享數(shù)據(jù)的語句,只能讓一個線程都執(zhí)行完,在執(zhí)行過程中,其他線程不可以參與執(zhí)行。
Java
對于多線程的安全問題提供了專業(yè)的解決方式:同步機制。
-
-
方式一:同步代碼塊
-
格式
synchronized(同步監(jiān)視器) {// 需要被同步的代碼 }
-
操作共享數(shù)據(jù)的代碼,即為需要被同步的代碼。
-
共享數(shù)據(jù):多個線程共同操作的變量。
-
同步監(jiān)視器【俗稱:鎖】。任何一個類的對象,都可以充當鎖。多個線程必須要共用同一把鎖。
-
解決實現(xiàn)
Runnable
接口方式的線程安全問題class Window1 implements Runnable {private int ticket = 100;public void run() {while(true) {synchronized (this) {if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(getName() + ":買票,票號為:" + ticket);ticket--;} else {break;}}}} }public class WindowTest1 {public static void main(String[] args) {Window1 w = new Window1();Thread t1 = new Thread(w);Thread t2 = new Thread(w);Thread t3 = new Thread(w);t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");t1.start();t2.start();t3.start();} }
在實現(xiàn)
Runnable
接口創(chuàng)建多線程的方式中,我們可以考慮使用this
充當同步監(jiān)視器。 -
解決繼承
Thread
類方式的線程安全問題class Window2 extends Thread {private static int ticket = 100;public void run() {while(true) {synchronized(Window2.class) {if(ticket > 0) {try{Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(getName() + ":買票,票號為:" + ticket);ticket--;} else {break;}}}} }public class WindowTest2 {public static void main(String[] args) {Window2 t1 = new Window2();Window2 t2 = new Window2();Window2 t3 = new Window2();t1.setName("窗口一");t2.setName("窗口二");t3.setName("窗口三");t1.start();t2.start();t3.start();} }
在繼承
Thread
類創(chuàng)建多線程的方式中,慎用this
充當同步監(jiān)視器,考慮使用當前類充當同步監(jiān)視器。
-
-
方式二:同步方法
-
格式
權(quán)限修飾符 synchronized 返回值類型 方法名() {}
如果操作共享數(shù)據(jù)的代碼完整的聲明在以方法中,我們不妨將此方法聲明為同步的。
-
同步方法仍然設(shè)計到同步監(jiān)視,只是不需要我們顯式的聲明。
-
非靜態(tài)的同步方法,同步監(jiān)視器是
this
。靜態(tài)的同步方式,同步監(jiān)視器是當前類本身。 -
解決實現(xiàn)
Runnable
接口方式的線程安全問題class Window3 implements Runnable {private int ticket = 100;public void run() {while(true) {show();}}private synchronized void show() {if (ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(getName() + ":買票,票號為:" + ticket);ticket--;}} }public class WindowTest3 {public static void main(String[] args) {Window3 w = new Window3();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();} }
-
解決繼承
Thread
類方式的線程安全問題class Window4 extends Thread {private static int ticket = 100;public void run() {while(true) {show();}}public static synchronized void show() {if(ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + ":買票,票號為:" + ticket);ticket--;}} }public class WindowTest4 {public static void main(String[] args) {Window4 t1 = new Window4();Window4 t2 = new Window4();Window4 t3 = new Window4();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();} }
-
-
方式三:
Lock
(鎖)- 從
JDK 5.0
開始,Java
提供了更強大的線程同步機制——通過顯示定義同步鎖對象來實現(xiàn)同步。同步鎖使用Lock
對象充當。 java.util.concurrent.locks.Lock
接口是控制多個線程對共享資源進行訪問的工具。鎖提供了對共享資源的獨占訪問,每次只能有一個線程對Lock
對象加鎖,線程開始訪問共享資源之前應(yīng)先獲得Lock
對象。ReentrantLock
類實現(xiàn)了Lock
,它擁有synchronized
相同的并發(fā)性和內(nèi)存語義,在實現(xiàn)線程安全的控制中,比較常用的是ReentrantLock
,可以顯示加鎖、釋放鎖。
class Window implements Runnable {private int ticket = 100;// 1.實例化ReentrantLockprivate ReentrantLock lock = new ReentrantLock();public void run() {while(true) {try {// 2.調(diào)用鎖定方法lock()lock.lock();if(ticket > 0) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(getName() + ":售票,票號為:" + ticket);ticket--;} else {break;}} finally {// 3.調(diào)用解鎖方法: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("窗口一");t2.setName("窗口二");t3.setName("窗口三");t1.start();t2.start();t3.start();} }
- 從
-
synchronized
與Lock
的異同相同點:二者都可以解決線程安全問題。
不同點:
synchronized
機制在執(zhí)行完相應(yīng)的同步代碼以后,自動的釋放同步監(jiān)視器。Lock
需要手動的啟動同步lock()
,同時結(jié)束也需要手動的實現(xiàn)unlock()
。
-
線程死鎖
不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖。
public class ThreadTest {public static void main(String[] args) {StringBuffer s1 = new StringBuffer();StringBuffer s2 = new StringBuffer();new Thread() {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() {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();} }
出現(xiàn)死鎖后,不會出現(xiàn)異常,不會出現(xiàn)提示,只是所有的線程都處于阻塞狀態(tài),無法繼續(xù)。
- 解決方法:
- 專門的算法、原則。
- 盡量減少同步資源的定義。
- 盡量避免嵌套同步。
- 解決方法:
6.5:線程的通信
-
使用兩個線程交替打印
1-100
class Number implements Runnable {private int number = 1;public void run() {while(true) {synchronized (this) {// 線程通信方法二notify();if(number <= 100) {System.out.println(Thread.currentThread().getName() + ":" + number);number++;try{// 線程通信方法一wait();} catch (InterruptedException e) {e.printStackTrace();}} else {break;}}}} }public class CommunicationTest {public static void main(String[] args){Number number = new Number();Thread t1 = new Thread(number);Thread t2 = new Thread(number);t1.setName("線程1");t2.setName("線程2");t1.start();t2.start();} }
-
線程通信涉及到的方法
wait()
:一旦執(zhí)行此方法,當前線程就進入阻塞狀態(tài),并釋放同步監(jiān)視器。notify()
:一旦執(zhí)行此方法,就會喚醒被wait
的一個線程。如果有多個線程被wait
,就喚醒優(yōu)先級高的那個。notifyAll()
:一旦執(zhí)行此方法,就會喚醒所有被wait
的線程。
-
說明
wait()
、notify()
、notifyAll()
這三個方法必須使用在同步代碼塊或同步方法中。wait()
、notify()
、notifyAll()
這三個方法的調(diào)用這必須是同步代碼塊或同步方法中的同步監(jiān)視器。否則會出現(xiàn)IllegalMonitorStateException
異常。wait()
、notify()
、notifyAll()
這三個方法是定義在java.lang.Object
類中。
-
sleep()
和wait()
的異同-
相同點
一旦執(zhí)行方法,都可以使得線程進入阻塞狀態(tài)。
-
不同點
- 兩個方法聲明的位置不同:
Thread
類中聲明sleep()
,Object
類中聲明wait()
。 - 調(diào)用的要求不同:
sleep()
可以在任何場景下調(diào)用。wait()
必須使用在同步代碼塊或同步方法中。 - 關(guān)于釋放同步監(jiān)視器:如果兩個方法都使用在同步代碼塊或同步方法中,
sleep()
不會釋放鎖,wait()
會釋放鎖。
- 兩個方法聲明的位置不同:
-
6.6:JDK5.0新增線程創(chuàng)建方式
-
方式一:實現(xiàn)
Callable
接口- 與使用
Runnable
相比,Callable
功能更強大一些。- 相比
run()
方法,可以有返回值。 - 方法可以拋出異常。
- 支持泛型的返回值。
- 相比
FutrueTask
類FutrueTask
是Futrue
接口的唯一實現(xiàn)類。FutrueTask
同時實現(xiàn)了Runnable
、Future
接口。它既可以作為Runnable
被線程執(zhí)行,又可以作為Future
得到Callable
的返回值。
//1.創(chuàng)建一個實現(xiàn)Callable的實現(xiàn)類 class NumThread implements Callable{//2.實現(xiàn)call方法,將此線程需要執(zhí)行的操作聲明在call()中@Overridepublic 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.創(chuàng)建Callable接口實現(xiàn)類的對象NumThread numThread = new NumThread();//4.將此Callable接口實現(xiàn)類的對象作為傳遞到FutureTask構(gòu)造器中,創(chuàng)建FutureTask的對象FutureTask futureTask = new FutureTask(numThread);//5.將FutureTask的對象作為參數(shù)傳遞到Thread類的構(gòu)造器中,創(chuàng)建Thread對象,并調(diào)用start()new Thread(futureTask).start();try {//6.獲取Callable中call方法的返回值//get()返回值即為FutureTask構(gòu)造器參數(shù)Callable實現(xiàn)類重寫的call()的返回值。Object sum = futureTask.get();System.out.println("總和為:" + sum);} catch (InterruptedException e) {e.printStackTrace();} catch (ExecutionException e) {e.printStackTrace();}} }
- 與使用
-
方式二:使用線程池
-
背景
經(jīng)常創(chuàng)建和銷毀、使用量特別大的資源【并發(fā)情況下的線程】,對性能影響很大。
-
思路
提前創(chuàng)建好多個線程,放入線程池中,使用時直接獲取,用完放回池中??梢员苊忸l繁創(chuàng)建銷毀、實現(xiàn)重復利用
-
好處
- 提高響應(yīng)速度【減少了創(chuàng)建新線程的時間】。
- 降低資源消耗【重復利用線程池中線程,不需要每次都創(chuàng)建】。
- 便于線程管理。
-
線程池相關(guān)
API
JDK 5.0
起提供了線程池相關(guān)API
:ExecutoService
和Executors
。ExecutorService
:真正的線程池接口。常見子類ThreadPoolExecutor
void execute(Runnable command)
:執(zhí)行任務(wù)/命令,沒有返回值,一般用來執(zhí)行Runnable
<T> Future<T> submit(Callable<T> task)
:執(zhí)行任務(wù),有返回值,一般又來執(zhí)行Callable
void shutdown()
:關(guān)閉連接池
Executors
:工具類、線程池的工廠類,用于創(chuàng)建并返回不同類型的線程池。Executors.newCachedThreadPool()
:創(chuàng)建一個可根據(jù)需要創(chuàng)建新線程的線程池Executors.newFixedThreadPool(n)
:創(chuàng)建一個可重用固定線程數(shù)的線程池Executors.newSingleThreadExecutor()
:創(chuàng)建一個只有一個線程的線程池Executors.newScheduledThreadPool(n)
:創(chuàng)建一個線程池,它可安排在給定延遲后運行命令或定期執(zhí)行
class NumberThread implements Runnable{@Overridepublic 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. 提供指定線程數(shù)量的線程池ExecutorService service = Executors.newFixedThreadPool(10);//2.執(zhí)行指定的線程的操作。需要提供實現(xiàn)Runnable接口或Callable接口實現(xiàn)類的對象service.execute(new NumberThread());//3.關(guān)閉連接池service.shutdown();} }
-