Java synchronized 關鍵字詳解 前置技能點 進程和線程的概念 線程創建方式 線程的狀態狀態轉換 線程安全的概念 synchronized 關鍵字的幾種用法 1. 修飾非靜態成員方法 2. 修飾靜態成員方法 3. 類鎖代碼塊 4. 對象鎖代碼塊 synchronized 修飾非靜態方法 ...
Java synchronized 關鍵字詳解
前置技能點
- 進程和線程的概念
- 線程創建方式
- 線程的狀態狀態轉換
- 線程安全的概念
synchronized 關鍵字的幾種用法
修飾非靜態成員方法
synchronized public void sync(){ }
修飾靜態成員方法
synchronized public static void sync(){ }
類鎖代碼塊
synchronized (類.class){ }
對象鎖代碼塊
synchronized (this|對象){ }
synchronized 修飾非靜態方法時可以看做是鎖 this 對象,修飾靜態方法時可以看做是鎖方法所在的類。
synchronized 關鍵字的根本機制
各個線程想要訪問被 synchronized 修飾的代碼塊,就要取得 synchronized 聲明的鎖。如果兩個線程的目標是同一個鎖,就會出現阻塞的現象,所以兩個線程不能同時訪問同一個鎖下的代碼,保證了多線程在執行時最終結果不會出錯。這與共用變數是否為靜態無關。
幾個例子
對象鎖
public class ThreadDemo extends Thread {
@Override
public synchronized void run() {
for (int i = 0; i < 10000; i++) {
Main.i++;
}
System.out.println("執行完成");
}
}
直接將繼承的 run() 方法標記為 synchronized ,作用是對 Main 類中的 i 變數做 10000 次累加操作。
public class Main {
static int i = 0;
public static void main(String[] args) throws InterruptedException {
ThreadDemo threadDemo=new ThreadDemo();
Thread t1 = new Thread(threadDemo);
Thread t2 = new Thread(threadDemo);
Thread t3 = new Thread(threadDemo);
Thread t4 = new Thread(threadDemo);
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println(i);
}
}
//輸出結果:
//執行完成
//執行完成
//執行完成
//執行完成
//40000
可以看到當4個線程全部執行完畢之後,變數 i 成功的累加了 40000 次,沒有出現丟失操作的情況。
如果我們將 main() 方法修改如下:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new ThreadDemo();
Thread t2 = new ThreadDemo();
Thread t3 = new ThreadDemo();
Thread t4 = new ThreadDemo();
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println(i);
}
//輸出結果:
//執行完成
//執行完成
//執行完成
//執行完成
//27579
可以看到丟失了不少的累加操作。觀察前後兩個 main() 方法創建線程的方式可以發現,前面的 main() 方法是使用了同一個對象來創建了4個不同的線程,而後一個 main() 方法使用了4個不同的 ThreadDemo 對象創建了4個線程。我們用 synchronized 修飾的是一個非靜態成員函數,相當於對該方法創建了 this 的對象鎖。在第一個 main() 方法中使用同一個對象來創建 4 個不同線程就會讓 4 個線程爭奪同一個對象鎖,這樣,在同一時間內,僅能有一個線程能訪問 synchronized 修飾的方法。而在第二種 main() 方法中,4 個線程各自對應一個對象鎖,4 個線程之間沒有競爭關係,對象鎖自然無法生效。
類鎖
public class ThreadDemo extends Thread {
@Override
public void run() {
synchronized (ThreadDemo.class) {
for (int i = 0; i < 10000; i++) {
Main.i++;
}
System.out.println("執行完成");
}
}
}
將修飾方法的 synchronized 改為對 ThreadDemo.class 上鎖的代碼塊
public class ThreadDemo2 extends Thread {
@Override
public void run() {
synchronized (ThreadDemo2.class) {
for (int i = 0; i < 10000; i++) {
Main.i++;
}
System.out.println("執行完成");
}
}
}
再創建一個相同的類命名為 ThreadDemo2 ,與 ThreadDemo 不同的是,ThreadDemo2 中,synchronized 對 ThreadDemo2.class 上鎖。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new ThreadDemo();
Thread t2 = new ThreadDemo();
Thread t3 = new ThreadDemo2();
Thread t4 = new ThreadDemo2();
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println(i);
}
//輸出結果:
//執行完成
//執行完成
//執行完成
//執行完成
//33054
4 個線程分別由 ThreadDemo 和 ThreadDemo2 來創建,顯然得到的結果與預期的 40000 不符。如果我們將 ThreadDemo2 中的 synchronized 改為對 ThreadDemo.class 上鎖:
public class ThreadDemo2 extends Thread {
@Override
public void run() {
synchronized (ThreadDemo.class) {
for (int i = 0; i < 10000; i++) {
Main.i++;
}
System.out.println("執行完成");
}
}
}
//輸出結果:
//執行完成
//執行完成
//執行完成
//執行完成
//40000
可以看到,雖然是聲明在兩個不同的類中的 synchronized 代碼塊,但是由於都是對 ThreadDemo.class 上鎖,所以 4 個線程之間還是建立了競爭關係,同時只能有一個線程訪問被 synchronized 修飾的代碼。
總結
所以 synchronized 關鍵字的本質是限制線程訪問一段代碼,而限制的條件就是,在所有被加上相同鎖的代碼上,同一時間,只能有一個線程在運行。這與你要修改什麼樣的共用變數無關。在我剛接觸到的時候以為類鎖和對象鎖是分別針對靜態共用變數和非靜態共用變數的,但事實上鎖的是要執行的代碼塊,而不是代碼塊將要訪問的共用變數。