第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 方法的簽名{
方法體
}
過多的同步可能造成死鎖。
五、生產者消費者模式
六、任務調度