线程并发库和线程池的作用_线程和并发介绍
線程并發庫和線程池的作用
本文是我們名為Java Concurrency Essentials的學院課程的一部分。
在本課程中,您將深入探討并發的魔力。 將向您介紹并發和并發代碼的基礎知識,并學習諸如原子性,同步和線程安全性的概念。 在這里查看 !
目錄
1.有關線程的基本知識 2.創建和啟動線程 3.睡覺和打斷 4.連接線程 5.同步 6.原子訪問1.有關線程的基本知識
并發是程序同時執行多個計算的能力。 這可以通過將計算分布在計算機的可用CPU內核上,甚至在同一網絡內的不同計算機上來實現??。
為了更好地理解并行執行,我們必須區分進程和線程。 進程是操作系統提供的執行環境,它具有自己的一組私有資源(例如,內存,打開的文件等)。 相反, Threads是指生活在一個流程中并與該流程的其他線程共享資源(內存,打開的文件等)的流程。
在不同線程之間共享資源的能力使線程更適合于對性能有重要要求的任務。 盡管可以在同一計算機上甚至在同一網絡內的不同計算機上運行的不同進程之間建立進程間通信,但是出于性能原因,通常會選擇線程來并行化單臺計算機上的計算。
在Java中,進程對應于正在運行的Java虛擬機(JVM),而線程位于同一個JVM中,并且可以由Java應用程序在運行時動態創建和停止。 每個程序至少有一個線程:主線程。 這個主線程是在每個Java應用程序啟動期間創建的,它是調用程序的main()方法的那個線程。 從這一點開始,Java應用程序可以創建新的線程并使用它們。
下面的源代碼對此進行了演示。 JDK類java.lang.Thread的靜態方法currentThread()提供對當前Thread訪問:
public class MainThread {public static void main(String[] args) {long id = Thread.currentThread().getId();String name = Thread.currentThread().getName();int priority = Thread.currentThread().getPriority();State state = Thread.currentThread().getState();String threadGroupName = Thread.currentThread().getThreadGroup().getName();System.out.println("id="+id+"; name="+name+"; priority="+priority+"; state="+state+"; threadGroupName="+threadGroupName);}}從這個簡單應用程序的源代碼中可以看到,我們直接在main()方法中訪問當前Thread ,并打印出有關它的一些信息:
id=1; name=main; priority=5; state=RUNNABLE; threadGroupName=main輸出揭示了有關每個線程的一些有趣信息。 每個線程都有一個標識符,該標識符在JVM中是唯一的。 線程的名稱有助于在監視運行中的JVM的外部應用程序(例如調試器或JConsole工具)中找到某些線程。 當執行多個線程時,優先級決定下一個應該執行的任務。
關于線程的真相是,并非所有線程都真正同時執行,而是將每個CPU內核上的執行時間分成小片,并將下一個時間片分配給具有最高優先級的下一個等待線程。 JVM的調度程序根據線程的優先級確定下一個要執行的線程。
在優先級旁邊,線程還具有狀態,可以是以下狀態之一:
- 新:尚未啟動的線程處于此狀態。
- 可運行:在Java虛擬機中執行的線程處于此狀態。
- BLOCKED:處于等待監視器鎖定狀態的被阻塞線程處于此狀態。
- 等待:無限期等待另一個線程執行特定操作的線程處于此狀態。
- TIMED_WAITING:正在等待另一個線程執行操作的線程最多達到指定的等待時間,該線程處于此狀態。
- 終止:退出的線程處于此狀態。
上面示例中的主線程當然處于RUNNABLE狀態。 像BLOCKED這樣的狀態名稱已經在這里表明線程管理是高級主題。 如果處理不當,線程可能會相互阻塞,進而導致應用程序掛起。 但是我們稍后會談到。
最后但并非最threadGroup是,線程的屬性threadGroup指示線程是按組管理的。 每個線程都屬于一組線程。 JDK類java.lang.ThreadGroup提供了一些方法來處理整個Threads組。 通過這些方法,我們可以例如中斷組中的所有線程或設置其最大優先級。
2.創建和啟動線程
現在,我們已經仔細研究了線程的屬性,是時候創建和啟動我們的第一個線程了。 基本上,有兩種方法可以用Java創建線程。 第一個是編寫一個擴展JDK類java.lang.Thread類:
public class MyThread extends Thread {public MyThread(String name) {super(name);}@Overridepublic void run() {System.out.println("Executing thread "+Thread.currentThread().getName());}public static void main(String[] args) throws InterruptedException {MyThread myThread = new MyThread("myThread");myThread.start();}}從上面可以看到,類MyThread擴展了Thread類并覆蓋了run()方法。 虛擬機啟動線程后,將執行run()方法。 由于虛擬機必須做一些工作才能設置線程的執行環境,因此我們無法直接調用此方法來啟動線程。 相反,我們在類MyThread的實例上調用方法start() 。 當此類從其超類繼承方法stop() ,該方法背后的代碼告訴JVM為線程分配所有必需的資源并啟動該線程。 當我們運行上面的代碼時,我們看到輸出“ Executing thread myThread”。 與我們的介紹示例相反,方法run()的代碼不是在“主”線程中執行的,而是在我們自己的名為“ myThread”的線程中執行的。
創建線程的第二種方法是實現接口Runnable :
public class MyRunnable implements Runnable {public void run() {System.out.println("Executing thread "+Thread.currentThread().getName());}public static void main(String[] args) throws InterruptedException {Thread myThread = new Thread(new MyRunnable(), "myRunnable");myThread.start();}}與子類化方法的主要區別在于,我們創建了java.lang.Thread的實例,并提供了將Runnable接口實現為Thread構造函數的參數的類的實例。 在此實例旁邊,我們還傳遞了Thread的名稱,以便從命令行執行程序時看到以下輸出:“ Executing thread myRunnable”。
是否應使用子類化或接口方法,取決于您的喜好。 該接口是一種更輕便的方法,因為您要做的就是實現接口。 該類仍然可以是某些其他類的子類。 您還可以將自己的參數傳遞給構造函數,而Thread子類將您限制為Thread類帶來的可用構造函數。
在本系列的后面部分,我們將了解線程池,并了解如何啟動相同類型的多個線程。 在這里,我們將再次使用Runnable方法。
3.睡覺和打斷
一旦啟動了Thread ,它將一直運行直到run()方法結束。 在上面的示例中, run()方法所做的只是打印出當前線程的名稱。 因此線程很快完成。
在現實世界的應用程序中,通常必須實現某種后臺處理,在這種處理中,線程必須運行,直到例如已經處理了目錄結構中的所有文件。 另一個常見的用例是有一個后臺線程,如果發生任何事情(例如,已創建文件),則每隔n秒查看一次,并啟動某種操作。 在這種情況下,您將必須等待n秒或毫秒。 您可以使用while循環來實現這一點,該循環的主體獲取當前的毫秒數并查看下一秒的時間。 盡管這樣的實現可行,但是由于您的線程占用了CPU并一次又一次地獲取當前時間,因此浪費了CPU處理時間。
對于此類用例,更好的方法是調用java.lang.Thread類的sleep()方法,如以下示例所示:
public void run() {while(true) {doSomethingUseful();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}sleep()的調用使當前Thread進入睡眠狀態,而不消耗任何處理時間。 這意味著當前線程將從活動線程列表中刪除自己,并且調度程序不會在第二次(以毫秒為單位)過去之前將其調度用于下一次執行。
請注意,傳遞給sleep()方法的時間只是調度程序的指示,而不是絕對準確的時間范圍。 由于實際的調度,線程可能會提前幾納秒或幾毫秒返回。 因此,您不應將此方法用于實時調度。 但是對于大多數使用情況,所達到的精度是足夠的。
在上面的代碼示例中,您可能已經注意到sleep()可能拋出的InterruptedException 。 中斷是線程交互的一個非?;镜墓δ?#xff0c;可以理解為一個線程發送到另一個線程的簡單中斷消息。 接收線程可以通過調用Thread.interrupted()方法來顯式詢問它是否已被中斷,或者在將其時間花在諸如sleep()之類的方法上時會隱式中斷,該方法在發生中斷的情況下會引發異常。
讓我們用下面的代碼示例仔細看一下中斷:
public class InterruptExample implements Runnable {public void run() {try {Thread.sleep(Long.MAX_VALUE);} catch (InterruptedException e) {System.out.println("["+Thread.currentThread().getName()+"] Interrupted by exception!");}while(!Thread.interrupted()) {// do nothing here}System.out.println("["+Thread.currentThread().getName()+"] Interrupted for the second time.");}public static void main(String[] args) throws InterruptedException {Thread myThread = new Thread(new InterruptExample(), "myThread");myThread.start();System.out.println("["+Thread.currentThread().getName()+"] Sleeping in main thread for 5s...");Thread.sleep(5000);System.out.println("["+Thread.currentThread().getName()+"] Interrupting myThread");myThread.interrupt();System.out.println("["+Thread.currentThread().getName()+"] Sleeping in main thread for 5s...");Thread.sleep(5000);System.out.println("["+Thread.currentThread().getName()+"] Interrupting myThread");myThread.interrupt();}}在main方法中,我們首先啟動一個新線程,如果不中斷它將會Hibernate很長時間(大約290.000年)。 為了在這段時間過去之前完成程序,通過在main方法中對其實例變量調用interrupt()來中斷myThread 。 這會在sleep()調用中引起InterruptedException ,并在控制臺上顯示為“ Interrupted by exception!”。 記錄了異常后,線程會進行一些繁忙的等待,直到設置了線程上的中斷標志為止。 通過在線程的實例變量上調用interrupt()再次從主線程進行設置。 總的來說,我們在控制臺上看到以下輸出:
[main] Sleeping in main thread for 5s... [main] Interrupting myThread [main] Sleeping in main thread for 5s... [myThread] Interrupted by exception! [main] Interrupting myThread [myThread] Interrupted for the second time.此輸出中有趣的是第3行和第4行。如果我們遍歷代碼,我們可能期望字符串“ Interrupted by exception!”。 在主線程再次開始Hibernate之前,將打印出“Hibernate5s…”。 但是從輸出中可以看到,調度程序在再次啟動myThread之前已經執行了主線程。 因此,在主線程開始Hibernate之后,myThread打印出接收到的異常。
當使用多個線程進行編程時,這是一個基本觀察結果,即很難預測線程的日志記錄輸出,因為很難計算下一個執行的線程。 當您不得不處理更多的線程(如上例所示)的暫停沒有被硬編碼時,情況變得更加糟糕。 在這些情況下,整個程序會獲得某種內部動力,這使得并發編程成為一項艱巨的任務。
4.連接線程
正如在上一節中所看到的,我們可以讓我們的線程進入睡眠狀態,直到被另一個線程喚醒。 您將不時使用的線程的另一個重要功能是線程等待另一個線程終止的能力。
假設您必須實施某種數字運算,可以將其分為幾個并行運行的線程。 啟動所謂的工作線程的主線程必須等待,直到其所有子線程都終止。 以下代碼顯示了如何實現此目的:
public class JoinExample implements Runnable {private Random rand = new Random(System.currentTimeMillis());public void run() {//simulate some CPU expensive taskfor(int i=0; i<100000000; i++) {rand.nextInt();}System.out.println("["+Thread.currentThread().getName()+"] finished.");}public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[5];for(int i=0; i<threads.length; i++) {threads[i] = new Thread(new JoinExample(), "joinThread-"+i);threads[i].start();}for(int i=0; i<threads.length; i++) {threads[i].join();}System.out.println("["+Thread.currentThread().getName()+"] All threads have finished.");}}在我們的main方法中,我們創建了一個由5個Threads的數組,它們全部一個接一個地啟動。 啟動它們后,我們在主Thread等待其終止。 線程本身通過計算一個隨機數來模擬一些數字運算。 完成后,將打印“完成”。 最后,主線程確認其所有子線程的終止:
[joinThread-4] finished. [joinThread-3] finished. [joinThread-2] finished. [joinThread-1] finished. [joinThread-0] finished. [main] All threads have finished.您會發現,“完成”消息的順序因執行而異。 如果您多次執行該程序,您可能會看到最先完成的線程并不總是相同的。 但是最后一條語句始終是等待其子級的主線程。
5.同步
正如我們在最后一個示例中所看到的,執行所有正在運行的線程的確切順序取決于線程配置,例如優先級還取決于可用的CPU資源以及調度程序選擇下一個線程執行的方式。 盡管調度程序的行為是完全確定的,但是很難預測在給定時間點的哪個時刻哪個線程執行。 這使得對共享資源的訪問變得至關重要,因為很難預測哪個線程將是嘗試訪問它的第一個線程。 通常,對共享資源的訪問是排他性的,這意味著在給定時間點只有一個線程應訪問此資源,而沒有任何其他線程干擾此訪問。
一個并發訪問獨占資源的簡單示例是一個靜態變量,該變量增加一個以上線程:
public class NotSynchronizedCounter implements Runnable {private static int counter = 0;public void run() {while(counter < 10) {System.out.println("["+Thread.currentThread().getName()+"] before: "+counter);counter++;System.out.println("["+Thread.currentThread().getName()+"] after: "+counter);}}public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[5];for(int i=0; i<threads.length; i++) {threads[i] = new Thread(new NotSynchronizedCounter(), "thread-"+i);threads[i].start();}for(int i=0; i<threads.length; i++) {threads[i].join();}}}當我們仔細查看此簡單應用程序的輸出時,我們看到類似以下內容的內容:
[thread-2] before: 8 [thread-2] after: 9 [thread-1] before: 0 [thread-1] after: 10 [thread-2] before: 9 [thread-2] after: 11在這里,線程2將當前值檢索為8,然后將其遞增,然后是9。這就是我們以前期望的值。 但是以下線程執行的內容可能使我們感到驚訝。 線程1將當前值輸出為零,將其遞增,然后為10。這怎么辦? 當線程1讀取變量計數器的值時,該值為0。然后上下文切換執行第二個線程,并且當線程1再次輪到該線程時,其他線程已經將計數器遞增到9。結果是10。
此類問題的解決方案是Java中的同步關鍵字。 使用同步,您可以創建只能由線程訪問的語句塊,該線程獲得了對同步資源的鎖定。 讓我們從上一個示例中更改run()方法,并為整個類引入一個同步塊:
public void run() {while (counter < 10) {synchronized (SynchronizedCounter.class) {System.out.println("[" + Thread.currentThread().getName() + "] before: " + counter);counter++;System.out.println("[" + Thread.currentThread().getName() + "] after: " + counter);}}}synchronized(SynchronizedCounter.class)語句就像一個屏障,在該屏障中,所有線程都必須停止并要求進入。 只有第一個獲得資源鎖的線程才被允許通過。 一旦離開同步塊,另一個等待線程可以進入,依此類推。
在輸出周圍有同步塊且輸出上方計數器遞增的情況下,如下例所示:
[thread-1] before: 11 [thread-1] after: 12 [thread-4] before: 12 [thread-4] after: 13 現在,您將只看到計數器變量加1之前和之后的后續輸出。
可以以兩種不同的方式使用synced關鍵字。 可以在上述方法中使用它。 在這種情況下,您必須提供一個被當前線程鎖定的資源。 必須謹慎選擇此資源,因為基于變量的范圍,線程屏障的含義完全不同。
如果變量是當前類的成員,則所有線程都將與該類的實例同步,因為每個LocalSync實例都存在變量sync:
public class LocalSync {private Integer sync = 0;public void someMethod() {synchronized (sync) {// synchronized on instance level}}}除了創建覆蓋整個方法主體的塊之外,您還可以添加與方法簽名同步的關鍵字。 下面的代碼與上面的代碼具有相同的作用:
public class MethodSync {private Integer sync = 0;public synchronized void someMethod() {// synchronized on instance level}}兩種方法之間的主要區別在于,第一種方法的粒度更細,因為您可以使同步塊比方法主體小。 請記住,同步塊一次只能由一個線程執行,因此每個同步塊都是潛在的性能問題,因為所有并發運行的線程可能必須等待直到當前線程離開該塊。 因此,我們應始終嘗試使塊盡可能小。
大多數情況下,您將不得不同步對每個JVM僅存在一次的某些資源的訪問。 常用的方法是使用類的靜態成員變量:
public class StaticSync {private static Integer sync = 0;public void someMethod() {synchronized (sync) {// synchronized on ClassLoader/JVM level}}}上面的代碼同步在同一JVM中通過方法someMethod()運行的所有線程,因為靜態變量在同一JVM中僅存在一次。 如您所知,一個類只有在由同一類加載器加載的情況下,才在一個JVM中是唯一的。 如果使用多個類加載器加載類StaticSync ,則靜態變量將不止一次存在。 但是在大多數日常應用程序中,您不會有多個類加載器來兩次加載同一類,因此您可以假定靜態變量僅存在一次,因此同一JVM中的所有線程都必須等待障礙,直到它們獲得鎖。
6.原子訪問
在上一節中,我們看到了當許多并發線程必須執行代碼的特定部分但每個時間點僅一個線程應執行該代碼時,如何同步對某些復雜資源的訪問。 我們還看到,如果不同步對公共資源的訪問,則對這些資源的操作會交織并可能導致非法狀態。
Java語言提供了一些原子性的基本操作,因此可用于確保并發線程始終看到相同的值:
- 對引用變量和原始變量(長整型和雙精度型除外)的讀寫操作
- 對所有聲明為易失性的變量的讀寫操作
為了更詳細地了解這一點,我們假設我們有一個HashMap填充了從文件讀取的屬性,以及一堆使用這些屬性的線程。 顯然,我們這里需要某種同步,因為讀取文件和更新Map花費時間,并且在此期間將執行其他線程。
我們無法輕松地在所有線程之間共享此Map一個實例,并且無法在更新過程中使用此Map 。 這將導致Map狀態不一致,該狀態由訪問線程讀取。 有了上一節的知識,我們當然可以在映射的每次訪問(讀/寫)周圍使用一個同步塊,以確保所有線程僅看到一個狀態,而不是部分更新的Map 。 但是,如果必須非常頻繁地從Map讀取并發線程,則會導致性能問題。
為同步塊中的每個線程克隆Map并讓每個線程在單獨的副本上工作也是一種解決方案。 但是每個線程都必須不時請求更新的副本,并且該副本占用內存,這在每種情況下都不可行。 但是有一個更簡單的解決方案。
由于我們知道對引用的寫操作是原子的,因此每次讀取文件并在一個原子操作中更新線程之間共享的引用時,就可以創建一個新的Map 。 在此實現中,工作線程將永遠不會讀取不一致的Map因為使用一個原子操作更新了Map :
import java.text.SimpleDateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map;public class AtomicAssignment implements Runnable {private static volatile Map<String, String> configuration = new HashMap<String, String>();public void run() {for (int i = 0; i < 10000; i++) {Map<String, String> currConfig = configuration;String value1 = currConfig.get("key-1");String value2 = currConfig.get("key-2");String value3 = currConfig.get("key-3");if (!(value1.equals(value2) && value2.equals(value3))) {throw new IllegalStateException("Values are not equal.");}try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}public static void readConfig() {Map<String, String> newConfig = new HashMap<String, String>();Date now = new Date();SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss:SSS");newConfig.put("key-1", sdf.format(now));newConfig.put("key-2", sdf.format(now));newConfig.put("key-3", sdf.format(now));configuration = newConfig;}public static void main(String[] args) throws InterruptedException {readConfig();Thread configThread = new Thread(new Runnable() {public void run() {for (int i = 0; i < 10000; i++) {readConfig();try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}}, "configuration-thread");configThread.start();Thread[] threads = new Thread[5];for (int i = 0; i < threads.length; i++) {threads[i] = new Thread(new AtomicAssignment(), "thread-" + i);threads[i].start();}for (int i = 0; i < threads.length; i++) {threads[i].join();}configThread.join();System.out.println("[" + Thread.currentThread().getName() + "] All threads have finished.");} }上面的例子稍微復雜一點,但并不難理解。 共享的Map是AtomicAssignment的配置變量。 在main()方法中,我們最初讀取配置一次,然后向Map添加三個具有相同值的鍵(此處為當前時間,包括毫秒)。 然后,我們啟動一個“配置線程”,該線程通過將當前時間戳始終添加到地圖的三倍來模擬配置的讀取。 然后,五個工作線程使用配置變量讀取Map并比較三個值。 如果它們不相等,則拋出IllegalStateException。
您可以運行該程序一段時間,并且不會看到任何IllegalStateException 。 這是由于我們通過一次原子操作將新Map分配給共享配置變量的事實:
configuration = newConfig;我們還可以在一個原子步驟中讀取共享變量的值:
Map<String, String> currConfig = configuration;由于這兩個步驟都是原子步驟,因此我們將始終引用所有三個值相等的有效Map實例。 例如,如果更改run()方法的方式是直接使用配置變量,而不是先將其復制到本地變量,則很快就會看到IllegalStateExceptions因為配置變量始終指向“當前”配置。 當配置線程更改了它之后,對Map后續讀取訪問將已經讀取新值,并將它們與舊Map中的值進行比較。
如果直接在配置變量上使用readConfig()方法而不是創建新的Map并通過一次原子操作將其分配給共享變量,則情況也是如此。 但是可能要花一些時間,直到看到第一個IllegalStateException為止。 這對于使用多線程的所有應用程序都是如此。 并發問題乍一看并不總是很明顯,但是它們需要在重負載條件下進行一些測試才能出現。
翻譯自: https://www.javacodegeeks.com/2015/09/introduction-to-threads-and-concurrency.html
線程并發庫和線程池的作用
總結
以上是生活随笔為你收集整理的线程并发库和线程池的作用_线程和并发介绍的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 垂直同步有什么用 垂直同步用处简述
- 下一篇: 猪鼻筋怎么做去腥 猪鼻筋去腥的方法