Java 併發(1)——線程安全

咱們對併發一詞並不陌生,它一般指多個任務同時執行。實際上這不徹底對,「並行」纔是真正意義上的同時執行,而「併發」則更偏重於多個任務交替執行。有時候咱們會看見一些人一邊嘴裏嚼着東西一邊講話,這是並行;固然,更文明禮貌的方式是講話前先把嘴裏的東西嚥下去,這是併發。併發早期被用來提升單處理器的性能,好比I/O阻塞。在多核處理器被普遍應用的今天,並行和併發的概念已經被模糊了,或許咱們沒必要太過糾結兩者之間的微妙差異。java

Java的併發是經過多線程實現的,若是有多個處理器,線程調度機制會自動向各處理器分派線程。線程不一樣於進程,它的級別比進程更低,一個進程能夠衍生出多個線程。現代操做系統都是多進程的,不一樣的程序分屬於不一樣的進程,各進程之間不會共享同一塊內存空間。由於進程之間沒有交集,因此各進程可以相安無事地運行,這就比如同一棟樓裏的不一樣住戶,你們關起門來各過各的,別人家夫妻吵架跟你一點關係都沒有。計算機中運行的各個程序都分屬於不一樣的進程,你在使用IDE時沒必要擔憂播放器會修改你的代碼,也不會擔憂通信軟件會對IDE有什麼影響。可是到了多線程,一切都變得複雜了,原來不一樣的住戶如今要搬到一塊兒合租,衛生間、廚房都變成了公用的。每一個線程都共享其所屬進程的資源,多線程的困難就在於協調不一樣線程所驅動的任務之間對共享資源的使用。程序員

既然多線程這麼困難,爲何不直接使用多進程呢?一個緣由是進程及其昂貴,操做系統會限制進程的數量。另外一個緣由來自遙遠的蠻荒年代,當時一些中古系統並不支持多進程,java爲了實現可移植的目的,用多線程實現了併發。面試

Java的多線程無處不在,然而實際狀況是,不多有人真正編寫過併發代碼,實際上有至關多的技術人員從未寫過真正意義上的併發。緣由是一些諸如Servlets的框架幫助咱們處理了併發問題。設計模式

任務與線程

Java的線程是經過Runnable接口實現的,能夠這樣實現一個線程:安全

 1 class Task implements Runnable {
 2     private int n = 1;
 3     private String tName = "";
 4     
 5     public Task(String tName) {
 6         this.tName = tName;
 7     }
 8     
 9     @Override
10     public void run() {
11         while(n <= 10) {
12             System.out.print(this.tName + "#" + n + "  ");
13             n++;
14         }
15         System.out.println(this.tName + " is over.");
16     }
17 }
18 
19 public class C_1 {
20     public static void main(String[] args) {
21         Task A = new Task("A");
22         Task B = new Task("B");
23         A.run();
24         B.run();
25         System.out.println("main is over.");
26     }
27 }

 運行結果與順序執行沒什麼不一樣:多線程

這說明實現了Runnable的類實際上與普通類沒什麼不一樣,它充其量只是個任務,想要實現併發,必須把任務附着在一個線程上:併發

1 public class C_1 {
2     public static void main(String[] args) {
3         Thread t1 = new Thread(new Task("A"));
4         Thread t2 = new Thread(new Task("B"));
5         t1.start();
6         t2.start();
7         System.out.println("main is over.");
8     }
9 }

此次纔是真正意義上的併發:框架

 

start()會爲線程啓動作好必要的準備,以後調用任務的run()方法,讓任務運行在線程上。在JDK1.5以後加入了線程管理器,能夠沒必要顯示地把任務附着在線程上,同時線程管理器還會自動管理線程的生命週期。ide

1 public class C_1 {
2     public static void main(String[] args) {        
3         ExecutorService es = Executors.newCachedThreadPool();
4         es.execute(new Task("A"));
5         es.execute(new Task("B"));
6         es.shutdown();
7         System.out.println("main is over.");
8     }
9 }

 shutdown()方法用於阻止向ExecutorService中提交新線程。若是在es.shutdown()時候仍然提交新線程,將會拋出java.util.concurrent.RejectedExecutionException。性能

JDK8以後加入了lambda表達式,對於一些短小的不須要重用的任務,能夠沒必要單獨寫成一個類:

 1 public class C_1 {
 2     public static void main(String[] args) {
 3         ExecutorService es = Executors.newCachedThreadPool();
 4         es.execute(new Task("A"));
 5         es.execute(new Task("B"));
 6         es.execute(new Runnable() {
 7             @Override
 8             public void run() {
 9                 System.out.println("I am in lambda.");
10             }
11         });
12         es.shutdown();
13         System.out.println("main is over.");
14     }
15 }

因爲每一個lambda表達式的初始化都會耽誤一點時間,所以在執行短小的運行速度很快的多線程程序時,這種方式每每看不出效果,程序更像是順序的。

線程安全

