1. 程式人生 > 其它 >Java基礎 第四節 第二課

Java基礎 第四節 第二課

技術標籤:# Java 基礎第四節我要偷偷學 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) {
                    e.printStackTrace();
                }
                // 獲取當前執行緒物件的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣: " + ticket--);
            }
        }
    }
}

測試類

public class Test51 {
    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正在賣: 99
視窗3正在賣: 98
視窗2正在賣: 100
視窗2正在賣: 97
視窗1正在賣: 96
視窗3正在賣: 95
視窗2正在賣: 94
視窗1正在賣: 93
視窗3正在賣: 92
視窗3正在賣: 91
視窗1正在賣: 90
視窗2正在賣: 89
視窗2正在賣: 88
視窗3正在賣: 86
視窗1正在賣: 87
視窗1正在賣: 85
視窗2正在賣: 84
視窗3正在賣: 83
視窗1正在賣: 82
視窗2正在賣: 81
視窗3正在賣: 80
視窗3正在賣: 79
視窗2正在賣: 78
視窗1正在賣: 77
視窗3正在賣: 76
視窗2正在賣: 75
視窗1正在賣: 74
視窗1正在賣: 73
視窗2正在賣: 72
視窗3正在賣: 71
視窗1正在賣: 70
視窗2正在賣: 69
視窗3正在賣: 68
視窗3正在賣: 67
視窗1正在賣: 65
視窗2正在賣: 66
視窗1正在賣: 64
視窗2正在賣: 62
視窗3正在賣: 63
視窗3正在賣: 61
視窗1正在賣: 60
視窗2正在賣: 59
視窗2正在賣: 58
視窗1正在賣: 57
視窗3正在賣: 56
視窗3正在賣: 55
視窗2正在賣: 53
視窗1正在賣: 54
視窗1正在賣: 52
視窗3正在賣: 51
視窗2正在賣: 50
視窗1正在賣: 49
視窗2正在賣: 48
視窗3正在賣: 47
視窗2正在賣: 46
視窗3正在賣: 45
視窗1正在賣: 44
視窗1正在賣: 43
視窗3正在賣: 42
視窗2正在賣: 41
視窗2正在賣: 40
視窗3正在賣: 39
視窗1正在賣: 38
視窗1正在賣: 37
視窗3正在賣: 36
視窗2正在賣: 35
視窗1正在賣: 34
視窗3正在賣: 33
視窗2正在賣: 32
視窗2正在賣: 31
視窗3正在賣: 30
視窗1正在賣: 29
視窗3正在賣: 28
視窗1正在賣: 27
視窗2正在賣: 26
視窗2正在賣: 25
視窗3正在賣: 24
視窗1正在賣: 23
視窗3正在賣: 22
視窗2正在賣: 21
視窗1正在賣: 22
視窗2正在賣: 20
視窗1正在賣: 19
視窗3正在賣: 18
視窗3正在賣: 17
視窗2正在賣: 16
視窗1正在賣: 15
視窗3正在賣: 14
視窗2正在賣: 13
視窗1正在賣: 12
視窗1正在賣: 11
視窗2正在賣: 10
視窗3正在賣: 9
視窗3正在賣: 8
視窗2正在賣: 7
視窗1正在賣: 6
視窗2正在賣: 5
視窗1正在賣: 5
視窗3正在賣: 4
視窗2正在賣: 3
視窗1正在賣: 2
視窗3正在賣: 1
視窗2正在賣: 0
視窗1正在賣: -1

發現程式出現了兩個問題:

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

這種問題, 幾個視窗 (執行緒)票數不同了, 這種問題成為執行緒不安全.

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

執行緒同步

當我們使用多個執行緒訪問同一資源的時候, 且多個執行緒中對資源有寫的操作, 就容易出現執行緒安全問題.

要解決上述多執行緒併發訪問一個資源的安全性問題. 也就是解決重複票與不存在票問題. Java 中提供了 (synchronized) 來解決.

根據案例描述:

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

為了保證每個執行緒都能正常秩序原子操作 Java 引入了執行緒同步機制.

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

  1. 同步程式碼塊
  2. 同步方法
  3. 鎖機制

同步程式碼塊

同步程式碼塊: 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) {
                        e.printStackTrace();
                    }
                    // 獲取當前執行緒物件的名字
                    String name = Thread.currentThread().getName();
                    System.out.println(name + "正在賣: " + ticket--);
                }
            }
        }
    }
}

當使用了同步程式碼塊後, 上述的執行緒的安全問題, 解決了.

同步方法

同步方法: 使用 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){
            
        }
    }

    /**
     * 鎖物件是誰呼叫這個方法就是誰
     * 隱含鎖物件就是this
     */
    public synchronized void sellTicket(){
        if(ticket > 0){  // 有票可以賣
            // 出票操作
            // 使用sleep模擬一下出票時間
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 獲取當前執行緒物件的名字
            String name = Thread.currentThread().getName();
            System.out.println(name + "正在賣: " + ticket--);
        }
    }
}

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) {
                    e.printStackTrace();
                }
                // 獲取當前執行緒物件的名字
                String name = Thread.currentThread().getName();
                System.out.println(name + "正在賣: " + ticket--);
            }
        }
    }
}