Java - Lock - 解決執行緒不安全

By sunwc 2023-03-24 Java

為什麼要用鎖(Lock)? 鎖 相對於 synchronized 機制,是更加靈活的,它一樣能夠解決執行緒不安全的問題;

  • 不同點在於:synchronized機制在執行完相應的區塊後,會自動釋放同步鎖;而Lock就需要手動啟動同步 以及 手動結束同步
  • java.util.concurrent.locks.ReentrantLock; 是JDK 5.0新增的特性,新特性可以考慮優先使用

例子

/**
 * 演示使用Lock解決執行緒不安全問題
 * @author sunwc
 * @create 2023-03-24 上午 11:18
 */
public class LockTest {

    public static void main(String[] args) {

        Window window = new Window();

        Thread t1 = new Thread(window);
        t1.setName("窗口1");
        Thread t2 = new Thread(window);
        t2.setName("窗口2");
        Thread t3 = new Thread(window);
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}

/**
* 窗口類
*/
class Window implements Runnable {

    private int ticketAmount = 20;

    @Override
    public void run() {

        while (ticketAmount > 0) {

            if (ticketAmount > 0) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() +" : 售出的票號 - "+ ticketAmount);
                ticketAmount--;
            }
        }
    }
}

演示執行緒不安全輸出結果:

窗口3 : 售出的票號 - 20
窗口1 : 售出的票號 - 20
窗口2 : 售出的票號 - 20
窗口1 : 售出的票號 - 17
窗口3 : 售出的票號 - 17
窗口2 : 售出的票號 - 17
窗口3 : 售出的票號 - 14
窗口2 : 售出的票號 - 14
窗口1 : 售出的票號 - 14
窗口2 : 售出的票號 - 11
窗口1 : 售出的票號 - 11
窗口3 : 售出的票號 - 11
窗口2 : 售出的票號 - 8
窗口1 : 售出的票號 - 8
窗口3 : 售出的票號 - 8
窗口3 : 售出的票號 - 5
窗口1 : 售出的票號 - 5
窗口2 : 售出的票號 - 5
窗口1 : 售出的票號 - 2
窗口3 : 售出的票號 - 2
窗口2 : 售出的票號 - 2
窗口1 : 售出的票號 - -1

使用 ReentrantLock 使得 Thread-safety

修改窗口類

class Window implements Runnable {

    private int ticketAmount = 20;

    // 1.創建Lock物件
    // new ReentrantLock(boolean fair) fair默認false;若設置true,執行緒先進來先執行(先進先出)
    ReentrantLock reentrantLock = new ReentrantLock(true);

    @Override
    public void run() {

        while (ticketAmount > 0) {

            try {
                // 2. 手動取得同步鎖
                reentrantLock.lock();

                if (ticketAmount > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() +" : 售出的票號 - "+ ticketAmount);
                    ticketAmount--;
                }
            } finally {
                // 3.手動釋放同步鎖
                reentrantLock.unlock();
            }

        }
    }
}

解決執行緒不安全輸出結果:

窗口1 : 售出的票號 - 20
窗口2 : 售出的票號 - 19
窗口3 : 售出的票號 - 18
窗口1 : 售出的票號 - 17
窗口2 : 售出的票號 - 16
窗口3 : 售出的票號 - 15
窗口1 : 售出的票號 - 14
窗口2 : 售出的票號 - 13
窗口3 : 售出的票號 - 12
窗口1 : 售出的票號 - 11
窗口2 : 售出的票號 - 10
窗口3 : 售出的票號 - 9
窗口1 : 售出的票號 - 8
窗口2 : 售出的票號 - 7
窗口3 : 售出的票號 - 6
窗口1 : 售出的票號 - 5
窗口2 : 售出的票號 - 4
窗口3 : 售出的票號 - 3
窗口1 : 售出的票號 - 2
窗口2 : 售出的票號 - 1

總結

本文介紹了如何使用鎖 將部分程式邏輯達到同步的效果,Lock的使用實際上會更靈活,因為它比較偏向人為去控制,哪時候開執行同步、哪時候結束同步,達到立竿見影的效果

基礎鞏固:有關透過實現類 implements interface Runnable 達成 Multi-threading,可以參考我的另一篇文章 Java - Thread 執行緒(二) - interface Runnable