線程安全

目錄:

  1. 線程安全
  2. 線程同步
  3. 同步代碼塊
  4. 同步方法
  5. Lock鎖
  6. 線程狀態圖
  7. sleep睡眠
  8. 等待和喚醒

1. 線程安全

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

我們通過一個案例,演示線程的安全問題:

電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “葫蘆娃大戰奧特曼”,本次電影的座位共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 class Demo {
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這張票被賣了3回。
  2. 不存在的票,比如0票與-1票,是不存在的。
  • 這種問題,幾個窗口(線程)票數不同步了,這種問題稱爲線程不安全。

2. 線程同步:

線程同步是爲了解決線程安全問題。
Java中提供了同步機制(synchronized)來解決。
根據案例簡述:

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

有三種方式完成同步操作:

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

3.同步代碼塊

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

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

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

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

代碼演示:

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(50);
                    } catch (InterruptedException e) {
// TODO Auto-generated catch block
                        e.printStackTrace();
                    }
//獲取當前線程對象的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在賣:" + ticket--);
                }
            }
        }
    }
}

4.同步方法

  • 同步方法:使用synchronized修飾的方法,就叫做同步方法,保證A線程執行該方法的時候,其他線程只能在方法外等着
    格式:
public synchronized void method(){
可能會產生線程安全問題的代碼
}

同步鎖是誰?

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

使用同步方法代碼如下:

public class Ticket implements Runnable{
    private int ticket = 100;
    /*
     * 執行賣票操作
     */
    @Override
    public void run() {
//每個窗口賣票的操作
//窗口 永遠開啓
        while(true){
            sellTicket();
        }
    }
    /*
     * 鎖對象 是 誰調用這個方法 就是誰
     * 隱含 鎖對象 就是 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--);
        }
    }
}

5.Lock鎖

Lock介紹:

Lock更加靈活,它裏面有相應的方法:

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

Lock實現類:

ReentrantLock

Lock使用標準方式

 l.lock(); 獲得鎖
        try {
            操作共享資源的代碼
        } finally {
            l.unlock();
        }
public class SaleTicketRunnable implements Runnable {
    int ticketNum = 100; // 表示100張票
    Lock l = new ReentrantLock();
    @Override
    public void run() {
        // 2.重寫run方法,在run方法中實現這個賣票邏輯
        String name = Thread.currentThread().getName();

        while (true) {
            // 如果票數大於0,就賣一張
            l.lock(); // 獲得鎖
            try {
                if (ticketNum > 0) {
                    try { // 模擬賣票要花的時間
                        Thread.sleep(10);
                    } catch (InterruptedException e) {}
                    ticketNum--;
                    System.out.println(name + "賣了一張票,剩餘: " + ticketNum);
                } else {
                    // 沒有票了,結束循環,停止了賣票
                    break;
                }
            } finally {
                l.unlock(); // 釋放鎖
            }

        }
    }
}

6. 線程狀態:

線程狀態圖:
在這裏插入圖片描述

7. 睡眠sleep方法

public static void sleep(long time) 讓當前線程進入到睡眠狀態,到毫秒後自動醒來繼續執行

public class Test{
public static void main(String[] args){
for(int i = 1;i<=5;i++){
Thread.sleep(1000);//主線程執行到sleep方法會休眠1秒後再繼續執行。
System.out.println(i)
		}
	}
}

8. 等待和喚醒

講解:

  • public void wait() : 讓當前線程進入到等待狀態 此方法必須鎖對象調用.
  • 等待和喚醒,通常是兩個線程之間的事情,一個線程等待,另外一個線程負責喚醒

Object類的方法:

  • void wait​() 導致當前線程等待
  • void notify​() 喚醒正在等待的單個線程

注意: wait​和notify​必須在同步代碼塊中,使用鎖對象來調用

兩道面試題:

1.爲什麼wait和notify方法放在Object

wait​和notify​使用鎖對象來調用,任何對象都可以作爲鎖,所以就放在Object類中

2.sleep和wait的區別

1.sleep睡眠的時候不會釋放鎖
2.wait等待的時候會釋放鎖

 public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        // obj.wait(); // IllegalMonitorStateException: 非法監視狀態異常

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    try {
                        System.out.println("wait前");
                        obj.wait();
                        System.out.println("wait後");
                    } catch (InterruptedException e) {}
                }
            }
        }).start();
        Thread.sleep(3000);
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("準備喚醒");
                    obj.notify();
                }
            }
        }).start();
    }
發佈了49 篇原創文章 · 獲贊 171 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章