順着我的思路,一步一步往下看,你會有所收獲。。。。
實現多線程有兩種方式,代碼如下
1.繼承Thread類:
code1:
public class Test { public static void main(String[] args) { Ticket ticket = new Ticket(); ticket.start(); } } class Ticket extends Thread{ @Override public void run() { System.out.println("Hello ...."); } } 執行結果:Hello ....
2.實現Runnable接口
code2:
public class Test { public static void main(String[] args) { Ticket ticket = new Ticket(); new Thread(ticket).start(); } } class Ticket implements Runnable{ @Override public void run() { System.out.println("Hello ...."); } } 執行結果:Hello ....
在Java API 中,我們可以找到很多Thread封裝的方法,當我們創建的線程數比較多的時候,我們可以為每個線程創建名稱
code3:
class Ticket implements Runnable{ @Override public void run() { System.out.println("Hello ...."+Thread.currentThread().getName()); } } 執行結果:Hello ....Thread-0 是不是覺得這個名字不好看? 線程默認名稱都是:Thread-0、Thread-1 。。n
查找API,我們得知Thread類中有一個super(String name)方法,這個方法是給線程命名的,也就是說,我們繼承了Thread類的子類,能夠將線程名稱替換掉
code4:
public class Test { public static void main(String[] args) { Ticket ticket = new Ticket("Ticket"); ticket.start(); } } class Ticket extends Thread{ Ticket(String name){ super(name); } @Override public void run() { System.out.println("Hello ...."+Thread.currentThread().getName()); } } 執行結果:Hello ....Ticket
閱讀到此處,相信你已經了解了創建線程的方法,接下來,我們看一個簡單的售票例子,假設同時有兩個售票窗口售票,一共有5張票可以賣:code:5
public class Test { public static void main(String[] args) { Ticket one = new Ticket("一號"); Ticket two = new Ticket("二號"); one.start(); two.start(); } } class Ticket extends Thread{ private int ticket = 5; Ticket(String name){ super(name); } @Override public void run() { while(true){ if(ticket>0) System.out.println(Thread.currentThread().getName()+"窗口賣票..."+ ticket--); } } }
執行結果: 一號窗口賣票...5 一號窗口賣票...4 一號窗口賣票...3 一號窗口賣票...2 一號窗口賣票...1 二號窗口賣票...5 二號窗口賣票...4 二號窗口賣票...3 二號窗口賣票...2 二號窗口賣票...1
共賣出了10張票,什么原因導致的?我們來分析下:
通過繼承Thread類,定義了ticket=5(票數),然后在main方法中創建了兩個Ticket售票窗口線程,再調用start方法來開啟線程,問題就在,線程中的票數ticket沒有被共享,它是屬於每個單獨的線程的,
一號有5張票,二號有5張票,So.... 問題找到了,既然繼承Thread類搞定不了,那么我們來試試實現Runnable方法
code6:
public class Test { public static void main(String[] args) { Ticket one = new Ticket(); new Thread(one).start(); new Thread(one).start(); } } class Ticket implements Runnable{ private int ticket = 5; @Override public void run() { while(true){ if(ticket>0) System.out.println(Thread.currentThread().getName()+"窗口賣票..."+ ticket--); } } }
執行結果: Thread-0窗口賣票...5 Thread-0窗口賣票...3 Thread-0窗口賣票...2 Thread-0窗口賣票...1 Thread-1窗口賣票...4
每次執行,順序可能都不一致,但結果是正確的,賣出了5張票。
你可能會想,為什么不創建兩個Ticket對象,再創建兩個線程分別來start()呢,如下代碼
code7:
public static void main(String[] args) { Ticket one = new Ticket(); Ticket two = new Ticket(); new Thread(one).start(); new Thread(two).start(); } class Ticket { 內容不變... }
執行結果: Thread-0窗口賣票...5 Thread-1窗口賣票...5 Thread-0窗口賣票...4 Thread-1窗口賣票...4 Thread-0窗口賣票...3 Thread-1窗口賣票...3 Thread-0窗口賣票...2 Thread-1窗口賣票...2 Thread-0窗口賣票...1 Thread-1窗口賣票...1
看執行結果,賣出了雙份票,成員變量ticket還是沒有被共享。。。懂了吧。。。。
回過頭來看代碼code:6,這一步執行結果正確,難道就真的沒問題了嗎?看下面代碼
code8:
class Ticket implements Runnable{ private int ticket = 1000; @Override public void run() { while(true){ if(ticket>0){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"窗口賣票..."+ ticket--); } } } }
分析:在判斷ticket條件中,加了一個Thread.sleep(10)方法,讓當前線程進來的是時候睡個10毫秒,你會發現結果與預期的不一致
執行結果: .... Thread-1窗口賣票...4 Thread-0窗口賣票...3 Thread-1窗口賣票...2 Thread-1窗口賣票...1 Thread-0窗口賣票...0
我們賣出了0號票,多執行幾次,可能還會賣出-1、-2號票
這里涉及一個知識點:線程安全,那我們接下來就學習下,什么是線程安全,百度百科如下:
定義:
個人總結:多線程訪問同一代碼,不會產生不確定的結果
如何做到線程安全?兩個字:同步(synchronized),百度到同步的方式有多種,同步代碼塊、同步函數(方法)
1.同步代碼塊:
語法:synchronized (鎖對象){ 需要被同步的代碼 }
同步前提:
1.必須要有兩個或以上的線程
2.必須是多個線程使用同一個鎖
怎么判斷哪些代碼需要同步:
1.哪些代碼是多線程運行代碼
2.哪些數據是共享數據
3.哪些多線程代碼是操作共享數據的
下面的ticket就是共享數據(A窗口賣過了的票,B窗口就不能再賣了)
code9:
class Ticket implements Runnable{ private int ticket = 100; Object obj = new Object(); @Override public void run() { while(true){ synchronized (obj){ if(ticket>0){ try { Thread.sleep(10) System.out.println(Thread.currentThread().getName()+"窗口賣票..."+ ticket--); } catch (InterruptedException e) { e.printStackTrace(); } } } } } }
執行結果: ..... Thread-0窗口賣票...6 Thread-0窗口賣票...5 Thread-1窗口賣票...4 Thread-1窗口賣票...3 Thread-1窗口賣票...2 Thread-1窗口賣票...1
暫時先不講為什么要放一個obj(你可以放別的,例如this,下文中會介紹這個鎖對象的),加了同步后結果正確了。為什么加了同步代碼塊,就Ok了呢 ?
分析:現在有兩個線程(上面說的兩個買票窗口),分別叫A跟B,假設A調用run方法時進入同步代碼快,獲得了當前代碼的執行權並鎖定,此時如果B進來,B是執行不了同步代碼塊中的內容的,B要等待A執行完成,才能進入同步代碼塊內鎖定代碼並執行相應內容
案例:大家都坐過火車吧,你進廁所,把門鎖了,就你能上,別人要在門口等着你,你上完了(代碼執行完了),把門打開了(釋放鎖),別人才能進去,當然也有可能你剛打開門,然后你又拉肚子了,然后又進去了。。。哈哈。。
好處:解決了多線程的安全問題
弊端: 多個線程需要判斷鎖,比較消耗資源
2.同步函數(方法),既然同步代碼塊是用來封裝代碼的,函數也有同樣的功能,那么我們來試試
code10:
class Ticket implements Runnable{ private int ticket = 100; @Override public void run() { while(true){ this.sale(); } } public synchronized void sale(){ if(ticket>0){ try { Thread.sleep(10); System.out.println(Thread.currentThread().getName()+"窗口賣票..."+ ticket--); } catch (InterruptedException e) { e.printStackTrace(); } } } } 執行結果與code9 一致,正確。
區別於code9中的同步代碼塊中的obj鎖對象,那么同步函數的鎖對象是誰呢?
猜想:code10中用的this.sale()調用售票方法,this代表當前對象Ticket,那么同步函數的鎖,就是當前對象Ticket,看下面代碼,證明這個猜想
code11:
public class Test { public static void main(String[] args) { try { Ticket one = new Ticket(); new Thread(one).start(); Thread.sleep(10); one.flag = false; new Thread(one).start(); } catch (Exception e) { e.printStackTrace(); } } } class Ticket implements Runnable{ private int ticket = 1000; private Object obj = new Object(); boolean flag = true; @Override public void run() { if(flag){ synchronized(obj){ while(true){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+"同步代碼塊..."+ ticket--); } } } }else{ while(true) this.sale(); } } public synchronized void sale(){ //this if(ticket>0){ System.out.println(Thread.currentThread().getName()+"同步方法..."+ ticket--); } } }
執行結果(可能與你的執行結果不一致): ..... Thread-1同步代碼塊...3 Thread-0同步代碼塊...2 Thread-0同步代碼塊...1 Thread-0同步代碼塊...0
代碼分析: main方法執行,創建兩個線程,第一個線程調用start()獲得執行權,主線程main繼續往下執行,睡10毫秒,將變量設置為false,另一個線程調用start()獲得執行權,主線程執行結束,現在就剩兩個售票線程了(一個線程執行同步代碼塊中的內容,另一個線程執行同步函數的內容)
我們發現出現了0號票,也就是線程不安全了?為什么?我明明加了同步方法,也加了同步代碼塊,為什么還是線程不安全的呢?
回顧上面所說的同步的兩個前提:
1.必須要有兩個或以上的線程
2.必須是多個線程使用同一個鎖
兩個條件都滿足了嗎?看看條件1,滿足了,那就是條件2出了問題了咯 ???
code11中,同步代碼塊中,用的是obj對象,而同步函數中,用的是this,那么到此,我們可以肯定的是,同步函數肯定用的不是obj,對吧? 上面猜想中,我說的同步函數用的是this,那么,我們把obj改成this,如下:
code12:
class Ticket implements Runnable{ private int ticket = 1000; //private Object obj = new Object(); boolean flag = true; @Override public void run() { if(flag){ synchronized(this){ while(true){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+"同步代碼塊..."+ ticket--); } } } }else{ while(true) this.sale(); } } public synchronized void sale(){ //this if(ticket>0){ System.out.println(Thread.currentThread().getName()+"同步方法..."+ ticket--); } } }
執行結果: ..... Thread-1同步代碼塊...3 Thread-0同步代碼塊...2 Thread-0同步代碼塊...1
線程安全了,沒有出現0號票。
結論:同步函數用的鎖是this
此時,我們了解到,同步函數用的鎖是 this ,那么我們接下來,在同步函數上加下個靜態標示符static試試:
public class Test { public static void main(String[] args) { try { Ticket one = new Ticket(); new Thread(one).start(); Thread.sleep(10); one.flag = false; new Thread(one).start(); } catch (Exception e) { e.printStackTrace(); } } } class Ticket implements Runnable{ private static int ticket = 1000; boolean flag = true; @Override public void run() { if(flag){ synchronized(this){ while(true){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+"同步代碼塊..."+ ticket--); } } } }else{ while(true) this.sale(); } } public static synchronized void sale(){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+"同步方法..."+ ticket--); } } }
執行結果: .... 二號窗口賣票...2 二號窗口賣票...1 二號窗口賣票...0
好吧,又出現了0號票。線程又不安全了。思考線程安全的連個前提:
1.必須要有兩個或以上的線程
2.必須是多個線程使用同一個鎖
肯定是2沒滿足,那么,靜態同步函數的鎖對象不是this,是什么呢?
我們知道靜態資源的特點:進內存的時候,內存中沒有本類的對象,那么有誰?靜態方法是不是由類調用的 ?類在進內存的時候,有對象嗎? 有,就是那份字節碼文件對象(Ticket.class),Ticket進內存,緊跟着,靜態資源進內存,OK,我們來試試。。
將上面同步代碼塊中的this鎖換成如下:
synchronized(Ticket.class){ while(true){ if(ticket>0){ System.out.println(Thread.currentThread().getName()+"同步代碼塊..."+ ticket--); } }
執行結果: Thread-0同步代碼塊...5 Thread-0同步代碼塊...4 Thread-0同步代碼塊...3 Thread-0同步代碼塊...2 Thread-0同步代碼塊...1
最后一張為1號票,線程安全。
結論:靜態同步函數使用的鎖是該方法所在類的字節碼文件對象,也就是 類名.class。