Java多線程技術學習筆記(一)

目錄:

  1. 概述
  2. 多線程的好處與弊端
  3. JVM中的多線程解析
  4. 多線程的創建方式之一:繼承Thread類
  5. 線程的狀態
  6. 多線程創建的方式之二:實現Runnable接口
  7. 使用方式二創建多線程的好處
  8. 多線程示例
  9. 線程安全問題現象
  10. 線程安全問題產生的原因
  11. 同步代碼塊
  12. 同步的好處與弊端
  13. 同步的前提
  14. 同步函數
  15. 驗證同步函數的鎖
  16. 單例模式的線程安全問題的解決方案
  17. 死鎖示例

 

一、概述 目錄

首先得了解進程,打開我們電腦的windows資源管理器,可以直觀看到進程的樣子:

進程直觀上理解就是正在進行的程序。而每個進程包含一個或者多個線程。也就是說一個進程是由若干線程組成的,在程序執行期間,真正執行的是線程,而進程只是負責給該進程中的線程分配執行路徑,

所以,線程就是進程中負責程序執行的控制單元(執行路徑),一個進程可以有多個執行路徑,稱爲多線程。就像我們再使用QQ給多個好友聊天一樣,每一個聊天過程都是一個線程,這些線程都屬於QQ這個進程。

而開啓多線程就是爲了同時運行多部分代碼。每一線程都有自己運行的內容,這個內容可以稱爲線程要執行的任務

二、多線程的好處與弊端 目錄

上一部分說到多線程是爲了同時運行多部分代碼,但是對於一個cpu而言,在每一個時刻只能執行一個線程,它會在不同線程之間快速切換,由於切換速度很快,所以感覺上去像是多個線程在”同時”執行,現在雖然出現多核技術核數是幾乎不可能多過線程數的,所以仍然需要cpu不斷在多個線程之間切換,以提高cpu的利用效率。 然而,但是每一個線程都需要一定的內存空間去執行,線程一多,內存空間不足,就會使得電腦顯得特別卡,這就是多線程的弊端。注意到cpu在線程之間的切換是隨機的

三、JVM中的多線程解析 目錄

JVM啓動時就啓動了多個線程,至少有兩個線程可以分析出來:一個是執行main函數的線程(也稱爲主線程),另一個是負責垃圾回收的線程。

在JVM垃圾回收方法是finalize方法,該方法由垃圾回收器來調用,而gc() 方法是用來運行垃圾回收器的:

下面展示主線程和垃圾回收線程的運行:

複製代碼
 1 package thread.demo;
 2 
 3 class Demo extends Object{
 4     public void finalize(){
 5         System.out.println("demo ok");
 6     }
 7 }
 8 public class ThreadDemo_1 {
 9 
10     /**
11      * @param args
12      */
13     public static void main(String[] args) {
14         // TODO Auto-generated method stub
15         new Demo();
16         new Demo();
17         System.gc();
18         new Demo();
19         System.out.println("Hello World!"); 
20     }
21 
22 }
複製代碼

 

運行結果:

Hello World!
demo ok
demo ok

 

可以發現,雖然第19行在第17行代碼(垃圾回收)之前,但是第19行代碼卻先執行,怎麼回事呢?因爲垃圾回收(第17行)是由垃圾回收線程執行,而第19行代碼主線程的部分,cpu從主線程開始執行,然後在主線程和垃圾回收線程之間切換,創建完兩個Demo()對象之後,雖然我們調用垃圾回收器,但是垃圾回收程序還沒來得及執行,cpu切換到了主線程,於是先打印出了“Hello World!”

於是雖然第19行代碼,執行完了,看似整個程序都執行完了,但是JVM(Java 虛擬機)還沒有結束,即雖然主線程結束了,但是JVM還要執行垃圾回收線程。 

四、多線程創建方式之一:繼承Thread類 目錄

首先看一看一個簡單的打印程序:

 1 package thread.demo;
 2 class Demo_2{
 3     private String name;
 4     Demo_2(String name){
 5         this.name = name;
 6     }
 7     public void show(){
 8         for (int x = -99999999; x < 99999999; x++){};
 9         for (int i = 0; i < 10; i++){
10             System.out.println(name + "...i" + i);
11         }
12     }
13 }
14 public class ThreadDemo_2 {
15 
16     /**
17      * @param args
18      */
19     public static void main(String[] args) {
20         // TODO Auto-generated method stub
21         Demo_2 d1 = new Demo_2("旺財");
22         Demo_2 d2 = new Demo_2("xiaoqiang");
23         d1.show();
24         d2.show();
25     }
26 
27 }

View Code

 

運行結果:

旺財...i0
旺財...i1
旺財...i2
旺財...i3
旺財...i4
旺財...i5
旺財...i6
旺財...i7
旺財...i8
旺財...i9
xiaoqiang...i0
xiaoqiang...i1
xiaoqiang...i2
xiaoqiang...i3
xiaoqiang...i4
xiaoqiang...i5
xiaoqiang...i6
xiaoqiang...i7
xiaoqiang...i8
xiaoqiang...i9

View Code

 

這時顯示的只是主線程運行的結果,很容易理解!下面通過一種方式,讓“旺財”和“xiaoqiang”的打印能夠分別運行在不同的線程中,首先查看java的API文檔:

翻譯:線程 是程序中的執行線程。Java 虛擬機允許應用程序併發地執行多個執行線程。 

接着文檔中給出了創建線程的方法:

總結出來就是繼承Thread類然後重寫Thread類中的run()方法,run方法中的代碼就是線程要執行的代碼,然後調用start方法開啓一個線程

 1 package thread.demo;
 2 class Demo_2 extends Thread {
 3     private String name;
 4     Demo_2(String name){
 5         this.name = name;
 6     }
 7     public void run() {
 8         show();
 9     }
10     public void show(){
11         for (int x = -99999999; x < 99999999; x++){};
12         for (int i = 0; i < 10; i++){
13             System.out.println(name + "...i" + i);
14         }
15     }
16 }
17 public class ThreadDemo_2 extends Thread {
18 
19     /**
20      * @param args
21      */
22     public static void main(String[] args) {
23         /* 創建線程的目的是爲了開啓一條執行路徑,去運行指定代碼和
24         其他代碼實現同時運行,而運行的指定代碼就是這個執行路徑的
25         任務。 JVM創建的主線程的任務都定義在主函數中。
26         而自定義的線程它的任務:
27         Thread類用於描述線程,線程需要任務,所以Thread類也有對
28         任務的描述,這個任務就通過Thread類中的run方法來體現的。
29         也就是說,run方法就是封裝自定義線程任務的函數。
30         run方法中定義的就是線程要運行的任務代碼!!!
31         
32         開啓線程是爲了運行指定代碼,所以只有繼承Thread類,並重寫run方法。
33         並將運行代碼定義在run方法中即可
34         */
35         Demo_2 d1 = new Demo_2("旺財");
36         Demo_2 d2 = new Demo_2("xiaoqiang");
37         d1.start();
38         d2.start();
39     }
40 
41 }

View Code

 

運行結果:

xiaoqiang...i0
旺財...i0
xiaoqiang...i1
旺財...i1
旺財...i2
旺財...i3
xiaoqiang...i2
xiaoqiang...i3
xiaoqiang...i4
xiaoqiang...i5
xiaoqiang...i6
xiaoqiang...i7
旺財...i4
xiaoqiang...i8
xiaoqiang...i9
旺財...i5
旺財...i6
旺財...i7
旺財...i8
旺財...i9

View Code

 

可以看出,運行結果正如上面分析的一樣,cpu在多個線程之間隨機切換,於是打印出的結果與上面只有主線程時結果差別很大。

下面我們讓主線程參與進來,即同時看看線程d1,d2和主線程的運行結果:

