【Java從入門到頭禿專欄 6】語法篇(五) :多線程 線程池 可見、原子性 并發包 Lambda表達式
目錄
1 多線程
1.1 基本概念
1.2 創建線程的三種方式
1.4 解決線程安全問題的三種方法
1.5 線程通信
1.6 線程狀態
2 線程池
2.1線程池的概念
2.2 創建并提交任務
3 可見性
3.1 變量不可見性
3.2 變量不可見性的解決方案
4 原子性
4.1 原子性的概念
4.2 保證原子性的方案
4.3 原子類的CAS機制
5 多線程的并發包
5.1 ConcurrentHashMap類
5.2 CountDownLatch類
5.3 CyclicBarrier類
5.4 Semaphore類
5.5? Exchanger類
1 多線程
1.1 基本概念
程序(program):為了完成特定的任務,使用某種語言編寫的一組指令的集合,也就是一段靜態的代碼。
進程(process):程序加載到內存中的一次執行過程,或者是正在運行中的一個程序。進程作為
資源分配的單位
,系統在運行時會為每個進程分配不同的內存區域。
線程(thread):一個進程可被進一步細化成一個或多個線程,線程就是一個程序內部的一條執行路徑。如果一個程序可以同時并行執行多個線程,我們就稱它是支持多線程的。線程作為
調度和執行的單位
,每個線程都擁有獨立的運行棧和程序計數器,所有的線程共享進程分配的堆和方法區,都從同一個堆中分配對象訪問相同的變量和對象。這就是的線程之間的通信更加簡便、高效,但是由于共享系統資源也就帶來了安全隱患。
單CPU與并發:CPU相當于人的大腦,用來動態的為程序的運行分配內存空間,之所以是動態的是因為一個CPU一次只能執行一個進程,但是同一時間一臺電腦幾乎不可能只開啟一個進程,一個CPU會不停的切換執行多個進程也就是并發執行,由于切換的速度比較快在人類看來計算機就是在同時執行多個進程。
多CPU與并行:多CPU是相對于單CPU而言的概念,多CPU就是多個CPU同時執行不同的進程也就是并行執行,與此同時每個CPU還會不停的切換執行多個進程也就是并發執行。
并發與并行:舉個例子,比如說今年暑假的抗洪救災現場,需要將裝成袋的沙子搬到決堤口擋水,并發:這里有20袋沙子(相當于20個進程),但是只有一個人來搬(單CPU),這個人搬完一袋換一袋由于換的速度比較快,看起來就好像是20袋沙子一塊被搬一樣。并發和并行同步執行:這里有20袋沙子(相當于20個進程),但是有四個人來搬(多CPU),四個人同時搬就是并行,這四個人搬完各自的一袋換一袋由于換的速度比較快看起來也好像是20袋沙子一塊被搬一樣,這里的每個人搬完換另一袋就叫并發。于是大部分情況下的單CPU的性能要優于多CPU的。
一個java應用程序java.exe至少應該包三個線程:main()主線程、gc()垃圾回收線程、異常處理線程。
1.2 創建線程的三種方式
方法一:繼承Thread類
四步:創建類并繼承Thread-->重寫run方法-->創建線程對象-->調用start方法
?? 創建線程對象調用start方法才會產生新的線程(start方法底層會先向CPU注冊線程,在調用run方法),如果調用run方法會被當做是一個普通類執行,這樣進程里面也就還只有一個主線程。
?? main方法里面要先創建子線程出來再分配主線程的任務,否則在進程執行的時候會認為只有一個主線程,因為從代碼的執行順序來看此時還沒有創建子線程,從而會導致永遠都是先執行完主線程任務再執行子線程任務。
這樣創建線程的優點是編碼簡單,缺點是通過繼承Thread類創建線程會導致線程類無法在對其他類進行繼承,功能無法通過繼承來拓展(單繼承的局限性)
編輯
Thread的常用API
編輯
方法二:實現Runnable接口
五步:創建任務類并實現Runnable接口-->重寫run方法-->創建任務對象-->將任務對象包裝成線程對象-->調用start方法
這個方法創建線程的缺點:比上一種方法多了一步,下一個方法可以獲取重新寫call方法的返回結果而這個的run方法沒有返回值。優點有:由于任務類沒有繼承任何類,可以繼續繼承其他類拓展功能;同一個任務類可以被包裝成多個線程對象;適合多個線程共享同一個資源;實現解耦操作,任務可以被多個線程共享,任務與任務之間有相互獨立不影響 編輯
創建線程的簡化寫法(匿名內部類)
編輯
方法三:實現Callable接口
六步:創建任務類并實現Callable接口-->重寫call方法-->創建任務對象-->將任務對象包裝成FutureTask對象-->將FutureTask對象包裝成線程對象-->調用start方法
第三種方法和第二種方法的差別就是這個方法可以獲取返回值
編輯
1.3 線程安全問題
當多個線程操作同一個共享資源的時候就有可能會出現線程安全問題。比如說,小明和小紅有一個共同情侶賬戶里面有100塊錢,小明和小紅同時登錄系統取錢,會出現以下情況:
由于線程的執行時隨機且無法回退的,所以可能會導致兩人都查詢賬戶余額有100塊的情況,線程繼續往后執行就會導致賬戶被兩次取錢成為負值,這肯定是有問題的。
賬戶bean類:
編輯
主類:
編輯
取錢任務類:
編輯
控制臺運行結果:
編輯
1.4 解決線程安全問題的三種方法
方法一:同步代碼塊
synchronized(鎖對象) {
訪問共享資源的核心代碼;
}
?? 在實例方法中建議使用this作為鎖對象,靜態方法中建議使用類名.class作為鎖對象
編輯
方法二:同步方法
在方法的定義時使用synchronized修飾即可
同步方法與同步代碼塊的方法差不多,同步方法的底層是將整個方法都鎖了起來
編輯
方法三:Lock顯式鎖
創建鎖對象: 編輯
上鎖: 編輯
解鎖: 編輯
編輯?使用該方法上鎖的話,盡量要按照這種try-catch-finally的方式,否則可能遇到上鎖之后出現異常,此時程序就無法繼續運行,也就是說永遠無法解鎖導致出現問題。
1.5 線程通信
現在有這么一個需求
編輯
使用IDEA進行實現代碼:
賬戶bean類:
編輯
主類:
編輯
取錢任務類:
編輯存錢任務類: 編輯
控制臺運行結果:
這是個死循環運行了一會就暫停了截圖
編輯
1.6 線程狀態
?? sleep方法只是計時等待,不會把鎖放開;wait方法是把鎖放開進入等待。
編輯
死鎖:
死鎖就是不同的線程同時分別占用著對方需要的鎖不放,都在等待著對方放鎖。出現死鎖之后不會產生任何的異常和提示,只是所有的線程都處于阻塞狀態無法繼續。
編輯
死鎖產生的四個必要條件:
互斥使用:即共享資源一次只能被一個線程使用
不可搶占:即線程不能從正在使用共享資源的線程手中奪取資源
請求保持:一個線程在請求另一個線程資源的同時依然占有著那個線程所請求的資源
循環等待:1要2的資源,2要1的資源,形成了一個循環等待
2 線程池
2.1線程池的概念
前面講過,每當我們需要使用線程的時候,不管是使用哪個方法都需要去創建一個線程,實現起來并不難但是會產生一個問題:如果并發的線程數量很多且線程的執行時間都很短的時候,線程的創建和銷毀都需要時間,頻繁的創建銷毀線程就會導致系統的效率大大降低。
解決以上問題就用到了線程池的概念,線程池就是一個可以容納固定多個線程的容器,線程池中的線程可以反復使用。線程池中工作線程(PoolWorker)的個數是固定的,而任務接口(Task)想要使用工作線程的話就需要在任務隊列(TaskQueue)中排隊等待,任務執行完畢之后工作線程歸還線程池出于空閑狀態。
2.2 創建并提交任務
創建線程池并指定線程數量: 編輯
無返回值的Runnable任務:
編輯
有返回值的Callable任務:
編輯
?? 線程池對象調用submit(任務對象)方法將任務對象提交給線程池執行,線程池在執行完所有的任務之后并不會直接關閉,而是處于等待狀態等待其他任務的使用,如果沒有其他任務就一直處于等待狀態,可以調用shutdown()方法等待任務執行完畢之后關閉線程池。
3 可見性
3.1 變量不可見性
首先,Java專門為多線程定義了一種Java內存模型(Java Memory Model JMM),這種內存模型要不同于單線程的內存模型JVM。JMM描述了Java程序中各種共享變量的訪問規則,以及在JVM中將變零存儲在內存中和從內存中讀取像變量的底層細節。
JMM的規定:
所有的共享變量都存儲于主內存。這里的變量指的是實例變量和類變量,并不包含局部變量,因為局部變量是線程私有的不存在競爭問題。
每個線程有自己的工作內存,里面存放的是從主內存中拷貝來的共享變量副本。
線程對變量的所有操作都在線程的工作內存中完成,而不是直接操作主內存中的共享變量。
不同線程之間也不能訪問對方的工作內存,線程間變量的值傳遞通過主內存中轉完成。
編輯
不可見性描述:
并發編程下,也就是說當存在多個線程訪問一個共享資源時,一個線程改變了這個資源的變量值,但是其他線程并不能看到這個變量值的改變,讀取到的依然是變量修改之前的值。以上現象又被稱為是多線程間變量的不可見性
變量不可見性的原理:
編輯
3.2 變量不可見性的解決方案
方案一:加鎖
對線程任務進行加鎖。其底層原理在于:線程在獲得鎖對象之后會清空線程的工作內存,從主內存中再次拷貝共享變量的值成為共享變量副本,此時線程工作內存中共享變量副本就是最新的變量值了。
方案二:volatile關鍵字修飾
定義變量的時候使用volatile關鍵字進行修飾。其底層原理與加鎖不同的是:volatile關鍵字是在主內存發現有線程對共享變量的值進行修改之后,通知其他線程工作內存中的共享變量副本的值失效,其他線程在訪問共享變量的副本的時候發現值已失效,于是重新拷貝共享變量至工作內存中。
4 原子性
4.1 原子性的概念
原子性指的是:一批操作是一個整體,要么同時成功要么同時失敗,不能被其他干擾。volatile只能保證線程之間變量的可見性,但是不能保證變量操作的原子性。
4.2 保證原子性的方案
方案一:加鎖
加鎖就是對線程任務進行加鎖。加鎖不僅能夠保證線程的原子性,還能保證線程之間變量值修改的可見性。但是加鎖會降低程序的性能,故又有了第二種方法。
方案二:原子類
Java提供了java.util.concurrent.atomic包(簡稱atomic包),包里面有各種類類中有很多方法。
編輯
原子類包含有很多種,其中包括AtomicInteger、AtomicDouble……對不同數據類型的數據進行操作更新的類,這些類中定義了一些API去代替運算,與普通運算方式的區別在于,這種方法的運算能在不加鎖的情況下保證線程的原子性。
原子類的使用:
原子類中定義了很多的API根據自己的需求選擇使用
編輯
從上圖中可以看出來,一個線程執行完所有的任務下一線程再執行,這種模式與前面的上鎖很像,其實原子類就是加鎖機制的高性能版本,在實現加鎖機制保證線程安全的同時又保證了原子性。
4.3 原子類的CAS機制
CAS的全稱為Compare And Swap譯為先比較再交換,CAS可以將read-modify-check-write操作轉換為原子操作,保證了線程的原子性。CAS機制不鎖任務,任意線程的任何時候都可以操作任務,就是操作完任務之后要將操作前的共享變量副本與主內存中的共享變量值進行對比,一致的話就修改主內存中共享變量的值,不一致的話就將之前的任務操作作廢,重新開啟一次任務(拷貝、修改、對比)。
編輯
5 多線程的并發包
5.1 ConcurrentHashMap類
java.util.concurrent.ConcurrentHashMap
在創建HashMap集合的時候使用即可,創建之后即可保證線程安全,類下面的API操作和HashMap一樣,正常使用即可。
編輯
5.2 CountDownLatch類
java.util.concurrent.CountDownLatch
創建一個計數器,用于實現線程執行時的計數等待,使用有參構造創建對象的同時給定計數步數,也就是說等待計數器減幾次,await()方法讓當前線程讓出CPU等待計數器的值清零,countDown()方法可以將計數器的值減1
編輯
5.3 CyclicBarrier類
java.util.concurrent.CyclicBarrier
創建一個循環屏障對象,傳入兩個參數阻擋線程個數和一個Runnable任務對象,意思就是屏障阻擋了相應的線程個數之后就執行這個Runnable任務
編輯
5.4 Semaphore類
java.util.concurrent.Semaphore
Semaphore對象的主要作用就是控制線程并發的數量,也就是說使用有參構造創建一個Semaphore對象設置最大允許進入acquire()方法和release()方法之間任務代碼的線程個數。可以用來限制一個資源同一時間的的最大訪問人數,使用synchronized上鎖相當于創建Semaphore對象的時候傳參為1。
編輯
5.5? Exchanger類
java.util.concurrent.Exchanger
Exchanger類適用于線程間協作通信的類,利用構造器定義一個Exchanger對象容器使用泛型規定容器暫存數據的類型,可以是無參構造器也可以是有參構造器,兩個參數超時不再交換時間和超時時間單位,調用exchange(V x)方法進行數據交換并返回交換之后對方傳過來的結果。
Java 任務調度 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。