JAVA 系列——>線程同步 線程安全 線程狀態

如果有多個線程在同時運行,而這些線程可能會同時運行這段代碼。
程序每次運行結果和單線程運行的結果是一樣 的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。

我們通過一個案例,演示線程的安全問題:
電影院要賣票,我們模擬電影院的賣票過程。
假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共100個 (本場電影只能賣100張票)。
我們來模擬電影院的售票窗口,實現多個窗口同時賣 “葫蘆娃大戰奧特曼”這場電影票(多個窗口一起賣這100張票)
需要窗口,採用線程對象來模擬;需要票,Runnable接口子類來模擬

模擬票:

public class Ticket implements Runnable {
    private int ticket = 100;
    /**
     * 執行賣票操作
     */
    @Override
    public void run() { //每個窗口賣票的操作 //窗口 永遠開啓
        while (true) {
            if (ticket > 0) {//有票 可以賣 //出票操作 //使用sleep模擬一下出票時間
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto‐generated catch block
                    e.printStackTrace();
                }//獲取當前線程對象的名字 
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣:" + ticket--);
            }
        }
    }
}
         

測試類:

    public static void main(String[] args) {

        //創建線程任務對象
        Ticket ticket = new Ticket(); 
        //創建三個窗口對象 
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");
        //同時賣票
        t1.start();
        t2.start();
        t3.start();
    }

運行結果
在這裏插入圖片描述
發現程序出現了兩個問題:

  1. 相同的票數,比如5這張票被賣了兩回。
  2. 不存在的票,比如0票與-1票,是不存在的。

這種問題,幾個窗口(線程)票數不同步了,這種問題稱爲線程不安全。

線程安全問題都是由全局變量及靜態變量引起的。若每個線程中對全局變量、靜態變量只有讀操作,而無寫 操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步, 否則的話就可能影響線程安全。

線程同步

當我們使用多個線程訪問同一資源的時候,且多個線程中對資源有寫的操作,就容易出現線程安全問題。
要解決上述多線程併發訪問一個資源的安全性問題:也就是解決重複票與不存在票問題,Java中提供了同步機制 (synchronized)來解決。
PS:synchronized可修飾main方法( public static synchronized void main(String[] args) {})

根據案例簡述:

窗口1線程進入操作的時候,窗口2和窗口3線程只能在外等着,窗口1操作結束,窗口1和窗口2和窗口3纔有機會進入代碼 去執行。
也就是說在某個線程修改共享資源的時候,其他線程不能修改該資源,等待修改完畢同步之後,才能去搶奪CPU 資源,完成對應的操作,保證了數據的同步性,解決了線程不安全的現象。

爲了保證每個線程都能正常執行原子操作,Java引入了線程同步機制。

那麼怎麼去使用呢?有三種方式完成同步操作

  1. 同步代碼塊。
  2. 同步方法。
  3. 鎖機制。

1.同步代碼塊

synchronized 關鍵字可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。 格式:

 synchronized(同步鎖){ 
     需要同步操作的代碼
  }

同步鎖:
對象的同步鎖只是一個概念,可以想象爲在對象上標記了一個鎖.

  1. 鎖對象 可以是任意類型。
  2. 多個線程對象 要使用同一把鎖

注意:在任何時候,最多允許一個線程擁有同步鎖,誰拿到鎖就進入代碼塊,其他的線程只能在外等着 (BLOCKED)。

使用同步代碼塊解決代碼:

public class Ticket implements Runnable {
    private int ticket = 100;
    Object lock = new Object();
    /**
     * 執行賣票操作
     */
    @Override
    public void run() { //每個窗口賣票的操作 //窗口 永遠開啓
        while (true) {
            synchronized (lock) {
                if (ticket > 0) {//有票 可以賣
                    // 出票操作
                    // 使用sleep模擬一下出票時間
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        // TODO Auto‐generated catch block
                        e.printStackTrace();
                    }//獲取當前線程對象的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在賣:" + ticket--);
                }
            }
        }
    }
}

在這裏插入圖片描述

2.同步方法

