一、并發編程三要素
1、原子性
原子性指的是一個或者多個操作,要么全部執行并且在執行的過程中不被其他操作打斷,要么就全部都不執行。
2、可見性
可見性指多個線程操作一個共享變量時,其中一個線程對變量進行修改后,其他線程可以立即看到修改的結果。
3、有序性
有序性,即程序的執行順序按照代碼的先后順序來執行
二、實現可見性的方法
synchronized 或者 Lock:保證同一個時刻只有一個線程獲取鎖執行代碼,鎖釋放
之前把最新的值刷新到主內存,實現可見性
三、多線程的價值
1、發揮多核 CPU 的優勢
多線程,可以真正發揮出多核 CPU 的優勢來,達到充分利用 CPU 的目的,采用多線程的方式去同時完成幾件事情而不互相干擾。
2、防止阻塞
從程序運行效率的角度來看,單核 CPU 不但不會發揮出多線程的優勢,反而會因為在單核 CPU 上運行多線程導致線程上下文的切換,而降低程序整體的效率。但是單核 CPU 我們還是要應用多線程,就是為了防止阻塞。試想,如果單核 CPU 使用單線程,那么只要這個線程阻塞了,比方說遠程讀取某個數據吧,對端遲遲未返回又沒有設置超時時間,那么你的整個程序在數據返回回來之前就停止運行了。多線程可以防止這個問題,多條線程同時運行,哪怕一條線程的代碼執行讀取數據阻塞,也不會影響其它任務的執行。
3、便于建模
這是另外一個沒有這么明顯的優點了。假設有一個大的任務 A,單線程編程,那么就要考慮很多,建立整個程序模型比較麻煩。但是如果把這個大的任務 A 分解成幾個小任務,任務 B、任務 C、任務 D,分別建立程序模型,并通過多線程分別運行這幾個任務,那就簡單很多了。
三、創建線程的方式
1、繼承 Thread 類創建線程類
2、通過 Runnable 接口創建線程類
3、通過 Callable 和 Future 創建線程
4、通過線程池創建
四、創建線程的三種方式的對比
1、采用實現 Runnable、Callable 接口的方式創建多線程。
優勢是:
線程類只是實現了 Runnable 接口或 Callable 接口,還可以繼承其他類。在這種方式下,多個線程可以共享同一個 target 對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將 CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。
劣勢是:
編程稍微復雜,如果要訪問當前線程,則必須使用 Thread.currentThread()方法。
2、使用繼承 Thread 類的方式創建多線程
優勢是:
編寫簡單,如果需要訪問當前線程,則無需使用 Thread.currentThread()方法,直接使用 this 即可獲得當前線程。
劣勢是:
線程類已經繼承了 Thread 類,所以不能再繼承其他父類。
3、Runnable 和 Callable 的區別
1)、Callable 規定(重寫)的方法是 call(),Runnable 規定(重寫)的方法是 run()。
2)、Callable 的任務執行后可返回值,而 Runnable 的任務是不能返回值的。
3)、Call 方法可以拋出異常,run 方法不可以。
4)、運行 Callable 任務可以拿到一個 Future 對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,并檢索計算的結果。通過 Future對象可以了解任務執行情況,可取消任務的執行,還可獲取執行結果。
五、Java 線程具有五中基本狀態
1、新建狀態(New):當線程對象對創建后,即進入了新建狀態,如:Thread t= new MyThread();
2、就緒狀態(Runnable):當調用線程對象的 start()方法(t.start();),線程即進入就緒狀態。處于就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待 CPU 調度執行,并不是說執行了 t.start()此線程立即就會執行;
3、運行狀態(Running):當 CPU 開始調度處于就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就 緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處于就緒狀態中;
4、阻塞狀態(Blocked):處于運行狀態中的線程由于某種原因,暫時放棄對 CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被 CPU 調用以進入到運行狀態。
根據阻塞產生的原因不同,阻塞狀態又可以分為三種:
1、等待阻塞:運行狀態中的線程執行 wait()方法,使本線程進入到等待阻塞狀態;
2、同步阻塞:線程在獲取 synchronized 同步鎖失敗(因為鎖被其它線程所占用),它會進入同步阻塞狀態;
3、其他阻塞:通過調用線程的 sleep()或 join()或發出了 I/O 請求時,線程會進入到阻塞狀態。當 sleep()狀態超時、join()等待線程終止或者超時、或者 I/O 處理完畢時,線程重新轉入就緒狀態。
4、死亡狀態(Dead):線程執行完了或者因異常退出了 run()方法,該線程結束生命周期。
六、線程池及創建方式
線程池就是提前創建若干個線程,如果有任務需要處理,線程池里的線程就會處理任務,處理完之后線程并不會被銷毀,而是等待下一個任務。由于創建和銷毀線程都是消耗系統資源的,所以當你想要頻繁的創建和銷毀線程的時候就可以考慮使用線程池來提升系統的性能。
java 提供了一個 java.util.concurrent.Executor 接口的實現用于創建線程池。
七、四種線程池的創建
1、newCachedThreadPool 創建一個可緩存線程池
2、newFixedThreadPool 創建一個定長線程池,可控制線程最大并發數。
3、newScheduledThreadPool 創建一個定長線程池,支持定時及周期性任務執行。
4、newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務。
八、線程池的優點
1、重用存在的線程,減少對象創建銷毀的開銷。
2、可有效的控制最大并發線程數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞。
3、提供定時執行、定期執行、單線程、并發數控制等功能。
九、CyclicBarrier 和 CountDownLatch 的區別
1、CountDownLatch 簡單的說就是一個線程等待,直到他所等待的其他線程都執行完成并且調用 countDown()方法發出通知后,當前線程才可以繼續執行。
2、cyclicBarrier 是所有線程都進行等待,直到所有線程都準備好進入 await()方法之后,所有線程同時開始執行!
3、CountDownLatch 的計數器只能使用一次。而 CyclicBarrier 的計數器可以使用 reset() 方法重置。所以 CyclicBarrier 能處理更為復雜的業務場景,比如如果計算發生錯誤,可以重置計數器,并讓線程們重新執行一次。
4、CyclicBarrier 還提供其他有用的方法,比如 getNumberWaiting 方法可以獲得 CyclicBarrier 阻塞的線程數量。isBroken 方法用來知道阻塞的線程是否被中斷。如果被中斷返回 true,否則返回 false。
十、synchronized 的作用
在 Java 中,synchronized 關鍵字是用來控制線程同步的,就是在多線程的環境下,控制 synchronized 代碼段不被多個線程同時執行。synchronized 既可以加在一段代碼上,也可以加在方法上。
十一、volatile 關鍵字的作用
對于可見性,Java 提供了 volatile 關鍵字來保證可見性。當一個共享變量被 volatile 修飾時,它會保證修改的值會立即被更新到主存,當有其他線程需要讀取時,它會去內存中讀取新值。
從實踐角度而言,volatile 的一個重要作用就是和 CAS 結合,保證了原子性,詳細的可以參見 java.util.concurrent.atomic 包下的類,比如 AtomicInteger。
十二、CAS
是一種基于鎖的操作,而且是樂觀鎖。在 java 中鎖分為樂觀鎖和悲觀鎖。悲觀鎖是將資源鎖住,等一個之前獲得鎖的線程釋放鎖之后,下一個線程才可以訪問。而樂觀鎖采取了一種寬泛的態度,通過某種方式不加鎖來處理資源,比如通過給記錄加 version 來獲取數據,性能較悲觀鎖有很大的提高。
CAS 操作包含三個操作數 —— 內存位置(V)、預期原值(A)和新值(B)。如果內存地址里面的值和 A 的值是一樣的,那么就將內存里面的值更新成 B。CAS是通過無限循環來獲取數據的,如果在第一輪循環中,a 線程獲取地址里面的值被b 線程修改了,那么 a 線程需要自旋,到下次循環才有可能機會執行。java.util.concurrent.atomic 包下的類大多是使用 CAS 操作來實現的( AtomicInteger,AtomicBoolean,AtomicLong)。
十三、CAS的問題
1、CAS 容易造成 ABA 問題
一個線程 a 將數值改成了 b,接著又改成了 a,此時 CAS 認為是沒有變化,其實是已經變化過了,而這個問題的解決方案可以使用版本號標識,每操作一次version 加 1。在 java5 中,已經提供了 AtomicStampedReference 來解決問題。
2、不能保證代碼塊的原子性
CAS 機制所保證的知識一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證 3 個變量共同進行原子性的更新,就不得不使用 synchronized 了。
3、CAS 造成 CPU 利用率增加
之前說過了 CAS 里面是一個循環判斷的過程,如果線程一直沒有獲取到狀態,cpu資源會一直被占用。
十四、Future
在并發編程中,我們經常用到非阻塞的模型,在之前的多線程的三種實現中,不管是繼承 thread 類還是實現 runnable 接口,都無法保證獲取到之前的執行結果。通過實現 Callback 接口,并用 Future 可以來接收多線程的執行結果。Future 表示一個可能還沒有完成的異步任務的結果,針對這個結果可以添加
Callback 以便在任務執行成功或失敗后作出相應的操作
十五、AQS
AbustactQueuedSynchronizer 的簡稱,它是一個 Java 提高的底層同步工具類,用一個 int 類型的變量表示同步狀態,并提供了一系列的 CAS 操作來管理這個同步狀態。
AQS 是一個用來構建鎖和同步器的框架,使用 AQS 能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的 ReentrantLock,Semaphore,其他的諸如ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于AQS 的。
十六、AQS 支持兩種同步方式
1、獨占式
2、共享式
這樣方便使用者實現不同類型的同步組件,獨占式如 ReentrantLock,共享式如Semaphore,CountDownLatch,組合式的如 ReentrantReadWriteLock。總之,AQS 為使用提供了底層支撐,如何組裝實現,使用者可以自由發揮。
十七、ReadWriteLock
首先明確一下,不是說 ReentrantLock 不好,只是 ReentrantLock 某些時候有局限。如果使用 ReentrantLock,可能本身是為了防止線程 A 在寫數據、線程 B 在讀數據造成的數據不一致,但這樣,如果線程 C 在讀數據、線程 D 也在讀數據,讀數據是不會改變數據的,沒有必要加鎖,但是還是加鎖了,降低了程序的性能。因為這個,才誕生了讀寫鎖 ReadWriteLock。ReadWriteLock 是一個讀寫鎖接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一個具體實現,實現了讀寫的分離,讀鎖是共享的,寫鎖是獨占的,讀和讀之間不會互斥,讀和寫、寫和讀、寫和寫之間才會互斥,提升了讀寫的性能。
十八、FutureTask
FutureTask 表示一個異步運算的任務。FutureTask 里面可以傳入一個 Callable 的具體實現類,可以對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作。當然,由于 FutureTask 也是Runnable 接口的實現類,所以 FutureTask 也可以放入線程池中。
十九、synchronized 和 ReentrantLock的區別
synchronized 是和 if、else、for、while 一樣的關鍵字,ReentrantLock 是類,這是二者的本質區別。既然 ReentrantLock 是類,那么它就提供了比
synchronized 更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock 比 synchronized 的擴展性體現在幾點上:
1、ReentrantLock 可以對獲取鎖的等待時間進行設置,這樣就避免了死鎖
2、ReentrantLock 可以獲取各種鎖的信息
3、ReentrantLock 可以靈活地實現多路通知
另外,二者的鎖機制其實也是不一樣的。ReentrantLock 底層調用的是 Unsafe 的park 方法加鎖,synchronized 操作的應該是對象頭中 mark word,這點我不能確定。
二十、樂觀鎖和悲觀鎖
1、樂觀鎖:就像它的名字一樣,對于并發間操作產生的線程安全問題持樂觀狀態,樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改內存中的變量,如果失敗則表示發生沖突,那么就應該有相應的重試邏輯。
2、悲觀鎖:還是像它的名字一樣,對于并發間操作產生的線程安全問題持悲觀狀態,悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨占的鎖,就像 synchronized,不管三七二十一,直接上了鎖就操作資源了。
二十一、synchronized、volatile、CAS 比較
1、synchronized 是悲觀鎖,屬于搶占式,會引起其他線程阻塞。
2、volatile 提供多線程共享變量可見性和禁止指令重排序優化。
3、CAS 是基于沖突檢測的樂觀鎖(非阻塞)
二十三、ThreadLocal
一個本地線程副本變量工具類。主要用于將私有線程和該線程存放的副本對象做一個映射,各個線程之間的變量互不干擾,在高并發場景下,可以實現無狀態的調用,特別適用于各個線程依賴不通的變量值完成操作的場景。簡單說 ThreadLocal 就是一種以空間換時間的做法,在每個 Thread 里面維護了一個以開地址法實現的 ThreadLocal.ThreadLocalMap,把數據進行隔離,數據不共享,自然就沒有線程安全方面的問題了。
二十四、線程的調度策略
線程調度器選擇優先級最高的線程運行,但是,如果發生以下情況,就會終止線
程的運行:
1、線程體中調用了 yield 方法讓出了對 cpu 的占用權利
2、線程體中調用了 sleep 方法使線程進入睡眠狀態
3、線程由于 IO 操作受到阻塞
4、另外一個更高優先級線程出現
5、在支持時間片的系統中,該線程的時間片用完
二十五、ConcurrentHashMap 的并發度
ConcurrentHashMap 的并發度就是 segment 的大小,默認為 16,這意味著最多同時可以有 16 條線程操作 ConcurrentHashMap,這也是ConcurrentHashMap 對 Hashtable 的最大優勢。
二十六、死鎖的原因
1、是多個線程涉及到多個鎖,這些鎖存在著交叉,所以可能會導致了一個鎖依賴的閉環。
例如:線程在獲得了鎖 A 并且沒有釋放的情況下去申請鎖 B,這時,另一個線程已經獲得了鎖 B,在釋放鎖 B 之前又要先獲得鎖 A,因此閉環發生,陷入死鎖循環。
2、默認的鎖申請操作是阻塞的
所以要避免死鎖,就要在一遇到多個對象鎖交叉的情況,就要仔細審查這幾個對象的類中的所有方法,是否存在著導致鎖依賴的環路的可能性。總之是盡量避免在一個同步方法中調用其它對象的延時方法和同步方法。
二十七、線程調度器(Thread Scheduler)和時間分片(Time Slicing)
線程調度器是一個操作系統服務,它負責為 Runnable 狀態的線程分配 CPU 時間。一旦我們創建一個線程并啟動它,它的執行便依賴于線程調度器的實現。時間分片是指將可用的 CPU 時間分配給可用的 Runnable 線程的過程。分配 CPU 時間可以基于線程優先級或者線程等待的時間。線程調度并不受到 Java 虛擬機控制,所以由應用程序來控制它是更好的選擇(也就是說不要讓你的程序依賴于線程的優
先級)。
二十八、自旋
很多 synchronized 里面的代碼只是一些很簡單的代碼,執行時間非常快,此時等待的線程都加鎖可能是一種不太值得的操作,因為線程阻塞涉及到用戶態和內核態切換的問題。既然 synchronized 里面的代碼執行得非常快,不妨讓等待鎖的線程不要被阻塞,而是在 synchronized 的邊界做忙循環,這就是自旋。如果做了多次忙循環發現還沒有獲得鎖,再阻塞,這樣可能是一種更好的策略。
二十九、Java Concurrency API中的Lock接口
Lock 接口比同步方法和同步塊提供了更具擴展性的鎖操作。他們允許更靈活的結構,可以具有完全不同的性質,并且可以支持多個相關類的條件對象。
它的優勢有:
1、可以使鎖更公平
2、可以使線程在等待鎖的時候響應中斷
3、可以讓線程嘗試獲取鎖,并在無法獲取鎖的時候立即返回或者等待一段時間
4、可以在不同的范圍,以不同的順序獲取和釋放鎖
三十、單例模式的線程安全性
1、餓漢式單例模式的寫法:線程安全
2、懶漢式單例模式的寫法:非線程安全
3、雙檢鎖單例模式的寫法:線程安全
三十一、Semaphore作用
是一個信號量,它的作用是限制某段代碼塊的并發數。Semaphore有一個構造函數,可以傳入一個 int 型整數 n,表示某段代碼最多只有 n 個線程可以訪問,如果超出了 n,那么請等待,等到某個線程執行完畢這段代碼塊,下一個線程再進入。由此可以看出如果 Semaphore 構造函數中傳入的 int 型整數 n=1,
相當于變成了一個 synchronized 了。
三十二、Java線程數過多造成異常
1、線程的生命周期開銷非常高
2、消耗過多的 CPU 資源
如果可運行的線程數量多于可用處理器的數量,那么有線程將會被閑置。大量空閑的線程會占用許多內存,給垃圾回收器帶來壓力,而且大量的線程在競爭 CPU資源時還將產生其他性能的開銷。
3、降低穩定性
JVM 在可創建線程的數量上存在一個限制,這個限制值將隨著平臺的不同而不同,并且承受著多個因素制約,包括 JVM 的啟動參數、Thread 構造函數中請求棧的大小,以及底層操作系統對線程的限制等。如果破壞了這些限制,那么可能拋出OutOfMemoryError 異常。