面試官:加鎖就必定線程安全了嗎?

咱們都知道,當多個線程併發地操做同一共享資源的時候,容易發生線程安全問題,解決這個問題的一個辦法是加鎖,那麼問題來了:加鎖就必定線程安全了嗎?html

各位小夥伴,大家的答案是什麼?是,仍是不是?java

其實這種面試問題,面試官可能會但願你能根據不一樣的場景展開闡述,而不是簡單的回答是或不是,這既可表現出你對多線程中的線程安全問題的理解到位,同時也體現了你分析問題的能力比別的候選人強,考慮問題周到。面試

1. 加同一個內置鎖或者顯式獨佔鎖,必定線程安全

這種方式其實是將並行變成了串行,全部須要進入同步區的線程,都須要先獲取到這把鎖,一旦某個線程獲取到了鎖,其餘線程就須要等待,即同時間在同步區範圍內,只能容許一個線程進行共享資源的訪問,所以會下降性能!數據庫

1) 加同一個內置鎖

import java.util.concurrent.CountDownLatch;

public class ThreadSafeDemo {

    private int anInt = 0;

    public synchronized void incr() {
        anInt++;
    }

    public void decr() {
        synchronized (this) {
            anInt--;
        }
    }

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(5);

        ThreadSafeDemo demo = new ThreadSafeDemo();
        for (int threadIdx = 0; threadIdx < 5; threadIdx++) {
            if (threadIdx % 2 == 0) { // threadIdx 等於 0、二、4 時
                new Thread(() -> {
                    for (int i = 0; i < 10000; i++) {
                        demo.incr();
                    }
                    latch.countDown();
                }).start();
            } else {                  // threadIdx 等於 一、3 時
                new Thread(() -> {
                    for (int i = 10000; i > 0; i--) {
                        demo.decr();
                    }
                    latch.countDown();
                }).start();
            }
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 指望值:10000
        System.out.println("當前 anInt 的值爲:" + demo.anInt);
    }
}

如以上代碼,開啓 5 個併發線程,其中 3 個線程分別自增 10000,2 個線程分別自減 10000,因此最終指望正確的值應該是 30000 - 20000 = 10000,執行結果以下:api

結果正確,線程安全。緩存

2) 加同一個顯式獨佔鎖

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

public class ThreadSafeDemo {

    private int anInt = 0;

    public void incr() {
        anInt++;
    }

    public void decr() {
        anInt--;
    }

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(5);

        ReentrantLock lock = new ReentrantLock();

        ThreadSafeDemo demo = new ThreadSafeDemo();
        for (int threadIdx = 0; threadIdx < 5; threadIdx++) {
            if (threadIdx % 2 == 0) { // threadIdx 等於 0、二、4 時
                new Thread(() -> {
                    for (int i = 0; i < 10000; i++) {
                        // 顯式獨佔鎖加鎖
                        lock.lock();
                        demo.incr();
                        // 顯式獨佔鎖解鎖
                        lock.unlock();
                    }
                    latch.countDown();
                }).start();
            } else {                 // threadIdx 等於 一、3 時
                new Thread(() -> {
                    for (int i = 10000; i > 0; i--) {
                        // 顯式獨佔鎖加鎖
                        lock.lock();
                        demo.decr();
                        // 顯式獨佔鎖解鎖
                        lock.unlock();
                    }
                    latch.countDown();
                }).start();
            }
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 指望值:10000
        System.out.println("當前 anInt 的值爲:" + demo.anInt);
    }
}

同 1) 同樣,只不過這裏換成了顯式的獨佔鎖(ReentrantLock),因此執行結果是同樣的!安全

2. 加不一樣的鎖,必定線程不安全

咱們對 1 中的內置鎖部分代碼作一些修改,注意 incr()decr() 方法:多線程

import java.util.concurrent.CountDownLatch;

public class ThreadSafeDemo {

    private static int anInt = 0;

    public synchronized void incr() {
        anInt++;
    }

    public static synchronized void decr() {
        anInt--;
    }

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(5);

        ThreadSafeDemo demo = new ThreadSafeDemo();
        for (int threadIdx = 0; threadIdx < 5; threadIdx++) {
            if (threadIdx % 2 == 0) {  // threadIdx 等於 0、二、4 時
                new Thread(() -> {
                    for (int i = 0; i < 10000; i++) {
                        demo.incr();
                    }
                    latch.countDown();
                }).start();
            } else {                  // threadIdx 等於 一、3 時
                new Thread(() -> {
                    for (int i = 10000; i > 0; i--) {
                        ThreadSafeDemo.decr();
                    }
                    latch.countDown();
                }).start();
            }
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 指望值:10000
        System.out.println("當前 anInt 的值爲:" + anInt);
    }
}