使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等着。
格式:

public synchronized void method(){ 
   可能會產生線程安全問題的代碼 
}

同步鎖是誰?
對於非static方法,同步鎖就是this。
對於static方法,我們使用當前方法所在類的字節碼對象(類名.class)。

使用同步方法代碼如下:

public class Ticket implements Runnable {
    private int ticket = 100;
    Object lock = new Object();

    /**
     * 執行賣票操作
     */
    @Override
    public void run() {
        //每個窗口賣票的操作
        // 窗口 永遠開啓
        while (true) {
            sellTicket();
        }
    }
  /*
        定義一個同步方法
        同步方法也會把方法內部的代碼鎖住
        只讓一個線程執行
        同步方法的鎖對象是誰?
        就是實現類對象 new RunnableImpl()
        也是就是this
     */
    public synchronized void sellTicket() {
        if (ticket > 0) {//有票 可以賣
            // 出票操作
            // 使用sleep模擬一下出票時間
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                // TODO Auto‐generated catch block
                e.printStackTrace();
            }//獲取當前線程對象的名字
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在賣:" + ticket--);
        }
    }
}
****************************************************************************************************************
靜態同步方法
  /*
        靜態的同步方法
        鎖對象是誰?
        不能是this
        this是創建對象之後產生的,靜態方法優先於對象
        靜態方法的鎖對象是本類的class屬性-->class文件對象(反射)
     */
    public static /*synchronized*/ void payTicketStatic(){
        synchronized (RunnableImpl.class){
            //先判斷票是否存在
            if(ticket>0){
                //提高安全問題出現的概率,讓程序睡眠
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //票存在,賣票 ticket--
                System.out.println(Thread.currentThread().getName()+"-->正在賣第"+ticket+"張票");
                ticket--;
            }
        }

    }

3.Lock鎖

java.util.concurrent.locks.Lock 機制提供了比synchronized代碼塊和synchronized方法更廣泛的鎖定操作, 同步代碼塊/同步方法具有的功能Lock都有,除此之外更強大,更體現面向對象。

Lock鎖也稱同步鎖,加鎖與釋放鎖方法化了,如下:

 public void lock() :加同步鎖。
 public void unlock() :釋放同步鎖。

使用如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket implements Runnable {
    private int ticket = 100;
    Lock lock = new ReentrantLock();

    /**
     * 執行賣票操作
     */
    @Override
    public void run() { //每個窗口賣票的操作 //窗口 永遠開啓
        while (true) {
            lock.lock();
            if (ticket > 0) {//有票 可以賣
                // 出票操作
                // 使用sleep模擬一下出票時間
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto‐generated catch block
                    e.printStackTrace();
                }//獲取當前線程對象的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣:" + ticket--);
            }
            lock.unlock();
        }

    }
}

線程狀態

線程狀態概述

當線程被創建並啓動以後,它既不是一啓動就進入了執行狀態,也不是一直處於執行狀態。
在線程的生命週期中, 有幾種狀態呢?
在API中 java.lang.Thread.State 這個枚舉中給出了六種線程狀態
這裏先列出各個線程狀態發生的條件,下面將會對每種狀態進行詳細解析

我們不需要去研究這幾種狀態的實現原理,我們只需知道在做線程操作中存在這樣的狀態。
那我們怎麼去理解這幾個狀態呢,新建與被終止還是很容易理解的,我們就研究一下線程從Runnable(可運行)狀態與非運行狀態之間 的轉換問題。
在這裏插入圖片描述

Timed Waiting(計時等待)

Timed Waiting在API中的描述爲:一個正在限時等待另一個線程執行一個(喚醒)動作的線程處於這一狀態。單獨 的去理解這句話,真是玄之又玄,其實我們在之前的操作中已經接觸過這個狀態了,在哪裏呢?

在我們寫賣票的案例中,爲了減少線程執行太快,現象不明顯等問題,我們在run方法中添加了sleep語句,這樣就 強制當前正在執行的線程休眠(暫停執行),以“減慢線程”。

