synchronized關鍵字在Java中用于實現線程同步,確保多線程并發訪問共享資源時的互斥執行。其主要使用方式包括:1. 同步代碼塊,通過指定對象作為鎖;2. 同步方法,鎖為當前對象(this)或類對象(class);3. 靜態同步方法,等價于使用類對象作為鎖;4. 同步靜態變量,通常使用靜態對象作為鎖。此外,synchronized依賴jvm的monitor機制,通過monitorenter和monitorexit指令實現鎖的獲取與釋放,并在jdk 1.6后通過鎖優化提升了性能。相比reentrantlock,synchronized是隱式鎖,自動釋放,而reentrantlock需要手動釋放且提供更多功能,但更易出錯。避免死鎖的方法包括統一鎖順序、限制鎖持有時間、使用定時鎖及工具檢測。
synchronized關鍵字在Java中用于實現線程同步,確保多個線程在并發訪問共享資源時,只有一個線程可以執行特定代碼塊或方法,從而避免數據競爭和不一致性。它本質上提供了一種互斥鎖機制。
解決方案
synchronized關鍵字主要有四種使用方式:
立即學習“Java免費學習筆記(深入)”;
-
同步代碼塊(Synchronized Block):
通過synchronized(Object)來指定一個對象作為鎖。任何時刻,只能有一個線程獲得該對象的鎖,其他線程必須等待。
public class counter { private int count = 0; private Object lock = new Object(); // 鎖對象 public void increment() { synchronized (lock) { count++; } } public int getCount() { return count; } }
這里,lock對象充當了鎖,只有獲得lock鎖的線程才能執行count++操作。 選擇哪個對象作為鎖至關重要。 如果多個線程訪問不同的Counter實例,那么每個實例都有自己的lock對象,同步就無效了。
-
同步方法(Synchronized Method):
將整個方法聲明為synchronized。 對于實例方法,鎖是當前對象(this);對于靜態方法,鎖是當前類(Class對象)。
public class Counter { private int count = 0; public synchronized void increment() { count++; } public int getCount() { return count; } }
等價于:
public class Counter { private int count = 0; public void increment() { synchronized (this) { count++; } } public int getCount() { return count; } }
靜態同步方法:
public class Counter { private static int count = 0; public static synchronized void increment() { count++; } public static int getCount() { return count; } }
等價于:
public class Counter { private static int count = 0; public static void increment() { synchronized (Counter.class) { count++; } } public static int getCount() { return count; } }
-
同步靜態變量(不常用):
雖然不常見,但也可以通過同步靜態變量來實現同步。 這本質上和同步代碼塊使用Class對象作為鎖是一樣的。
public class Counter { private static int count = 0; private static Object staticLock = new Object(); public static void increment() { synchronized (staticLock) { count++; } } public static int getCount() { return count; } }
-
隱式鎖(Intrinsic Lock or Monitor Lock):
當線程進入synchronized塊或方法時,它會自動獲得與對象關聯的隱式鎖。 當線程退出synchronized塊或方法時,鎖會被自動釋放。 這種鎖是可重入的,意味著同一個線程可以多次獲得同一個鎖而不會阻塞。
Synchronized 和 ReentrantLock 的區別是什么?
- synchronized 是 Java 關鍵字,屬于 JVM 層面,而 ReentrantLock 是一個類,屬于 API 層面。
- synchronized 會自動釋放鎖,而 ReentrantLock 需要手動釋放鎖(lock.unlock()),如果忘記釋放,可能會導致死鎖。
- ReentrantLock 提供了更多的功能,例如可中斷的鎖、公平鎖等,而 synchronized 相對簡單。
- 性能方面,在 JDK 1.6 之后,synchronized 進行了大量的優化,在低并發情況下,性能可能優于 ReentrantLock,但在高并發情況下,ReentrantLock 通常性能更好。 理論上,ReentrantLock更靈活,但用不好也更容易出錯。
如何避免死鎖?
死鎖是指兩個或多個線程互相持有對方需要的資源,導致所有線程都無法繼續執行的情況。 避免死鎖的一些常見策略:
- 避免循環等待: 確保線程獲取鎖的順序是一致的,避免形成循環依賴。
- 限制鎖的持有時間: 盡量縮短持有鎖的時間,減少其他線程等待鎖的機會。
- 使用定時鎖: 使用 ReentrantLock 的 tryLock(long timeout, TimeUnit unit) 方法,設置超時時間,避免無限等待。
- 死鎖檢測: 一些工具可以檢測死鎖,例如 JConsole 和 VisualVM。
舉個例子,假設有兩個線程 A 和 B,它們都需要獲取鎖 lock1 和 lock2。如果線程 A 先獲取了 lock1,然后嘗試獲取 lock2,而線程 B 先獲取了 lock2,然后嘗試獲取 lock1,那么就可能發生死鎖。
Object lock1 = new Object(); Object lock2 = new Object(); // 線程 A new Thread(() -> { synchronized (lock1) { System.out.println("線程 A 獲取了 lock1"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lock2) { System.out.println("線程 A 獲取了 lock2"); } } }).start(); // 線程 B new Thread(() -> { synchronized (lock2) { System.out.println("線程 B 獲取了 lock2"); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lock1) { System.out.println("線程 B 獲取了 lock1"); } } }).start();
在這個例子中,如果線程 A 獲取了 lock1,線程 B 獲取了 lock2,然后它們互相等待對方釋放鎖,就會發生死鎖。 避免這種情況,可以確保兩個線程以相同的順序獲取鎖,例如都先獲取 lock1,再獲取 lock2。
Synchronized的底層實現原理是什么?
synchronized 的實現依賴于 JVM 的 monitor(監視器鎖)機制。 每個 Java 對象都關聯一個 monitor,當線程執行到 synchronized 塊或方法時,會嘗試獲取 monitor 的所有權。 如果 monitor 沒有被其他線程持有,則該線程成功獲取 monitor,并將 monitor 的計數器加 1。 如果 monitor 已經被其他線程持有,則該線程會被阻塞,直到 monitor 的計數器變為 0,即持有 monitor 的線程釋放了鎖。
在 JVM 層面,synchronized 是通過 monitorenter 和 monitorexit 指令來實現的。 monitorenter 指令用于獲取 monitor 的所有權,monitorexit 指令用于釋放 monitor 的所有權。 JVM 會確保 monitorenter 和 monitorexit 指令是成對出現的,即使在發生異常的情況下,也會確保鎖被釋放,從而避免死鎖。 早期的synchronized效率比較低,因為是重量級鎖,會涉及到用戶態和內核態的切換,開銷較大。但是現在JVM對synchronized做了很多優化,比如鎖升級,輕量級鎖,偏向鎖等,在很多場景下synchronized的效率已經很高了。