順着個人思路,一步一步往下看,你會有所收穫。。。。java
實現多線程有兩種方式,代碼以下安全
1.繼承Thread類:多線程
code1:ide
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封裝的方法,當咱們建立的線程數比較多的時候,咱們能夠爲每一個線程建立名稱this
code3:spa
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:code
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號票,多執行幾回,可能還會賣出-一、-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。