一、Java 線程實現/創建方式
注意:
• 新建的線程不會自動開始運行,必須通過start( )方法啟動
• Java程序啟動時,會立刻創建主線程,main就是在這個線程上運行。當不再產生新線程時,程序是單線程的
1.1 繼承Thread 類
Thread 類本質上是實現了 Runnable 接口的一個實例,代表一個線程的實例。啟動線程的唯一方法就是通過 Thread 類的 start()實例方法。start()方法是一個 native 方法,它將啟動一個新線程,並執行 run()方法。
• 優勢:編寫簡單
1.1.1 創建:繼承Thread+重寫run
1.1.2 啟動:創建子類對象+調用start
public class StartThread extends Thread{ //線程入口點 @Override public void run() { for(int i=0;i<10;i++) { System.out.println("listen music"); } } public static void main(String[] args) { //創建子類對象 StartThread st=new StartThread(); //調用start方法 st.start();//開啟新線程交於cpu決定執行順序 for(int i=0;i<10;i++) { System.out.println("coding"); } } }
1.2 實現runnable接口
如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實現一個Runnable 接口。
• 優勢:可以繼承其它類,多線程可共享同一個Runnable對象
1.2.1 創建:實現runnable接口+重寫run
1.2.2 啟動:創建實現類對象+Thread類對象+調用start
public class StartRun implements Runnable{ //線程入口點 @Override public void run() { for(int i=0;i<10;i++) { System.out.println("listen music"); } } public static void main(String[] args) { //創建實現類對象 StartRun st=new StartRun(); //創建代理類對象 //啟動 MyThread,需要首先實例化一個 Thread,並傳入自己的 MyThread 實例: Thread t=new Thread(st); //事實上,當傳入一個 Runnable target 參數給 Thread 后,Thread 的 run()方法就會調用target.run() //調用start方法 t.start();//開啟新線程交於cpu決定執行順序 //匿名法 // new Thread(new StartRun()).start(); for(int i=0;i<10;i++) { System.out.println("coding"); } } }
1.3 實現Callable接口
1.3.1 創建:實現callable接口+重寫call
1.3.2 啟動:創建Callable實現類的實現,使用FutureTask類包裝Callable對象,該FutureTask對象封裝了Callable對象的Call方法的返回值
1.3.3 使用FutureTask對象作為Thread對象的target創建並啟動線程
1.3.4 調用FutureTask對象的get()來獲取子線程執行結束的返回值
有返回值的任務必須實現 Callable 接口,類似的,無返回值的任務必須 Runnable 接口。執行Callable 任務后,可以獲取一個 Future 的對象,在該對象上調用 get 就可以獲取到 Callable 任務返回的 Object 了,再結合線程池接口 ExecutorService 就可以實現傳說中有返回結果的多線程了。
• 與實行Runnable相比, Callable功能更強大些
• 方法不同
• 可以有返回值,支持泛型的返回值
• 需要借助FutureTask,比如獲取返回結果
Future接口
• FutrueTask是Futrue接口的唯一的實現類
public class ThreadTest { public static void main(String[] args) { Callable<Integer> myCallable = new MyCallable(); // 創建MyCallable對象 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象 for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 30) { Thread thread = new Thread(ft); //FutureTask對象作為Thread對象的target創建新的線程 thread.start(); //線程進入到就緒狀態 } } System.out.println("主線程for循環執行完畢..");
try { int sum = ft.get(); //取得新創建的新線程中的call()方法返回的結果 System.out.println("sum = " + sum); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } } class MyCallable implements Callable<Integer> { private int i = 0; // 與run()方法不同的是,call()方法具有返回值 @Override public Integer call() { int sum = 0; for (; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); sum += i; } return sum; } }
//創建一個線程池 ExecutorService pool = Executors.newFixedThreadPool(taskSize); //創建多個有返回值的任務 List<Future> list = new ArrayList<Future>(); for (int i = 0; i < taskSize; i++) { Callable c = new MyCallable(i + " "); //執行任務並獲取 Future 對象 Future f = pool.submit(c); list.add(f); } //關閉線程池 pool.shutdown(); //獲取所有並發任務的運行結果 for (Future f : list) { //從 Future 對象上獲取任務的返回值,並輸出到控制台 System.out.println("res:" + f.get().toString()); }
1.4 基於線程池的方式
線程和數據庫連接這些資源都是非常寶貴的資源。那么每次需要的時候創建,不需要的時候銷毀,是非常浪費資源的。那么我們就可以使用緩存的策略,也就是使用線程池。
// 創建線程池 ExecutorService threadPool = Executors.newFixedThreadPool(10); while(true) { threadPool.execute(new Runnable() { // 提交多個線程任務,並執行 @Override public void run() { System.out.println(Thread.currentThread().getName() + " is running .."); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } }
二、四種線程池
線程組
• 線程組表示一個線程的集合。
• 頂級線程組名system,線程的默認線程組名稱是main
線程組的作用
• 安全隔離:允許線程訪問有關自己的線程組的信息,但是不允許它訪問有關其線程組的父線程組或其他任何線程組的信息
線程池(JDK1.5起,提供了內置線程池)
• 創建對象:需要分配內存等資源
• 對於經常創建和銷毀、使用量特別大的資源,比如並發情況下的線程,對性能影響很大。
線程池作用:
• 提高響應速度(減少了創建新線程的時間)
• 降低資源消耗(重復利用線程池中線程,不需要每次都創建)
線程池應用場合
• 對性能要求苛刻
• 接受突發性的大量請求
一般的線程池主要分為以下 4 個組成部分:
Java 里面線程池的頂級接口是 Executor,但是嚴格意義上講 Executor 並不是一個線程池,而只是一個執行線程的工具。真正的線程池接口是 ExecutorService。
圖解:
• Executor:線程池頂級接口,只有一個方法
• void execute(Runnable command) :執行任務/命令,沒有返回值,一般用來執行Runnable
• AbstractExecutorService:基本實現了ExecutorService的所有方法
• ThreadPoolExecutor:默認的線程池實現類
• ScheduledThreadPoolExecutor:實現周期性任務調度的線程池
2.1 Executors.newCachedThreadPool
2.2 Executors.newFixedThreadPool
2.3 Executors.newScheduledThreadPool
創建一個線程池,它可安排在給定延遲后運行命令或者定期地執行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(new Runnable(){ @Override public void run() { System.out.println("延遲三秒"); } }, 3, TimeUnit.SECONDS); scheduledThreadPool.scheduleAtFixedRate(new Runnable(){ @Override public void run() { System.out.println("延遲 1 秒后每三秒執行一次"); } },1,3,TimeUnit.SECONDS);
2.4 Executors.newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一個線程池(這個線程池只有一個線程),這個線程池可以在線程死后(或發生異常時)重新啟動一個線程來替代原來的線程繼續執行下去!
線程池參數:
• corePoolSize:核心池的大小
• 默認情況下,創建了線程池后,線程數為0,當有任務來之后,就會創建一個線程去執行任務。
• 但是當線程池中線程數量達到corePoolSize,就會把到達的任務放到隊列中等待。
• corePoolSize和maximumPoolSize之間的線程數會自動釋放,小於等於corePoolSize的不會釋放。當大於了這個值就會將任務由一個丟棄處理機制來處理。
• keepAliveTime:線程沒有任務時最多保持多長時間后會終止
• 默認只限於corePoolSize和maximumPoolSize之間的線程
• BlockingQueue:存儲等待執行的任務的阻塞隊列,有多中選擇,可以是順序隊列、鏈式隊列等。
• ThreadFactory:線程工廠,默認是DefaultThreadFactory,Executors的靜態內部類
三、線程的生命周期(狀態)
3.1 新生狀態(NEW)
• 用new關鍵字建立一個線程對象后,該線程對象就處於新生狀態。
3.2 就緒狀態(RUNNABLE)
• 當線程對象調用了 start()方法之后,該線程處於就緒狀態。Java 虛擬機會為其創建方法調用棧和程序計數器,等待調度運行。
• 處於就緒狀態線程具備了運行條件,但還沒分配到CPU,處於線程就緒隊列,等待系統為其分配CPU
3.3 運行狀態(RUNNING)
• 如果處於就緒狀態的線程獲得了 CPU,開始執行 run()方法的線程執行體,則該線程處於運行狀態。
• 在運行狀態的線程執行自己的run方法中代碼,直到等待某資源而阻塞或完成任務而死亡。
3.4 阻塞狀態(BLOCKED)
阻塞的情況分三種:
• 等待阻塞(o.wait->等待對列):運行(running)的線程執行 o.wait()方法,JVM 會把該線程放入等待隊列(waitting queue)中。
• 同步阻塞(lock->鎖池) :運行(running)的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則 JVM 會把該線程放入鎖池(lock pool)中。
3.5 死亡狀態(DEAD)
• 正常結束 : run()或 call()方法執行完成,線程正常運行結束。
• 異常結束 :線程拋出一個未捕獲的 Exception 或 Error。
四、終止線程的四種方式
4.1 正常運行結束
4.2 使用退出標志退出線程
public class ThreadSafe extends Thread { public volatile boolean exit = false; public void run() { while (!exit){ //do something } } }
定義了一個退出標志 exit,當 exit 為 true 時,while 循環退出,exit 的默認值為 false.在定義 exit時,使用了一個 Java 關鍵字 volatile(保證可見性但是不保證原子性,線程不安全),這個關鍵字的目的是使 exit 同步,也就是說在同一時刻只能由一個線程來修改 exit 的值。
4.3 Interrupt 方法結束線程
使用 interrupt()方法來中斷線程有兩種情況:
4.3.2 線程未處於阻塞狀態:使用 isInterrupted()判斷線程的中斷標志來退出循環。當使用interrupt()方法時,中斷標志就會置 true,和使用自定義的標志來控制循環是一樣的道理。
public class ThreadSafe extends Thread { public void run() { while (!isInterrupted()){ //非阻塞過程中通過判斷中斷標志來退出 try{ Thread.sleep(5*1000);//阻塞過程捕獲中斷異常來退出 }catch(InterruptedException e){ e.printStackTrace(); break;//捕獲到異常之后,執行 break 跳出循環 } } } }
4.4 stop 方法終止線程(線程不安全)
五、線程控制方法
5.1 優先級控制
線程的優先級用數字表示,范圍從1到10:
• Thread.MIN_PRIORITY = 1
• Thread.NORM_PRIORITY = 5
使用下述方法獲得或設置線程對象的優先級。
• void setPriority(int newPriority);
5.2 線程啟動(start)
線程由新生態進入就緒態,等待cpu調度運行。
5.3 線程等待(wait)
調用該方法的線程進入 WAITING 狀態,只有等待另外線程的通知或被中斷才會返回,需要注意的是調用 wait()方法后,會釋放對象的鎖。因此,wait 方法一般用在同步方法或同步代碼塊中。
5.4 線程睡眠(sleep)
sleep 導致當前線程休眠,與 wait 方法不同的是 sleep 不會釋放當前占有的鎖,sleep(long)會導致線程進入 TIMED-WATING 狀態,而 wait()方法會導致當前線程進入 WATING 狀態。
5.5 線程讓步(yield)
5.6 線程中斷(interrupt)
1. 調用 interrupt()方法並不會中斷一個正在運行的線程。也就是說處於 Running 狀態的線程並不會因為被中斷而被終止,僅僅改變了內部維護的中斷標識位而已。
3. 許多聲明拋出 InterruptedException 的方法(如 Thread.sleep(long mills 方法)),拋出異常前,都會清除中斷標識位,所以拋出異常后,調用 isInterrupted()方法將會返回 false。
5.7 插隊線程(join)
System.out.println(Thread.currentThread().getName() + "線程運行開始!"); Thread6 thread1 = new Thread6(); thread1.setName("線程 B"); thread1.join(); System.out.println("這時 thread1 執行完畢之后才能執行主線程");
5.8 設置為守護線程(setDaemon)
• 創建后台線程的線程結束時,后台線程也隨之消亡
5.9 線程喚醒(notify)
Object 類中的 notify() 方法,喚醒在此對象監視器上等待的單個線程,如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程,選擇是任意的,並在對實現做出決定時發生,線程通過調用其中一個 wait() 方法,在對象的監視器上等待,直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程,被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭。類似的方法還有 notifyAll() ,喚醒再次監視器上等待的所有線程。
5.10 終止線程(stop)
結束線程,不推薦使用
5.11 其他方法
5.11.2 activeCount(): 程序中活躍的線程數。
5.11.4 currentThread(): 得到當前線程。
5.11.6 setName(): 為線程設置一個名稱。
5.11.8 getPriority():獲得一個線程的優先級。
六、線程上下文切換
6.1 進程
(有時候也稱做任務)是指一個程序運行的實例。在 Linux 系統中,線程就是能並行運行並且與他們的父進程(創建他們的進程)共享同一地址空間(一段內存區域)和其他資源的輕量級的進程。
6.2 上下文
是指某一時間點 CPU 寄存器和程序計數器的內容。
6.3 寄存器
是 CPU 內部的數量較少但是速度很快的內存(與之對應的是 CPU 外部相對較慢的 RAM 主內存)。寄存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算機程序運行的速度。
6.4 程序計數器
6.5 PCB-“切換楨”
6.6 上下文切換的活動:
6.7 引起線程上下文切換的原因
七、Java后台線程
2. 優先級:守護線程的優先級比較低,用於為系統中的其它對象和線程提供服務。
4. 在 Daemon 線程中產生的新線程也是 Daemon 的。
5. 線程則是 JVM 級別的,以 Tomcat 為例,如果你在 Web 應用中啟動一個線程,這個線程的生命周期並不會和 Web 應用程序保持同步。也就是說,即使你停止了 Web 應用,這個線程依舊是活躍的。
6. example: 垃圾回收線程就是一個經典的守護線程,當我們的程序中不再有任何運行的Thread,程序就不會再產生垃圾,垃圾回收器也就無事可做,所以當垃圾回收線程是 JVM 上僅剩的線程時,垃圾回收線程會自動離開。它始終在低級別的狀態中運行,用於實時監控和管理系統中的可回收資源。
八、同步鎖與死鎖
同步鎖:當多個線程同時訪問同一個數據時,很容易出現問題。為了避免這種情況出現,我們要保證線程同步互斥,就是指並發執行的多個線程,在同一時間內只允許一個線程訪問共享數據。 Java 中可以使用 synchronized 關鍵字來取得一個對象的同步鎖。
8.1 線程同步
當兩個或兩個以上線程訪問同一資源時,需要某種方式來確保資源在某一時刻只被一個線程使用---線程同步。
8.2 Synchronized 同步鎖
synchronized 它可以把任意一個非 NULL 的對象當作鎖。他屬於獨占式的悲觀鎖,同時屬於可重入鎖。
Synchronized 作用范圍:
Synchronized 核心組件:
Synchronized 實現:
1. JVM 每次從隊列的尾部取出一個數據用於鎖競爭候選者(OnDeck),但是並發情況下,ContentionList 會被大量的並發線程進行 CAS 訪問,為了降低對尾部元素的競爭,JVM 會將一部分線程移動到 EntryList 中作為候選競爭線程。
2. Owner 線程會在 unlock 時,將 ContentionList 中的部分線程遷移到 EntryList 中,並指定EntryList 中的某個線程為 OnDeck 線程(一般是最先進去的那個線程)。
4. OnDeck 線程獲取到鎖資源后會變為 Owner 線程,而沒有得到鎖資源的仍然停留在 EntryList中。如果 Owner 線程被 wait 方法阻塞,則轉移到 WaitSet 隊列中,直到某個時刻通過 notify或者 notifyAll 喚醒,會重新進去 EntryList 中。
5. 處於 ContentionList、EntryList、WaitSet 中的線程都處於阻塞狀態,該阻塞是由操作系統來完成的(Linux 內核下采用 pthread_mutex_lock 內核函數實現的)。
7. 每個對象都有個 monitor 對象,加鎖就是在競爭 monitor 對象,代碼塊加鎖是在前后分別加上 monitorenter 和 monitorexit 指令來實現的,方法加鎖是通過一個標記位來判斷的
8. synchronized 是一個重量級操作,需要調用操作系統相關接口,性能是低效的,有可能給線程加鎖消耗的時間比有用操作消耗的時間更多。
10. 鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖。這種升級過程叫做鎖膨脹;
8.3 同步監視器
• 同步代碼塊中同步監視器可以是任何對象,但是推薦使用共享資源作為同步監視器
同步監視器的執行過程
8.4 Lock鎖
• JDK1.5后新增功能,與采用synchronized相比,lock可提供多種鎖方案,更靈活
• ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的並發性和內存語義, 但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。
8.5 Lock和synchronized的區別
• Lock只有代碼塊鎖,synchronized有代碼塊鎖和方法鎖
8.6 線程同步的優缺點
• 解決了線程安全問題
線程同步的缺點
死鎖
九、volatile 關鍵字的作用(變量可見性、禁止重排序)
變量可見性:其一是保證該變量對所有線程可見,這里的可見性指的是當一個線程修改了變量的值,那么新的值對於其他線程是可以立即獲取的。
9.1 是比 sychronized 更輕量級的同步鎖
9.2 適用場景
(1)對變量的寫操作不依賴於當前值(比如 i++),或者說是單純的變量賦值(boolean flag = true)。
十、線程通信
10.1 Java提供了3個方法解決線程之間的通信問題

均是java.lang.Object類的方法都只能在同步方法或者同步代碼塊中使用,否則會拋出異常
10.2 兩個線程之間共享數據
public class MyData { private int j=0; public synchronized void add(){ j++; System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j); } public synchronized void dec(){ j--; System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j); } public int getData(){ return j; } } public class AddRunnable implements Runnable{ MyData data; public AddRunnable(MyData data){ this.data= data; } public void run() { data.add(); } } public class DecRunnable implements Runnable { MyData data; public DecRunnable(MyData data){ this.data = data; } public void run() { data.dec(); } } public static void main(String[] args) { MyData data = new MyData(); Runnable add = new AddRunnable(data); Runnable dec = new DecRunnable(data); for(int i=0;i<2;i++){ new Thread(add).start(); new Thread(dec).start(); } }
Runnable 對象作為一個類的內部類:將 Runnable 對象作為一個類的內部類,共享數據作為這個類的成員變量,每個線程對共享數據的操作方法也封裝在外部類,以便實現對數據的各個操作的同步和互斥,作為內部類的各個 Runnable 對象調用外部類的這些方法。
public class MyData { private int j=0; public synchronized void add(){ j++; System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j); } public synchronized void dec(){ j--; System.out.println("線程"+Thread.currentThread().getName()+"j 為:"+j); } public int getData(){ return j; } } public class TestThread { public static void main(String[] args) { final MyData data = new MyData(); for(int i=0;i<2;i++){ new Thread(new Runnable(){ public void run() { data.add(); } }).start(); new Thread(new Runnable(){ public void run() { data.dec(); } }).start(); } } }
十一、ThreadLocal 作用(線程本地存儲)
ThreadLocal,很多地方叫做線程本地變量,也有些地方叫做線程本地存儲,ThreadLocal 的作用是提供線程內的局部變量,這種變量在線程的生命周期內起作用,減少同一個線程內多個函數或者組件之間一些公共變量的傳遞的復雜度。
1. 每個線程中都有一個自己的 ThreadLocalMap 類對象,可以將線程自己的對象保持到其中,各管各的,線程可以正確的訪問到自己的對象。
3. ThreadLocalMap 其實就是線程里面的一個屬性,它在 Thread 類中定義ThreadLocal.ThreadLocalMap threadLocals = null;
使用場景:最常見的 ThreadLocal 使用場景為 用來解決 數據庫連接、Session 管理等。
private static final ThreadLocal threadSession = new ThreadLocal(); public static Session getSession() throws InfrastructureException { Session s = (Session) threadSession.get(); try { if (s == null) { s = getSessionFactory().openSession(); threadSession.set(s); } } catch (HibernateException ex) { throw new InfrastructureException(ex); } return s; }
十二、Java 中用到的線程調度
12.1 搶占式調度:

12.2 協同式調度
12.3 JVM 的線程調度實現(搶占式調度)
12.4 線程讓出 cpu 的情況
十三、進程調度算法
13.1 優先調度算法
1. 先來先服務調度算法(FCFS):當在作業調度中采用該算法時,每次調度都是從后備作業隊列中選擇一個或多個最先進入該隊列的作業,將它們調入內存,為它們分配資源、創建進程,然后放入就緒隊列。在進程調度中采用 FCFS 算法時,則每次調度是從就緒隊列中選擇一個最先進入該隊列的進程,為之分配處理機,使之投入運行。該進程一直運行到完成或發生某事件而阻塞后才放棄處理機,特點是:算法比較簡單,可以實現基本上的公平。
13.2 高優先權優先調度算法
1. 非搶占式優先權算法:在這種方式下,系統一旦把處理機分配給就緒隊列中優先權最高的進程后,該進程便一直執行下去,直至完成;或因發生某事件使該進程放棄處理機時。這種調度算法主要用於批處理系統中;也可用於某些對實時性要求不嚴的實時系統中。
13.3.高響應比優先調度算法
在批處理系統中,短作業優先算法是一種比較好的算法,其主 要的不足之處是長作業的運行得不到保證。如果我們能為每個作業引入前面所述的動態優先權,並使作業的優先級隨着等待時間的增加而以速率 a 提高,則長作業在等待一定的時間后,必然有機會分配到處理機。該優先權的變化規律可描述為:(1) 如果作業的等待時間相同,則要求服務的時間愈短,其優先權愈高,因而該算法有利於短作業。
(3) 對於長作業,作業的優先級可以隨等待時間的增加而提高,當其等待時間足夠長時,其優先級便可升到很高,從而也可獲得處理機。簡言之,該算法既照顧了短作業,又考慮了作業到達的先后次序,不會使長作業長期得不到服務。因此,該算法實現了一種較好的折衷。當然,在利用該算法時,每要進行調度之前,都須先做響應比的計算,這會增加系統開銷。
13.4 基於時間片的輪轉調度算法
2. 多級反饋隊列調度算法:
(1) 應設置多個就緒隊列,並為各個隊列賦予不同的優先級。第一個隊列的優先級最高,第二個隊列次之,其余各隊列的優先權逐個降低。該算法賦予各個隊列中進程執行時間片的大小也各不相同,在優先權愈高的隊列中,為每個進程所規定的執行時間片就愈小。例如,第二個隊列的時間片要比第一個隊列的時間片長一倍,……,第 i+1 個隊列的時間片要比第 i 個隊列的時間片長一倍。
(3) 僅當第一隊列空閑時,調度程序才調度第二隊列中的進程運行;僅當第 1~(i-1)隊列均空時,才會調度第 i 隊列中的進程運行。如果處理機正在第 i 隊列中為某進程服務時,又有新進程進入優先權較高的隊列(第 1~(i-1)中的任何一個隊列),則此時新進程將搶占正在運行進程的處理機,即由調度程序把正在運行的進程放回到第 i 隊列的末尾,把處理機分配給新到的高優先權進程。在多級反饋隊列調度算法中,如果規定第一個隊列的時間片略大於多數人機交互所需之處理時間時,便能夠較好的滿足各種類型用戶的需要。