複製代碼
 1 package thread.demo;
 2 class Demo_2 extends Thread {
 3     private String name;
 4     Demo_2(String name){
 5         this.name = name;
 6     }
 7     public void run() {
 8         show();
 9     }
10     public void show(){
11         for (int x = -99999999; x < 99999999; x++){};
12         for (int i = 0; i < 10; i++){
13             System.out.println(name + "...i" + i);
14         }
15     }
16 }
17 public class ThreadDemo_2 extends Thread {
18 
19     /**
20      * @param args
21      */
22     public static void main(String[] args) {
23         /* 創建線程的目的是爲了開啓一條執行路徑,去運行指定代碼和
24         其他代碼實現同時運行,而運行的指定代碼就是這個執行路徑的
25         任務。 JVM創建的主線程的任務都定義在主函數中。
26         而自定義的線程它的任務:
27         Thread類用於描述線程,線程需要任務,所以Thread類也有對
28         任務的描述,這個任務就通過Thread類中的run方法來體現的。
29         也就是說,run方法就是封裝自定義線程任務的函數。
30         run方法中定義的就是線程要運行的任務代碼!!!
31         
32         開啓線程是爲了運行指定代碼,所以只有繼承Thread類,並重寫run方法。
33         並將運行代碼定義在run方法中即可
34         */
35         Demo_2 d1 = new Demo_2("旺財");
36         Demo_2 d2 = new Demo_2("xiaoqiang");
37         d1.start();
38         d2.start();
39         System.out.println("over");
40     }
41 
42 }
複製代碼

 

運行結果:

複製代碼
over
xiaoqiang...i0
旺財...i0
xiaoqiang...i1
旺財...i1
xiaoqiang...i2
旺財...i2
xiaoqiang...i3
xiaoqiang...i4
旺財...i3
xiaoqiang...i5
旺財...i4
xiaoqiang...i6
旺財...i5
xiaoqiang...i7
旺財...i6
xiaoqiang...i8
旺財...i7
xiaoqiang...i9
旺財...i8
旺財...i9
複製代碼

 

多次執行,會發現顯示結果一直變化,這就是多個線程隨機佔用cpu的結果。 當然,如果想看到到底是哪個線程正在執行,可以調用Thread中的currentThread().getName()方法,其中currentThread()是用來返回當前的線程對象,然後用線程對象繼續調用getName()就是返回當前線程的名字。程序如下:

複製代碼
 1 package thread.demo;
 2 class Demo_2 extends Thread {
 3     private String name;
 4     Demo_2(String name){
 5         this.name = name;
 6     }
 7     public void run() {
 8         show();
 9     }
10     public void show(){
11         for (int x = -99999999; x < 99999999; x++){};
12         for (int i = 0; i < 10; i++){
13             System.out.println(name + "...i" + "...name: " + getName());
14         }
15     }
16 }
17 public class ThreadDemo_2 extends Thread {
18 
19     /**
20      * @param args
21      */
22     public static void main(String[] args) {
23         /* 創建線程的目的是爲了開啓一條執行路徑,去運行指定代碼和
24         其他代碼實現同時運行,而運行的指定代碼就是這個執行路徑的
25         任務。 JVM創建的主線程的任務都定義在主函數中。
26         而自定義的線程它的任務:
27         Thread類用於描述線程,線程需要任務,所以Thread類也有對
28         任務的描述,這個任務就通過Thread類中的run方法來體現的。
29         也就是說,run方法就是封裝自定義線程任務的函數。
30         run方法中定義的就是線程要運行的任務代碼!!!
31         
32         開啓線程是爲了運行指定代碼,所以只有繼承Thread類,並重寫run方法。
33         並將運行代碼定義在run方法中即可
34         */
35         Demo_2 d1 = new Demo_2("旺財");
36         Demo_2 d2 = new Demo_2("xiaoqiang");
37         d1.start();
38         d2.start();
39         System.out.println("over" + "..." + Thread.currentThread().getName());
40     }
41 
42 }
複製代碼

 

運行結果:

複製代碼
over...main
旺財...i...name: Thread-0
旺財...i...name: Thread-0
旺財...i...name: Thread-0
旺財...i...name: Thread-0
旺財...i...name: Thread-0
旺財...i...name: Thread-0
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
xiaoqiang...i...name: Thread-1
旺財...i...name: Thread-0
旺財...i...name: Thread-0
旺財...i...name: Thread-0
旺財...i...name: Thread-0
複製代碼

 

注意主線程的名字是固定的就是main,然後自定義的線程從0開始編號

五、線程的狀態 目錄

 

 

六、多線程創建的方式之二:實現Runnable接口 目錄

首先來讀一下API文檔關於Runnable的描述:

劃紅線部分:Runnable接口由那些打算通過某一線程執行其實例的類來實現。類必須定義一個稱爲run的無參數方法。於是採用實現接口的方式來實現多線程:

  • 定義Runnable接口
  • 覆蓋Runnable接口中的run方法,將線程任務代碼封裝到run方法中。
  • 通過Thread類創建對線,並將Runnable接口中的子類對象作爲參數傳遞給Thread類的構造函數。因爲線程任務都封裝在Runnable接口子類對象的run方法中,所以在創建時必須要明確要運行的任務,如果不傳遞這個對象,Thread類會運行它自己的run方法,而不是我們定義在接口中的方法。
  • 調用線程的start方法開啓線程。

代碼如下:

複製代碼
 1 package thread.demo;
 2 //通過實現接口的方式實現多線程創建
 3 class Demo_3 implements Runnable {
 4     public void run() {
 5         show();
 6     }
 7     public void show(){
 8         for (int x = -99999999; x < 99999999; x++){};
 9         for (int i = 0; i < 10; i++){
10             System.out.println(Thread.currentThread().getName());
11         }
12     }
13 }
14 
15     public class ThreadDemo_3 extends Thread {
16 
17         /**
18          * @param args
19          */
20         public static void main(String[] args) {
21             //創建一個Runnable接口的子類對象
22             Demo_3 d = new Demo_3(); 
23             //將上述Runnable接口的子類對象傳入Thread構造函數,
24             //創建線程
25             Thread t1 = new Thread(d);
26             Thread t2 = new Thread(d);
27             t1.start();
28             t2.start();
29     }
30 
31 }
複製代碼

 

運行結果:

複製代碼
Thread-1
Thread-0
Thread-1
Thread-0
Thread-1
Thread-0
Thread-0
Thread-1
Thread-0
Thread-1
Thread-0
Thread-1
Thread-0
Thread-1
Thread-0
Thread-1
Thread-1
Thread-1
Thread-0
Thread-0
複製代碼

 

同樣可以看見兩個線程在隨機切換執行。

細節:通過閱讀API文檔,可以發現,Thread類裏面定義了自己的run方法,而Runnable也有run方法,而Thread的構造方法包含着下面兩種(當然不止兩種):

第一種構造方法不包含任何參數,那麼在使用Thread創建的線程對象在運行時,就調用Thread類自己的run方法,如果傳入一個Runnable子類對象,那麼在使用Thread類創建對象時,運行的任務就是Runnable接口中定義run方法。原理用代碼簡單解釋如下:

複製代碼
 1 package thread.demo;
 2 
 3 class Thread {
 4     private Runnable r;
 5     Thread() {}
 6     Thread(Runnable) {
 7         this.r = r;
 8     }
 9     
10     public void run() {
11         r.run();
12     }
13     
14     public void start() {
15         run();
16     }
17 }
18 
19 public class SubThread extends Thread {
20 
21     public void run() {
22         System.out.println("dsa");
23     }
24 
25 }
複製代碼

 

在Thread內部有一個私有的Runable子類對象,可以看出,當我們把Runable子類 r 對象傳遞給Thread類構造函數的時候,啓動start()就會調用run(),而run()接着調用 r 的run方法;

但是當我們直接通過上面介紹的方式一,即直接繼承Thread類創建線程的時候,如19-23行所示,我們需要覆蓋Thread類中的run方法,那麼SubThread類的對象就在通過start方法啓動線程的時候調用的run方法時就會調用我們在Thread子類中自己定義的run方法(21-22行)!