其實當我們調用了sleep方法之後,當前執行的線程就進入到“休眠狀態”,其實就是所謂的Timed Waiting(計時等 待),那麼我們通過一個案例加深對該狀態的一個理解。

實現一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字輸出一個字符串

代碼:

public class MyThread extends Thread {
    public void run() {
        for (int i = 0; i < 100; i++) {
            if ((i) % 10 == 0) {
                System.out.println("‐‐‐‐‐‐‐" + i);
            }
            System.out.print(i);
            try {
                Thread.sleep(1000);
                System.out.print(" 線程睡眠1秒!\n");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        new MyThread().start();
    }
}

部分結果
在這裏插入圖片描述
通過案例可以發現,sleep方法的使用還是很簡單的。我們需要記住下面幾點:

  1. 進入 TIMED_WAITING 狀態的一種常見情形是調用的 sleep 方法,單獨的線程也可以調用,不一定非要有協 作關係。
  2. 爲了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程 中會睡眠
  3. sleep與鎖無關,線程睡眠到期自動甦醒,並返回到Runnable(可運行)狀態。

小提示sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期後就開始立刻執行

Timed Waiting 線程狀態圖:

BLOCKED(鎖阻塞)

Blocked狀態在API中的介紹爲:一個正在阻塞等待一個監視器鎖(鎖對象)的線程處於這一狀態。

我們已經學完同步機制,那麼這個狀態是非常好理解的了。比如,線程A與線程B代碼中使用同一鎖,如果線程A獲 取到鎖,線程A進入到Runnable狀態,那麼線程B就進入到Blocked鎖阻塞狀態。

這是由Runnable狀態進入Blocked狀態。除此Waiting以及Time Waiting狀態也會在某種情況下進入阻塞狀態,而 這部分內容作爲擴充知識點帶領大家瞭解一下。

在這裏插入圖片描述

Waiting(無限等待)

Wating狀態在API中介紹爲:一個正在無限期等待另一個線程執行一個特別的(喚醒)動作的線程處於這一狀態。

在這裏插入圖片描述
那麼我們之前遇到過這種狀態嗎?答案是並沒有,但並不妨礙我們進行一個簡單深入的瞭解。我們通過一段代碼來 學習一下:

public class WaitingTest {
    public static Object obj = new Object();

    public static void main(String[] args) {
        // 演示waiting
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronized (obj) {
                        try {
                            System.out.println(Thread.currentThread().getName() + "=== 獲取到鎖對象,調用wait方法,進入waiting狀態,釋放鎖對象");
                            obj.wait();//無限等待
                            // obj.wait(5000); //計時等待, 5秒 時間到,自動醒來
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + " == = 從waiting狀 態醒來,獲取到鎖對象,繼續執行了");
                    }
                }
            }
        }, " 等待線程").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                // while (true){ //每隔3秒 喚醒一次
                try {
                    System.out.println(Thread.currentThread().getName() + "‐‐‐‐‐ 等待3秒鐘");
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj) {
                    System.out.println(Thread.currentThread().getName() + "‐‐‐‐‐ 獲取到鎖對象, 調用notify方法,釋放鎖對象");
                    obj.notify();
                }
            }
            // }
        }, " 喚醒線程").start();
    }
}

通過上述案例我們會發現,一個調用了某個對象的 Object.wait 方法的線程會等待另一個線程調用此對象的 Object.notify()方法 或 Object.notifyAll()方法。

其實waiting狀態並不是一個線程的操作,它體現的是多個線程間的通信,可以理解爲多個線程之間的協作關係, 多個線程會爭取鎖,同時相互之間又存在協作關係。就好比在公司裏你和你的同事們,你們可能存在晉升時的競爭,但更多時候你們更多是一起合作以完成某些任務。

當多個線程協作時,比如A,B線程,如果A線程在Runnable(可運行)狀態中調用了wait()方法那麼A線程就進入 了Waiting(無限等待)狀態,同時失去了同步鎖。假如這個時候B線程獲取到了同步鎖,在運行狀態中調用了 notify()方法,那麼就會將無限等待的A線程喚醒。注意是喚醒,如果獲取到鎖對象,那麼A線程喚醒後就進入 Runnable(可運行)狀態;如果沒有獲取鎖對象,那麼就進入到Blocked(鎖阻塞狀態)。

