對線程的理解總結


說到線程,我們一定首先想到的是線程的創建,線程的創建一般有兩種方式 一種是實現 Runnable 接口,另一種就是 繼承 Thread 類 ,因為Java的單繼承多實現機制,所以,優先選擇 實現 Runnable 接口。

 1 package test;
 2 
 3 class ThreadTest extends Thread{
 4 
 5 public void run(){
 6 
 7 System.out.println("實現了Thread類");
 8 
 9 }
10 
11 }
12 
13 class RunnableTest implements Runnable{
14 
15 public void run(){
16 
17 System.out.println("實現了runnable接口");
18 
19 }
20 
21 }
22 
23  
24 
25 public class ThreadStartTest {
26 
27 public static void main(String[] args) {
28 
29 //直接繼承了Thread ,創建一個Thread的實例
30 
31 ThreadTest t1 = new ThreadTest();
32 
33 //t1啟動
34 
35 t1.start();
36 
37 //如果是實現了接口的線程類,需要用對象的實例作為Thread類構造方法的參數
38 
39 Thread t2 = new Thread(new RunnableTest());
40 
41 t2.start();
42 
43 }
44 
45 }

這兒就有一個我很久之前一直不了解的坑。那時因為不經常使用線程類,所以,對線程的開啟僅停留在有兩種方式上面。在使用繼承的方式時,通過new xxxThread()的方式調用Start()方法,但使用接口的方式時 一直也是new xxxThread()d的方式,發現調不了start()方法,就調用了run()方法。.....其實這樣是不對的,對於Java來說,通過new的方式調用內部run()方法一點問題都沒有,但並不會開啟新線程,那樣做只會使用main線程。。正確的方式為Thread t2 = new Thread(new RunnableTest()); 然后調用start()方法。

總之一定要調用start()方法的。

1、那線程開啟了就要考慮線程安全了

線程安全,說到底是數據的安全,我可不認識線程是誰,它安不安全,跟我沒有半毛錢的關系。但數據不能不安全。這里就要提到內存了,因為,造成數據不安全的就是內存。

對於一個程序來說,就是一個進程,一個線程是其中的一部分。當系統為進程分配空間的時候,就會有公共空間(堆,公共方法區),和棧等。而造成不安全的就是這塊公共的內存空間。

當一個線程在數據處理的過程中有另一個線程對數據進行了修改,就會造成數據不安全,程序混亂。這樣我們就說這是線程不安全的。

1.1、怎么解決線程安全問題

解決線程安全問題,就要找到線程到底是怎么不安全的根本原因。其次安全與不安全是相對的。如果你的系統只有一個線程運行,或同一時間段不可能有兩個線程同時運行。那也就不存在線程安全問題了。

那線程不安全是怎么造成的呢?

原因一:

“每條線程有自己的工作內存,線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程對該變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。不同線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。”

原因二:線程搶奪

根本原因:線程內的關於外部變量的語境,與真實外部語境不一致。

針對這幾個原因,我們來提出解決的方案。

解決方案一:避重就輕

對於原因一中 線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,那就不要拷貝,我們所有的方法的參數都使用方法的局部變量,這樣,就不會產生從主內存拷貝的問題。當每一個線程來執行方法的時候,系統都會為該線程分配一塊屬於自己的棧內存,這樣,每個執行這個方法的線程都會有屬於自己的局部變量,那么操作自己的局部變量就不會產生安全問題了。

解決方法二:只讀不寫

對於原因一種的對主內存的拷貝,有時候是不能不拷貝的那,我們就要看看能不能只允許它讀取,不允許修改,也就是使用 final 修飾等...,這樣,你只能看看我的數據,不能修改,就不會造成安全問題了。

解決方案三:人手一份