七、使用方式二創建多線程的好處 目錄

創建線程的兩種方式:

  • 方式一:繼承Thread
  • 方式二:實現Runnable接口

好處:

1)將線程的任務從線程的子類中分離出來,進行了單獨封裝,按照面向對象的思想將任務封裝成對象。

2)避免了java單繼承的侷限性

所以,創建線程第二種方式較爲常用!!! 

八、多線程示例 目錄

下面實現四個售票員(四個線程)一起賣100張票的示例。

複製代碼
 1 package thread.demo;
 2 //買票:四個售票員一起賣100張票
 3 class Ticket implements Runnable {
 4     private int num = 100;
 5     public void run() {
 6         while(true) {
 7             if (num > 0) {
 8                 System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
 9             }
10         }
11     }
12 }
13 
14 public class TicketDemo {
15 
16 
17     public static void main(String[] args) {
18 
19         Ticket t = new Ticket();
20         /*
21         Ticket t1 = new Ticket();
22         Ticket t2 = new Ticket();
23         Ticket t3 = new Ticket();
24         Ticket t4 = new Ticket();
25         */
26         Thread seller1 = new Thread(t);
27         Thread seller2 = new Thread(t);
28         Thread seller3 = new Thread(t);
29         Thread seller4 = new Thread(t);
30         
31         seller1.start();
32         seller2.start();
33         seller3.start();
34         seller4.start();
35     }
36 
37 }
複製代碼

 

運行結果:

複製代碼
Thread-0...sale...100
Thread-3...sale...97
Thread-2...sale...98
Thread-1...sale...99
Thread-2...sale...94
Thread-3...sale...95
Thread-0...sale...96
Thread-3...sale...91
Thread-2...sale...92
Thread-2...sale...88
Thread-2...sale...87
Thread-2...sale...86
Thread-1...sale...93
Thread-1...sale...84
Thread-1...sale...83
Thread-2...sale...85
Thread-3...sale...89
Thread-0...sale...90
Thread-3...sale...80
Thread-2...sale...81
Thread-1...sale...82
Thread-1...sale...76
Thread-1...sale...75
Thread-2...sale...77
Thread-3...sale...78
Thread-0...sale...79
Thread-3...sale...72
Thread-2...sale...73
Thread-1...sale...74
Thread-2...sale...69
Thread-3...sale...70
Thread-0...sale...71
Thread-3...sale...66
Thread-2...sale...67
Thread-1...sale...68
Thread-2...sale...63
Thread-2...sale...61
Thread-3...sale...64
Thread-0...sale...65
Thread-3...sale...59
Thread-2...sale...60
Thread-1...sale...62
Thread-2...sale...56
Thread-3...sale...57
Thread-0...sale...58
Thread-3...sale...53
Thread-2...sale...54
Thread-1...sale...55
Thread-1...sale...49
Thread-2...sale...50
Thread-3...sale...51
Thread-0...sale...52
Thread-0...sale...45
Thread-3...sale...46
Thread-2...sale...47
Thread-1...sale...48
Thread-2...sale...42
Thread-3...sale...43
Thread-0...sale...44
Thread-3...sale...39
Thread-2...sale...40
Thread-1...sale...41
Thread-2...sale...36
Thread-3...sale...37
Thread-0...sale...38
Thread-3...sale...33
Thread-2...sale...34
Thread-1...sale...35
Thread-2...sale...30
Thread-3...sale...31
Thread-0...sale...32
Thread-3...sale...27
Thread-2...sale...28
Thread-1...sale...29
Thread-2...sale...24
Thread-3...sale...25
Thread-0...sale...26
Thread-3...sale...21
Thread-2...sale...22
Thread-1...sale...23
Thread-2...sale...18
Thread-3...sale...19
Thread-0...sale...20
Thread-3...sale...15
Thread-2...sale...16
Thread-1...sale...17
Thread-2...sale...12
Thread-3...sale...13
Thread-0...sale...14
Thread-3...sale...9
Thread-2...sale...10
Thread-1...sale...11
Thread-2...sale...6
Thread-3...sale...7
Thread-0...sale...8
Thread-3...sale...3
Thread-2...sale...4
Thread-1...sale...5
Thread-3...sale...1
Thread-0...sale...2
複製代碼

 

看到四個線程一起把100張票賣完了.

九、線程安全問題現象 目錄

分析上面的示例,由於線程之間隨機切換執行,假如售票員0(線程0),賣到了1號票,此時 num = 1 ;

            if (num > 0) {
                System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
            }

 

經過判斷,滿足if 條件,進入後面的代碼塊,但是存在一種情況就是,線程0還沒有來得及執行打印語句,就切換到了線程1,此時線程0處於等待狀態(等待繼續執行…), 此時num仍然爲1, 然後線程1判斷

滿足條件,順利執行完,之後num–, 於是num = 0; 恰好隨機切換到線程0, 然後線程0執行打印語句(Thread-0…sale…0),就是說售票員0 把0號票賣出去了,顯然不可以!這種情況就導致了線程不安全!

爲了使得程序出現我們分析的這種不安全的情況,需要在示例代碼在第7行之後稍微停頓一下,然後執行後面的打印語句,如果不停頓,現在cpu的計算速度很快,判斷 if (num > 0)爲真之後往往很快就會執行到打印語句,上面分析的情況很難觀測到,於是爲了說明問題,做如下添加:

複製代碼
 1 package thread.demo;
 2 //買票:四個售票員一起賣100張票
 3 class Ticket implements Runnable {
 4     private int num = 100;
 5     public void run() {
 6         while(true) {
 7             if (num > 0) {
 8                 // 讓線程sleep一會,好讓打印語句還沒來得及執行,其他線程
 9                 // 就切換進來,這樣方便我們觀測線程安全隱患
10                 try {
11                     Thread.sleep(20);
12                 } catch (InterruptedException e) {
13                     // TODO Auto-generated catch block
14                     e.printStackTrace();
15                 }
16                 
17                 System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
18             }
19         }
20     }
21 }
22 
23 public class TicketDemo {
24 
25 
26     public static void main(String[] args) {
27 
28         Ticket t = new Ticket();
29         /*
30         Ticket t1 = new Ticket();
31         Ticket t2 = new Ticket();
32         Ticket t3 = new Ticket();
33         Ticket t4 = new Ticket();
34         */
35         Thread seller1 = new Thread(t);
36         Thread seller2 = new Thread(t);
37         Thread seller3 = new Thread(t);
38         Thread seller4 = new Thread(t);
39         
40         seller1.start();
41         seller2.start();
42         seller3.start();
43         seller4.start();
44     }
45 
46 }
複製代碼

 

運行結果:

