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


順着我的思路,一步一步往下看,你會有所收獲。。。。

實現多線程有兩種方式,代碼如下

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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM