第10次文章:深入線程

  • 2019 年 10 月 8 日
  • 筆記

時間真的是快,經不起浪費啊!加油!

一、同步

當多個線程訪問同一個資源時,由於每個線程訪問同一份資源的時候,會有時間差。所以很有可能多個線程同時進入同一份資源,然後使得資源的自身信息沒有及時得到更新,造成錯誤輸出的情況出現,這就是所謂的線程不安全。為了確保資源的安全,也就是確保線程安全,我們使用關鍵字synchronized,對需要確保安全的代碼進行同步處理。

使用synchronized的基本原理是:當已經有線程進入資源時,此時計算機會給當前資源一把鎖,鎖住當前資源,其他的線程只能在外部進行等待,線程被阻塞掛起。當訪問該資源的線程結束訪問的時候,系統會將該鎖釋放,整個程序進入運行狀態,這樣就避免了多個進程同時訪問同一份資源的問題。

有兩種方法可確保線程的同步:

方法1、同步方法:

synchronized

方法2、同步塊:

synchronized(引用類型 | this | 類.class){

}

public class SynDemo01 {    public static void main(String[] args) {      //新建實體對象      Web12306 web = new Web12306();      //創建代理      Thread t1 = new Thread(web,"黃牛1");      Thread t2 = new Thread(web,"黃牛2");      Thread t3 = new Thread(web,"黃牛3");      //啟動線程      t1.start();      t2.start();      t3.start();    }  }  class Web12306 implements Runnable{    private int num = 10;    private boolean flag = true;    @Override    public void run() {      while(flag) {        test3();      }    }    //線程不安全    private void test1() {      if (0 >= num) {        this.flag = false;//跳出循環        return;      }      try {        Thread.sleep(100);//模擬網絡延時      } catch (InterruptedException e) {        e.printStackTrace();      }      System.out.println(Thread.currentThread().getName()+"搶到了第"+num--+"張票");    }    //線程安全    private synchronized void test2() {      if (0 >= num) {        this.flag = false;//跳出循環        return;      }      try {        Thread.sleep(500);//模擬網絡延時      } catch (InterruptedException e) {        e.printStackTrace();      }      System.out.println(Thread.currentThread().getName()+"搶到了第"+num--+"張票");    }    //線程安全  資源鎖定正確    private  void test3() {      synchronized(this) {        if (0 >= num) {          this.flag = false;//跳出循環          return;        }        try {          Thread.sleep(500);//模擬網絡延時        } catch (InterruptedException e) {          e.printStackTrace();        }        System.out.println(Thread.currentThread().getName()+"搶到了第"+num--+"張票");      }    }  }

解析:test1方法中,沒有加入synchronized關鍵字進行同步。使用此方法的時候,會導致線程"黃牛1","黃牛2","黃牛3"同時進入引用類對象"Web12306"當中訪問剩餘的票數,所以輸出的結果會有:「黃牛1搶到了第-1張票」,這種明顯錯誤的結果,就是因為未同步而造成的。

test2方法中,使用的是利用synchronized關鍵字鎖定整個方法,也就是我們上面介紹的方法1:同步方法。將整個方法進行同步處理。但是正如我們所講述的原理一樣,同步方法的關鍵就在於阻塞線程,所以阻塞的內容越多,整體的運行速度會明顯下降。最終造成低效率的結果。

test3方法中,使用的是我們介紹的方法2:同步塊。在此方法中我們可以根據自己的分析,判斷哪一個地方最有可能出現安全隱患,然後加入同步塊,這樣就可以適當的減少相應的阻塞內容,在一定的程度上提高代碼運行效率。

二、死鎖

在我們使用多個同步的時候,假如我們的多線程訪問的資源相互同步,然後每個線程都不釋放自己的鎖,那麼就很容易造成死鎖的情況。此時,所有的線程都會被掛起,然後相互等待,一直到系統奔潰。所以過多的同步容易造成死鎖。

解決死鎖的一種方式:生產者與消費者模式

當生產者進行生產操作的時候,消費者被掛起,停止消費;當消費者在消費的時候,生產者被掛起,消費者進行消費。可以使用一種信號燈法進行操作。

信號燈法:

1、wait():等待,釋放鎖

2、notify()/notifyAll():喚醒

與synchronized一起使用

第一步:我們創建一個電影院場景,其中包含有play(生產者)和watch(消費者)

public class Movie {    private String pic;    //信號燈    //flag---->T 生產者生產,消費者等待,生產完成後通知消費    //flag---->F 消費者消費,生產者等待,消費完成後通知生產    private boolean flag = true;    /**     * 播放,相當於生產者     * @param pic     */    public synchronized void play(String pic) {      if(!flag) {//即:生產者等待        try {          this.wait();        } catch (InterruptedException e) {          // TODO Auto-generated catch block          e.printStackTrace();        }      }      //生產者生產      try {        Thread.sleep(500);//模擬生產了500毫秒      } catch (InterruptedException e) {        // TODO Auto-generated catch block        e.printStackTrace();      }      //生產結束      this.pic = pic;      System.out.println("生產了:"+pic);      //生產者停下      this.flag = false;      //通知消費者      this.notifyAll();    }    /**     * 觀看,相當於消費者     */    public synchronized void watch() {      if(flag) {//生產者在生產,消費者等待        try {          this.wait();        } catch (InterruptedException e) {          // TODO Auto-generated catch block          e.printStackTrace();        }      }      //消費者消費      try {        Thread.sleep(200);//假設消費200毫秒就停下      } catch (InterruptedException e) {        // TODO Auto-generated catch block        e.printStackTrace();      }      //消費結束      System.out.println("消費了:"+pic);      //消費者停下      this.flag = true;      //通知生產者生產      this.notifyAll();      }  }

第二步:我們創建相應的生產者player和消費者watcher,對同一份資源movie進行訪問。

public class Player implements Runnable {    private Movie m ;    public Player(Movie m) {      super();      this.m = m;    }    @Override    public void run() {      for(int i = 0; i < 20 ;i++) {        if(0 == i%2) {          m.play("左青龍"+i);        }else {          m.play("右白虎"+i);        }      }    }  }
public class Watcher implements Runnable {    private Movie m ;    public Watcher(Movie m) {      super();      this.m = m;    }    @Override    public void run() {      for(int i = 0; i < 20 ;i++) {        m.watch();      }    }  }

第三步:對生產者和消費者進行應用

public class App {    public static void main(String[] args) {      Movie m = new Movie();      //共享資源      Player p = new Player(m);      Watcher w = new Watcher(m);      new Thread(p).start();      new Thread(w).start();    }  }

第四步:查看一下運行結果

生產了:左青龍0  消費了:左青龍0  生產了:右白虎1  消費了:右白虎1  生產了:左青龍2  消費了:左青龍2  生產了:右白虎3  消費了:右白虎3  生產了:左青龍4  消費了:左青龍4  生產了:右白虎5  消費了:右白虎5  生產了:左青龍6  消費了:左青龍6  生產了:右白虎7  消費了:右白虎7  生產了:左青龍8  消費了:左青龍8  生產了:右白虎9  消費了:右白虎9  生產了:左青龍10  消費了:左青龍10  生產了:右白虎11  消費了:右白虎11  生產了:左青龍12  消費了:左青龍12  生產了:右白虎13  消費了:右白虎13  生產了:左青龍14  消費了:左青龍14  生產了:右白虎15  消費了:右白虎15  生產了:左青龍16  消費了:左青龍16  生產了:右白虎17  消費了:右白虎17  生產了:左青龍18  消費了:左青龍18  生產了:右白虎19  消費了:右白虎19

解析:對最終的結果,可以明顯看出所有線程都是一種規律性的出現,不會是隨機出現的結果。在線程等待的時候需要注意一點:wait是將線程進行阻塞掛起,並且釋放鎖。而sleep方法,僅僅是將線程掛起,不釋放鎖。所以當我們使用sleep的時候,將會使得整個線程阻塞相應的時間後,再重新開始運行。與此同時,其他線程的狀態並不會有所改變。

三、任務調度

了解一個類:Timer()

主要用於任務在不同時間的執行情況,具體使用如下所示:

public class TimeDemo01 {    public static void main(String[] args) {      Timer time = new Timer();      //語法:schedule(TimerTask task, Date firstTime, long period)      time.schedule(new TimerTask() {//使用匿名內部類        @Override        public void run() {          System.out.println("所謂的線程,也就是換一種類,然後運行run裏面的代碼");          }},new  Date(System.currentTimeMillis()+2000),//當前時間過後兩秒開始運行          2000);//每間隔2秒運行1次    }  }

Timer類主要是使用schedule方法,該方法主要的幾條語句如下所示:

僅將線程運行一次:

schedule(TimerTask task, Date time)

schedule(TimerTask task, long delay)

間隔period時間後,再運行的語句:

schedule(TimerTask task, long delay, long period)

schedule(TimerTask task, Date firstTime, long period)

注意:在使用schedule的時候,我們涉及到了TimerTask類別,這個類別實現了Runnable接口,所以在創建該類別的時候,就可以將其當做一個實現了Runable接口的類來處理,直接新建之後,重寫它的Run()方法就好了。

結合上一篇文章,我們對線程進行總結,同時結束線程的學習,進入下一個內容:

一、創建線程 重點

1、繼承 Thread

2、實現 Runnable

3、實現 Callable (了解即可)

二、線程的狀態

1、新生—>start—>就緒—->運行—>阻塞—>終止

2、終止線程(重點)

3、阻塞:join yield sleep(不是釋放鎖)

三、線程的信息

1、Therad.currentThread

2、獲取名稱 設置名稱 設置優先級 判斷狀態

四、同步:多線程使用同一份資源

synchronized (引用類型變量|this|類.class){

}

修飾符 synchronized 方法的簽名{

方法體

}

過多的同步可能造成死鎖。

五、生產者消費者模式

六、任務調度