首先先看看下面的程式碼:
public class checkSynchronized extends Thread{
static volatile int i = 0;
public static void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
}
public static void main(String args[]) throws InterruptedException {
Thread t1 = new checkSynchronized();
Thread t2 = new checkSynchronized();
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("i : " + i);
}
}
複製程式碼
執行上述程式碼,你會發現大部分情況下i的最終值都會小於2000000的
為什麼加了volatile關鍵詞的變數依舊會出現執行緒安全問題呢?
圖為兩條執行緒同時對i進行寫入時,一個執行緒的結果會覆蓋另一執行緒的結果,造成執行緒安全問題。 解決此問題就應該線上程甲進行寫入值時,執行緒乙不僅不能寫入、而且還不能讀取值,如果讀取值的話就會讀取到一箇舊值,依舊會造成執行緒安全問題。那該如何實現呢?這是因為volatile只保證可見性,不保證原子性
在這裡就要引出今天的主角了:"Synchronized"
關鍵字synchronized的作用就是實現執行緒間的同步問題,它能將同步區的程式碼進行加鎖,一次只能允許一條執行緒進入同步區,以保證同步區中的執行緒安全問題。 下面就來測試下該關鍵字的作用:
public class checkSynchronized extends Thread{
Object lock;
checkSynchronized(Object lock) {
this.lock = lock;
}
static volatile int i = 0;
public static void increase() {
i++;
}
@Override
public void run() {
synchronized (lock) {
for (int j = 0; j < 1000000; j++) {
increase();
}
}
}
public static void main(String args[]) throws InterruptedException {
Object lock = new Object();
Thread t1 = new checkSynchronized(lock);
Thread t2 = new checkSynchronized(lock);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("i : " + i);
}
}
複製程式碼
大家可以看到在run()方法里加了synchronized,並且指定了鎖物件。
執行結果:i : 2000000
在這裡簡單地整理下synchronized的多種用法:
- 指定鎖物件,進入前須先獲得給定物件的鎖(如上述程式碼所述)
- 加在例項方法,進入前須獲得當前例項的鎖
- 加在靜態方法,進入前須獲得當前類的鎖
在這裡給出一段錯誤程式碼,大家需要明白:
public static void main(String args[]) throws InterruptedException {
Object lock1 = new Object(); //生成第一個鎖
Object lock2 = new Object(); //生成第二個鎖
Thread t1 = new checkSynchronized(lock1);
Thread t2 = new checkSynchronized(lock2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("i : " + i);
}
複製程式碼
這段程式碼表示出各自執行緒最終使用了各自的鎖,執行緒安全是無法保證的。
對於作用在例項方法上的也要注意,因為其可能會發生和上述相同原因的執行緒安全錯誤。
public class checkSynchronized implements Runnable{
static volatile int i = 0;
public synchronized void increase() {
i++;
}
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
increase();
}
}
public static void main(String args[]) throws InterruptedException {
checkSynchronized onlyOne = new checkSynchronized();
Thread t1 = new Thread(onlyOne);
Thread t2 = new Thread(onlyOne);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("i : " + i);
}
}
複製程式碼
上述程式碼的synchronized作用在例項方法上,鎖為當前例項。所以在main方法中只例項了一個checkSynchronized例項,因為鎖只需要一個、多了就會出現安全問題。
如果例項了兩個物件,則會出現安全問題(鎖多了和不加鎖就沒啥區別了),如下述程式碼:
public static void main(String args[]) throws InterruptedException {
checkSynchronized onlyOne = new checkSynchronized();
checkSynchronized wrong = new checkSynchronized();
Thread t1 = new Thread(onlyOne);
Thread t2 = new Thread(wrong);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("i : " + i);
}
複製程式碼
解決的辦法非常容易,只需在加了鎖的例項方法加上"static"關鍵字就行。
public static synchronized void increase()
這樣鎖就作用在類方法上了。當執行緒要執行該同步方法時是請求當前類的鎖並非例項的鎖,所以再多的例項執行緒之間依舊能正確同步。
synchronized不僅用於執行緒同步、確保執行緒安全問題外,還能保證執行緒之間的可見性和有序性問題。相當於是volatile的升級版。但被synchronized限制的多執行緒之間是序列執行的,所帶來的效能消耗是很大的。