前段時間看了一篇關於"一名3年工做經驗的程序員應該具有的技能"文章,倍受打擊。不少熟悉而又陌生的知識讓我懷疑本身是一個假的程序員。本章從線程池,阻塞隊列,synchronized 和 volatile關鍵字,wait,notify方法實現線程之間的通信,死鎖,常考面試題。將這些零碎的知識整合在一塊兒。以下圖所示。java
學習流程圖:
技術:Executors,BlockingQueue,synchronized,volatile,wait,notify
說明:文章學習思路:線程池---->隊列---->關鍵字---->死鎖---->線程池實戰
源碼:https://github.com/ITDragonBl...git
線程池,顧名思義存放線程的池子,能夠類比數據庫的鏈接池。由於頻繁地建立和銷燬線程會給服務器帶來很大的壓力。若能將建立的線程再也不銷燬而是存放在池中等待下一個任務使用,能夠不只減小了建立和銷燬線程所用的時間,提升了性能,同時還減輕了服務器的壓力。程序員
初始化線程池有五個核心參數,分別是 corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue。還有兩個默認參數 threadFactory, handler
corePoolSize:線程池初始核心線程數。初始化線程池的時候,池內是沒有線程,只有在執行任務的時會建立線程。
maximumPoolSize:線程池容許存在的最大線程數。若超過該數字,默認提示RejectedExecutionException
異常
keepAliveTime:當前線程數大於核心線程時,該參數生效,其目的是終止多餘的空閒線程等待新任務的最長時間。即指定時間內將還未接收任務的線程銷燬。
unit:keepAliveTime 的時間單位
workQueue:緩存任務的的隊列,通常採用LinkedBlockingQueue。
threadFactory:執行程序建立新線程時使用的工廠,通常採用默認值。
handler:超出線程範圍和隊列容量而使執行被阻塞時所使用的處理程序,通常採用默認值。github
開始,游泳館來了一名學員,因而館主安排一個教練負責培訓這名學員;
而後,游泳館來了六名學員,可館主只招了五名教練,因而有一名學員被安排到休息室等待;
後來,游泳館來了十六名學員,休息室已經滿了,館主覈算了開支,預計最多可招十名教練;
最後,游泳館只來了十名學員,館主對教練說,若是半天內接不到學員的教練就能夠走了;
結果,游泳館沒有學員,關閉了。
在接收任務前,線程池內是沒有線程。只有當任務來了纔開始新建線程。當任務數大於核心線程數時,任務進入隊列中等待。若隊列滿了,則線程池新增線程直到最大線程數。再超過則會執行拒絕策略。面試
shutdown: 線程池再也不接收任務,等待線程池中全部任務完成後,關閉線程池。經常使用
shutdownNow: 線程池再也不接收任務,忽略隊列中的任務,嘗試中斷正在執行的任務,返回未執行任務列表,關閉線程池。慎用
awaitTermination: 線程池能夠繼續接收任務,當任務都完成後,或者超過設置的時間後,關閉線程池。方法是阻塞的,考慮使用數據庫
1 newSingleThreadExecutor() 單線程線程池
初始線程數和容許最大線程數都是一,keepAliveTime 也就失效了,隊列是無界阻塞隊列。該線程池的主要做用是負責緩存任務。數組
2 newFixedThreadPool(n) 固定大小線程池
初始線程數和容許最大線程數相同,且大小自定義,keepAliveTime 也就失效了,隊列是無界阻塞隊列。符合大部分業務要求,經常使用。緩存
3 newCachedThreadPool() 無緩存無界線程池
初始線程數爲零,最大線程數爲無窮大,keepAliveTime 60秒類終止空閒線程,隊列是無緩存無界隊列。適合任務數很少的場景,慎用。安全
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * 線程池 * 優點,類比數據庫的鏈接池 * 1. 頻繁的建立和銷燬線程會給服務器帶來很大的壓力 * 2. 若建立的線程不銷燬而是留在線程池中等待下次使用,則會很大地提升效率也減輕了服務器的壓力 * * 三種workQueue策略 * 直接提交 SynchronousQueue * 無界隊列 LinkedBlockingQueue * 有界隊列 ArrayBlockingQueue * * 四種拒絕策略 * AbortPolicy : JDK默認,超出 MAXIMUM_POOL_SIZE 放棄任務拋異常 RejectedExecutionException * CallerRunsPolicy : 嘗試直接調用被拒絕的任務,若線程池被關閉,則丟棄任務 * DiscardOldestPolicy : 放棄隊列最前面的任務,而後從新嘗試執被拒絕的任務。若線程池被關閉,則丟棄任務 * DiscardPolicy : 放棄不能執行的任務但不拋異常 */ public class ThreadPoolExecutorStu { // 線程池中初始線程個數 private final static Integer CORE_POOL_SIZE = 3; // 線程池中容許的最大線程數 private final static Integer MAXIMUM_POOL_SIZE = 8; // 當線程數大於初始線程時。終止多餘的空閒線程等待新任務的最長時間 private final static Long KEEP_ALIVE_TIME = 10L; // 任務緩存隊列 ,即線程數大於初始線程數時先進入隊列中等待,此數字能夠稍微設置大點,避免線程數超過最大線程數時報錯。或者直接用無界隊列 private final static ArrayBlockingQueue<Runnable> WORK_QUEUE = new ArrayBlockingQueue<Runnable>(5); public static void main(String[] args) { Long start = System.currentTimeMillis(); /** * ITDragonThreadPoolExecutor 耗時 1503 * ITDragonFixedThreadPool 耗時 505 * ITDragonSingleThreadExecutor 語法問題報錯, * ITDragonCachedThreadPool 耗時506 * 推薦使用自定義線程池,或newFixedThreadPool(n) */ ThreadPoolExecutor threadPoolExecutor = ITDragonThreadPoolExecutor(); for (int i = 0; i < 8; i++) { // 執行8個任務,若超過MAXIMUM_POOL_SIZE則會報錯 RejectedExecutionException MyRunnableTest myRunnable = new MyRunnableTest(i); threadPoolExecutor.execute(myRunnable); System.out.println("線程池中如今的線程數目是:"+threadPoolExecutor.getPoolSize()+", 隊列中正在等待執行的任務數量爲:"+ threadPoolExecutor.getQueue().size()); } // 關掉線程池 ,並不會當即中止(中止接收外部的submit任務,等待內部任務完成後才中止),推薦使用。 與之對應的是shutdownNow,不推薦使用 threadPoolExecutor.shutdown(); try { // 阻塞等待30秒關掉線程池,返回true表示已經關閉。和shutdown不一樣,它能夠接收外部任務,而且還阻塞。這裏爲了方便統計時間,因此選擇阻塞等待關閉。 threadPoolExecutor.awaitTermination(30, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("耗時 : " + (System.currentTimeMillis() - start)); } // 自定義線程池,開發推薦使用 public static ThreadPoolExecutor ITDragonThreadPoolExecutor() { // 構建一個,初始線程數量爲3,最大線程數據爲8,等待時間10分鐘 ,隊列長度爲5 的線程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.MINUTES, WORK_QUEUE); return threadPoolExecutor; } /** * 固定大小線程池 * corePoolSize初始線程數和maximumPoolSize最大線程數同樣,keepAliveTime參數不起做用,workQueue用的是無界阻塞隊列 */ public static ThreadPoolExecutor ITDragonFixedThreadPool() { ExecutorService executor = Executors.newFixedThreadPool(8); ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor; return threadPoolExecutor; } /** * 單線程線程池 * 等價與Executors.newFixedThreadPool(1); */ public static ThreadPoolExecutor ITDragonSingleThreadExecutor() { ExecutorService executor = Executors.newSingleThreadExecutor(); ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor; return threadPoolExecutor; } /** * 無界線程池 * corePoolSize 初始線程數爲零 * maximumPoolSize 最大線程數無窮大 * keepAliveTime 60秒類將沒有被用到的線程終止 * workQueue SynchronousQueue 隊列,無容量,來任務就直接新增線程 * 不推薦使用 */ public static ThreadPoolExecutor ITDragonCachedThreadPool() { ExecutorService executor = Executors.newCachedThreadPool(); ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor; return threadPoolExecutor; } } class MyRunnableTest implements Runnable { private Integer num; // 正在執行的任務數 public MyRunnableTest(Integer num) { this.num = num; } public void run() { System.out.println("正在執行的MyRunnable " + num); try { Thread.sleep(500);// 模擬執行事務須要耗時 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("MyRunnable " + num + "執行完畢"); } }
隊列,是一種數據結構。大部分的隊列都是以FIFO(先進先出)的方式對各個元素進行排序的(PriorityBlockingQueue是根據優先級排序的)。隊列的頭移除元素,隊列的末尾插入元素。插入的元素建議不能爲null。Queue主要分兩類,一類是高性能隊列 ConcurrentLinkedQueue;一類是阻塞隊列 BlockingQueue。本章重點介紹BlockingQueue服務器
ConcurrentLinkedQueue性能好於BlockingQueue。是基於連接節點的無界限線程安全隊列。該隊列的元素遵循先進先出的原則。不容許null元素。
ArrayBlockingQueue: 基於數組的阻塞隊列,在內部維護了一個定長數組,以便緩存隊列中的數據對象。並無實現讀寫分離,也就意味着生產和消費不能徹底並行。是一個有界隊列
LinkedBlockingQueue:基於列表的阻塞隊列,在內部維護了一個數據緩衝隊列(由一個鏈表構成),實現採用分離鎖(讀寫分離兩個鎖),從而實現生產者和消費者操做的徹底並行運行。是一個無界隊列,
SynchronousQueue: 沒有緩衝的隊列,生存者生產的數據直接會被消費者獲取並消費。若沒有數據就直接調用出棧方法則會報錯。
三種隊列使用場景
newFixedThreadPool 線程池採用的隊列是LinkedBlockingQueue。其優勢是無界可緩存,內部實現讀寫分離,併發的處理能力高於ArrayBlockingQueue
newCachedThreadPool 線程池採用的隊列是SynchronousQueue。其優勢就是無緩存,接收到的任務都可直接處理,再次強調,慎用!
併發量不大,服務器性能較好,能夠考慮使用SynchronousQueue。
併發量較大,服務器性能較好,能夠考慮使用LinkedBlockingQueue。
併發量很大,服務器性能沒法知足,能夠考慮使用ArrayBlockingQueue。系統的穩定最重要。
import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; import org.junit.Test; /** * 阻塞隊列 * ArrayBlockingQueue :有界 * LinkedBlockingQueue :無界 * SynchronousQueue :無緩衝直接用 * 非阻塞隊列 * ConcurrentLinkedQueue :高性能 */ public class ITDragonQueue { /** * ArrayBlockingQueue : 基於數組的阻塞隊列實現,在內部維護了一個定長數組,以便緩存隊列中的數據對象。 * 內部沒有實現讀寫分離,生產和消費不能徹底並行, * 長度是須要定義的, * 能夠指定先進先出或者先進後出, * 是一個有界隊列。 */ @Test public void ITDragonArrayBlockingQueue() throws Exception { ArrayBlockingQueue<String> array = new ArrayBlockingQueue<String>(5); // 能夠嘗試 隊列長度由3改到5 array.offer("offer 插入數據方法---成功返回true 不然返回false"); array.offer("offer 3秒後插入數據方法", 3, TimeUnit.SECONDS); array.put("put 插入數據方法---但超出隊列長度則阻塞等待,沒有返回值"); array.add("add 插入數據方法---但超出隊列長度則提示 java.lang.IllegalStateException"); // java.lang.IllegalStateException: Queue full System.out.println(array); System.out.println(array.take() + " \t還剩元素 : " + array); // 從頭部取出元素,並從隊列裏刪除,若隊列爲null則一直等待 System.out.println(array.poll() + " \t還剩元素 : " + array); // 從頭部取出元素,並從隊列裏刪除,執行poll 後 元素減小一個 System.out.println(array.peek() + " \t還剩元素 : " + array); // 從頭部取出元素,執行peek 不移除元素 } /** * LinkedBlockingQueue:基於列表的阻塞隊列,在內部維護了一個數據緩衝隊列(該隊列由一個鏈表構成)。 * 其內部實現採用讀寫分離鎖,能高效的處理併發數據,生產者和消費者操做的徹底並行運行 * 能夠不指定長度, * 是一個無界隊列。 */ @Test public void ITDragonLinkedBlockingQueue() throws Exception { LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<String>(); queue.offer("1.無界隊列"); queue.add("2.語法和ArrayBlockingQueue差很少"); queue.put("3.實現採用讀寫分離"); List<String> list = new ArrayList<String>(); System.out.println("返回截取的長度 : " + queue.drainTo(list, 2)); System.out.println("list : " + list); } /** * SynchronousQueue:沒有緩衝的隊列,生存者生產的數據直接會被消費者獲取並消費。 */ @Test public void ITDragonSynchronousQueue() throws Exception { final SynchronousQueue<String> queue = new SynchronousQueue<String>(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { try { System.out.println("take , 在沒有取到值以前一直處理阻塞 : " + queue.take()); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread1.start(); Thread.sleep(2000); Thread thread2 = new Thread(new Runnable() { @Override public void run() { queue.add("進值!!!"); } }); thread2.start(); } /** * ConcurrentLinkedQueue:是一個適合高併發場景下的隊列,經過無鎖的方式,實現了高併發狀態下的高性能,性能好於BlockingQueue。 * 它是一個基於連接節點的無界限線程安全隊列。該隊列的元素遵循先進先出的原則。頭是最早加入的,尾是最後加入的,不容許null元素。 * 無阻塞隊列,沒有 put 和 take 方法 */ @Test public void ITDragonConcurrentLinkedQueue() throws Exception { ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<String>(); queue.offer("1.高性能無阻塞"); queue.add("2.無界隊列"); System.out.println(queue); System.out.println(queue.poll() + " \t : " + queue); // 從頭部取出元素,並從隊列裏刪除,執行poll 後 元素減小一個 System.out.println(queue.peek() + " \t : " + queue); // 從頭部取出元素,執行peek 不移除元素 } }
關鍵字是爲了線程安全服務的,哪什麼是線程安全呢?當多個線程訪問某一個類(對象或方法)時,這個對象始終都能表現出正確的行爲,那麼這個類(對象或方法)就是線程安全的。
線程安全的兩個特性:原子性和可見性。synchronized 同步,原子性。volatile 可見性。wait,notify 負責多個線程之間的通訊。
synchronized 能夠在任意對象及方法上加鎖,而加鎖的這段代碼稱爲"互斥區"或"臨界區",若一個線程想要執行synchronized修飾的代碼塊,首先要
step1 嘗試得到鎖
step2 若是拿到鎖,執行synchronized代碼體內容
step3 若是拿不到鎖,這個線程就會不斷的嘗試得到這把鎖,直到拿到爲止,並且是多個線程同時去競爭這把鎖。
注*(線程多了也就是會出現鎖競爭的問題,多個線程執行的順序是按照CPU分配的前後順序而定的,而並不是代碼執行的前後順序)
synchronized 能夠修飾方法,修飾代碼塊,這些都是對象鎖。若和static一塊兒使用,則升級爲類鎖。
synchronized 鎖是能夠重入的,當一個線程獲得了一個對象的鎖後,再次請求此對象時是能夠再次獲得該對象的鎖。鎖重入的機制,也支持在父子類繼承的場景。
synchronized 同步異步,一個線程獲得了一個對象的鎖後,其餘線程是能夠執行非加鎖的方法(異步)。可是不能執行其餘加鎖的方法(同步)。
synchronized 鎖異常,當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。
/** * synchronized 關鍵字,能夠修飾方法,也能夠修飾代碼塊。建議採用後者,經過減少鎖的粒度,以提升系統性能。 * synchronized 關鍵字,若是以字符串做爲鎖,請注意String常量池的緩存功能和字符串改變後鎖是否的狀況。 * synchronized 鎖重入,當一個線程獲得了一個對象的鎖後,再次請求此對象時是能夠再次獲得該對象的鎖。 * synchronized 同異步,一個線程得到鎖後,另一個線程能夠執行非synchronized修飾的方法,這是異步。若另一個線程執行任何synchronized修飾的方法則須要等待,這是同步 * synchronized 類鎖,用static + synchronized 修飾則表示對整個類進行加鎖 */ public class ITDragonSynchronized { private void thisLock () { // 對象鎖 synchronized (this) { System.out.println("this 對象鎖!"); } } private void classLock () { // 類鎖 synchronized (ITDragonSynchronized.class) { System.out.println("class 類鎖!"); } } private Object lock = new Object(); private void objectLock () { // 任何對象鎖 synchronized (lock) { System.out.println("object 任何對象鎖!"); } } private void stringLock () { // 字符串鎖,注意String常量池的緩存功能 synchronized ("string") { // 用 new String("string") t4 和 t5 同時進入。用string t4完成後,t5在開始 try { for(int i = 0; i < 3; i++) { System.out.println("thread : " + Thread.currentThread().getName() + " stringLock !"); Thread.sleep(500); } } catch (InterruptedException e) { e.printStackTrace(); } } } private String strLock = "lock"; // 字符串鎖改變 private void changeStrLock () { synchronized (strLock) { try { System.out.println("thread : " + Thread.currentThread().getName() + " changeLock start !"); strLock = "changeLock"; Thread.sleep(500); System.out.println("thread : " + Thread.currentThread().getName() + " changeLock end !"); } catch (InterruptedException e) { e.printStackTrace(); } } } private synchronized void method1() { // 鎖重入 System.out.println("^^^^^^^^^^^^^^^^^^^^ method1"); method2(); } private synchronized void method2() { System.out.println("-------------------- method2"); method3(); } private synchronized void method3() { System.out.println("******************** method3"); } private synchronized void syncMethod() { try { System.out.println(Thread.currentThread().getName() + " synchronized method!"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } // 若次方法也加上了synchronized,就必須等待t1線程執行完後,t2才能調用,兩個synchronized塊之間具備互斥性,synchronized塊得到的是一個對象鎖,鎖定的是整個對象 private void asyncMethod() { System.out.println(Thread.currentThread().getName() + " asynchronized method!"); } // static + synchronized 修飾則表示類鎖,打印的結果是thread1線程先執行完,而後在執行thread2線程。若沒有被static修飾,則thread1和 thread2幾乎同時執行,同時結束 private synchronized void classLock(String args) { System.out.println(args + "start......"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(args + "end......"); } public static void main(String[] args) throws Exception { final ITDragonSynchronized itDragonSynchronized = new ITDragonSynchronized(); System.out.println("------------------------- synchronized 代碼塊加鎖 -------------------------"); Thread thread1 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.thisLock(); } }); Thread thread2 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.classLock(); } }); Thread thread3 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.objectLock(); } }); thread1.start(); thread2.start(); thread3.start(); Thread.sleep(2000); System.out.println("------------------------- synchronized 字符串加鎖 -------------------------"); // 若是字符串鎖,用new String("string") t4,t5線程是能夠獲取鎖的,若是直接使用"string" ,若鎖不釋放,t5線程一直處理等待中 Thread thread4 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.stringLock(); } }, "t4"); Thread thread5 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.stringLock(); } }, "t5"); thread4.start(); thread5.start(); Thread.sleep(3000); System.out.println("------------------------- synchronized 字符串變鎖 -------------------------"); // 字符串變了,鎖也會改變,致使t7線程在t6線程未結束後變開始執行,但一個對象的屬性變了,不影響這個對象的鎖。 Thread thread6 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.changeStrLock(); } }, "t6"); Thread thread7 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.changeStrLock(); } }, "t7"); thread6.start(); thread7.start(); Thread.sleep(2000); System.out.println("------------------------- synchronized 鎖重入 -------------------------"); Thread thread8 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.method1(); } }, "t8"); thread8.start(); Thread thread9 = new Thread(new Runnable() { @Override public void run() { SunClass sunClass = new SunClass(); sunClass.sunMethod(); } }, "t9"); thread9.start(); Thread.sleep(2000); System.out.println("------------------------- synchronized 同步異步 -------------------------"); Thread thread10 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.syncMethod(); } }, "t10"); Thread thread11 = new Thread(new Runnable() { @Override public void run() { itDragonSynchronized.asyncMethod(); } }, "t11"); thread10.start(); thread11.start(); Thread.sleep(2000); System.out.println("------------------------- synchronized 同步異步 -------------------------"); ITDragonSynchronized classLock1 = new ITDragonSynchronized(); ITDragonSynchronized classLock2 = new ITDragonSynchronized(); Thread thread12 = new Thread(new Runnable() { @Override public void run() { classLock1.classLock("classLock1"); } }); thread12.start(); Thread thread13 = new Thread(new Runnable() { @Override public void run() { classLock2.classLock("classLock2"); } }); thread13.start(); } // 有父子繼承關係的類,若是都使用了synchronized 關鍵字,也是線程安全的。 static class FatherClass { public synchronized void fatherMethod(){ System.out.println("#################### fatherMethod"); } } static class SunClass extends FatherClass{ public synchronized void sunMethod() { System.out.println("@@@@@@@@@@@@@@@@@@@@ sunMethod"); this.fatherMethod(); } } }
volatile 關鍵字雖然不具有synchronized關鍵字的原子性(同步)但其主要做用就是使變量在多個線程中可見。也就是可見性。
用法很簡單,直接用來修飾變量。由於其不具有原子性,能夠用Atomic類代替。美中不足的是多個Atomic類也不具有原子性,因此還須要synchronized來修飾。
volatile 關鍵字工做原理
每一個線程都有本身的工做內存,若是線程須要用到一個變量的時,會從主內存拷貝一份到本身的工做內存中。從而提升了效率。每次執行完線程後再將變量從工做內存同步回主內存中。
這樣就存在一個問題,變量在不一樣線程中可能存在不一樣的值。若是用volatile 關鍵字修飾變量,則會讓線程的執行引擎直接從主內存中獲取值。
import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** * volatile 關鍵字主要做用就是使變量在多個線程中可見。 * volatile 關鍵字不具有原子性,但Atomic類是具有原子性和可見性。 * 美中不足的是多個Atomic類不具有原子性,仍是須要synchronized 關鍵字幫忙。 */ public class ITDragonVolatile{ private volatile boolean flag = true; private static volatile int count; private static AtomicInteger atomicCount = new AtomicInteger(0); // 加 static 是爲了不每次實例化對象時初始值爲零 // 測試volatile 關鍵字的可見性 private void volatileMethod() { System.out.println("thread start !"); while (flag) { // 若是flag爲true則一直處於阻塞中, } System.out.println("thread end !"); } // 驗證volatile 關鍵字不具有原子性 private int volatileCountMethod() { for (int i = 0; i < 10; i++) { // 第一個線程還未將count加到10的時候,就可能被另外一個線程開始修改。可能會致使最後一次打印的值不是1000 count++ ; } return count; } // 驗證Atomic類具備原子性 private int atomicCountMethod() { for (int i = 0; i < 10; i++) { atomicCount.incrementAndGet(); } // 若最後一次打印爲1000則表示具有原子性,中間打印的信息多是受println延遲影響。 return atomicCount.get();// 若最後一次打印爲1000則表示具有原子性 } // 驗證多個 Atomic類操做不具有原子性,加synchronized關鍵字修飾便可 private synchronized int multiAtomicMethod(){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } atomicCount.addAndGet(1); atomicCount.addAndGet(2); atomicCount.addAndGet(3); atomicCount.addAndGet(4); return atomicCount.get(); //若具有原子性,則返回的結果必定都是10的倍數,需屢次運行才能看到結果 } /** * volatile 關鍵字可見性緣由 * 這裏有兩個線程 :一個是main的主線程,一個是thread的子線程 * jdk線程工做流程 :爲了提升效率,每一個線程都有一個工做內存,將主內存的變量拷貝一份到工做內存中。線程的執行引擎就直接從工做內存中獲取變量。 * So 問題來了 :thread線程用的是本身的工做內存,主線程將變量修改後,thread線程不知道。這就是數據不可見的問題。 * 解決方法 :變量用volatile 關鍵字修飾後,線程的執行引擎就直接從主內存中獲取變量。 * */ public static void main(String[] args) throws InterruptedException { // 測試volatile 關鍵字的可見性 /*ITDragonVolatile itDragonVolatile = new ITDragonVolatile(); Thread thread = new Thread(itDragonVolatile); thread.start(); Thread.sleep(1000); // 等線程啓動了,再設置值 itDragonVolatile.setFlag(false); System.out.println("flag : " + itDragonVolatile.isFlag());*/ // 驗證volatile 關鍵字不具有原子性 和 Atomic類具備原子性 final ITDragonVolatile itDragonVolatile = new ITDragonVolatile(); List<Thread> threads = new ArrayList<Thread>(); for (int i = 0; i < 100; i++) { threads.add(new Thread(new Runnable() { @Override public void run() { // 中間打印的信息多是受println延遲影響,請看最後一次打印的結果 System.out.println(itDragonVolatile.multiAtomicMethod()); } })); } for(Thread thread : threads){ thread.start(); } } public boolean isFlag() { return flag; } public void setFlag(boolean flag) { this.flag = flag; } }
使用 wait/ notify 方法實現線程間的通訊,模擬BlockingQueue隊列。有兩點須要注意:
1)wait 和 notify 必需要配合 synchronized 關鍵字使用
2)wait方法是釋放鎖的, notify方法不釋放鎖。
線程通訊概念:線程是操做系統中獨立的個體,但這些個體若是不通過特殊的處理,就不能成爲一個總體,線程之間的通訊就成爲總體的必用方法之一。
import java.util.LinkedList; import java.util.concurrent.atomic.AtomicInteger; /** * synchronized 能夠在任意對象及方法上加鎖,而加鎖的這段代碼稱爲"互斥區"或"臨界區",通常給代碼塊加鎖,經過減少鎖的粒度從而提升性能。 * Atomic* 是爲了彌補volatile關鍵字不具有原子性的問題。雖然一個Atomic*對象是具有原子性的,但不能確保多個Atomic*對象也具有原子性。 * volatile 關鍵字不具有synchronized關鍵字的原子性其主要做用就是使變量在多個線程中可見。 * wait / notify * wait() 使線程阻塞運行,notify() 隨機喚醒等待隊列中等待同一共享資源的一個線程繼續運行,notifyAll() 喚醒全部等待隊列中等待同一共享資源的線程繼續運行。 * 1)wait 和 notify 必需要配合 synchronized 關鍵字使用 * 2)wait方法是釋放鎖的, notify方法不釋放鎖 */ public class ITDragonMyQueue { //1 須要一個承裝元素的集合 private LinkedList<Object> list = new LinkedList<Object>(); //2 須要一個計數器 AtomicInteger (保證原子性和可見性) private AtomicInteger count = new AtomicInteger(0); //3 須要制定上限和下限 private final Integer minSize = 0; private final Integer maxSize ; //4 構造方法 public ITDragonMyQueue(Integer size){ this.maxSize = size; } //5 初始化一個對象 用於加鎖 private final Object lock = new Object(); //put(anObject): 把anObject加到BlockingQueue裏,若是BlockQueue沒有空間,則調用此方法的線程被阻斷,直到BlockingQueue裏面有空間再繼續. public void put(Object obj){ synchronized (lock) { while(count.get() == this.maxSize){ try { lock.wait(); // 當Queue沒有空間時,線程被阻塞 ,這裏爲了區分,命名爲wait1 } catch (InterruptedException e) { e.printStackTrace(); } } list.add(obj); //1 加入元素 count.incrementAndGet(); //2 計數器累加 lock.notify(); //3 新增元素後,通知另一個線程wait2,隊列多了一個元素,能夠作移除操做了。 System.out.println("新加入的元素爲: " + obj); } } //take: 取走BlockingQueue裏排在首位的對象,若BlockingQueue爲空,阻斷進入等待狀態直到BlockingQueue有新的數據被加入. public Object take(){ Object ret = null; synchronized (lock) { while(count.get() == this.minSize){ try { lock.wait(); // 當Queue沒有值時,線程被阻塞 ,這裏爲了區分,命名爲wait2 } catch (InterruptedException e) { e.printStackTrace(); } } ret = list.removeFirst(); //1 作移除元素操做 count.decrementAndGet(); //2 計數器遞減 lock.notify(); //3 移除元素後,喚醒另一個線程wait1,隊列少元素了,能夠再添加操做了 } return ret; } public int getSize(){ return this.count.get(); } public static void main(String[] args) throws Exception{ final ITDragonMyQueue queue = new ITDragonMyQueue(5); queue.put("a"); queue.put("b"); queue.put("c"); queue.put("d"); queue.put("e"); System.out.println("當前容器的長度: " + queue.getSize()); Thread thread1 = new Thread(new Runnable() { @Override public void run() { queue.put("f"); queue.put("g"); } },"thread1"); Thread thread2 = new Thread(new Runnable() { @Override public void run() { System.out.println("移除的元素爲:" + queue.take()); // 移除一個元素後再進一個,而並不是同時移除兩個,進入兩個元素。 System.out.println("移除的元素爲:" + queue.take()); } },"thread2"); thread1.start(); Thread.sleep(2000); thread2.start(); } }
死鎖是一個很糟糕的狀況,鎖遲遲不能解開,其餘線程只能一直處於等待阻塞狀態。好比線程A擁有鎖一,卻還想要鎖二。線程B擁有鎖二,卻還想要鎖一。兩個線程各執己見,兩個線程將永遠等待。
排查:
第一步:控制檯輸入jps用於得到當前JVM進程的pid
第二步:jstack pid 用於打印堆棧信息
第三步:解讀,"Thread-1" 是線程的名字,prio 是線程的優先級,tid 是線程id, nid 是本地線程id, waiting to lock 等待去獲取的鎖,locked 本身擁有的鎖。
"Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000] java.lang.Thread.State: BLOCKED (on object monitor) at com.itdragon.keyword.ITDragonDeadLock.rightLeft(ITDragonDeadLock.java:37) - waiting to lock <0x00000000ecfdf9d0> (a java.lang.Object) - locked <0x00000000ecfdf9e0> (a java.lang.Object) at com.itdragon.keyword.ITDragonDeadLock$2.run(ITDragonDeadLock.java:54) at java.lang.Thread.run(Thread.java:748)
/** * 死鎖: 線程A擁有鎖一,卻還想要鎖二。線程B擁有鎖二,卻還想要鎖一。兩個線程各執己見,兩個線程將永遠等待。 * 避免: 在設計階段,瞭解鎖的前後順序,減小鎖的交互數量。 * 排查: * 第一步:控制檯輸入 jps 用於得到當前JVM進程的pid * 第二步:jstack pid 用於打印堆棧信息 * "Thread-1" #11 prio=5 os_prio=0 tid=0x0000000055ff1800 nid=0x1bd4 waiting for monitor entry [0x0000000056e2e000] * - waiting to lock <0x00000000ecfdf9d0> - locked <0x00000000ecfdf9e0> * "Thread-0" #10 prio=5 os_prio=0 tid=0x0000000055ff0800 nid=0x1b14 waiting for monitor entry [0x0000000056c7f000] * - waiting to lock <0x00000000ecfdf9e0> - locked <0x00000000ecfdf9d0> * 能夠看出,兩個線程持有的鎖都是對方想要獲得的鎖(得不到的永遠在騷動),並且最後一行也打印了 Found 1 deadlock. */ public class ITDragonDeadLock { private final Object left = new Object(); private final Object right = new Object(); public void leftRight(){ synchronized (left) { try { Thread.sleep(2000); // 模擬持有鎖的過程 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (right) { System.out.println("leftRight end!"); } } } public void rightLeft(){ synchronized (right) { try { Thread.sleep(2000); // 模擬持有鎖的過程 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (left) { System.out.println("rightLeft end!"); } } } public static void main(String[] args) { ITDragonDeadLock itDragonDeadLock = new ITDragonDeadLock(); Thread thread1 = new Thread(new Runnable() { @Override public void run() { itDragonDeadLock.leftRight(); } }); thread1.start(); Thread thread2 = new Thread(new Runnable() { @Override public void run() { itDragonDeadLock.rightLeft(); } }); thread2.start(); } }
如有Thread一、Thread二、Thread三、Thread4四條線程分別統計C、D、E、F四個盤的大小,全部線程都統計完畢交給Thread5線程去作彙總,應當如何實現
import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.Callable; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 如有Thread一、Thread二、Thread三、Thread4四條線程分別統計C、D、E、F四個盤的大小,全部線程都統計完畢交給Thread5線程去作彙總,應當如何實現? * 思考:彙總,說明要把四個線程的結果返回給第五個線程,若要線程有返回值,推薦使用callable。Thread和Runnable都沒返回值 */ public class ITDragonThreads { public static void main(String[] args) throws Exception { // 無緩衝無界線程池 ExecutorService executor = Executors.newFixedThreadPool(8); // 相對ExecutorService,CompletionService能夠更精確和簡便地完成異步任務的執行 CompletionService<Long> completion = new ExecutorCompletionService<Long>(executor); CountWorker countWorker = null; for (int i = 0; i < 4; i++) { // 四個線程負責統計 countWorker = new CountWorker(i+1); completion.submit(countWorker); } // 關閉線程池 executor.shutdown(); // 主線程至關於第五個線程,用於彙總數據 long total = 0; for (int i = 0; i < 4; i++) { total += completion.take().get(); } System.out.println(total / 1024 / 1024 / 1024 +"G"); } } class CountWorker implements Callable<Long>{ private Integer type; public CountWorker() { } public CountWorker(Integer type) { this.type = type; } @Override public Long call() throws Exception { ArrayList<String> paths = new ArrayList<>(Arrays.asList("c:", "d:", "e:", "f:")); return countDiskSpace(paths.get(type - 1)); } // 統計磁盤大小 private Long countDiskSpace (String path) { File file = new File(path); long totalSpace = file.getTotalSpace(); System.out.println(path + " 總空間大小 : " + totalSpace / 1024 / 1024 / 1024 + "G"); return totalSpace; } }
1 常見建立線程的方式和其優缺點
(1)繼承Thread類 (2)實現Runnable接口
優缺點:實現一個接口比繼承一個類要靈活,減小程序之間的耦合度。缺點就是代碼多了一點。
2 start()方法和run()方法的區別
start方法能夠啓動線程,而run方法只是thread的一個普通方法調用。
3 多線程的做用
(1)發揮多核CPU的優點,提升CPU的利用率(2)防止阻塞,提升效率
4 什麼是線程安全
當多個線程訪問某一個類(對象或方法)時,這個對象始終都能表現出正確的行爲,那麼這個類(對象或方法)就是線程安全的。
5 線程安全級別
(1)不可變(2)絕對線程安全(3)相對線程安全(4)線程非安全
6 如何在兩個線程之間共享數據
線程之間數據共享,其實能夠理解爲線程之間的通訊,能夠用wait/notify/notifyAll 進行等待和喚醒。
7 用線程池的好處
避免頻繁地建立和銷燬線程,達到線程對象的重用,提升性能,減輕服務器壓力。使用線程池還能夠根據項目靈活地控制併發的數目。
8 sleep方法和wait方法有什麼區別
sleep方法和wait方法均可以用來放棄CPU必定的時間,sleep是thread的方法,不會釋放鎖。wait是object的方法,會釋放鎖。
1 線程池核心參數有 初始核心線程數,線程池運行最大線程數,空閒線程存活時間,時間單位,任務隊列。
2 隊列是一種數據結構,主要有兩類 阻塞隊列BlockingQueue,和非阻塞高性能隊列ConcurrentLinkedQueue。
3 線程安全的兩個特性,原子性和可見性。synchronized 關鍵字具有原子性。volatile 關鍵字具有可見性。
4 單個Atomic類具有原子性和可見性,多個Atomic類不具有原子性,須要synchronized 關鍵字修飾。
5 兩個線程持有的鎖都是對方想要獲得的鎖時容易出現死鎖的狀況,從設計上儘可能減小鎖的交互。
本章到這裏就結束了,涉及的知識點比較多,請參考流程圖來學習。若有什麼問題能夠指出。喜歡的朋友能夠點個"推薦"