咱們常常說某個方法是線程安全的。我並不以爲「線程安全」是個易於理解的詞。簡單地說,若是某個方法是「線程安全」的,那麼這個方法在多線程環境下的運行結果也將是可預期的。

 1 import java.util.concurrent.ExecutorService;
 2 import java.util.concurrent.Executors;
 3 
 4 class Task2 implements Runnable {
 5     String tName = "";
 6 
 7     public Task2(String tName) {
 8         this.tName = tName;
 9     }
10 
11     @Override
12     public void run() {
13         for(int i = 1; i <= 10; i++) {
14             System.out.print(tName + "#" + i + " ");
15         }
16     }
17 }
18 
19 public class C_2 {
20     public static void main(String[] args) {
21         ExecutorService es = Executors.newCachedThreadPool();
22         es.execute(new Task2("A"));
23         es.execute(new Task2("B"));
24         es.shutdown();
25     }
26 }

運行結果多是:

做爲一個任務,Task2每次運行都會將10個編號依次打印出來,儘管每次打印的順序可能有所區別,但咱們仍然認爲它是可預期的,是線程安全的。

Task2之因此安全,是由於它沒有共享的狀態。若是加入狀態,就很容易把一個本來線程安全的方法變成不安全。

 1 class Task2 implements Runnable {
 2     String tName = "";
 3     static int no = 1;
 4     
 5     public Task2(String tName) {
 6         this.tName = tName;
 7     }
 8 
 9     @Override
10     public void run() {
11         for(int i = 1; i <= 10; i++) {
12             System.out.print(tName + "#" + i + " ");
13             no++;
14         }
15     }
16 }

這裏僅僅是對Task2稍加修改,讓兩個任務共享同一個序號,每次執行循環時都會對no加1。咱們預期的效果是每次打印出不一樣的no值,然而實際的運行結果多是:

出現了A#9和B#9。其緣由是兩個線程同時對no產生了競爭,而no++並又是經過多條指令完成的。在no=9時,A線程將其打印出來,以後執行++操做,在執行到一半的時候B進來了,因爲++操做並未結束,所以B看見的還是上一狀態。

無狀態的程序必定是線程安全的。HTTP是無狀態的,處理HTTP請求的servlet也是無狀態的,所以servlet是線程安全的。儘管如此,你仍需時刻保持警戒,由於沒有任何約束阻止你把一個本來無狀態的方法變成有狀態的。

1 public class MyServlet extends HttpServlet {
2 
3     private static int no = 1; 
4     
5     @Override
6     protected void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException {
7         arg0.setAttribute("no", no++);
8     }
9 }

有了共享就有了競爭,此時本來的線程安全也將變成不安全。

單例模式

我曾經面試過不少程序員,問他們知道哪些經常使用的設計模式,不少人的第一個回答就是單例模式,可見單例模式的深刻人心。下面是個典型的單例。

 1 public class Singleton {
 2     private static Singleton sl = null;
 3     
 4     private Singleton() {
 5         System.out.println("OK");
 6     }
 7     
 8     public static Singleton getInstance() {
 9         if(sl == null)
10             sl = new Singleton();
11         return sl;
12     }
13     
14     public static void main(String[] args) {
15         Singleton.getInstance();
16         Singleton.getInstance();
17         Singleton.getInstance();
18 }

Singleton在執行初始化後會打印OK,因爲Singleton只會執行一次初始化,所以程序最終僅僅會打印一次OK。然而一切在多線程中變得就不一樣了。把單例放在線程中:

 1 class Task3 implements Runnable {
 2 
 3     @Override
 4     public void run() {
 5         Singleton.getInstance();
 6     }
 7 }
 8 
 9 public class Singleton {
10     private static Singleton sl = null;
11     
12     private Singleton() {
13         System.out.println("OK");
14     }
15     
16     public static Singleton getInstance() {
17         if(sl == null)
18             sl = new Singleton();
19         return sl;
20     }
21     
22     public static void main(String[] args) {
23         ExecutorService es = Executors.newCachedThreadPool();
24         es.execute(new Task3());
25         es.execute(new Task3());
26         es.execute(new Task3());
27         es.shutdown();
28     }
29 }

3個線程同時發現了sl==null,此時可能會執行3次初始化,打印3次OK。這也成爲單例模式被人詬病的緣由,雖然能夠經過雙檢查鎖和volatile關鍵字解決上述狀況,但代碼較爲複雜,性能也讓人捉急。一個好的方式是使用主動初始化代替單例:

 1 public class Singleton_better {
 2 
 3     private static Singleton_better sl = new Singleton_better();
 4     
 5     public static Singleton_better getInstance() {
 6         return sl;
 7     }
 8     
 9     public Singleton_better() {
10         System.out.println("OK");
11     }
12 }

另外一種方式是惰性初始化, 它在解決了線程安全的同時還保留了單例的優勢:

 1 public class Single_lazy {
 2         
 3     private static class Handle {
 4         public static Single_lazy sl = new Single_lazy();
 5     }
 6     
 7     public static Single_lazy getInstance() {
 8         return Handle.sl;
 9     }
10 }

  做者:我是8位的

  出處:http://www.cnblogs.com/bigmonkey

  本文以學習、研究和分享爲主,如需轉載,請聯繫本人,標明做者和出處,非商業用途! 

  掃描二維碼關注公做者衆號「我是8位的」

相關文章
相關標籤/搜索