咱們都知道,當多個線程併發地操做同一共享資源的時候,容易發生線程安全問題,解決這個問題的一個辦法是加鎖,那麼問題來了:加鎖就必定線程安全了嗎?html
各位小夥伴,大家的答案是什麼?是,仍是不是?java
其實這種面試問題,面試官可能會但願你能根據不一樣的場景展開闡述,而不是簡單的回答是或不是,這既可表現出你對多線程中的線程安全問題的理解到位,同時也體現了你分析問題的能力比別的候選人強,考慮問題周到。面試
這種方式其實是將並行變成了串行,全部須要進入同步區的線程,都須要先獲取到這把鎖,一旦某個線程獲取到了鎖,其餘線程就須要等待,即同時間在同步區範圍內,只能容許一個線程進行共享資源的訪問,所以會下降性能!數據庫
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
結果正確,線程安全。緩存
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
),因此執行結果是同樣的!安全
咱們對 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) {...}
的做用是相似的。
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();
如以上代碼,在前面的代碼基礎上,⑴ 處第一次獲取到讀鎖後,在釋放讀鎖以前,對共享資源進行了修改,執行結果以下:
能夠看到,由於在讀鎖區域內對共享資源進行了修改,致使出現了線程安全問題,而這種問題是因爲不正確地使用了讀寫鎖致使的。也就是說,在使用讀寫鎖時,不能在讀鎖範圍內對共享資源進行「寫」操做,須要理解讀寫鎖的適用場景而且正確地使用它。
此次經過一個面試題,簡單地梳理了一下多線程的線程安全問題與鎖的關係,但願對各位能有幫助!因爲我的能力所限,若是各位小夥伴在閱讀文章時發現有錯誤的地方,歡迎反饋給我勘正,萬分感謝。