1、線程與進程安全
線程:一條線程指的是進程中一個單一順序的控制流,一個進程中能夠併發多個線程,每條線程並行執行不一樣的任務。多線程是多任務的一種特別形式,但多線程使用了更小的資源開銷。多線程
進程:一個進程包括有操做系統分配的內存空間,包含一個或多個線程。一個線程不能獨立的存在。它必須是進程的一部分。一個進程一直運行直到全部的非守護線程都結束運行後才能結束。併發
進程 | 線程 | |
定義 | 進程是指處於運行中的程序,而且具備必定的獨立功能。進程是系統進行資源分配和調度的一個單位。異步 當程序進入內存時,即爲進程。socket |
線程是進程的組成部分,一個進程能夠擁有多個線程,而一個線程必須擁有一個父進程。函數 線程能夠擁有本身的堆棧,本身的程序計數器和局部變量,但不能擁有系統資源。this 它與父進程的其餘線程共享該進城的全部資源。spa |
特色 | 1)獨立性:進程是系統中獨立存在的實體,它能夠獨立擁有資源,每個進程都有本身獨立的地址空間,操作系統 沒有進程自己的運行,用戶進程不能夠直接訪問其餘進程的地址空間。線程 2)動態性:進程和程序的區別在於進程是動態的,進程中有時間的概念,進程具備本身的生命週期和各類 不一樣的狀態。 3)併發性:多個進程能夠在單個處理器上併發執行,互不影響。 |
1)線程能夠完成必定任務,能夠和其餘線程共享父進程的共享變量和部分環境,相互協做來完成任務。 2)線程是獨立運行的,其不知道進程中是否還有其餘線程存在。 3)線程的執行是搶佔式的,也就是說,當前執行的線程隨時可能被掛起,一邊運行另外一個線程。 4)一個線程能夠建立或撤銷另外一個線程,一個進程中的多個線程能夠併發執行。 |
2、線程的生命週期
新建狀態(New)
使用new關鍵字和Thread類或七子類創建一個線程對象後,該線程對象就處於新建狀態。此時僅由JVM爲其分配內存,並初始化其成員變量的值。它保持這個狀態知道程序start()這個線程。
就緒狀態(runnable)
當線程對象調用了start()方法以後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,Java虛擬機會爲其建立方法調用棧和程序計數器,等待JVM裏線程調度器的調度。
運行狀態(running)
若是就緒狀態的線程獲取CPU資源,就能夠執行run方法,此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它能夠變爲阻塞狀態、就緒狀態和死亡狀態。
阻塞狀態(blocked)
線程由於某種緣由放棄了CPU使用權,暫時中止運行。直到線程進入可運行狀態,纔有機會再次得到CPU timeslice 轉到運行狀態。
若是一個線程執行了sleep(睡眠),suspend(掛起)等方法,失去所佔用資源以後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或得到設備資源後能夠從新進入就緒狀態。能夠分爲三種:
1)等待阻塞:運行狀態中的線程執行wait方法,使線程進入到等待阻塞狀態;
2)同步阻塞:線程在獲取synchronized同步鎖失敗(由於同步鎖被其餘線程佔用);
3)其餘阻塞:經過調用線程的sleep()或join()發出了IO請求時,線程就會進入到阻塞狀態。當sleep()狀態超時,join()等待線程終止或超時,或IO請求處理完畢,線程從新轉入就緒狀態。
死亡狀態(dead)
一個運行狀態的線程完成任務或其餘終止條件發生時,該線程就切換到終止狀態。
正常結束:run()或call()方法執行完成,線程正常結束。
異常結束:線程拋出一個未捕獲的Exception或Error。
調用stop:直接調用該線程的stop()方法結束該線程(該方法容易致使死鎖,不推薦使用)。
爲了肯定線程在當前是否存活着(就是要麼是可運行的,要麼是被阻塞了),須要使用isAlive方法。若是是可運行或被阻塞,這個方法返回true;若是線程仍舊是new狀態且不是可運行的,或線程死亡了,則返回false。
3、線程優先級
每一個線程都有一個優先級,方便操做系統肯定線程的調度順序。Java線程的優先級是一個整數,其取值範圍是1(Thread.MIN_PRIORITY)-10(Thread.MAX_PRIORITY)。
默認狀況下,每個線程都會分配一個優先級 NORM_PRIORITY(5)。具備較高優先級的線程在低優先級的線程以前分配處理器資源。但線程優先級不能保證線程執行的順序,並且很是依賴於平臺。
4、建立線程的方式
1)繼承Thread類:Thread類本質上是實現了Runnable接口的一個實例。啓動線程的方法就是經過Thread類的start()方法。他是一個native方法,它將啓動一個新線程,並執行其中的run()方法。
1 public class MyThread extends Thread { 2 public void run() { 3 System.out.println("MyThread.run()"); 4 } 5 } 6 MyThread myThread1 = new MyThread(); 7 myThread1.start();
2)實現Runnable接口
1 public class MyThread extends OtherClass implements Runnable { 2 public void run() { 3 System.out.println("MyThread.run()"); 4 } 5 } 6 //啓動 MyThread,須要首先實例化一個 Thread,並傳入本身的 MyThread 實例:
7 MyThread myThread = new MyThread(); 8 Thread thread = new Thread(myThread); 9 thread.start(); 10 target.run(); //當傳入一個 Runnable target 參數給 Thread 後,Thread 的 run()方法就會調用
11 public void run() { 12 if (target != null) { 13 target.run(); 14 } 15 }
3)經過Callable和Future建立線程
i. 建立Callable接口的實現類,並實現call()方法,該call()方法將做爲線程執行體,而且有返回值。
ii. 建立Callable實現類的實例,使用FutureTask類包裝Callable對象,該FutureTask對象封裝了Callable對象的call()方法的返回值。
iii. 使用FutureTask對象做爲Thread對象的target建立並啓動新線程。
iv. 調用FutureTask對象的get()方法來得到子線程執行結束後的返回值。
1 //建立一個線程池
2 ExecutorService pool = Executors.newFixedThreadPool(taskSize); 3 // 建立多個有返回值的任務
4 List<Future> list = new ArrayList<Future>(); 5 for (int i = 0; i < taskSize; i++) { 6 Callable c = new MyCallable(i + " "); 7 // 執行任務並獲取 Future 對象
8 Future f = pool.submit(c); 9 list.add(f); 10 } 11 // 關閉線程池
12 pool.shutdown(); 13 // 獲取全部併發任務的運行結果
14 for (Future f : list) { 15 // 從 Future 對象上獲取任務的返回值,並輸出到控制檯
16 System.out.println("res:" + f.get().toString()); 17 }
4)基於線程池的方式
1 ExecutorService threadPool = Executors.newFixedThreadPool(10); // 建立線程池
2 while(true) { 3 threadPool.execute(new Runnable() { // 提交多個線程任務,並執行
4 public void run() { 5 System.out.println(Thread.currentThread().getName() + " is running .."); 6 try { 7 Thread.sleep(3000); 8 } catch (InterruptedException e) { 9 e.printStackTrace(); 10 } 11 } 12 }); 13 }
5、終止線程的方式
1.正常運行結束:程序運行結束,線程自動結束。
2.使用退出標誌退出線程
通常run()方法執行完,線程就會正常結束,然而,經常有些線程是伺服線程。它們須要長時間的運行,只有在外部某些條件知足的狀況下,才能關閉這些線程。使用一個變量來控制循環,例如:最直接的方法就是設一個boolean類型的標誌,並經過設置這個標誌爲true或false來控制while循環是否退出,代碼示例:
1 public class ThreadSafe extends Thread { 2 public volatile boolean exit = false; 3 public void run() { 4 while (!exit){ 5 //do something
6 } 7 } 8 }
定義了一個退出標誌exit,當exit爲true時,while循環退出,exit的默認值爲false。在定義exit時,使用了一個Java關鍵字volatile,這個關鍵字的目的是使exit同步,也就是說在同一時刻只能由一個線程來修改exit值。
3.Interrupt方法結束線程
使用interrupt()方法來中斷線程有兩種狀況:
1)線程處於阻塞狀態:如使用了sleep,同步鎖的wait、socket中的receiver、accept等方法時,會使線程處於阻塞狀態。當調用線程的interrupt()方法時,會拋出InterruptException異常。阻塞中的那個方法拋出這個異常,經過代碼捕獲該異常,而後break跳出循環狀態,從而讓咱們有機會結束這個線程的執行。一般不少人認爲只要調用interrupt方法線程就會結束,其實是錯的,必定要先捕獲InterruptedException異常以後經過break來跳出循環,才能正常結束run方法。
2)線程未處於阻塞狀態:使用isInterrupted()方法判斷線程的中斷標誌來退出循環。當使用interrupt方法時,中斷標誌就會置true,和使用自定義的標誌來控制循環是同樣的道理。
1 public class ThreadSafe extends Thread { 2 public void run() { 3 while (!isInterrupted()){ //非阻塞過程當中經過判斷中斷標誌來退出
4 try{ 5 Thread.sleep(5*1000);//阻塞過程捕獲中斷異常來退出
6 }catch(InterruptedException e){ 7 e.printStackTrace(); 8 break;//捕獲到異常以後,執行 break 跳出循環
9 } 10 } 11 } 12 }
4.Stop方法終止線程(線程不安全)
程序中能夠直接使用thread.stop()來強行終止線程,可是stop方法很危險,就像忽然關閉計算機電源,而不是按正常程序關機同樣,可能會產生不可預料的結果,不安全主要是:
thread.stop()調用後,建立子線程的線程就會拋出ThreadDeatherror的錯誤,而且會釋放子線程所持有的全部鎖。通常任何進行加鎖的代碼塊,都是爲了保護數據的一致性,若是在調用thread.stop()後致使了該線程所持有的全部鎖被忽然釋放(不可控制),那麼被保護數據就有可能呈現不一致性,其餘線程在使用這些被破壞的數據時,有可能致使一些很奇怪的應用程序錯誤。所以,並不推薦使用stop方法來終止線程。
6、線程同步的方式
臨界區:經過對多線程的串行化來訪問公共資源或一段代碼,速度快,適應控制數據訪問。
互斥量:採用互斥對象機制,只有擁有互斥對象的線程纔有訪問公共資源的權限,由於互斥對象只有一個,因此能夠保證公共資源不會同時被多個線程訪問。
信號量:它容許多個線程統一時刻訪問同一資源,可是須要限制同一時刻訪問此資源的最大線程數目。信號量對象對線程的同步方式與前面幾種方法不一樣,信號容許多個線程同時使用共享資源,這與操做系統中PV操做類似。
事件(信號):經過通知操做的方式來保持對線程的同步,還能夠方便的實現多線程的優先級比較的操做。
7、進程同步與互斥的區別
互斥:是指某一資源同時只容許一個訪問者對其進行訪問,具備惟一性和排他性。但互斥沒法限制訪問者對資源的訪問順序,即訪問是無序的。
同步:是指在互斥的基礎上(大多數狀況),經過其餘機制實現訪問者對資源的有序訪問。在大多數狀況下,同步已經實現了互斥,特別是全部寫入資源的狀況一定是互斥的。少數狀況是指能夠容許多個訪問者同時訪問資源。
同步體現的是一種協做性,護持體現的是一種排他性。
8、Java後臺線程
守護線程(Daemon):也稱服務線程,是後臺線程。爲用戶線程提供公共服務,在沒有用戶線程可服務是會自動離開。
1.優先級較低,用於爲系統中的其餘對象和線程提供服務。
2.設置:經過setDaemon(true)來設置線程爲「守護線程」。
3.垃圾回收線程就是守護線程,它始終在低級別的狀態運行,用於監視和管理系統中的可回收資源。
4.生命週期:守護線程不依賴於終端但依賴於系統,與系統「同生共死」。
9、如何在兩個線程之間共享數據?
Java裏面進行多線程痛心的主要方式就是共享內存的方式,共享內存主要的關注點有兩個:可見性和有序性。Java內存模型(JMM)解決了可見性和有序性的問題,而鎖解決了原子性的問題,理想狀況下咱們但願作到「同步」和「互斥」。有如下常規實現方法:
1.將數據抽象成一個類,並將數據的操做做爲這個類的方法,這麼設計能夠很容易作到同步,只要在方法上加「synchronized」
1 public class MyData { 2 private int j=0; 3 public synchronized void add(){ 4 j++; 5 System.out.println("線程"+Thread.currentThread().getName()+"j 爲:"+j); 6 } 7 public synchronized void dec(){ 8 j--; 9 System.out.println("線程"+Thread.currentThread().getName()+"j 爲:"+j); 10 } 11 public int getData(){ 12 return j; 13 } 14 } 15 public class AddRunnable implements Runnable{ 16 MyData data; 17 public AddRunnable(MyData data){ 18 this.data= data; 19 } 20 public void run() { 21 data.add(); 22 } 23 } 24 public class DecRunnable implements Runnable { 25 MyData data; 26 public DecRunnable(MyData data){ 27 this.data = data; 28 } 29 public void run() { 30 data.dec(); 31 } 32 } 33 public static void main(String[] args) { 34 MyData data = new MyData(); 35 Runnable add = new AddRunnable(data); 36 Runnable dec = new DecRunnable(data); 37 for(int i=0;i<2;i++){ 38 new Thread(add).start(); 39 new Thread(dec).start(); 40 } 41 }
2.將Runnable對象做爲一個類的內部類,共享數據做爲這個類的成員變量,每一個線程對共享數據的操做方法也封裝在外部類,以便實現對數據的各個操做的同步和互斥,做爲內部類的各個Runnable對象調用外部類的這些方法。
1 public class MyData { 2 private int j=0; 3 public synchronized void add(){ 4 j++; 5 System.out.println("線程"+Thread.currentThread().getName()+"j 爲:"+j); 6 } 7 public synchronized void dec(){ 8 j--; 9 System.out.println("線程"+Thread.currentThread().getName()+"j 爲:"+j); 10 } 11 public int getData(){ 12 return j; 13 } 14 } 15 public class TestThread { 16 public static void main(String[] args) { 17 final MyData data = new MyData(); 18 for(int i=0;i<2;i++){ 19 new Thread(new Runnable(){ 20 public void run() { 21 data.add(); 22 } 23 }).start(); 24 new Thread(new Runnable(){ 25 public void run() { 26 data.dec(); 27 } 28 }).start(); 29 } 30 } 31 }
10、多線程中的常見問題?
1. run()方法和start()方法之間的區別?
只有調用了start方法,纔會表現出多線程的特性,不一樣線程的run方法裏面的代碼交替執行。若是隻是調用run方法,那麼代碼仍是同步執行的,必須等待一個線程的run方法裏面的代碼所有執行完畢以後,另一個線程才能夠執行其run方法裏面的代碼。
i. start()方法來啓動線程,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,能夠直接繼續執行下面的代碼。
ii. 經過調用Thread類的start方法來啓動一個線程,這時此線程是處於就緒狀態,並無運行。
iii. 方法run稱爲線程體,它包含了要執行的這個線程的內容,線程就進入了運行狀態,開始運行run函數中的代碼。run方法運行結束,此線程終止,而後CPU再調度其餘線程。
2. Runnable接口和Callable接口的區別?
Runnable接口中的run()方法的返回值爲void,它作的事情只是純粹地去執行run()方法中的代碼而已。Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合能夠用來獲取異步執行的結果。
Callable+Future/FutureTask能夠獲取多線程運行的結果,能夠在等待時間太長沒獲取須要的數據的狀況下取消該線程的任務。
3. Sleep()方法和Wait()方法的區別?
sleep方法和wait方法均可以用來放棄CPU必定的時間,不一樣點在於若是線程持有某個對象的監視器(監視對象同步),sleep方法不會放棄這個對象的監視器, wait方法會放棄這個對象的監視器,而且wait只能在同步中使用。
sleep方法屬於Thread類,sleep方法致使了程序暫停執行指定的時間,讓出CPU給其餘線程,可是它的監控狀態依然保持着,當指定的時間到了又會自動恢復運行狀態。在調用sleep方法過程當中,線程不會釋放對象鎖。
wait方法屬於Object來,當調用方法時,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對對象調用notify方法後本線程才進入對象鎖定池,準備獲取對象鎖進入運行狀態。
4.線程讓步(yield)
yield會使當前線程讓出CPU執行時間片,與其餘線程一塊兒從新競爭CPU時間片。通常狀況下,優先級高的線程有更大的可能性成功競爭獲得CPU時間片,但這又不是絕對的,有的操做系統對線程優先級並不敏感。
5. join 等待其餘線程終止
join方法,等待其餘線程終止,在當前線程中調用一個線程的join方法,則當前線程轉爲阻塞狀態,當另外一個線程結束,當前線程再由阻塞狀態變爲就緒狀態,等待CPU。
爲何要用join方法?
不少狀況下,主線程生成並啓動子線程,須要用到子線程返回結果,也就是主線程須要在子線程結束後再結束,這時就要用到join方法。
1 System.out.println(Thread.currentThread().getName() + "線程運行開始!"); 2 Thread6 thread1 = new Thread6(); 3 thread1.setName("線程 B"); 4 thread1.join(); 5 System.out.println("這時 thread1 執行完畢以後才能執行主線程");