就是把變量分給每一個線程,讓他們獨立運行。在是實際的開發當中我們可能會遇到變量在線程中共享的需求。這時我們可以使用 ThreadLocal 定義線程的變量,使用ThreadLocal 定義的變量只在本線程中有效,這樣也不會有安全問題。

 1 @RestController
 2 @RequestMapping("/test")
 3 public class TestController {
 4  8 
 9     static class MyThread implements Runnable {
10         Test test;
11 
12         public MyThread(Test test) {
13             this.test = test;
14         }
15 
16         @Override
17         public void run() {
18             for (int i = 0; i < 3; i++) {
19                 test.dd();
20             }
21 
22         }
23 
24     }
25 
26     static class Test {
27         ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
28 
29         public void dd() {
30             try {
31                 if (threadLocal.get() == null)
32                     threadLocal.set(0);
33                 Integer tt = threadLocal.get();
34                 tt += 60;
35                 threadLocal.set(tt);
36                 System.out.println(Thread.currentThread().getName() + "輸出值為:" + threadLocal.get());
37 
38                 Thread.sleep(100);
39             } catch (InterruptedException e) {
40                 e.printStackTrace();
41             }
42         }
43     }
44 
45     public static void main(String[] args) {
46         Test test = new Test();
47         for (int i = 0; i < 3; i++) {
48             MyThread myThread = new MyThread(test);
49             new Thread(myThread).start();
50 
51         }
52     }
53 }

在本例中有三個線程,變量使用 ThreadLocal 定義,通過 Test test = new Test(); 對線程傳入相同的 Test 實例。這樣避免使用不同的Test 實例 產生不同的ThreadLocal 變量對象。進而在 每個線程中循環3次進行 ThreadLocal 累加

上例運行結果如下:

可以看到線程之間沒有產生累加,但同一線程中進行了累加。

 

解決方案四:加悲觀鎖

對於以上方法都不能滿足我們的需求,那我們就只能采取更加嚴格的方式了,那就是加鎖,只要加鎖,那就要產生線程的阻塞,性能就會打折扣了。悲觀鎖就是悲觀的認為只要我不加鎖,那我的數據就會被其他線程修改,所以每次操作都要加鎖,直到操作完成。

下面我將上面的代碼修改一下,成為加悲觀鎖的情況:

 1 public class TestController {
 2 
 3     static class MyThread implements Runnable{
 4         Test test;
 5         public MyThread(Test test){
 6             this.test=test;
 7         }
 8         @Override
 9         public void run() {
10             for (int i = 0; i <3 ; i++) {
11                 test.dd();
12             }
13 
14         }
15 
16     }
17     static class Test{
18         Integer threadLocal=0;
19         Lock lock=new ReentrantLock();
20         public  void dd(){
21             try {
22                 lock.lock();
23 //            Integer tt=threadLocal.get();
24                 threadLocal+=60;
25 //            threadLocal.set(tt);
26                 System.out.println(Thread.currentThread().getName()+ "輸出值為:"+    threadLocal);
27 
28                 Thread.sleep(100);
29             } catch (InterruptedException e) {
30                 e.printStackTrace();
31             }finally {
32                 lock.unlock();
33             }
34         }
35     }
36 
37     public static void main(String[] args) {
38         Test test=new Test();
39         for (int i = 0; i <3 ; i++) {
40             MyThread myThread=new MyThread(test);
41             new Thread(myThread).start();
42 
43         }
44     }
45 }

在這個例子中我們把 ThreadLocal 替換為了普通的 Integer 變量,並使用了 Lock 進行加鎖。我們同樣開啟三個線程,並在線程中進行3次循環。並執行累加,沒有ThreadLocal 所有的線程公用一個變量,結果如下:

可以看到線程執行的順序不一定,但輸出的結果,沒有出現錯誤。

 解決方案五:加樂觀鎖

樂觀鎖與悲觀鎖相對,樂觀鎖認為,大概率沒有線程會修改我的數據,如果修改了那就只能重新執行操作。如果在高並發情況下使用樂觀鎖,可能會更加浪費系統資源。那具體怎么操作呢?

  • synchronized是悲觀鎖,這種線程一旦得到鎖,其他需要鎖的線程就掛起的情況就是悲觀鎖。
  • CAS操作的就是樂觀鎖,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。

參考:Java:CAS(樂觀鎖)

volatile 關鍵字

我們知道volatile關鍵字的作用是保證變量在多線程之間的可見性,它是java.util.concurrent包的核心,沒有volatile就沒有這么多的並發類給我們使用。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM