Java 多線程詳解——創建、使用、同步和通信
基本概念
程序(program):
是為完成特定任務、用某種語言編寫的一組指令的集合。即指一段靜態的代碼,靜態對象。
進程(process):
是程序的一次執行過程,或是正在運行的一個程序。是一個動態的過程:有它自身的產生、存在和消亡的過程。——生命周期
程序是靜態的,進程是動態的
進程作為資源分配的單位,系統在運行時會為每個進程分配不同的內存區域
線程(thread):
進程可進一步細化為線程,是一個程序內部的一條執行路徑;可以將線程看作一種輕量級的進程。
若一個進程同一時間并行執行多個線程,就是支持多線程的
線程作為調度和執行的單位,每個線程擁有獨立的運行棧和程序計數器(pc),線程切換的開銷小
一個進程中的多個線程共享相同的內存單元/內存地址空間;它們從同一堆中分配對象,可以訪問相同的變量和對象
共享數據使得線程間通信更簡便、高效。但多個線程操作共享的系統資源可能就會帶來安全的隱患
一個Java應用程序java.exe,其實至少有三個線程:main()主線程,gc() 垃圾回收線程,異常處理線程。當然如果發生異常,會影響主線程
并行:
多個CPU同時執行多個任務
并發:
一個CPU(采用時間片)同時執行多個任務
多線程程序的優點:
提高應用程序的響應。對圖形化界面更有意義,可增強用戶體驗。
提高計算機系統CPU的利用率
改善程序結構。將既長又復雜的進程分為多個線程,獨立運行,利于理解和
修改
何時需要多線程:
程序需要同時執行兩個或多個任務
程序需要實現一些需要等待的任務時,如用戶輸入、文件讀寫操作、網絡操作、搜索等
需要一些后臺運行的程序時
對于多核CPU而言,只有多線程才能最大化利用CPU
線程的創建和使用
Java 語言的 JVM 允許程序運行多個線程,它通過 java.lang.Thread 類來體現
Thread 類的特性
每個線程都是通過某個特定 Thread 對象的 run() 方法來完成操作的,經常把 run() 方法的主體稱為線程體
通過該 Thread 對象的 start() 方法來啟動這個線程,而非直接調用 run()
構造器
Thread():創建新的 Thread 對象
Thread(String threadname):創建線程并指定線程實例名
Thread(Runnable target):指定創建線程的目標對象,它實現了 Runnable 接口中的 run() 方法
Thread(Runnable target, String name):創建新的 Thread 對象
JDK1.5 之前 API 中創建線程的兩種方式
JDK1.5 之前創建新執行線程有兩種方法:
繼承 Thread 類的方式
實現 Runnable 接口的方式
定義子類繼承 Thread 類
子類中重寫 Thread 類中的 run 方法
創建 Thread 子類對象,即創建了線程對象
調用線程對象 start 方法:1、啟動線程;2、調用 run 方法
//1、創建一個繼承于 Thread 類的子類 class MyThread extends Thread { //2、重寫 Thread 類的 run() // 將此線程執行的操作聲明在 run 中 @Override public void run() { // super.run(); for (int i = 0; i < 100; i++) { if (i % 2 == 0) { System.out.println(getName() + ":" + i + " "); } } } } public class ThreadTest1 { public static void main(String[] args) { // 以下程序都是 main 線程執行 //3、創建 Thread 類的子類對象 MyThread t1 = new MyThread(); //4、通過此對象調用 start() // start() 作用1: 啟動當前線程 // start() 作用2: 調用當前線程的 run() t1.start(); //問題一:我們不能通過直接調用run()的方式啟動線程。 // t1.run(); //問題二:再啟動一個線程,遍歷100以內的偶數。不可以還讓已經start()的線程去執行。會報IllegalThreadStateException // t1.start(); //我們需要重新創建一個線程的對象 MyThread t2 = new MyThread(); t2.start(); System.out.println("Hello main Thread"); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + ":" + i + " "); } } }
注意:
如果自己手動調用 run() 方法,那么就只是普通方法,沒有啟動多線程模式
run() 方法由 JVM 調用,什么時候調用,執行的過程控制都有操作系統的 CPU 調度決定
想要啟動多線程,必須調用 start 方法
一個線程對象只能調用一次 start() 方法啟動,如果重復調用了,則將拋出以上的異常“IllegalThreadStateException”
定義子類,實現 Runnable 接口
子類中重寫 Runnable 接口中的 run 方法
通過 Thread 類含參構造器創建線程對象
將 Runnable 接口的子類對象作為實際參數傳遞給 Thread 類的構造器中
調用 Thread 類的 start 方法:開啟線程,調用 Runnable 子類接口的 run 方法
//1. 創建一個實現了Runnable接口的類 class MThread implements Runnable{ //2. 實現類去實現Runnable中的抽象方法:run() @Override public void run() { for (int i = 0; i < 100; i++) { if(i % 2 == 0){ System.out.println(Thread.currentThread().getName() + ":" + i); } } } } public class ThreadTest2 { public static void main(String[] args) { //3. 創建實現類的對象 MThread mThread = new MThread(); //4. 將此對象作為參數傳遞到Thread類的構造器中,創建Thread類的對象 Thread t1 = new Thread(mThread); t1.setName("線程1"); //5. 通過Thread類的對象調用start(): // ① 啟動線程 // ② 調用當前線程的run()-->調用了Runnable類型的target的run() t1.start(); //再啟動一個線程,遍歷100以內的偶數 Thread t2 = new Thread(mThread); t2.setName("線程2"); t2.start(); } }
區別:
繼承 Thread:線程代碼存放 Thread 子類 run 方法中
實現 Runnable:線程代碼存在接口的子類的 run 方法
開發中:
優先選擇:實現 Runnable 接口的方式
原因:
1. 實現的方式沒有類的單繼承性的局限性
2. 實現的方式更適合來處理多個線程有共享數據的情況
聯系:
public class Thread implements Runnable
相同點:
1. 兩種方式都需要重寫 run(), 將線程要執行的邏輯聲明在 run() 中
2. 啟動線程,都是調用 Thread 類的 start()
Thread類的有關方法
void start(): 啟動線程,并執行對象的 run() 方法
run(): 線程在被調度時執行的操作
String getName(): 返回線程的名稱
void setName(String name): 設置該線程名稱
static Thread currentThread(): 返回當前線程。在 Thread 子類中就是 this,通常用于主線程和 Runnable 實現類
static void yield():
線程讓步
暫停當前正在執行的線程,把執行機會讓給優先級相同或更高的線程
若隊列中沒有同優先級的線程,忽略此方法
join():
當某個程序執行流中調用其他線程的 join() 方法時,調用線程將被阻塞,直到 join() 方法加入的 join 線程執行完為止
低優先級的線程也可以獲得執行
static void sleep(long millis):
(指定時間:毫秒)
令當前活動線程在指定時間段內放棄對 CPU 控制, 使其他線程有機會被執行, 時間到后重排隊;
拋出InterruptedException異常
stop(): 強制線程生命期結束,不推薦使用
boolean isAlive():返回 boolean,判斷線程是否還活著
線程的調度與優先級
Java 的調度方法
同優先級線程組成先進先出隊列(先到先服務),使用時間片策略
對高優先級,使用優先調度的搶占式策略
線程的優先級等級
MAX_PRIORITY:10
MIN _PRIORITY:1
NORM_PRIORITY:5
涉及的方法
getPriority() :返回線程優先值
setPriority(int newPriority) :改變線程的優先級
說明:
線程創建時繼承父線程的優先級
低優先級只是獲得調度的概率低,并非一定是在高優先級線程之后才被調用
線程的分類
Java中的線程分為兩類:一種是守護線程,一種是用戶線程。
它們在幾乎每個方面都是相同的,唯一的區別是判斷JVM何時離開。
守護線程是用來服務用戶線程的,通過在 start() 方法前調用 thread.setDaemon(true) 可以把一個用戶線程變成一個守護線程。
Java 垃圾回收就是一個典型的守護線程。
若 JVM 中都是守護線程,當前 JVM 將退出。
JDK5.0 新增線程創建方式
與使用 Runnable 相比, Callable 功能更強大些
相比 run() 方法,call() 方法可以有返回值
方法可以拋出異常
支持泛型的返回值
需要借助 FutureTask 類,獲取返回結果
Future 接口
可以對具體 Runnable、Callable任務的執行結果進行取消、查詢是否完成、獲取結果等。
FutrueTask 是 Futrue 接口的唯一的實現類
FutureTask 同時實現了Runnable, Future接口。它既可以作為 Runnable被線程執行,又可以作為Future得到Callable的返回值
//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(); } } }
背景:
經常創建和銷毀、使用量特別大的資源,比如并發情況下的線程, 對性能影響很大。
思路:
提前創建好多個線程,放入線程池中,使用時直接獲取,使用完放回池中。可以避免頻繁創建銷毀、實現重復利用。類似生活中的公共交通工具。
好處:
提高響應速度(減少了創建新線程的時間)
降低資源消耗(重復利用線程池中線程,不需要每次都創建)
便于線程管理
corePoolSize:核心池的大小
maximumPoolSize:最大線程數
keepAliveTime:線程沒有任務時最多保持多長時間后會終止
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()); // ThreadPoolExecutor // 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(); } }
JDK 5.0 起提供了線程池相關 API:ExecutorService 和 Executors
ExecutorService:真正的線程池接口。常見子類 ThreadPoolExecutor
void execute(Runnable command) :執行任務/命令,沒有返回值,一般用來執行 Runnable
void shutdown() :關閉連接池
Executors:工具類、線程池的工廠類,用于創建并返回不同類型的線程池
Executors.newCachedThreadPool():創建一個可根據需要創建新線程的線程池
Executors.newFixedThreadPool(n):創建一個可重用固定線程數的線程池
Executors.newSingleThreadExecutor():創建一個只有一個線程的線程池
Executors.newScheduledThreadPool(n):創建一個線程池,它可安排在給定延遲后運行命令或者定期地執行。
jdk 自帶的四種策略:
(1)ThreadPoolExecutor.AbortPolicy: 丟棄任務,并拋出 RejectedExecutionException 異常。
(2)ThreadPoolExecutor.CallerRunsPolicy:該任務被線程池拒絕,由調用 execute方法的線程執行該任務。
(3)ThreadPoolExecutor.DiscardOldestPolicy : 拋棄隊列最前面的任務,然后重新嘗試執行任務。
(4)ThreadPoolExecutor.DiscardPolicy:丟棄任務,不過也不拋出異常。
當線程池的任務緩存隊列已滿并且線程池中的線程數目達到maximumPoolSize,如果還有任務到來就會采取任務拒絕策略
線程的生命周期
Java語言使用Thread類及其子類的對象來表示線程,在它的一個完整的生命周期中通常要經歷如下的五種狀態:
新建:
當一個 Thread 類或其子類的對象被聲明并創建時,新生的線程對象處于新建狀態
就緒:
處于新建狀態的線程被 start() 后,將進入線程隊列等待CPU時間片,此時它已具備了運行的條件,只是沒分配到CPU資源
運行:
當就緒的線程被調度并獲得CPU資源時, 便進入運行狀態, run() 方法定義了線程的操作和功能
阻塞:
在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出 CPU 并臨時中止自己的執行,進入阻塞狀態
死亡:
線程完成了它的全部工作或線程被提前強制性地中止或出現異常導致結束
線程的同步
多線程的安全問題:
當多條語句在操作同一個線程共享數據時,一個線程對多條語句只執行了一部分,還沒有執行完,另一個線程參與進來執行。導致共享數據的錯誤。
解決方法:
對多條操作共享數據的語句,只能讓一個線程都執行完,在執行過程中,其他線程不可以參與執行
Java 對于多線程的安全問題提供了專業的解決方式:同步機制
同步代碼塊:
synchronized (對象){ // 需要被同步的代碼; }
synchronized 還可以放在方法聲明中,表示整個方法為同步方法
public synchronized void show (String name){ … }
對于并發工作,需要某種方式來防止兩個任務訪問相同的資源(其實就是共享資源競爭)
防止這種沖突的方法就是當資源被一個任務使用時,在其上加鎖。第一個訪問某項資源的任務必須鎖定這項資源,使其他任務在其被解鎖之前,就無法訪問它了,而在其被解鎖之時,另一個任務就可以鎖定并使用它了
任意對象都可以作為同步鎖。所有對象都自動含有單一的鎖(監視器)。
同步方法的鎖:靜態方法(類名.class)、非靜態方法(this)
同步代碼塊:自己指定,很多時候也是指定為 this 或 類名.class
==必須確保使用同一個資源的多個線程共用一把鎖,這個非常重要,否則就無法保證共享資源的安全==
一個線程類中的所有靜態方法共用同一把鎖(類名.class),所有非靜態方法共用同一把鎖(this),同步代碼塊(指定需謹慎)
在實現 Runnable 接口創建多線程的方式中,我們可以考慮使用 this 充當同步監視器
在繼承 Thread 類創建多線程的方式中,慎用 this 充當同步監視器,考慮使用當前類充當同步監視器
同步的方式,解決了線程的安全問題,但是有其局限性:
操作同步代碼時,只能有一個線程參與,其他線程等待;相當于是一個單線程的過程,效率低
釋放鎖的操作:
當前線程的同步方法、同步代碼塊執行結束。
當前線程在同步代碼塊、同步方法中遇到 break、return 終止了該代碼塊、該方法的繼續執行。
當前線程在同步代碼塊、同步方法中出現了未處理的 Error 或Exception,導致異常結束。
當前線程在同步代碼塊、同步方法中執行了線程對象的 wait() 方法,當前線程暫停,并釋放鎖。
不會釋放鎖的操作:
線程執行同步代碼塊或同步方法時,程序調用 Thread.sleep()、Thread.yield() 方法暫停當前線程的執行
線程執行同步代碼塊時,其他線程調用了該線程的 suspend() 方法將該線程掛起,該線程不會釋放鎖(同步監視器)。
應盡量避免使用 suspend() 和 resume() 來控制線程
單例設計模式之懶漢式(線程安全)
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; } }
線程的死鎖問題
不同的線程分別占用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了線程的死鎖
出現死鎖后,不會出現異常,不會出現提示,只是所有的線程都處于阻塞狀態,無法繼續
解決方法
專門的算法、原則
盡量減少同步資源的定義
盡量避免嵌套同步
//死鎖的演示 class A { public synchronized void foo(B b) { //同步監視器:A類的對象:a System.out.println("當前線程名: " + Thread.currentThread().getName() + " 進入了A實例的foo方法"); // ① // try { // Thread.sleep(200); // } catch (InterruptedException ex) { // ex.printStackTrace(); // } System.out.println("當前線程名: " + Thread.currentThread().getName() + " 企圖調用B實例的last方法"); // ③ b.last(); } public synchronized void last() {//同步監視器:A類的對象:a System.out.println("進入了A類的last方法內部"); } } class B { public synchronized void bar(A a) {//同步監視器:B類的對象:b System.out.println("當前線程名: " + Thread.currentThread().getName() + " 進入了B實例的bar方法"); // ② // try { // Thread.sleep(200); // } catch (InterruptedException ex) { // ex.printStackTrace(); // } System.out.println("當前線程名: " + Thread.currentThread().getName() + " 企圖調用A實例的last方法"); // ④ a.last(); } public synchronized void last() {//同步監視器:b System.out.println("進入了B類的last方法內部"); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init() { Thread.currentThread().setName("主線程"); // 調用a對象的foo方法 a.foo(b); System.out.println("進入了主線程之后"); } public void run() { Thread.currentThread().setName("副線程"); // 調用b對象的bar方法 b.bar(a); System.out.println("進入了副線程之后"); } public static void main(String[] args) { DeadLock dl = new DeadLock(); new Thread(dl).start(); dl.init(); } }
鎖(Lock)
從 JDK 5.0 開始,Java 提供了更強大的線程同步機制——通過顯式定義同步鎖對象來實現同步。同步鎖使用 Lock 對象充當。
java.util.concurrent.locks.Lock 接口是控制多個線程對共享資源進行訪問的工具。鎖提供了對共享資源的獨占訪問,每次只能有一個線程對 Lock 對象加鎖,線程開始訪問共享資源之前應先獲得 Lock 對象。
ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的并發性和內存語義,在實現線程安全的控制中,比較常用的是 ReentrantLock,可以顯式加鎖、釋放鎖。
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(); } } // 注意:如果同步代碼有異常,要將unlock()寫入finally語句塊
synchronized 與 Lock 的異同?
相同:二者都可以解決線程安全問題
不同:
synchronized 是隱式鎖,在執行完相應的同步代碼以后,自動的釋放同步監視器;Lock 是顯示鎖,需要手動的啟動同步(lock()),同時結束同步也需要手動的實現(unlock())
Lock 只有代碼塊鎖,synchronized 有代碼塊鎖和方法鎖
用 Lock 鎖,JVM將花費較少的時間來調度線程,性能更好。并且具有更好的擴展性(提供更多的子類)
優先使用順序:
Lock > 同步代碼塊(已經進入了方法體,分配了相應資源) > 同步方法(在方法體之外)
線程的通信
wait() 與 notify() 和 notifyAll()
wait(): 一旦執行此方法,當前線程就進入阻塞狀態,并釋放同步監視器。
notify(): 一旦執行此方法,就會喚醒被 wait 的一個線程。如果有多個線程被 wait,就喚醒優先級高的那個。
notifyAll(): 一旦執行此方法,就會喚醒所有被 wait 的線程。
注意:
wait(),notify(),notifyAll() 三個方法必須使用在同步代碼塊或同步方法中。
wait(),notify(),notifyAll() 三個方法的調用者必須是同步代碼塊或同步方法中的同步監視器。否則,會出現 IllegalMonitorStateException 異常
wait(),notify(),notifyAll()三個方法是定義在java.lang.Object 類中。
sleep() 和 wait() 的異同?
相同點:
一旦執行方法,都可以使得當前的線程進入阻塞狀態。
不同點:
兩個方法聲明的位置不同:Thread 類中聲明 sleep() , Object 類中聲明 wait()
調用的要求不同:sleep() 可以在任何需要的場景下調用。 wait() 必須使用在同步代碼塊或同步方法中
關于是否釋放同步監視器:如果兩個方法都使用在同步代碼塊或同步方法中,sleep() 不會釋放鎖,wait() 會釋放鎖。
Java 任務調度 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。