複製代碼
Thread-3...sale...100
Thread-2...sale...98
Thread-1...sale...100
Thread-0...sale...99
Thread-0...sale...95
Thread-3...sale...97
Thread-2...sale...97
Thread-1...sale...96
Thread-2...sale...94
Thread-3...sale...94
Thread-1...sale...94
Thread-0...sale...94
Thread-0...sale...93
Thread-3...sale...91
Thread-1...sale...92
Thread-2...sale...93
Thread-3...sale...88
Thread-0...sale...89
Thread-2...sale...89
Thread-1...sale...90
Thread-0...sale...87
Thread-2...sale...84
Thread-3...sale...85
Thread-1...sale...86
Thread-2...sale...83
Thread-3...sale...82
Thread-1...sale...82
Thread-0...sale...83
Thread-0...sale...81
Thread-3...sale...80
Thread-1...sale...80
Thread-2...sale...81
Thread-3...sale...78
Thread-0...sale...79
Thread-1...sale...79
Thread-2...sale...77
Thread-2...sale...76
Thread-1...sale...75
Thread-3...sale...75
Thread-0...sale...76
Thread-2...sale...74
Thread-1...sale...73
Thread-3...sale...73
Thread-0...sale...74
Thread-0...sale...72
Thread-1...sale...70
Thread-3...sale...71
Thread-2...sale...69
Thread-0...sale...68
Thread-2...sale...65
Thread-1...sale...67
Thread-3...sale...66
Thread-1...sale...64
Thread-2...sale...61
Thread-0...sale...62
Thread-3...sale...63
Thread-2...sale...59
Thread-1...sale...57
Thread-3...sale...60
Thread-0...sale...58
Thread-2...sale...56
Thread-1...sale...55
Thread-3...sale...54
Thread-0...sale...53
Thread-1...sale...52
Thread-2...sale...51
Thread-0...sale...50
Thread-3...sale...49
Thread-2...sale...48
Thread-1...sale...47
Thread-3...sale...46
Thread-0...sale...45
Thread-1...sale...44
Thread-3...sale...42
Thread-0...sale...43
Thread-2...sale...41
Thread-1...sale...39
Thread-0...sale...38
Thread-3...sale...40
Thread-2...sale...37
Thread-2...sale...36
Thread-0...sale...34
Thread-3...sale...35
Thread-1...sale...33
Thread-1...sale...32
Thread-2...sale...31
Thread-3...sale...30
Thread-0...sale...29
Thread-0...sale...28
Thread-3...sale...27
Thread-1...sale...26
Thread-2...sale...25
Thread-1...sale...24
Thread-2...sale...23
Thread-0...sale...22
Thread-3...sale...21
Thread-3...sale...20
Thread-1...sale...18
Thread-2...sale...19
Thread-0...sale...20
Thread-2...sale...17
Thread-3...sale...15
Thread-0...sale...16
Thread-1...sale...14
Thread-3...sale...13
Thread-0...sale...12
Thread-2...sale...11
Thread-1...sale...10
Thread-0...sale...9
Thread-3...sale...8
Thread-1...sale...7
Thread-2...sale...6
Thread-3...sale...5
Thread-0...sale...4
Thread-2...sale...3
Thread-1...sale...2
Thread-3...sale...1
Thread-0...sale...0
Thread-2...sale...-1
Thread-1...sale...-2
複製代碼

 

可以看到售票員竟然賣出了0,-1,-2號票!!這就是線程安全問題的簡單演示,每次運行出現的結果不一樣,也有可能恰好正常,沒有出現安全問題,以爲線程切換的不確定性,但是這種問題一旦出現,通常很致命,所以必須注意!

十、線程安全問題產生的原因 目錄

由上面可以知道,線程安全問題必須要解決,但是解決問題關鍵是分析原因!根據上面示例總結出以下原因:

  • 多個線程操作共享的數據
  • 操作共享數據的線程代碼有多條

當一個線程在執行操作共享數據的多條代碼過程中,其他線程參與了運算,就會導致線程安全問題的產生

十一、同步代碼塊 目錄

既然知道產生線程安全問題的原因,就開始着手解決。

解決思路:

將多條操作操作共享數據的代碼封裝起來, 當有線程在執行這些代碼的時候,其他線程是不可以參與運算。

必須要當前線程把這些代碼都執行完畢以後,其他線程纔可以參與運算.

Java中,用同步代碼塊就可以解決這個問題。同步代碼塊的格式

synchronized(對象){

  需要被同步的代碼;

}

其中的“對象”可以看做一個標記,可以使用任何類型的對象,包括自定義的對象。當然,爲了方便,使用Object類的對象即可。

複製代碼
 1 package thread.demo;
 2 //買票:四個售票員一起賣100張票
 3 class Ticket implements Runnable {
 4     private int num = 100;
 5     Object obj = new Object();
 6     public void run() {
 7         while(true) {
 8             synchronized(obj) {
 9                 if (num > 0) {
10                     // 讓線程sleep一會,好讓打印語句還沒來得及執行後面的打印語句,其他線程
11                     // 就切換進來,這樣方便我們觀測線程安全隱患
12                     try {
13                         Thread.sleep(20);
14                     } catch (InterruptedException e) {
15                         // TODO Auto-generated catch block
16                         e.printStackTrace();
17                     }
18                     
19                     System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
20                 }
21             }
22         }
23     }
24 }
25 
26 public class TicketDemo {
27 
28 
29     public static void main(String[] args) {
30 
31         Ticket t = new Ticket();
32         /*
33         Ticket t1 = new Ticket();
34         Ticket t2 = new Ticket();
35         Ticket t3 = new Ticket();
36         Ticket t4 = new Ticket();
37         */
38         Thread seller1 = new Thread(t);
39         Thread seller2 = new Thread(t);
40         Thread seller3 = new Thread(t);
41         Thread seller4 = new Thread(t);
42         
43         seller1.start();
44         seller2.start();
45         seller3.start();
46         seller4.start();
47     }
48 
49 }
複製代碼

 

運行結果:

複製代碼
Thread-0...sale...100
Thread-0...sale...99
Thread-0...sale...98
Thread-0...sale...97
Thread-0...sale...96
Thread-0...sale...95
Thread-0...sale...94
Thread-0...sale...93
Thread-0...sale...92
Thread-0...sale...91
Thread-0...sale...90
Thread-0...sale...89
Thread-0...sale...88
Thread-2...sale...87
Thread-2...sale...86
Thread-2...sale...85
Thread-3...sale...84
Thread-3...sale...83
Thread-3...sale...82
Thread-3...sale...81
Thread-1...sale...80
Thread-1...sale...79
Thread-1...sale...78
Thread-1...sale...77
Thread-1...sale...76
Thread-1...sale...75
Thread-3...sale...74
Thread-2...sale...73
Thread-2...sale...72
Thread-2...sale...71
Thread-2...sale...70
Thread-2...sale...69
Thread-2...sale...68
Thread-0...sale...67
Thread-0...sale...66
Thread-0...sale...65
Thread-0...sale...64
Thread-0...sale...63
Thread-0...sale...62
Thread-2...sale...61
Thread-2...sale...60
Thread-3...sale...59
Thread-3...sale...58
Thread-3...sale...57
Thread-3...sale...56
Thread-3...sale...55
Thread-3...sale...54
Thread-3...sale...53
Thread-3...sale...52
Thread-3...sale...51
Thread-3...sale...50
Thread-3...sale...49
Thread-3...sale...48
Thread-3...sale...47
Thread-3...sale...46
Thread-3...sale...45
Thread-3...sale...44
Thread-3...sale...43
Thread-3...sale...42
Thread-3...sale...41
Thread-1...sale...40
Thread-1...sale...39
Thread-1...sale...38
Thread-1...sale...37
Thread-1...sale...36
Thread-1...sale...35
Thread-1...sale...34
Thread-1...sale...33
Thread-1...sale...32
Thread-1...sale...31
Thread-1...sale...30
Thread-1...sale...29
Thread-1...sale...28
Thread-1...sale...27
Thread-1...sale...26
Thread-1...sale...25
Thread-3...sale...24
Thread-2...sale...23
Thread-2...sale...22
Thread-2...sale...21
Thread-0...sale...20
Thread-0...sale...19
Thread-0...sale...18
Thread-0...sale...17
Thread-2...sale...16
Thread-2...sale...15
Thread-2...sale...14
Thread-2...sale...13
Thread-3...sale...12
Thread-3...sale...11
Thread-3...sale...10
Thread-3...sale...9
Thread-3...sale...8
Thread-3...sale...7
Thread-1...sale...6
Thread-1...sale...5
Thread-1...sale...4
Thread-1...sale...3
Thread-3...sale...2
Thread-3...sale...1
複製代碼

 

可以看出,問題得到了很好地解決!

十二、同步的好處與弊端 目錄

首先討論下,同步到底是如何實現的。

