說到線程,我們一定首先想到的是線程的創建,線程的創建一般有兩種方式 一種是實現 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操作的就是樂觀鎖,每次不加鎖而是假設沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。
volatile 關鍵字
我們知道volatile關鍵字的作用是保證變量在多線程之間的可見性,它是java.util.concurrent包的核心,沒有volatile就沒有這么多的並發類給我們使用。