執行結果以下:併發

能夠看到,結果並不正確,線程不安全。oracle

那這是爲何呢?其實就是由於這裏有兩把鎖,不一樣的鎖,也就不能保證多線程對同一共享資源的併發操做是線程安全的。也就是說 0、二、4 線程獲取的鎖跟 一、3 線程獲取的鎖不是同一個鎖,0、二、4 線程獲取的鎖做用的對象是調用 incr() 這個方法的對象,也就是 demo,而 一、3 線程獲取的鎖做用的對象是 ThreadSafeDemo 這個類的 Class 對象,跟 synchronized (ThreadSafeDemo.class) {...} 的做用是相似的。

3. 加同一讀寫鎖,不必定線程安全

1 中使用的是獨佔鎖,會下降性能。實際上在一些場景下,多線程也能夠同時訪問共享資源,而不會產生線程安全的問題。例如多線程的「讀」操做與「讀」操做之間。

下面以 Java 8 的 ReentrantReadWriteLock 例子做示例說明,該示例參考了 Oracle 官方的 API 文檔中的例子,>> 傳送門

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ThreadSafeDemo {

    /**
     * 數據
     */
    private String data = null;

    /**
     * 緩存是否有效
     */
    private volatile boolean cache = false;

    public String getDataFromDb() {
        // 模擬從數據庫中獲取數據,耗時 0.5 秒
        String data = null;
        try {
            TimeUnit.MILLISECONDS.sleep(500L);
            data = String.valueOf(System.currentTimeMillis());
            System.out.println("[" + Thread.currentThread().getName()
                    + "] 緩存無效,從數據庫中獲取數據:" + data);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return data;
    }

    public void use() {
        System.out.println("[" + Thread.currentThread().getName()
                + "] 當前 data 的值爲:" + data);
    }

    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(5);

        ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
        ThreadSafeDemo demo = new ThreadSafeDemo();

        for (int threadIdx = 0; threadIdx < 5; threadIdx++) {
            new Thread(() -> {
                // 獲取讀鎖:⑴
                rwLock.readLock().lock();
                // 若是緩存無效
                if (!demo.cache) {
                    // 釋放讀鎖(讀鎖不能升級爲寫鎖):⑴ 處獲取的
                    rwLock.readLock().unlock();

                    // 獲取寫鎖
                    rwLock.writeLock().lock();
                    try {
                        // 再次檢查緩存是否有效,由於其餘線程有可能先於當前線程獲取到寫鎖並修改了它的值
                        if (!demo.cache) {
                            demo.data = demo.getDataFromDb();
                            // 緩存設爲有效
                            demo.cache = true;
                        }

                        // 獲取讀鎖(在釋放寫鎖以前,再獲取讀鎖,進行鎖降級):⑵
                        rwLock.readLock().lock();
                    } finally {
                        // 釋放寫鎖,此時線程仍持有讀鎖(⑵ 處獲取的)
                        rwLock.writeLock().unlock();
                    }
                }

                try {
                    // 模擬 1 秒的處理時間,並打印出當前值
                    TimeUnit.SECONDS.sleep(1);
                    demo.use();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // 釋放讀鎖:⑴ 或 ⑵ 處獲取的
                    rwLock.readLock().unlock();
                }

                latch.countDown();
            }).start();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

執行結果:

乍一看,這不是正確的嗎?別急,咱們再來加點東西看看:

new Thread(() -> {
    // 獲取讀鎖:⑴
    rwLock.readLock().lock();
    // 若是緩存無效
    if (!demo.cache) {
        // 錯誤示範,在讀鎖裏面修改了數據
        demo.cache = true;
        demo.data = demo.getDataFromDb();
        demo.cache = false;

        // 釋放讀鎖(讀鎖不能升級爲寫鎖):⑴ 處獲取的
        rwLock.readLock().unlock();

        // Omit code...
    }

    // Omit code...
}).start();

如以上代碼,在前面的代碼基礎上,⑴ 處第一次獲取到讀鎖後,在釋放讀鎖以前,對共享資源進行了修改,執行結果以下:

能夠看到,由於在讀鎖區域內對共享資源進行了修改,致使出現了線程安全問題,而這種問題是因爲不正確地使用了讀寫鎖致使的。也就是說,在使用讀寫鎖時,不能在讀鎖範圍內對共享資源進行「寫」操做,須要理解讀寫鎖的適用場景而且正確地使用它。

總結

此次經過一個面試題,簡單地梳理了一下多線程的線程安全問題與鎖的關係,但願對各位能有幫助!因爲我的能力所限,若是各位小夥伴在閱讀文章時發現有錯誤的地方,歡迎反饋給我勘正,萬分感謝。

相關文章
相關標籤/搜索