假如0線程執行到run方法的同步代碼塊,那麼 0 線程就持有了 obj, 即obj 被加載到了0線程裏面,當其他線程過來時,它們也需要加載obj才能進入同步代碼塊,但是在線程沒有執行完同步代碼塊之前,obj一直被0線程佔有,所以其他線程無法進入同步代碼塊,知道0線程執行完同步代碼塊釋放 obj,其他線程纔有機會加載obj, 然後進入同步代碼塊。所以obj就像一把鎖一樣,裏面的不出來,外面的就進不去。所以常常稱爲同步鎖!誰拿到了鎖,誰就進得去,出來之後釋放鎖,以便其他線程有機會使用。

同步的好處:解決了線程安全問題。

同步的弊端:相對降低了效率,因爲同步外的線程都會去判斷同步鎖。

十三、同步的前提 目錄

同步中,必須有多個線程並使用同一個,因爲一個線程沒必要同步,數據全被這一個線程使用,就不存在所謂的線程安全問題。而如果不使用同一個鎖,即使一個線程在執行加了鎖的代碼塊,其他線程同樣可以通過拿到其他鎖進來參與運算。

十四、同步函數 目錄

一個稍微簡單點的多線程程序示例:

複製代碼
 1 package thread.demo;
 2 /*
 3  需求:兩個儲戶,每個都到銀行存錢,每次存100,共存3次 
 4  */
 5 class Bank {
 6     private int sum;
 7     public void add(int num) {
 8         sum += num;
 9         System.out.println("sum = "  + sum);
10     }
11 }
12 
13 class Customer implements Runnable {
14     Bank bank = new Bank();
15     public void run() {
16         for (int x = 0; x < 3; x++) {
17             bank.add(100);
18         }
19     }
20 }
21 
22 public class BankDemo {
23 
24     /**
25      * @param args
26      */
27     public static void main(String[] args) {
28         // TODO Auto-generated method stub
29         Customer customer = new Customer();
30         Thread t1 = new Thread(customer);
31         Thread t2 = new Thread(customer);
32 
33         t1.start();
34         t2.start();
35 
36     }
37 
38 }
複製代碼

 

運行結果:

複製代碼
sum = 100
sum = 200
sum = 300
sum = 500
sum = 400
sum = 600
複製代碼

 

當然,由於線程的隨機切換,顯示結果有點亂,最終兩人共向銀行存入了600.

分析一下這個程序:run方法裏面的代碼調用了add方法,sum 是兩個線程的共享數據,對於共享數據的操作不止一條:

        sum += num;
        System.out.println("sum = "  + sum);

 

假如第一個用戶(線程0)存入100,執行第一條語句,sum 就變爲100. 正常接下來就應該輸出 sum = 100, 但是這時存在一種情況:線程0 還沒來得及輸出,第二個用戶(線程1)存入100,於是sum = 100 + 100 = 200, 線程1 然後正常輸出 sum = 200,剛輸出完成,線程0又切入了,接着執行打印語句,本來他存入的只是100,但是由於sum = 200, 線程0 也打印出sum = 200, 對應這實際情況就是,用戶1存入了100,系統卻顯示存入了200,顯然存在着線程安全問題。同樣,爲了展示這個安全問題,在上面兩條語句之間假如一定的停頓:

複製代碼
        sum += num;
        try {
            Thread.sleep(20);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("sum = "  + sum);
複製代碼

 

運行結果:

複製代碼
sum = 200
sum = 200
sum = 400
sum = 400
sum = 600
sum = 600
複製代碼

 

可見,200顯示了兩次,肯定是有一個用戶存入100時,卻打印出了200. 顯然不合理!

首先想到的就是同步:

複製代碼
 1 class Bank {
 2     private int sum;
 3     Object obj = new Object();
 4     public void add(int num) {
 5         synchronized(obj) {
 6             sum += num;
 7             try {
 8                 Thread.sleep(20);
 9             } catch (InterruptedException e) {
10                 // TODO Auto-generated catch block
11                 e.printStackTrace();
12             }
13             System.out.println("sum = "  + sum);
14         }
15     }
16 }
複製代碼

 

運行結果:

複製代碼
sum = 100
sum = 200
sum = 300
sum = 400
sum = 500
sum = 600
複製代碼

 

問題解決!

但是,發現,要創建對象,寫synchronized代碼塊是不是有點麻煩呢???我們發現add函數裏面的內容都需要同步,就是說add函數裏面的代碼就是我們要同步的代碼,於是直接同步這個函數就可以了,即同步函數

複製代碼
 1 class Bank {
 2     private int sum;
 3     //Object obj = new Object();
 4     public synchronized void add(int num) {//同步函數
 5         //synchronized(obj) {
 6             sum += num;
 7             try {
 8                 Thread.sleep(20);
 9             } catch (InterruptedException e) {
10                 // TODO Auto-generated catch block
11                 e.printStackTrace();
12             }
13             System.out.println("sum = "  + sum);
14         //}
15     }
16 }
複製代碼

 

 

運行結果同上!

十五、同步函數鎖的驗證 目錄

由前面的分析知道,同步是需要一把“鎖”,而鎖可以是任意類型的對象,那麼同步函數到底使用的是什麼鎖呢??下面來驗證。

回到前面售票的程序,改成兩個售票員,一個售票員賣票線程放在同步代碼塊裏面,另一個售票員的的線程放在同步函數裏面,如果這兩個線程用的是同一個鎖,那麼就不會出現安全隱患。

於是在程序改爲下面:

複製代碼
 1 package thread.demo;
 2 //買票:四個售票員一起賣100張票
 3 class Ticket_2 implements Runnable {
 4     private int num = 100;
 5     Object obj = new Object();
 6     boolean flag = true;
 7     public void run() { 
 8         if (flag) 
 9         {
10             while (true) 
11             {
12                 synchronized(obj)
13                 {
14                     if (num > 0) 
15                     {
16                         // 讓線程sleep一會,好讓打印語句還沒來得及執行後面的打印語句,其他線程
17                         // 就切換進來,這樣方便我們觀測線程安全隱患
18                         try { 
19                             Thread.sleep(20);
20                         } catch (InterruptedException e) {
21                             // TODO Auto-generated catch block
22                             e.printStackTrace();
23                         }
24                         System.out.println(Thread.currentThread().getName() + "...bloc k..." + num--);
25                     }
26                 }
27             }
28                 
29         }//end if
30         else
31             while (true) show();
32     }
33     
34     public synchronized void show() 
35     {
36         if (num > 0) 
37         {
38             // 讓線程sleep一會,好讓打印語句還沒來得及執行,其他線程
39             // 就切換進來,這樣方便我們觀測線程安全隱患
40             try { 
41                 Thread.sleep(20);
42             } catch (InterruptedException e) {
43                 // TODO Auto-generated catch block
44                 e.printStackTrace();
45             }
46             System.out.println(Thread.currentThread().getName() + "...function..." + num--);
47         }
48     } 
49 }
50 
51 public class SynFunctionLockDemo
52 {
53 
54 
55     public static void main(String[] args) 
56     {
57 
58         Ticket_2 t = new Ticket_2();
59 
60         Thread seller1 = new Thread(t);
61         Thread seller2 = new Thread(t);
62 
63         
64         seller1.start(); //在同步代碼塊執行
65         t.flag = false; // 標誌變爲false,使得下一個線程在同步函數執行
66         seller2.start();
67 
68     }
69 }
複製代碼

 

 

如果是在同步函數裏面執行的代碼塊會打印出帶有“function”的字符串。如果是在同步代碼塊裏面執行的會打印出帶有”bloc  k”的字符串,執行如下:

複製代碼
Thread-0...function...100
Thread-0...function...99
Thread-0...function...98
Thread-0...function...97
Thread-0...function...96
Thread-0...function...95
Thread-0...function...94
Thread-0...function...93
Thread-0...function...92
Thread-0...function...91
Thread-0...function...90
Thread-0...function...89
Thread-0...function...88
Thread-0...function...87
Thread-0...function...86
Thread-0...function...85
Thread-1...function...84
Thread-1...function...83
Thread-1...function...82
Thread-1...function...81
Thread-0...function...80
Thread-0...function...79
Thread-0...function...78
Thread-0...function...77
Thread-0...function...76
Thread-0...function...75
Thread-0...function...74
Thread-1...function...73
Thread-1...function...72
Thread-0...function...71
Thread-0...function...70
Thread-0...function...69
Thread-1...function...68
Thread-1...function...67
Thread-1...function...66
Thread-0...function...65
Thread-0...function...64
Thread-0...function...63
Thread-0...function...62
Thread-1...function...61
Thread-1...function...60
Thread-1...function...59
Thread-1...function...58
Thread-0...function...57
Thread-0...function...56
Thread-0...function...55
Thread-1...function...54
Thread-1...function...53
Thread-1...function...52
Thread-1...function...51
Thread-1...function...50
Thread-1...function...49
Thread-1...function...48
Thread-1...function...47
Thread-0...function...46
Thread-0...function...45
Thread-0...function...44
Thread-0...function...43
Thread-1...function...42
Thread-1...function...41
Thread-1...function...40
Thread-1...function...39
Thread-1...function...38
Thread-1...function...37
Thread-1...function...36
Thread-1...function...35
Thread-1...function...34
Thread-1...function...33
Thread-1...function...32
Thread-1...function...31
Thread-1...function...30
Thread-1...function...29
Thread-1...function...28
Thread-1...function...27
Thread-0...function...26
Thread-0...function...25
Thread-0...function...24
Thread-1...function...23
Thread-1...function...22
Thread-1...function...21
Thread-1...function...20
Thread-1...function...19
Thread-1...function...18
Thread-1...function...17
Thread-1...function...16
Thread-0...function...15
Thread-0...function...14
Thread-0...function...13
Thread-1...function...12
Thread-1...function...11
Thread-1...function...10
Thread-1...function...9
Thread-1...function...8
Thread-1...function...7
Thread-0...function...6
Thread-0...function...5
Thread-1...function...4
Thread-1...function...3
Thread-1...function...2
Thread-1...function...1
複製代碼

 

發現竟然都在同步函數裏面執行,什麼原因呢?原因在於:主線程開啓以後一口氣運行到了66行代碼,注意這裏雖然線程0和線程1被開啓,即具有了執行資格,但是還不具備執行權,執行權仍然被主線程佔有着,等待主線程完事之後,線程0和線程1在真正搶到執行權的時候,發現flag = false,於是全部走進了同步函數!!! 要想使得線程0和線程1分別走進不同的同步方法,需要在開啓線程0之後,讓主線程停頓一會,讓線程0開始執行(此時線程1還沒有被開啓,是不可能執行的),進入同步代碼塊,然後等到主線程再執行的時候就會把flag置爲false,開啓線程1,線程1就可以進入同步函數。

具體改動如下:

複製代碼
 1 public static void main(String[] args) 
 2     {
 3 
 4         Ticket_2 t = new Ticket_2();
 5 
 6         Thread seller1 = new Thread(t);
 7         Thread seller2 = new Thread(t);
 8 
 9         
10         seller1.start(); //在同步代碼塊執行
11         
12         // 主線程停頓一會,給線程0執行的機會!!
13         try {
14             Thread.sleep(20);
15         } catch (InterruptedException e) {
16             // TODO Auto-generated catch block
17             e.printStackTrace();
18         }
19         
20         t.flag = false; // 標誌變爲false,使得下一個線程在同步函數執行
21         seller2.start();
22 
23     }
複製代碼

 

運行結果:

複製代碼
Thread-0...bloc k...100
Thread-0...bloc k...99
Thread-1...function...98
Thread-0...bloc k...96
Thread-1...function...97
Thread-0...bloc k...94
Thread-1...function...95
Thread-0...bloc k...93
Thread-1...function...92
Thread-0...bloc k...91
Thread-1...function...90
Thread-1...function...89
Thread-0...bloc k...89
Thread-0...bloc k...88
Thread-1...function...88
Thread-0...bloc k...87
Thread-1...function...87
Thread-1...function...86
Thread-0...bloc k...86
Thread-1...function...84
Thread-0...bloc k...85
Thread-1...function...82
Thread-0...bloc k...83
Thread-0...bloc k...80
Thread-1...function...81
Thread-0...bloc k...79
Thread-1...function...78
Thread-0...bloc k...77
Thread-1...function...76
Thread-1...function...75
Thread-0...bloc k...74
Thread-1...function...73
Thread-0...bloc k...72
Thread-1...function...71
Thread-0...bloc k...70
Thread-1...function...69
Thread-0...bloc k...68
Thread-1...function...67
Thread-0...bloc k...66
Thread-1...function...65
Thread-0...bloc k...64
Thread-1...function...63
Thread-0...bloc k...62
Thread-1...function...61
Thread-0...bloc k...60
Thread-1...function...59
Thread-0...bloc k...58
Thread-1...function...57
Thread-0...bloc k...56
Thread-1...function...55
Thread-0...bloc k...54
Thread-1...function...53
Thread-0...bloc k...52
Thread-1...function...51
Thread-0...bloc k...50
Thread-1...function...49
Thread-0...bloc k...48
Thread-1...function...47
Thread-0...bloc k...46
Thread-1...function...45
Thread-0...bloc k...44
Thread-1...function...43
Thread-0...bloc k...42
Thread-1...function...41
Thread-0...bloc k...40
Thread-1...function...39
Thread-0...bloc k...38
Thread-1...function...37
Thread-0...bloc k...36
Thread-1...function...35
Thread-0...bloc k...34
Thread-1...function...33
Thread-0...bloc k...32
Thread-1...function...31
Thread-0...bloc k...30
Thread-1...function...29
Thread-0...bloc k...28
Thread-1...function...27
Thread-0...bloc k...26
Thread-1...function...25
Thread-0...bloc k...24
Thread-1...function...23
Thread-0...bloc k...22
Thread-1...function...21
Thread-0...bloc k...20
Thread-1...function...19
Thread-0...bloc k...18
Thread-0...bloc k...17
Thread-1...function...16
Thread-1...function...15
Thread-0...bloc k...14
Thread-1...function...13
Thread-0...bloc k...12
Thread-1...function...11
Thread-0...bloc k...10
Thread-1...function...9
Thread-0...bloc k...8
Thread-1...function...7
Thread-0...bloc k...6
Thread-1...function...5
Thread-0...bloc k...4
Thread-1...function...3
Thread-0...bloc k...2
Thread-1...function...1
Thread-0...bloc k...0
複製代碼

 

可以看到,線程用到了不同的同步方式:同步函數(1線程)和同步代碼塊(0線程)!但是,也看到89,88,87,86號票被出售了兩次,而且還售出了0號票(每次執行情況會不同,但是都會出現類似的線程問題),這說明線程0和線程1沒有同步,即二者目前使用的鎖不一致,即同步函數使用的不是Object 類型的鎖!

我們知道類裏面的函數都默認持有this代表着調用該類的對象,同步函數當讓持有this啦,試着把同步代碼塊的鎖改爲this:

synchronized(this)

然後多次執行,結果如下:

複製代碼
Thread-0...bloc k...100
Thread-0...bloc k...99
Thread-0...bloc k...98
Thread-0...bloc k...97
Thread-0...bloc k...96
Thread-0...bloc k...95
Thread-0...bloc k...94
Thread-0...bloc k...93
Thread-0...bloc k...92
Thread-0...bloc k...91
Thread-0...bloc k...90
Thread-0...bloc k...89
Thread-0...bloc k...88
Thread-0...bloc k...87
Thread-0...bloc k...86
Thread-1...function...85
Thread-1...function...84
Thread-1...function...83
Thread-0...bloc k...82
Thread-0...bloc k...81
Thread-0...bloc k...80
Thread-0...bloc k...79
Thread-1...function...78
Thread-1...function...77
Thread-1...function...76
Thread-0...bloc k...75
Thread-0...bloc k...74
Thread-0...bloc k...73
Thread-0...bloc k...72
Thread-0...bloc k...71
Thread-1...function...70
Thread-1...function...69
Thread-0...bloc k...68
Thread-0...bloc k...67
Thread-0...bloc k...66
Thread-1...function...65
Thread-1...function...64
Thread-1...function...63
Thread-1...function...62
Thread-1...function...61
Thread-0...bloc k...60
Thread-0...bloc k...59
Thread-1...function...58
Thread-1...function...57
Thread-1...function...56
Thread-1...function...55
Thread-0...bloc k...54
Thread-0...bloc k...53
Thread-0...bloc k...52
Thread-0...bloc k...51
Thread-1...function...50
Thread-1...function...49
Thread-1...function...48
Thread-0...bloc k...47
Thread-0...bloc k...46
Thread-0...bloc k...45
Thread-1...function...44
Thread-1...function...43
Thread-1...function...42
Thread-0...bloc k...41
Thread-0...bloc k...40
Thread-0...bloc k...39
Thread-0...bloc k...38
Thread-0...bloc k...37
Thread-0...bloc k...36
Thread-0...bloc k...35
Thread-0...bloc k...34
Thread-1...function...33
Thread-1...function...32
Thread-1...function...31
Thread-1...function...30
Thread-1...function...29
Thread-1...function...28
Thread-1...function...27
Thread-0...bloc k...26
Thread-0...bloc k...25
Thread-0...bloc k...24
Thread-0...bloc k...23
Thread-0...bloc k...22
Thread-1...function...21
Thread-0...bloc k...20
Thread-0...bloc k...19
Thread-0...bloc k...18
Thread-0...bloc k...17
Thread-1...function...16
Thread-1...function...15
Thread-1...function...14
Thread-0...bloc k...13
Thread-0...bloc k...12
Thread-0...bloc k...11
Thread-0...bloc k...10
Thread-1...function...9
Thread-1...function...8
Thread-1...function...7
Thread-0...bloc k...6
Thread-0...bloc k...5
Thread-0...bloc k...4
Thread-0...bloc k...3
Thread-0...bloc k...2
Thread-0...bloc k...1
複製代碼

 發現線程問題被解決了!所以驗證了同步函數使用的同步鎖就是 this!

雖然同步函數書寫較爲簡單(作爲同步代碼塊的簡寫形式,二者功能一致),但是建議使用同步代碼塊,因爲同步函數使用的鎖唯一,而同步代碼塊可以使用任意對象作爲鎖,只在需要的若干條語句自由加鎖,直觀

但是,注意到,如果需要同步的函數是靜態的呢?因爲靜態函數屬於類而不是具體對象,所以靜態函數中是不存在this的,所以如果同步函數是靜態的,鎖顯然就不是this!!!

繼續按照上面思路驗證,在上面程序的基礎上,把同步函數改爲靜態,同時由於靜態函數使用了num, 所以把num也改爲靜態,改動如下:

複製代碼
private static int num = 100;

....


public static synchronized void show() 
複製代碼

 

運行如下:

複製代碼
Thread-0...block...100
Thread-0...block...99
Thread-0...block...98
Thread-1...function...97
Thread-1...function...96
Thread-0...block...95
Thread-0...block...94
Thread-1...function...93
Thread-1...function...92
Thread-0...block...91
Thread-0...block...89
Thread-1...function...90
Thread-1...function...88
Thread-0...block...87
Thread-1...function...86
Thread-0...block...86
Thread-1...function...85
Thread-0...block...85
Thread-1...function...84
Thread-0...block...83
Thread-1...function...82
Thread-0...block...81
Thread-1...function...80
Thread-0...block...80
Thread-0...block...79
Thread-1...function...78
Thread-1...function...77
Thread-0...block...77
Thread-0...block...76
Thread-1...function...75
Thread-0...block...74
Thread-1...function...73
Thread-1...function...72
Thread-0...block...72
Thread-0...block...71
Thread-1...function...71
Thread-1...function...70
Thread-0...block...69
Thread-1...function...68
Thread-0...block...67
Thread-1...function...66
Thread-0...block...65
Thread-0...block...64
Thread-1...function...63
Thread-0...block...62
Thread-1...function...61
Thread-1...function...60
Thread-0...block...59
Thread-1...function...58
Thread-0...block...57
Thread-1...function...56
Thread-0...block...55
Thread-1...function...53
Thread-0...block...54
Thread-0...block...52
Thread-1...function...52
Thread-0...block...51
Thread-1...function...50
Thread-1...function...49
Thread-0...block...48
Thread-1...function...47
Thread-0...block...46
Thread-1...function...45
Thread-0...block...44
Thread-0...block...43
Thread-1...function...42
Thread-0...block...41
Thread-1...function...40
Thread-1...function...39
Thread-0...block...38
Thread-0...block...37
Thread-1...function...36
Thread-0...block...35
Thread-1...function...34
Thread-1...function...33
Thread-0...block...32
Thread-0...block...31
Thread-1...function...30
Thread-0...block...29
Thread-1...function...28
Thread-0...block...27
Thread-1...function...26
Thread-1...function...25
Thread-0...block...24
Thread-0...block...23
Thread-1...function...22
Thread-0...block...21
Thread-1...function...20
Thread-1...function...19
Thread-0...block...18
Thread-0...block...17
Thread-1...function...16
Thread-0...block...15
Thread-1...function...14
Thread-1...function...13
Thread-0...block...12
Thread-0...block...11
Thread-1...function...10
Thread-0...block...9
Thread-1...function...8
Thread-1...function...7
Thread-0...block...6
Thread-1...function...5
Thread-0...block...4
Thread-0...block...3
Thread-1...function...2
Thread-1...function...1
Thread-0...block...0
複製代碼

 

看到86,85 ,0等處又出現了線程安全問題。這說明,顯然靜態函數使用的不是鎖不是this, 當然靜態函數不可能持有this的,與我們預料的一樣!

然而每一個類都屬於它所在的字節碼文件對象,雖然靜態函數不屬於具體的對象,而是屬於一個類,所以靜態函數必然持有字節碼文件的對象,顯然這個對象是靜態的!

於是在上面改動的基礎上再作如下改動:

 

synchronized(Ticket_2.class)

... ...

 

 

運行結果:

複製代碼
Thread-0...block...100
Thread-0...block...99
Thread-0...block...98
Thread-0...block...97
Thread-1...function...96
Thread-1...function...95
Thread-1...function...94
Thread-1...function...93
Thread-1...function...92
Thread-1...function...91
Thread-1...function...90
Thread-1...function...89
Thread-1...function...88
Thread-1...function...87
Thread-1...function...86
Thread-1...function...85
Thread-1...function...84
Thread-1...function...83
Thread-1...function...82
Thread-1...function...81
Thread-1...function...80
Thread-1...function...79
Thread-1...function...78
Thread-1...function...77
Thread-1...function...76
Thread-1...function...75
Thread-1...function...74
Thread-1...function...73
Thread-1...function...72
Thread-1...function...71
Thread-1...function...70
Thread-1...function...69
Thread-0...block...68
Thread-0...block...67
Thread-0...block...66
Thread-0...block...65
Thread-0...block...64
Thread-1...function...63
Thread-1...function...62
Thread-1...function...61
Thread-1...function...60
Thread-1...function...59
Thread-1...function...58
Thread-1...function...57
Thread-1...function...56
Thread-1...function...55
Thread-1...function...54
Thread-1...function...53
Thread-1...function...52
Thread-1...function...51
Thread-1...function...50
Thread-1...function...49
Thread-1...function...48
Thread-1...function...47
Thread-1...function...46
Thread-1...function...45
Thread-1...function...44
Thread-1...function...43
Thread-1...function...42
Thread-1...function...41
Thread-1...function...40
Thread-1...function...39
Thread-1...function...38
Thread-1...function...37
Thread-1...function...36
Thread-1...function...35
Thread-1...function...34
Thread-1...function...33
Thread-1...function...32
Thread-1...function...31
Thread-1...function...30
Thread-1...function...29
Thread-1...function...28
Thread-1...function...27
Thread-1...function...26
Thread-1...function...25
Thread-1...function...24
Thread-1...function...23
Thread-1...function...22
Thread-1...function...21
Thread-1...function...20
Thread-1...function...19
Thread-1...function...18
Thread-1...function...17
Thread-1...function...16
Thread-1...function...15
Thread-1...function...14
Thread-1...function...13
Thread-1...function...12
Thread-1...function...11
Thread-1...function...10
Thread-1...function...9
Thread-1...function...8
Thread-1...function...7
Thread-1...function...6
Thread-1...function...5
Thread-1...function...4
Thread-1...function...3
Thread-1...function...2
Thread-1...function...1
複製代碼

 

所以靜態的同步函數使用的鎖是 該函數所述的字節碼文件對象,該對象可以用 getClass方法獲取,也可以用當前 類名.class 表示,但是此時由於getClass()是非靜態方法,所以只能用類名.class。

十六、單例模式的線程問題的解決方案 目錄

複製代碼
 1 package thread.demo;
 2 //單例模式
 3 //餓漢式
 4 /*
 5 class Single
 6 {
 7     private static final Single s = new Single();
 8     private Single(){}
 9     public static Single getInstance()
10     {
11         return s;
12     }
13 }
14 */
15 
16 //懶漢式
17 
18 class Single_l
19 {
20     private static Single_l s = null;
21     private Single_l(){}
22     public static Single_l getInstance()
23     {
24         if (s == null)
25         {
26             synchronized(Single_l.class)
27             {
28                 if (s == null)
29                     s = new Single_l();
30             }
31         }
32         return s;
33     }    
34 }
35 
36 
37 public class SingleDemo {
38 
39     /**
40      * @param args
41      */
42     public static void main(String[] args) {
43         // TODO Auto-generated method stub
44 
45     }
46 
47 }
複製代碼

 

由於懶漢式單例模式的同步代碼塊的代碼有多條,所以可能會出現線程安全問題,所以需要進行同步。

十七、多線程死鎖示例 目錄

複製代碼
 1 package thread.demo;
 2 //買票:四個售票員一起賣100張票
 3 class Ticket_3 implements Runnable 
 4 {
 5     private int num = 100;
 6     Object obj = new Object();
 7     boolean flag = true;
 8     public void run() 
 9     { 
10         if (flag) 
11         {
12             while (true) 
13             {
14                 synchronized(obj)
15                 {
16                      show();//同步代碼塊中使用同步函數
17                 }
18             }    
19         }//end if
20         else
21             while (true) show();
22     }
23     
24     public synchronized void show() 
25     {
26         synchronized(obj)//同步函數中使用同步代碼塊
27         {
28             if (num > 0) 
29             {
30                 // 讓線程sleep一會,好讓打印語句還沒來得及執行,其他線程
31                 // 就切換進來,這樣方便我們觀測線程安全隱患
32                 try { 
33                     Thread.sleep(10);
34                 } catch (InterruptedException e) {
35                     // TODO Auto-generated catch block
36                     e.printStackTrace();
37                 }
38                 System.out.println(Thread.currentThread().getName() + "...function..." + num--);
39             } //end if 
40         }//end synchronized
41     } 
42 }
43 
44 public class DeadLock
45 {
46     public static void main(String[] args) 
47     {
48 
49         Ticket_3 t = new Ticket_3();
50 
51         Thread seller1 = new Thread(t);
52         Thread seller2 = new Thread(t);
53 
54         
55         seller1.start(); //在同步代碼塊執行
56         
57         // 主線程停頓一會,給線程0執行的機會!!
58         try {
59             Thread.sleep(20);
60         } catch (InterruptedException e) {
61             // TODO Auto-generated catch block
62             e.printStackTrace();
63         }
64         
65         t.flag = false; // 標誌變爲false,使得下一個線程在同步函數執行
66         seller2.start();
67 
68     }
69 }
複製代碼

 

程序解釋:這是一個同步鎖嵌套的情況,可以看到在第14行中的同步代碼塊中使用了同步函數,而在第24行的同步函數中使用了同步代碼塊,即兩個同步鎖互相嵌套。下面分析程序的運行:

  • 程序從主函數開始,運行到58行之後,主線程停頓,線程0得到cpu執行權,flag = true, 執行到14 行,線程 0 拿到同步鎖 obj, 執行同步代碼塊,而同步代碼塊的語句就是調用24行的同步函數,而同步函數裏面是26行開始的同步代碼塊,執行同步代碼塊,就需要判斷鎖 obj, 線程0確實持有鎖 obj,於是順利進入26行開始的同步代碼塊執行,
  • 線程0執行一會,由於代碼塊中有sleep的存在,主線程有可能切入進來佔有cpu執行權,線程0就停頓一會,主線程繼續由65行開始執行,flag = false, 線程1開啓,然後主線程,0線程,1線程隨機切換。
  • 假如剛纔停頓的線程0又搶到了cpu執行權,線程0繼續執行,再次拿到同步代碼塊鎖obj, 進入到第15行,存在一種情況,就在此時線程1搶到cpu執行權,線程0則持有着代碼塊的鎖obj,並瞅準機會去拿到同步函數的鎖this從16行出進入同步函數. 
  • 線程1獲得cpu執行權之後,由於flag == false, 執行到第21行,拿到同步函數鎖this進入同步函數show(), 然後在第26行準備拿鎖obj的時候,卻發現被obj被線程0佔有,只能等待,而線程0即使搶到了cpu執行權,但是同步函數鎖this卻被線程1佔有,也沒法繼續,於是程序就停頓不前,這就是死鎖產生的一個過程!

多次執行上面的代碼,會出現類似下面的情況:

 

即程序陷入了死鎖,無法繼續運行,所有線程0和線程1都處於等待狀態!!

上面的程序只展示了死鎖的樣子,下面給出一個較爲簡單的死鎖示例備用:

 

複製代碼
 1 package thread.demo;
 2 class MyLock
 3 {
 4     //“兩把鎖”
 5     public static final Object locka = new Object();
 6     public static final Object lockb = new Object();
 7 }
 8 class Test implements Runnable
 9 {
10     private boolean flag;
11     Test(boolean flag)
12     {
13         this.flag = flag;
14     }
15     @Override
16     public void run() 
17     {
18         if (flag)
19         {
20             synchronized(MyLock.locka)
21             {
22                 System.out.println("if...loacka");
23                 synchronized(MyLock.lockb)
24                 {
25                     System.out.println("if...loackb");
26                 }
27             }
28         }
29         else
30         {
31             synchronized(MyLock.lockb)
32             {
33                 System.out.println("else...loackb");
34                 synchronized(MyLock.locka)
35                 {
36                     System.out.println("else...loacka");
37                 }
38             }
39         }
40     }
41     
42 }
43 public class DeadLockTest {
44 
45     /**
46      * @param args
47      */
48     public static void main(String[] args) {
49         Test a = new Test(true);
50         Test b = new Test(false);
51         Thread t1 = new Thread(a);
52         Thread t2 = new Thread(b);
53         
54         t1.start();
55         t2.start();
56     }
57 
58 }
複製代碼

 

運行結果:

 

 


 

 

 

 

後續:下一篇博文將會記載多線程間通信的學習筆記。

參考文獻:傳智播客JAVA SE視頻教程,李剛《瘋狂JAVA講義》

 

請尊重原創知識,本人非常願意與大家分享 轉載請註明出處:http://www.cnblogs.com/90zeng/ 作者:博客園-90Zeng
分類: Java
3
0

« 上一篇:博客中最快捷的公式顯示方式:Mathjax + Lyx
» 下一篇:Java多線程技術學習筆記(二)
    </div>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章