Waiting 線程狀態圖

在這裏插入圖片描述

買包子案例

/*
    等待喚醒案例:線程之間的通信
        創建一個顧客線程(消費者):告知老闆要的包子的種類和數量,調用wait方法,放棄cpu的執行,進入到WAITING狀態(無限等待)
        創建一個老闆線程(生產者):花了5秒做包子,做好包子之後,調用notify方法,喚醒顧客吃包子

    注意:
        顧客和老闆線程必須使用同步代碼塊包裹起來,保證等待和喚醒只能有一個在執行
        同步使用的鎖對象必須保證唯一
        只有鎖對象才能調用wait和notify方法

    Obejct類中的方法
    void wait()
          在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。
    void notify()
          喚醒在此對象監視器上等待的單個線程。
          會繼續執行wait方法之後的代碼
 */
public class Demo01WaitAndNotify {
    public static void main(String[] args) {
        //創建鎖對象,保證唯一
        Object obj = new Object();
        // 創建一個顧客線程(消費者)
        new Thread(){
            @Override
            public void run() {
               //一直等着買包子
               while(true){
                   //保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                   synchronized (obj){
                       System.out.println("告知老闆要的包子的種類和數量");
                       //調用wait方法,放棄cpu的執行,進入到WAITING狀態(無限等待)
                       try {
                           obj.wait();
                       } catch (InterruptedException e) {
                           e.printStackTrace();
                       }
                       //喚醒之後執行的代碼
                       System.out.println("包子已經做好了,開吃!");
                       System.out.println("---------------------------------------");
                   }
               }
            }
        }.start();

        //創建一個老闆線程(生產者)
        new Thread(){
            @Override
            public void run() {
                //一直做包子
                while (true){
                    //花了5秒做包子
                    try {
                        Thread.sleep(5000);//花5秒鐘做包子
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    //保證等待和喚醒的線程只能有一個執行,需要使用同步技術
                    synchronized (obj){
                        System.out.println("老闆5秒鐘之後做好包子,告知顧客,可以吃包子了");
                        //做好包子之後,調用notify方法,喚醒顧客吃包子
                        obj.notify();
                    }
                }
            }
        }.start();
    }
}

補充知識點

到此爲止我們已經對線程狀態有了基本的認識,想要有更多的瞭解,詳情可以見下圖:
在這裏插入圖片描述

一條有意思的tips:
我們在翻閱API的時候會發現Timed Waiting(計時等待) 與 Waiting(無限等待) 狀態聯繫還是很緊密的, 比如Waiting(無限等待) 狀態中wait方法是空參的,而timed waiting(計時等待) 中wait方法是帶參的。 這種帶參的方法,其實是一種倒計時操作,相當於我們生活中的小鬧鐘,我們設定好時間,到時通知,可是 如果提前得到(喚醒)通知,那麼設定好時間在通知也就顯得多此一舉了,那麼這種設計方案其實是一舉兩 得。如果沒有得到(喚醒)通知,那麼線程就處於Timed Waiting狀態,直到倒計時完畢自動醒來;如果在倒計時期間得到(喚醒)通知,那麼線程從Timed Waiting狀態立刻喚醒。

/*
    進入到TimeWaiting(計時等待)有兩種方式
    1.使用sleep(long m)方法,在毫秒值結束之後,線程睡醒進入到Runnable/Blocked狀態
    2.使用wait(long m)方法,wait方法如果在毫秒值結束之後,還沒有被notify喚醒,就會自動醒來,線程睡醒進入到Runnable/Blocked狀態

    喚醒的方法:
         void notify() 喚醒在此對象監視器上等待的單個線程。
         void notifyAll() 喚醒在此對象監視器上等待的所有線程。
 */
發佈了65 篇原創文章 · 獲贊 45 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章