国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > Java > 正文

java多線程基礎知識

2019-11-06 08:06:21
字體:
來源:轉載
供稿:網友

一、線程的狀態

線程的所有狀態都在Thread類中State枚舉中 NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED

NEW:表示線程剛剛創建,還沒有開始執行RUNNABLE:表示已經創建好的線程,調用start()方法后,并且線程所需要的資源都已準備好BLOCKED:表示正在執行的線程遇到synchronized同步快,就會進入BLOCKED狀態,直到獲得請求的鎖WAITING,TIMED_WAITING:這兩個狀態都是表示等待區別是WAITING進入一個無時間限制的等待,TIMED_WAITING進入一個有時間限制的等待TERMINATED:表示線程執行完畢后,則進入此狀態

二、線程的基本操作

1. 新建線程 新建線程有兩種方法一個是繼承Thread類,另一個是實現Runnable接口

Thread thread = new Thread(){ @Override public void run() { // doSomthing } }; thread.start();
class A implements Runnable{ @Override public void run() { // doSomthing }}

繼承Thread類或者實現Runnable接口后,重寫run方法,然后調用start()方法,如果直接調用run()方法,就是普通的方法調用并不是開始一個新線程。

2. 終止線程

一般情況下,線程在執行完畢后都會正常結束,無須手動終止線程。但是,凡事也都有例外。想要正常關閉一個線程,JDK提供了stop()方法,但是已經被標記為了廢棄。原因就是因為,stop()方法太過于暴力,線程在執行到一半的時候強制關閉,可能就會導致數據不一致性。比如說,剛好這個線程在執行同步塊的代碼時,被強制關閉了,那么就會造成數據的不完整行。 怎么實現優雅的stop呢,其實可以自己加個boolean類型的變量,每次進入run方法時先判斷是否為true,想要終止時,把boolean的值設置為false,這樣可以保證不會在線程執行一半時,強制終止。

3. 線程中斷

這里說的線程中斷,并不是線程終止,嚴格的講,線程中斷并不會讓線程立即退出,而是發一個通知,告知目標線程,有人希望你退出了,但是目標線程接到通知后如何處理,則是完全由目標線程決定,這點很重要,如果中斷后線程立即無條件退出,那么跟stop()方法又會是相同的問題。

與線程中斷相關的有三個方法,長的看起來差不對,容易混肴

public void interrupt() //中斷線程 public boolean isInterrupted() //判斷是否被中斷 public static boolean interrupted() //判斷是否被中斷,并清除當前的中斷狀態interrupt():通知目標線程中斷,也就是設置中斷位。中斷標志位表示當前線程已經被中斷。isInterrupted():判斷當前線程是否有被中斷interrupted():判斷當前線程的中斷狀態,當同時會清除當前中斷標志位狀態。

4. 等待(wait)和通知(notify)

為了支持多線程之間的寫作,JDK提供了兩個非常重要的接口,等待wait()和通知notify()方法。這兩個方法不再Thread而是在Object類,這意味著任何對象都可以調用這兩個方法

public final void wait() throws InterruptedExceptionpublic final native void notify();

當在一個對象實例上調用Object.wait()方法后,當前線程就會在這個對象上等待,狀態:RUNNABLE → WAITING,一直等到其他線程調用了Object.notify()方法為止,狀態:WAITING → RUNNABLE wait()和notify()的工作過程,如果一個線程調用了Object.wait(),那么該線程就會進入object對象的等待隊列中。這個隊列中可能會有多個線程,因為系統可能同時運行多個線程都在等待這個對象。當Object.notify()被調用時,他就會從這個隊列中隨機選擇一個線程,并將其喚醒,這里是隨機的,并不是先等待先被喚醒。 除了notify()方法外,Object還有一個notifyAll()方法,他和notify()方法的基本功能一致,但不同的是,他會喚醒在這個等待隊列中所有的線程,而不是隨機選擇一個。 Object.wait()方法并不是想調用就能調用的,他必須在synchronized塊中調用,例如:

synchronized (object) { try { object.wait(); } catch (InterruptedException e) { e.PRintStackTrace(); }}

無論是wait()方法還是notify()方法,都需要首先獲得一個目標對象的監視器,如圖

這里寫圖片描述

T1和T2表示2個線程,T1在正確執行方法前,首先獲得一個object監視器,而wait()方法執行后,會釋放掉這個監視器。這樣做的目的是使得其他等待object對象的線程不至于,因為T1休眠而全部無法正常運行。notify()方法也是一樣的

Object.wait()和Thread.sleep()方法都可以讓線程等待若干時間,除了wait()可以被喚醒外,另一個主要區別就是wait()方法會釋放目標對象的鎖,而Thread.sleep()方法不會釋放人資源。

5. 掛起(suspend)和繼續執行(resume)線程 線程掛起(suspend)和繼續執行(resume)是一對相反的操作,被掛起的線程必須要等到resume()之后才能繼續。在JDK中,這個方法跟Thread.stop()一樣,被標記為了廢棄,并不推薦使用。原因就是因為suspend()在導致線程暫停的同時,并不會釋放任何資源鎖,這樣就導致,其他想要獲取這個鎖的線程,都要等著,直到這個線程被resume()釋放掉。但是,如果resume()操作意外的在suspend()之前執行了,那么被掛起的線程可能很難有機會被繼續執行,它所占用的鎖也不會得到釋放,造成死鎖。而且被掛起的線程,從他的線程狀態上來看還是Runnable。舉個例子

package com.example.thread;/** * Created by mazhenhua on 2017/3/6. */public class BadSuspend { public static Object obj = new Object(); static ChangObjectThread t1 = new ChangObjectThread("t1"); static ChangObjectThread t2 = new ChangObjectThread("t2"); public static class ChangObjectThread extends Thread{ public ChangObjectThread(String name) { super.setName(name); } @Override public void run() { synchronized (obj){ System.out.println("in " + getName()); Thread.currentThread().suspend(); } } } public static void main(String[] args) throws Exception { t1.start(); Thread.sleep(100); t2.start(); t1.resume(); t2.resume(); t1.join(); t2.join(); }}

執行結果:

這里寫圖片描述

你會發現t1,t1都被輸出出來了,但是左側紅色按鈕還是亮起的狀態,說明程序還沒有結束,正常結束應該一個綠色的三角,這說明t2在輸出線程名字之后被掛起來了。那t1為什么沒有掛起了,t1如果掛起來,就會把鎖占用掉,就不會打印出t2了,你可以把Thread.sleep(100); 這一行注釋掉,就只會打印t1。正是因為main線程中間休息了100毫秒,才讓t1的線程先掛起,再釋放。t2就沒有這么幸運了,t2和main線程一起跑的中間沒有停留,就導致t2.resume(); 跑在了t2線程掛起之前,所以線程就一直掛起沒有釋放。 怎么辦呢,既然不讓用,那我們就不用,自己寫一個,利用wait(),和notify(),自己寫個類似的功能,代碼:

package com.example.thread;/** * Created by mazhenhua on 2017/3/6. */public class GoodSuspend { public static Object object = new Object(); public static class ChangeObjectThread extends Thread { volatile boolean suspendme = false; public void suspendMe(){ suspendme = true; } public void resumeMe(){ suspendme = false; synchronized (object){ object.notify(); } } @Override public void run() { while (true){ synchronized (object){ while (suspendme){ try { object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } synchronized (object){ System.out.println("in ChangeObjectThread"); } Thread.yield(); } } } public static void main(String[] args) throws InterruptedException { ChangeObjectThread t1 = new ChangeObjectThread(); t1.start(); Thread.sleep(1000); t1.suspendMe(); System.out.println("suspend t1 2 sec"); Thread.sleep(2000); System.out.println("resume t1"); //t1.resumeMe(); }}

運行結果:

這里寫圖片描述

這里是把t1.resumeMe();注釋掉的結果,不注釋掉,被釋放了,會是個死循環。 事實證明,問題總會被解決的,沒有這條路不通,換條路走。

6. 等待線程結束(join)和謙讓(yield) 很多情況下,線程之前的協作和人與人之前是類似的。比如流水線的工人,前一個人裝上了主板,下一個人在裝內存條。裝內存條的說,我現在太忙了,你先裝硬盤吧,裝好硬盤,我再裝內存條。 這個關系,對應到多線程應用中,很多時候,一個線程的輸入依賴于另一個線程的輸出,此時,這個線程就需要等到另一個線程結束,才能繼續運行,JDK提供了join()方法來操作這個功能

public final void join() throws InterruptedExceptionpublic final synchronized void join(long millis)throws InterruptedException

不帶參數的,會一直阻塞當前線程,直到目標線程結束。帶參數的是給個最大等待時間,超過時間就不等了直接往下走。

public static native void yield();

這個靜態方法,一旦執行,就會讓當前線程退出cpu。但是,并不是說當前線程就不執行了,退出CPU后,還會再次加入CPU的資源爭奪中,但是能否再次被分配到就不一定了。因此對于Thread.yield()的調用就好像是在說:我已經完成了前面重要的工作,我應該休息一下,可以給其他線程一些工作機會啦。 如果你覺得一個線程不是那么重要,或者優先級非常滴,但是又擔心他占用太多CPU資源,可以在適當的時候,調用yield(),給予其他線程更多的機會。

7. volatile與java內存模型(JMM) JAVA內存模型都是圍繞著原子性,有序性和可見性展開的。為了在適當的場合下,確保線程之間的有序性,可見性,原子性。java使用了一些特殊的操作或者關鍵字來申明,告訴虛擬機,在這個地方,要尤其注意不能隨意變動優化目標的指令。關鍵字volatile就是其中之一。 volatile的英文翻譯是“易變的,不穩定的”這正是使用volatile關鍵字的語義 當用volatile去聲明一個變量時,就等于告訴了虛擬機,這個變量極有可能會被某些程序或線程修改,為了保證這個變量被修改后,應用程序范圍內的所有線程多能看到這個改動,虛擬機就必須采用一些特殊的手段,保證這個變量的可見性,等特點。 volatile關鍵字只保證可見性,并不保證同步,想要保證同步還是要加synchronized,下面舉個反例:

package com.example.thread;/** * Created by mazhenhua on 2017/3/6. */public class VolatileTest { static volatile int i = 0; public static class PlusTask implements Runnable{ @Override public void run() { for (int k =0; k < 10000; k++){ i ++; } } } public static void main(String[] args) throws InterruptedException { Thread[] threads = new Thread[10]; for (int i = 0; i < 10; i++){ threads[i] = new Thread(new PlusTask()); threads[i].start(); } for (int i = 0; i < 10; i++){ threads[i].join(); } System.out.println(i); }}

執行結果,總是一個低于10萬的數字。

8. 線程組 在一個系統中,如果線程數量很多,而且分工比較明確,就可以將相同功能的線程放置在一個線程組中。打個比方,你有一個蘋果你可以拿在手里,如果你有10個蘋果,你就需要找個籃子裝起來提著,線程組就是這個籃子。代碼:

package com.example.thread;/** * Created by mazhenhua on 2017/3/7. */public class ThreadGroupTest implements Runnable { @Override public void run() { String groupName = Thread.currentThread().getThreadGroup().getName() + "-" + Thread.currentThread().getName(); while (true){ System.out.println("I am " + groupName); try { Thread.sleep(3000); } catch (InterruptedException e){ e.printStackTrace(); } } } public static void main(String[] args) { ThreadGroup tg = new ThreadGroup("PrintGroup"); Thread t1 = new Thread(tg, new ThreadGroupTest(), "T1"); Thread t2 = new Thread(tg, new ThreadGroupTest(), "T2"); t1.start(); t2.start(); System.out.println(tg.activeCount()); System.out.println(); tg.list(); }}activeCount()方法可以獲得,活動線程的總數,但是由于線程是動態的,所以這個值只是個估計值,tg.list() 可以打印線程組中,所有線程的信息,對調試代碼有幫助 線程組也提供了stop()方法用來停止線程組中所有的線程,但是也會遇到和Thread.stop()一樣的問題,因此使用起來要格外謹慎。

對于編碼習慣而言,強烈建議大家在創建線程和線程組的時候,起一個有意義的名字,對于計算機來所,起什么名字無所謂,但是在系統出問題的時候,你很有可能導出所有的線程,如果你拿到的是一連串的Thread-0,Thread-1,Thread-2.。。。。。我想你一定會抓狂的。如果你拿到的是HttpHandler或者FTPService這樣的名字,你一定會心情倍爽,查問題的效率也會提高。

9. 駐守后臺,守護線程(Deamon)

守護線程是一種特殊的線程,就和他的名字一樣,是系統的守護者,在后臺默默地完成一些系統性的服務,比如垃圾回收線程,JIT線程就可以理解為守護線程。與之相對應的是用戶線程,用戶線程可以認為是系統的工作線程,他會完成這個程序應該完成的操作。如果一個java應用內,只有守護線程時,java虛擬機就會自然退出,因為已經沒有需要守護的東西了。代碼:

package com.example.thread;/** * Created by mazhenhua on 2017/3/7. */public class Deamon { public static class DeamonT extends Thread { @Override public void run() { while (true){ System.out.println("I am alive"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread t = new DeamonT(); t.setDaemon(true); t.start(); Thread.sleep(2000); }}

t.setDaemon(true); 即把線程,設置為守護線程,一定要在start線程之前設置為守護線程。這里只有一個main線程,所以在守護了2秒之后,main線程結束,接著守護線程結束。

10. 線程優先級

java中的線程可以有自己的優先級。優先級高的線程在競爭資源時會更有優勢,更可能搶占資源,當然,這只是一個概率問題。線程的優先級調度和底層操作系統有關系,在各個平臺表現不一樣,而且這種資源搶占的結果也不好預測,可能會導致,優先級低的始終搶不到資源。因此,在要求嚴格的場合,還是需要自己在應用層解決線程調度問題。看個例子:

package com.example.thread;/** * Created by mazhenhua on 2017/3/7. */public class PriorityDemo { public static final int MIN_PRIORITY = 1; public static final int NORM_PRIORITY = 5; public static final int MAX_PRIORITY = 10; public static class HightPriority extends Thread { static int count = 0; @Override public void run() { while (true){ synchronized (PriorityDemo.class){ count ++; if (count > 10000000) { System.out.println("HightPriority is complete"); break; } } } } } public static class LowPriority extends Thread { static int count = 0; @Override public void run() { while (true){ synchronized (PriorityDemo.class){ count ++ ; if (count > 10000000) { System.out.println("LowPriority is complete"); break; } } } } } public static void main(String[] args) throws InterruptedException { Thread high = new HightPriority(); LowPriority low = new LowPriority(); high.setPriority(Thread.MAX_PRIORITY); low.setPriority(Thread.MIN_PRIORITY); low.start(); high.start(); }}

代碼中,在每次count累加時,都加了個synchronized來獲取一次競爭,經過幾次嘗試HightPriority總比LowPriority跑的快,但是不保證,在所有情況下,都會是這樣。

11. 線程安全的概念與synchronized

并行程序開發的一大關注重點就是線程安全。一般來說,程序并行化,是為了獲得更高的執行效率,但前提是,高效率不能以犧牲正確性為代價。如果程序并行化后,連最基本的執行結果的正確性都無法保證,那么并行程序本身也就沒有任何意義了。因此線程安全是并行程序的跟本。 下面的代碼顯示了一個計數器,兩個線程同時對i進行累加操作,各執行10000000次,我們希望執行結果i的值是20000000,

package com.example.thread;/** * Created by mazhenhua on 2017/3/7. */public class Sync implements Runnable { static Sync instance = new Sync(); static volatile int i = 0; public static void increase(){ i ++; } @Override public void run() { for (int j = 0; j<10000000; j ++){ increase(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(instance); Thread t2 = new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); }}

但是事實上,結果總是小于我們的期望值,原因就是2個線程在同時對i進行累加的時候,會出現覆蓋,的情況,比如兩個線程同時拿到i的值為5,這是線程t1,累加得到結果為6,線程t2,累加得到結果6,然后同時又在賦值給i時,結果就是6,其實兩個線程各累加了一次。應該是7才對。 要從根本上解決這個問題,我們就必須要保證,多個線程對i進行累加時時同步的,也就是在t1線程進行寫入時,t2不能寫也不能讀,等t1寫完成,t2線程再開始讀并寫。如果線程t2再t1寫之前,就已經讀取了,那就會出現上面的情況,讀取一個已經過期的數據。java中,提供了一個重要的關鍵字synchronized來解決這個問題。

關鍵字synchronized的作用是實現線程之間的同步。他的工作室對同步代碼加鎖,使得每一次只有一個線程進入同步塊,從而保證線程之間的安全性。 關鍵字synchronized有多種用法,這里做一個簡單的整理 - 指定加鎖對象:對給定的對象加鎖,進入同步塊之前獲得對象鎖。 - 直接作用于實例方法:相當于對當前實例加鎖,進入同步代碼塊之前,要獲得當前實例的鎖 - 直接作用于靜態方法:相當于對當前的類加鎖,進入同步代碼塊之前要獲得當前類的鎖

改寫一下上面的錯誤例子

public static synchronized void increase(){ i ++; }

在上面這個方法上,加上synchronized 關鍵字這樣,每次只能一個線程處理完,另一個線程才能進入。 有個錯誤的示例:

package com.example.thread;/** * Created by mazhenhua on 2017/3/7. */public class Sync implements Runnable { /* static Sync instance = new Sync();*/ static volatile int i = 0; public synchronized void increase(){ i ++; } @Override public void run() { for (int j = 0; j<10000000; j ++){ increase(); } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new Sync()); Thread t2 = new Thread(new Sync()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); }}

問題就在與,加synchronized 關鍵字的方法不是static,也就不是作用在類上的,而且,他們倆跑的對象還不是一個,main方法中new出來了兩個對象。

三、線程中的幽靈:隱蔽的錯誤

作為一名軟件開發人員,修復程序bug應該說是基本的日常工作之一。最可怕的情況是,系統沒有任何異常表現,沒有日志,也沒有任何錯誤,卻給出了一個錯誤的結果,就想上面累加小于20000000的情況,這種情況,才真的會讓人抓狂

1. 并發下的ArrayList

我們都知道ArrayList并不是一個線程安全的容器,如果在多線程中使用ArrayList,可能會導致程序出錯,請看如下代碼:

package com.example.thread;import java.util.ArrayList;/** * Created by mazhenhua on 2017/3/7. */public class ArrayListTest { static ArrayList<Integer> al = new ArrayList<Integer>(10); public static class AddThread implements Runnable{ @Override public void run() { for (int i = 0; i < 1000000; i++){ al.add(i); } } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new AddThread()); Thread t2 = new Thread(new AddThread()); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(al.size()); }}

下面是我執行的結果:

這里寫圖片描述

這里寫圖片描述

執行上面的代碼實際上應該會出現三種情況(我跑了多次只出來這兩種情況) 第一,程序正常結束,ArrayList的最終大小確實是2000000,這說明程序中有問題,但是并不是每次都能表現出來。 第二,程序拋異常,就想上面第一次的結果,這是因為ArrayList在擴容的過程中,內部一致性被破壞了,另一個線程訪問到了不一致的內部狀態,導致出現越界問題。 第三,出現了一個非常隱蔽的錯誤,就像上面的第二個結果,打印出來的值小于預期的值。

顯然,這是由于多線程訪問沖突,是的保存容器大小的變量,被多線程不正常的訪問,同時,兩個線程也同時對ArrayList中的同一個位置進行賦值,導致的。 改進方法很簡單,使用線程安全的Vector代替ArrayList即可。

2. 并發下詭異的HashMap

HashMap同樣不是線程安全的。當你使用多線程訪問HashMap時,也可能會遇到想不到的錯誤。不過和ArrayList不同,HashMap的問題似乎更加詭異。以下代碼可能會占用2個cpu,我的是四核電腦,CPU使用率在60%左右, 這里寫圖片描述

package com.example.thread;import java.util.HashMap;import java.util.Map;/** * Created by mazhenhua on 2017/3/7. */public class HashMapTest { static Map<String, String> map = new HashMap<String, String>(); public static class AddThread implements Runnable{ int start = 0; public AddThread(int start) { this.start = start; } @Override public void run() { for (int i = start; i < 100000; i+=2){ map.put(Integer.toString(i),Integer.toBinaryString(i)); } } } public static void main(String[] args) throws InterruptedException{ Thread t1 = new Thread(new AddThread(0)); Thread t2 = new Thread(new AddThread(1)); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(map.size()); }}

上述代碼使用線程t1,t2兩個線程同時對HashMap進行put操作。如果一切正常,我們期望,得到map.size()就是100000,但實際上,你可能會得到如下三種請看(注意是在JDK1.7的情況下,因為JDK1.8對HashMap內部做了很大的重構) 第一,程序正常結束,并且結果也符合預期, 第二,程序正常結束,不符合預期,而是一個小于100000的數字 第三,程序永遠無法結束,就像上面的截圖。

前兩種情況都好解釋,跟ArrayList一樣,第三種情況是,多線程破壞了HashMap的內部結構,鏈表成了環,一直在迭代列表,就成死循環。最好的方法,就是在多線程的時候用ConcurrentHashMap替換掉。HashMap這個問題,在JDK1.8中已經被修改了。

完 結 。。。。


發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 湟中县| 海晏县| 山西省| 阳信县| 洞头县| 车致| 鄂托克前旗| 锦屏县| 南安市| 民勤县| 台湾省| 格尔木市| 天津市| 辽阳市| 土默特左旗| 余干县| 义马市| 遂昌县| 桃园市| 望江县| 南丹县| 五寨县| 武冈市| 丰县| 仪陇县| 朝阳县| 东山县| 井冈山市| 昭苏县| 葫芦岛市| 和林格尔县| 商河县| 原阳县| 铜梁县| 兴文县| 富裕县| 日喀则市| 西华县| 自贡市| 密山市| 县级市|