[一]多線程編程-實現及鎖機制

順着個人思路,一步一步往下看,你會有所收穫。。。。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。

相關文章
相關標籤/搜索