目錄:
一、概述 目錄
首先得了解進程,打開我們電腦的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.運行結果:
Hello World!demo okdemo 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 args18 */19 public static void main(String[] args) {20 // TODO Auto-generated method stub21 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旺財...i9xiaoqiang...i0xiaoqiang...i1xiaoqiang...i2xiaoqiang...i3xiaoqiang...i4xiaoqiang...i5xiaoqiang...i6xiaoqiang...i7xiaoqiang...i8xiaoqiang...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 args21 */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旺財...i0xiaoqiang...i1旺財...i1旺財...i2旺財...i3xiaoqiang...i2xiaoqiang...i3xiaoqiang...i4xiaoqiang...i5xiaoqiang...i6xiaoqiang...i7旺財...i4xiaoqiang...i8xiaoqiang...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 args21 */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 }運行結果:
overxiaoqiang...i0旺財...i0xiaoqiang...i1旺財...i1xiaoqiang...i2旺財...i2xiaoqiang...i3xiaoqiang...i4旺財...i3xiaoqiang...i5旺財...i4xiaoqiang...i6旺財...i5xiaoqiang...i7旺財...i6xiaoqiang...i8旺財...i7xiaoqiang...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 args21 */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-0xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...i...name: Thread-1xiaoqiang...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 args19 */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-1Thread-0Thread-1Thread-0Thread-1Thread-0Thread-0Thread-1Thread-0Thread-1Thread-0Thread-1Thread-0Thread-1Thread-0Thread-1Thread-1Thread-1Thread-0Thread-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...100Thread-3...sale...97Thread-2...sale...98Thread-1...sale...99Thread-2...sale...94Thread-3...sale...95Thread-0...sale...96Thread-3...sale...91Thread-2...sale...92Thread-2...sale...88Thread-2...sale...87Thread-2...sale...86Thread-1...sale...93Thread-1...sale...84Thread-1...sale...83Thread-2...sale...85Thread-3...sale...89Thread-0...sale...90Thread-3...sale...80Thread-2...sale...81Thread-1...sale...82Thread-1...sale...76Thread-1...sale...75Thread-2...sale...77Thread-3...sale...78Thread-0...sale...79Thread-3...sale...72Thread-2...sale...73Thread-1...sale...74Thread-2...sale...69Thread-3...sale...70Thread-0...sale...71Thread-3...sale...66Thread-2...sale...67Thread-1...sale...68Thread-2...sale...63Thread-2...sale...61Thread-3...sale...64Thread-0...sale...65Thread-3...sale...59Thread-2...sale...60Thread-1...sale...62Thread-2...sale...56Thread-3...sale...57Thread-0...sale...58Thread-3...sale...53Thread-2...sale...54Thread-1...sale...55Thread-1...sale...49Thread-2...sale...50Thread-3...sale...51Thread-0...sale...52Thread-0...sale...45Thread-3...sale...46Thread-2...sale...47Thread-1...sale...48Thread-2...sale...42Thread-3...sale...43Thread-0...sale...44Thread-3...sale...39Thread-2...sale...40Thread-1...sale...41Thread-2...sale...36Thread-3...sale...37Thread-0...sale...38Thread-3...sale...33Thread-2...sale...34Thread-1...sale...35Thread-2...sale...30Thread-3...sale...31Thread-0...sale...32Thread-3...sale...27Thread-2...sale...28Thread-1...sale...29Thread-2...sale...24Thread-3...sale...25Thread-0...sale...26Thread-3...sale...21Thread-2...sale...22Thread-1...sale...23Thread-2...sale...18Thread-3...sale...19Thread-0...sale...20Thread-3...sale...15Thread-2...sale...16Thread-1...sale...17Thread-2...sale...12Thread-3...sale...13Thread-0...sale...14Thread-3...sale...9Thread-2...sale...10Thread-1...sale...11Thread-2...sale...6Thread-3...sale...7Thread-0...sale...8Thread-3...sale...3Thread-2...sale...4Thread-1...sale...5Thread-3...sale...1Thread-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 block14 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...100Thread-2...sale...98Thread-1...sale...100Thread-0...sale...99Thread-0...sale...95Thread-3...sale...97Thread-2...sale...97Thread-1...sale...96Thread-2...sale...94Thread-3...sale...94Thread-1...sale...94Thread-0...sale...94Thread-0...sale...93Thread-3...sale...91Thread-1...sale...92Thread-2...sale...93Thread-3...sale...88Thread-0...sale...89Thread-2...sale...89Thread-1...sale...90Thread-0...sale...87Thread-2...sale...84Thread-3...sale...85Thread-1...sale...86Thread-2...sale...83Thread-3...sale...82Thread-1...sale...82Thread-0...sale...83Thread-0...sale...81Thread-3...sale...80Thread-1...sale...80Thread-2...sale...81Thread-3...sale...78Thread-0...sale...79Thread-1...sale...79Thread-2...sale...77Thread-2...sale...76Thread-1...sale...75Thread-3...sale...75Thread-0...sale...76Thread-2...sale...74Thread-1...sale...73Thread-3...sale...73Thread-0...sale...74Thread-0...sale...72Thread-1...sale...70Thread-3...sale...71Thread-2...sale...69Thread-0...sale...68Thread-2...sale...65Thread-1...sale...67Thread-3...sale...66Thread-1...sale...64Thread-2...sale...61Thread-0...sale...62Thread-3...sale...63Thread-2...sale...59Thread-1...sale...57Thread-3...sale...60Thread-0...sale...58Thread-2...sale...56Thread-1...sale...55Thread-3...sale...54Thread-0...sale...53Thread-1...sale...52Thread-2...sale...51Thread-0...sale...50Thread-3...sale...49Thread-2...sale...48Thread-1...sale...47Thread-3...sale...46Thread-0...sale...45Thread-1...sale...44Thread-3...sale...42Thread-0...sale...43Thread-2...sale...41Thread-1...sale...39Thread-0...sale...38Thread-3...sale...40Thread-2...sale...37Thread-2...sale...36Thread-0...sale...34Thread-3...sale...35Thread-1...sale...33Thread-1...sale...32Thread-2...sale...31Thread-3...sale...30Thread-0...sale...29Thread-0...sale...28Thread-3...sale...27Thread-1...sale...26Thread-2...sale...25Thread-1...sale...24Thread-2...sale...23Thread-0...sale...22Thread-3...sale...21Thread-3...sale...20Thread-1...sale...18Thread-2...sale...19Thread-0...sale...20Thread-2...sale...17Thread-3...sale...15Thread-0...sale...16Thread-1...sale...14Thread-3...sale...13Thread-0...sale...12Thread-2...sale...11Thread-1...sale...10Thread-0...sale...9Thread-3...sale...8Thread-1...sale...7Thread-2...sale...6Thread-3...sale...5Thread-0...sale...4Thread-2...sale...3Thread-1...sale...2Thread-3...sale...1Thread-0...sale...0Thread-2...sale...-1Thread-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 block16 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...100Thread-0...sale...99Thread-0...sale...98Thread-0...sale...97Thread-0...sale...96Thread-0...sale...95Thread-0...sale...94Thread-0...sale...93Thread-0...sale...92Thread-0...sale...91Thread-0...sale...90Thread-0...sale...89Thread-0...sale...88Thread-2...sale...87Thread-2...sale...86Thread-2...sale...85Thread-3...sale...84Thread-3...sale...83Thread-3...sale...82Thread-3...sale...81Thread-1...sale...80Thread-1...sale...79Thread-1...sale...78Thread-1...sale...77Thread-1...sale...76Thread-1...sale...75Thread-3...sale...74Thread-2...sale...73Thread-2...sale...72Thread-2...sale...71Thread-2...sale...70Thread-2...sale...69Thread-2...sale...68Thread-0...sale...67Thread-0...sale...66Thread-0...sale...65Thread-0...sale...64Thread-0...sale...63Thread-0...sale...62Thread-2...sale...61Thread-2...sale...60Thread-3...sale...59Thread-3...sale...58Thread-3...sale...57Thread-3...sale...56Thread-3...sale...55Thread-3...sale...54Thread-3...sale...53Thread-3...sale...52Thread-3...sale...51Thread-3...sale...50Thread-3...sale...49Thread-3...sale...48Thread-3...sale...47Thread-3...sale...46Thread-3...sale...45Thread-3...sale...44Thread-3...sale...43Thread-3...sale...42Thread-3...sale...41Thread-1...sale...40Thread-1...sale...39Thread-1...sale...38Thread-1...sale...37Thread-1...sale...36Thread-1...sale...35Thread-1...sale...34Thread-1...sale...33Thread-1...sale...32Thread-1...sale...31Thread-1...sale...30Thread-1...sale...29Thread-1...sale...28Thread-1...sale...27Thread-1...sale...26Thread-1...sale...25Thread-3...sale...24Thread-2...sale...23Thread-2...sale...22Thread-2...sale...21Thread-0...sale...20Thread-0...sale...19Thread-0...sale...18Thread-0...sale...17Thread-2...sale...16Thread-2...sale...15Thread-2...sale...14Thread-2...sale...13Thread-3...sale...12Thread-3...sale...11Thread-3...sale...10Thread-3...sale...9Thread-3...sale...8Thread-3...sale...7Thread-1...sale...6Thread-1...sale...5Thread-1...sale...4Thread-1...sale...3Thread-3...sale...2Thread-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 args26 */27 public static void main(String[] args) {28 // TODO Auto-generated method stub29 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 = 100sum = 200sum = 300sum = 500sum = 400sum = 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 = 200sum = 200sum = 400sum = 400sum = 600sum = 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 block11 e.printStackTrace();12 }13 System.out.println("sum = " + sum);14 }15 }16 }運行結果:
sum = 100sum = 200sum = 300sum = 400sum = 500sum = 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 block11 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 block22 e.printStackTrace();23 }24 System.out.println(Thread.currentThread().getName() + "...bloc k..." + num--);25 }26 }27 }28 29 }//end if30 else31 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 block44 e.printStackTrace();45 }46 System.out.println(Thread.currentThread().getName() + "...function..." + num--);47 }48 } 49 }50 51 public class SynFunctionLockDemo52 {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...100Thread-0...function...99Thread-0...function...98Thread-0...function...97Thread-0...function...96Thread-0...function...95Thread-0...function...94Thread-0...function...93Thread-0...function...92Thread-0...function...91Thread-0...function...90Thread-0...function...89Thread-0...function...88Thread-0...function...87Thread-0...function...86Thread-0...function...85Thread-1...function...84Thread-1...function...83Thread-1...function...82Thread-1...function...81Thread-0...function...80Thread-0...function...79Thread-0...function...78Thread-0...function...77Thread-0...function...76Thread-0...function...75Thread-0...function...74Thread-1...function...73Thread-1...function...72Thread-0...function...71Thread-0...function...70Thread-0...function...69Thread-1...function...68Thread-1...function...67Thread-1...function...66Thread-0...function...65Thread-0...function...64Thread-0...function...63Thread-0...function...62Thread-1...function...61Thread-1...function...60Thread-1...function...59Thread-1...function...58Thread-0...function...57Thread-0...function...56Thread-0...function...55Thread-1...function...54Thread-1...function...53Thread-1...function...52Thread-1...function...51Thread-1...function...50Thread-1...function...49Thread-1...function...48Thread-1...function...47Thread-0...function...46Thread-0...function...45Thread-0...function...44Thread-0...function...43Thread-1...function...42Thread-1...function...41Thread-1...function...40Thread-1...function...39Thread-1...function...38Thread-1...function...37Thread-1...function...36Thread-1...function...35Thread-1...function...34Thread-1...function...33Thread-1...function...32Thread-1...function...31Thread-1...function...30Thread-1...function...29Thread-1...function...28Thread-1...function...27Thread-0...function...26Thread-0...function...25Thread-0...function...24Thread-1...function...23Thread-1...function...22Thread-1...function...21Thread-1...function...20Thread-1...function...19Thread-1...functi
新聞熱點
疑難解答