1. 程式人生 > 實用技巧 >Java 多執行緒 - 初識 Synchronized

Java 多執行緒 - 初識 Synchronized

Synchronized 簡介

本文出自汪文君老師的《Java 併發程式設計》課程,如需轉載,請註明源出處!

先來看一個例子,這個例子是模擬銀行叫號的,使用三個執行緒模擬三個櫃檯一起叫號,總共50個號。在不加 synchronized 的關鍵字的情況下,很容易就會出現併發問題。

public class BankRunnable {
    public static void main(String[] args) {
        // 一個runnable例項被多個執行緒共享
        TicketWindowRunnable ticketWindow = new TicketWindowRunnable();

        Thread windowThread1 = new Thread(ticketWindow, "一號視窗");
        Thread windowThread2 = new Thread(ticketWindow, "二號視窗");
        Thread windowThread3 = new Thread(ticketWindow, "三號視窗");
        windowThread1.start();
        windowThread2.start();
        windowThread3.start();
    }
}

public class TicketWindowRunnable implements Runnable {
    private int index = 1;
    private static final int MAX = 50;

    @Override
    public void run() {
        while (true) {
            if (index > MAX) {//1
                break;
            }
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName()+" 的號碼是:"+(index++));//2
        }
    }
}

多執行幾遍程式,就會出現下面這個問題:

在一號視窗拿完最後一個號碼之後,二號視窗和三號視窗又後續拿到了 52 和 51 號。為什麼會出現這種現象呢?

首先當 index=499 的時候,三個執行緒均不滿足 index > MAX,都會向下執行。三個執行緒都可以向下執行,將 index 加 1。

為了解決這個問題,這裡引入了 synchronized 。Java通過 synchronized 對共享資料的執行緒訪問提供了一種避免競爭條件的機制。synchronized 可以修飾方法或者程式碼塊,被修飾的方法或者程式碼塊同一時間只會允許一個執行緒執行,這條執行的執行緒持有同步部分的鎖。

synchronized 關鍵字可以修飾方法或者程式碼塊,那麼這兩者有什麼區別呢?

// 同步程式碼塊
public class TicketWindowRunnable implements Runnable {
    private int index = 1;
    private static final int MAX = 500;

    private final Object MONITOR = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (MONITOR) {
                if (index > MAX) {
                    break;
                }
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + " 的號碼是:" + (index++));
            }
        }
    }
}

synchronized 方法修飾程式碼塊的時候,使用的是 LOCK 鎖。再來用 synchronized 修飾一下同步方法:

@Override
public synchronized void run() {
    while (true) {
        if (index > MAX) {
            break;
        }
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " 的號碼是:" + (index++));
    }
}

執行之後發現都是同一個執行緒在跑,另外兩個執行緒無法執行。這是因為 synchronized 在修飾方法的時候使用的是 this 鎖,當其中一個執行緒拿到鎖進到 while 迴圈之後,就一直去做事情,直到滿足條件退出為止。將 while 裡面的程式碼抽出來放到一個方法裡,用 synchronized 來修飾該方法就可以解決這個問題。

@Override
public void run() {
    while (true) {
        if (ticket()) {
            break;
        }
    }
}

private synchronized boolean ticket() {
    if (index > MAX) {
        return true;
    }
    try {
        Thread.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " 的號碼是:" + (index++));
    return false;
}

synchronized 修飾方法時預設是使用的 this 鎖,修飾程式碼塊時使用的是物件鎖。synchronized 關鍵字還可以用來修飾靜態方法和靜態程式碼塊。

public class SynchronizedStatic {

    public synchronized static void m1() {
        System.out.println("m1 " + Thread.currentThread().getName());
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized static void m2() {
        System.out.println("m2 " + Thread.currentThread().getName());
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class SynchronizedStaticTest {
    public static void main(String[] args) {
        new Thread("T1") {
            @Override
            public void run() {
                SynchronizedStatic.m1();
            }
        }.start();

        new Thread("T2") {
            @Override
            public void run() {
                SynchronizedStatic.m2();
            }
        }.start();
    }
}

// output
m1 T1
m2 T2

靜態方法 m1 和 m2 同時被 synchronized 修飾,這個時候執行緒 T2 會等到執行緒 T1 執行完再執行,說明這兩個方法使用的是同一把鎖,這就是 Class 鎖。我們把 sleep 的時間變長一點來觀察一下是不是 Class 鎖。

可以看到,執行緒 T1 執行的時候,持有的是 Class 鎖,此時執行緒 T2 在等待 T1 執行完釋放鎖,當 T1 執行完之後,T2 拿到 Class 鎖執行程式碼。

瞭解了 synchronized 修飾靜態方法使用的是 Class 鎖之後,我們再來驗證一下當它修飾靜態方法的時候是不是也是使用 Class 鎖?

public class SynchronizedStatic {
    public synchronized static void m1() {
        System.out.println("m1 " + Thread.currentThread().getName());
        try {
            Thread.sleep(100_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void m3() {
        System.out.println("m3 " + Thread.currentThread().getName());
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class SynchronizedStaticTest {
    public static void main(String[] args) {
        new Thread("T1") {
            @Override
            public void run() {
                SynchronizedStatic.m1();
            }
        }.start();

        new Thread("T3") {
            @Override
            public void run() {
                SynchronizedStatic.m3();
            }
        }.start();
    }
}

這裡加了一個沒有 synchronized 修飾的靜態方法 m3,執行之後很容易知道,這兩個執行緒是同時執行的。我們在 SynchronizedStatic 開始的地方加一個靜態程式碼塊,靜態程式碼塊內部使用 synchronized 鎖。

public class SynchronizedStatic {
    static {
        synchronized (SynchronizedStatic.class) {
            System.out.println("static " + Thread.currentThread().getName());
            try {
                Thread.sleep(10_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized static void m1() {
        System.out.println("m1 " + Thread.currentThread().getName());
        try {
            Thread.sleep(100_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void m3() {
        System.out.println("m3 " + Thread.currentThread().getName());
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//output
static T1
m1 T1
m3 T3

可以發現,T1 執行緒要先執行靜態程式碼塊才能往下走,說明靜態程式碼塊使用的鎖和靜態方法是一樣的,另外這個時候沒有用 synchronized 修飾的 m3 也要等靜態程式碼塊執行例項化才行。

總結一下,synchronized 關鍵字能夠避免多執行緒競爭導致的資料不一致,被 synchronized 修飾的方法或者程式碼塊同一時間只會允許一個執行緒執行,這條執行的執行緒持有同步部分的鎖。synchronized 關鍵字修飾普通方法時,使用的是 this 鎖,修飾靜態方法和靜態程式碼塊時,使用 Class 鎖,修飾程式碼塊時,使用 LOCK 鎖。