Java基礎篇——執行緒、併發程式設計知識點全面介紹(面試、學習的必備索引文件)
原創不易,如需轉載,請註明出處 https://www.cnblogs.com/baixianlong/p/10739579.html ,希望大家多多支援!!!
一、執行緒基礎
1、執行緒與程序
- 執行緒是指程序中的一個執行流程,一個程序中可以執行多個執行緒。
- 程序是指一個記憶體中執行的應用程式,每個程序都有自己獨立的一塊記憶體空間,即程序空間或(虛空間),比如一個qq.exe就是一個程序。
2、執行緒的特點
- 執行緒共享分配給該程序的所有資源
- 執行緒之間實際上輪換執行(也就是執行緒切換)
- 一個程式至少有一個程序,一個程序至少有一個執行緒
- 執行緒不能夠獨立執行,必須依存在應用程式中,由應用程式提供多個執行緒執行控制
- 執行緒有自己的堆疊和區域性變數,但執行緒之間沒有單獨的地址空間,一個執行緒包含以下內容:
- 一個指向當前被執行指令的指令指標
- 一個棧
- 一個暫存器值的集合,定義了一部分描述正在執行執行緒的處理器狀態的值
- 一個私有的資料區
3、執行緒的作用
- 程序在執行過程中擁有獨立的記憶體單元,而多個執行緒共享記憶體,從而極大地提高了程式的執行效率(併發執行)
4、執行緒的建立
- 繼承Thread類
- 實現Runnable介面
- 通過ThreadPool獲取
5、執行緒的狀態(生命週期)
- 建立:當用new操作符建立一個執行緒時。此時程式還沒有開始執行執行緒中的程式碼
- 就緒:當start()方法返回後,執行緒就處於就緒狀態
- 執行:當執行緒獲得CPU時間後,它才進入執行狀態,真正開始執行run()方法
- 阻塞:所謂阻塞狀態是正在執行的執行緒沒有執行結束,暫時讓出CPU,這時其他處於就緒狀態的執行緒就可以獲得CPU時間,進入執行狀態
-
死亡:1、run方法正常退出而自然死亡;2、一個未捕獲的異常終止了run方法而使執行緒猝死
6、執行緒的優先順序、執行緒讓步yield、執行緒合併join、執行緒睡眠sleep
- 優先順序:執行緒總是存在優先順序,優先順序範圍在1~10之間(數值越大優先順序越高),執行緒預設優先順序是5,優先順序高的理論上先執行,但實際不一定。
- 執行緒讓步:yield方法呼叫後 ,是直接進入就緒狀態,所以有可能剛進入就緒狀態,又被排程到執行狀態(使用yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行)。
- 執行緒合併:保證當前執行緒停止執行,直到該執行緒所加入的執行緒完成為止。然而,如果它加入的執行緒沒有存活,則當前執行緒不需要停止。
- 執行緒睡眠:sleep方法暫停當前執行緒後,會進入阻塞狀態,只有當睡眠時間到了,才會轉入就緒狀態。
7、執行緒的分類(以下兩者的唯一區別之處就在虛擬機器的離開)
- 守護執行緒: thread.setDaemon(true),必須在thread.start()之前設定,GC執行緒就是一個守護執行緒。
- 普通執行緒
8、正確結束執行緒(給出一些方案)
- 使用Thread.stop()方法,但是該方法已經被廢棄了,使用它是極端不安全的,會造成資料不一致的問題。
- 使用interrupt()方法停止一個執行緒,直接呼叫該方法不會終止一個正在執行的執行緒,需要加入一個判斷語句才可以完成執行緒的停止。
- 使用共享變數的方式,在這種方式中,之所以引入共享變數,是因為該變數可以被多個執行相同任務的執行緒用來作為是否中斷的訊號,通知中斷執行緒的執行。
二、執行緒同步
1、執行緒同步的意義
- 執行緒的同步是為了防止多個執行緒訪問一個數據物件時,對資料造成的破壞
2、鎖的原理
- Java中每個物件都有一個內建鎖,內建鎖是一個互斥鎖,這就是意味著最多隻有一個執行緒能夠獲得該鎖。
- 當程式執行到非靜態的synchronized同步方法上時,自動獲得與正在執行程式碼類的當前例項(this例項)有關的鎖。獲得一個物件的鎖也稱為獲取鎖、鎖定物件、在物件上鎖定或在物件上同步。
- 當程式執行到synchronized同步方法或程式碼塊時才該物件鎖才起作用。
- 一個物件只有一個鎖。所以,如果一個執行緒獲得該鎖,就沒有其他執行緒可以獲得鎖,直到第一個執行緒釋放(或返回)鎖。這也意味著任何其他執行緒都不能進入該物件上的synchronized方法或程式碼塊,直到該鎖被釋放。
- 釋放鎖是指持鎖執行緒退出了synchronized同步方法或程式碼塊。
3、鎖和同步的理解
- 只能同步方法,而不能同步變數和類。
- 每個物件只有一個鎖,當提到同步時,應該清楚在什麼上同步,也就是說,在哪個物件上同步。
- 不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
- 如果兩個執行緒要執行一個類中的synchronized方法,並且兩個執行緒使用相同的例項來呼叫方法,那麼一次只能有一個執行緒能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個執行緒在物件上獲得一個鎖,就沒有任何其他執行緒可以進入(該物件的)類中的任何一個同步方法。
- 如果執行緒擁有同步和非同步方法,則非同步方法可以被多個執行緒自由訪問而不受鎖的限制。
- 執行緒睡眠時,它所持的任何鎖都不會釋放。
- 執行緒可以獲得多個鎖。比如,在一個物件的同步方法裡面呼叫另外一個物件的同步方法,則獲取了兩個物件的同步鎖。
- 同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分程式碼塊。
- 在使用同步程式碼塊時候,應該指定在哪個物件上同步,也就是說要獲取哪個物件的鎖。
4、物件鎖和類鎖的區別
- 物件鎖是用於物件例項方法,或者一個物件例項上的,類鎖是用於類的靜態方法或者一個類的class物件上的。
- 類的物件例項可以有很多個,但是每個類只有一個class物件,所以不同物件例項的物件鎖是互不干擾的,但是每個類只有一個類鎖。
5、執行緒的死鎖及規避
- 死鎖是執行緒間相互等待鎖鎖造成的,一旦程式發生死鎖,程式將死掉。
- 如果我們能夠避免在物件的同步方法中呼叫其它物件的同步方法,那麼就可以避免死鎖產生的可能性。
6、volatile關鍵字(推薦大家一片文章)
- 正確使用 volatile變數
- 與鎖相比,Volatile 變數是一種非常簡單但同時又非常脆弱的同步機制,它在某些情況下將提供優於鎖的效能和伸縮性。
- 如果嚴格遵循 volatile 的使用條件 —— 即變數真正獨立於其他變數和自己以前的值 —— 在某些情況下可以使用 volatile 代替 synchronized 來簡化程式碼。
三、執行緒的互動
1、執行緒互動的基礎知識
- void notify()——喚醒在此物件監視器上等待的單個執行緒。
- void notifyAll()——喚醒在此物件監視器上等待的所有執行緒。
- void wait()——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法。
- void wait(longtimeout)——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者超過指定的時間量。
- void wait(longtimeout, int nanos)——導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify()方法或 notifyAll()方法,或者其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。
2、注意點
- 必須從同步環境內呼叫wait()、notify()、notifyAll()方法。執行緒不能呼叫物件上等待或通知的方法,除非它擁有那個物件的鎖。
- 當在物件上呼叫wait()方法時,執行該程式碼的執行緒立即放棄它在物件上的鎖。然而呼叫notify()時,並不意味著這時執行緒會放棄其鎖。如果執行緒仍然在完成同步程式碼,則執行緒在移出之前不會放棄鎖。因此,呼叫notify()並不意味著這時該鎖變得可用。
四、執行緒池
1、好處
- 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
- 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控。
- 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
2、執行緒池的建立使用
public class ThreadPoolExecutor extends AbstractExecutorService { public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler); public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler); }
- 引數介紹:
- corePoolSize:核心池的大小
- maximumPoolSize:執行緒池最大執行緒數
- keepAliveTime:表示執行緒沒有任務執行時最多保持多久時間會終止
- unit:引數keepAliveTime的時間單位,有7種取值,在TimeUnit類中有7種靜態屬性:
- TimeUnit.DAYS; //天
- TimeUnit.HOURS; //小時
- TimeUnit.MINUTES; //分鐘
- TimeUnit.SECONDS; //秒
- TimeUnit.MILLISECONDS; //毫秒
- TimeUnit.MICROSECONDS; //微妙
- TimeUnit.NANOSECONDS; //納秒
- workQueue:一個阻塞佇列,用來儲存等待執行的任務
- threadFactory:執行緒工廠,主要用來建立執行緒
- handler:表示當拒絕處理任務時的策略,有以下四種取值:
- ThreadPoolExecutor.AbortPolicy:丟棄任務並丟擲RejectedExecutionException異常。
- ThreadPoolExecutor.DiscardPolicy:也是丟棄任務,但是不丟擲異常。
- ThreadPoolExecutor.DiscardOldestPolicy:丟棄佇列最前面的任務,然後重新嘗試執行任務(重複此過程)
- ThreadPoolExecutor.CallerRunsPolicy:由呼叫執行緒處理該任務
3、java預設實現的4中執行緒池(不建議使用)
- newCachedThreadPool:建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。執行緒池為無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的執行緒,而不用每次新建執行緒。
- newFixedThreadPool:建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。定長執行緒池的大小最好根據系統資源進行設定。如Runtime.getRuntime().availableProcessors()。
- newScheduledThreadPool:建立一個定長執行緒池,支援定時及週期性任務執行。
- newSingleThreadExecutor:建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
五、併發程式設計相關內容
1、synchronized 的侷限性 與 Lock 的優點
- 首先synchronized是java內建關鍵字,在jvm層面,Lock是個java類;
- synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;
- synchronized會自動釋放鎖(a 執行緒執行完同步程式碼會釋放鎖 ;b 執行緒執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成執行緒死鎖;
- 用synchronized關鍵字的兩個執行緒1和執行緒2,如果當前執行緒1獲得鎖,執行緒2執行緒等待。如果執行緒1阻塞,執行緒2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,執行緒可以不用一直等待就結束了;
- synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)
- Lock鎖適合大量同步的程式碼的同步問題,synchronized鎖適合程式碼少量的同步問題。
2、Lock 和 ReadWriteLock
-
Lock介面,ReentrantLock(可重入鎖)是唯一的實現類。
public interface Lock { void lock(); //lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠響應中斷,即中斷執行緒的等待狀態。 //也就使說,當兩個執行緒同時通過lock.lockInterruptibly()想獲取某個鎖時,假若此時執行緒A獲取到了鎖,而執行緒B只有在等待,那麼對執行緒B呼叫threadB.interrupt()方法能夠中斷執行緒B的等待過程。 void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition(); }
-
ReadWriteLock介面,ReentrantReadWriteLock實現了ReadWriteLock介面。
public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
- 讀讀共享
- 寫寫互斥
- 讀寫互斥
- 寫讀互斥
3、訊號量(Semaphore)
- 介紹:Java的訊號量實際上是一個功能完畢的計數器,對控制一定資源的消費與回收有著很重要的意義,訊號量常常用於多執行緒的程式碼中,並能監控有多少數目的執行緒等待獲取資源,並且通過訊號量可以得知可用資源的數目等等。
- 特點:
- 以控制某個資源可被同時訪問的個數,通過 acquire() 獲取一個許可,如果沒有就等待,而 release() 釋放一個許可。
- 單個訊號量的Semaphore物件可以實現互斥鎖的功能,並且可以是由一個執行緒獲得了“鎖”,再由另一個執行緒釋放“鎖”,這可應用於死鎖恢復的一些場合。
- 訊號量解決了鎖一次只能讓一個執行緒訪問資源的問題,訊號量可以指定多個執行緒,同時訪問一個資源。
-
分為公平模式和非公平模式(預設非公平)
public Semaphore(int permits) { sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
區別在於:公平模式會考慮是否已經有執行緒在等待,如果有則直接返回-1表示獲取失敗;而非公平模式不會關心有沒有執行緒在等待,會去快速競爭資源的使用權。說到競爭就得提到AbstractQueuedSynchronizer同步框架,一個僅僅需要簡單繼承就可以實現複雜執行緒的同步方案,建議大家去研究一下。
4、閉鎖(CountDownLatch)
-
介紹:閉鎖是一種同步工具,可以延遲執行緒的進度直到終止狀態。可以把它理解為一扇門,當閉鎖到達結束狀態之前,這扇門一直是關閉的,沒有任何執行緒可以通過。當閉鎖到達結束狀態時,這扇門會開啟並允許所有執行緒通過,並且閉鎖開啟後不可再改變狀態。
閉鎖可以確保某些任務直到其他任務完成後才繼續往下執行。
- 使用介紹
- 構造器中的計數值(count)實際上就是閉鎖需要等待的執行緒數量。這個值只能被設定一次,而且CountDownLatch沒有提供任何機制去重新設定這個計數值。
- 與CountDownLatch的第一次互動是主執行緒等待其他執行緒。主執行緒必須在啟動其他執行緒後立即呼叫CountDownLatch.await()方法。這樣主執行緒的操作就會在這個方法上阻塞,直到其他執行緒完成各自的任務。
- 其他N 個執行緒必須引用閉鎖物件,因為他們需要通知CountDownLatch物件,他們已經完成了各自的任務。這種通知機制是通過 CountDownLatch.countDown()方法來完成的;每呼叫一次這個方法,在建構函式中初始化的count值就減1。所以當N個執行緒都調 用了這個方法,count的值等於0,然後主執行緒就能通過await()方法,恢復執行自己的任務。
4、柵欄(CyclicBarrier)
-
介紹:柵欄類似於閉鎖,它能阻塞一組執行緒直到某個事件的發生。柵欄與閉鎖的關鍵區別在於,所有的執行緒必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其他執行緒。CyclicBarrier可以使一定數量的執行緒反覆地在柵欄位置處彙集。當執行緒到達柵欄位置時將呼叫await方法,這個方法將阻塞直到所有執行緒都到達柵欄位置。如果所有執行緒都到達柵欄位置,那麼柵欄將開啟,此時所有的執行緒都將被釋放,而柵欄將被重置以便下次使用。
- CyclicBarrier和CountDownLatch的區別:
- CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能夠處理更為複雜的場景;
- CyclicBarrier還提供了一些其他有用的方法,比如getNumberWaiting()方法可以獲得CyclicBarrier阻塞的執行緒數量,isBroken()方法用來了解阻塞的執行緒是否被中斷;
- CountDownLatch允許一個或多個執行緒等待一組事件的產生,而CyclicBarrier用於等待其他執行緒執行到柵欄位置。
5、原子量 (Atomic)
-
介紹:Atomic一詞跟原子有點關係,後者曾被人認為是最小物質的單位。計算機中的Atomic是指不能分割成若干部分的意思。如果一段程式碼被認為是Atomic,則表示這段程式碼在執行過程中,是不能被中斷的。通常來說,原子指令由硬體提供,供軟體來實現原子方法(某個執行緒進入該方法後,就不會被中斷,直到其執行完成)
-
特性:在多執行緒環境下,當有多個執行緒同時執行這些類的例項包含的方法時,具有排他性,即當某個執行緒進入方法,執行其中的指令時,不會被其他執行緒打斷,而別的執行緒就像自旋鎖一樣,一直等到該方法執行完成,才由JVM從等待佇列中選擇一個另一個執行緒進入,這只是一種邏輯上的理解。實際上是藉助硬體的相關指令來實現的,不會阻塞執行緒(或者說只是在硬體級別上阻塞了)。
-
注意:原子量雖然可以保證單個變數在某一個操作過程的安全,但無法保證你整個程式碼塊,或者整個程式的安全性。因此,通常還應該使用鎖等同步機制來控制整個程式的安全性。
6、Condition
- 介紹:在Java程式中,任意一個Java物件,都擁有一組監視器方法(定義在java.lang.Object類上),主要包括wait()、wait(long)、notify()、notifyAll()方法,這些方法與synchronized關鍵字配合,可以實現等待/通知模式。Condition介面也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有區別的。
- Condition與Object中的wati,notify,notifyAll區別:
- Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。
不同的是,Object中的這些方法是和同步鎖捆綁使用的;而Condition是需要與互斥鎖/共享鎖捆綁使用的。 - Condition它更強大的地方在於:能夠更加精細的控制多執行緒的休眠與喚醒。對於同一個鎖,我們可以建立多個Condition,在不同的情況下使用不同的Condition。如果採用Object類中的wait(),notify(),notifyAll()實現該緩衝區,當向緩衝區寫入資料之後需要喚醒"讀執行緒"時,不可能通過notify()或notifyAll()明確的指定喚醒"讀執行緒",而只能通過notifyAll喚醒所有執行緒(但是notifyAll無法區分喚醒的執行緒是讀執行緒,還是寫執行緒)。 但是,通過Condition,就能明確的指定喚醒讀執行緒。
- Condition中的await()方法相當於Object的wait()方法,Condition中的signal()方法相當於Object的notify()方法,Condition中的signalAll()相當於Object的notifyAll()方法。
7、併發程式設計概覽
六、總結
- 到此執行緒的基本內容介紹就差不多了,這篇文章偏理論一些,每個知識點的介紹並不全面。
- 大家可以以此篇文章為索引,來展開對併發程式設計的深入學習,細細咀嚼每個知識點,相信你會有巨大的收穫!!!
個人部落格地址:
cnblogs: https://www.cnblogs.com/baixianlong
csdn: https://blog.csdn.net/tiantuo6513
segmentfault: https://segmentfault.com/u/baixianlong
github: https://github.com/xianlongbai