熟悉 Java 的多線程的一般都知道會有數據不一致的情況發生,比如兩個線程在操作同一個類變量時,而保護數據不至於錯亂的辦法就是讓方法同步或者代碼塊同步。同步時非原子操作就得同步,比如一個簡單的 1+1 運算也該同步,以保證一個代碼塊或方法成為一個原子操作。
對於給在多線程環境中可能會造成數據破壞的方法,做法基本都是使用同步,除了靈活複雜lock鎖,synchronized關鍵字也是用得最多了,聊下使用時相關的疑問:
1. 當一個線程訪問了對象的synchronized或synchronized(xxx)代碼塊時,非同步方法是否會阻塞?
2.不論是靜態的或非靜態的方法都加上 synchronized 關鍵字,那靜態的方法和非靜態的方法前加上 synchronized 關鍵字有區別嗎?
3. 或者在可疑的代碼塊兩旁用 synchronized(this) 或 synchronized(someObject) 包裹起來,而選用 this 還是某一個對象--someObject,又有什麼不同呢?
4. 對方法加了 synchronized 關鍵字或用 synchronized(xxx) 包裹了代碼,就一定能避免多線程環境下的數據破壞嗎?
5. 對方法加 synchronized 關鍵字與用 synchronized(xxx) 同步代碼塊兩種規避方法又有什麼分別和聯繫呢?
為了理解上面的問題,我們還得了解基本的東西,就是監視區域:從 Java 對線程同步的原理上說起, Java 直接在語言級上支持多線程的,在多線程環境中我們要小心的數據是:
1) 保存在堆中的實例變量2) 保存在方法區中的類變量。現實點說呢就是某個方法會觸及到的同一個變量,如類變量或單態實例的實例變量。避免衝突的最容易想到的辦法就是同一時刻只讓一個線程去執行某段代碼塊或方法,於是我們就要給一段代碼塊或整個方法體標記出來,被保護的代碼塊或方法體在 Java 裡叫做監視區域(Monitor Region)。
知道上面的針對上面的問題,我們通過下面代碼進行逐一驗證:
1)對於多個線程併發訪問對象來說,非同步方法不會阻塞。
2)靜態和非靜態方法加上synchronized的區分是,監視區域的範圍,非靜態方法監視範圍是對象級的,而非靜態方法監視範圍是類級的,和我們平常認識的類方法及實例方法一樣。
代碼說明:
public class Test3 {
private static int flag = 1;
public synchronized static void accum(int time){
flag++;
try{
Thread.sleep(time);
}catch(InterruptedException e){
e.printStackTrace();
}
flag--;
System.out.println("Thread: " + Thread.currentThread().getName()+ " /Current flag: " + flag);
}
public void acc(){
System.out.println("非同步方法:"+flag);
}
public synchronized void ac(){
flag--;
System.out.println("ac:"+flag);
}
public static void main(String[] args){
/*final Test3 test = new Test3(); ---同一個對象 監視範圍為對象的線程可以正常阻塞
new Thread("one"){
public void run(){
test.accum(2000);
}
}.start();
new Thread("two"){
public void run(){
test.accum(10);
}
}.start();*/
/*new Thread("tt"){
public void run(){
test.acc();
}
}.start();*/
new Thread("one"){
public void run(){
new Test3().accum(2000);
}
}.start();
new Thread("two"){
public void run(){
new Test3().accum(100);
}
}.start();
new Thread("tt"){
public void run(){
new Test().acc();
}
}.start();
}
}
-運行結果:
非同步方法:1
Thread: one /Current flag: 1 //等待2秒出現
Thread: two /Current flag: 1
去掉static標識,運行結果如下:
非同步方法:1
Thread: two /Current flag: 2
Thread: one /Current flag: 1 //等待2秒出現
3. 或者在可疑的代碼塊兩旁用 synchronized(this) 或 synchronized(someObject) 包裹起來,而選用 this 還是某一個對象--someObject,又有什麼不同呢?
驗證代碼如下:
private static int flag = 1;
private static Object obj = new Object();
public void accum(int time){
synchronized(this){ ------
flag++;
try{
Thread.sleep(time);
}catch(InterruptedException e){
e.printStackTrace();
}
flag--;
System.out.println("Thread: " + Thread.currentThread().getName()+ " /Current flag: " + flag);
}
}
public void acc(){
System.out.println("非同步方法:"+flag);
}
public void ac(){
synchronized(this){
flag--;
}
System.out.println("ac:"+flag);
}
public static void main(String[] args){
new Thread("one"){
public void run(){
new Test2().accum(1000);
}
}.start();
new Thread("two"){
public void run(){
new Test2().accum(20);
}
}.start();
}
運行結果:Thread: two /Current flag: 2 //證明第2個線程沒有被阻塞
Thread: one /Current flag: 1
-----對於synchronized(this)來說,其監視區域為對象,但上例中因為都是用new出來的對象,所以不同對象的監視區域不同,所以第二個線程不會被第一個線程阻塞掉
synchronized(this){ 把改行代碼改成synchronized(object)
運行結果如下:
Thread: one /Current flag: 1 //2秒後出現,證明第2個線程在等待中
Thread: two /Current flag: 1
synchronized(XX.class) XX來實際的類
運行結果和synchronized(object)一樣,證明了其監視區域為類級
4. 對方法加了 synchronized 關鍵字或用 synchronized(xxx) 包裹了代碼,就一定能避免多線程環境下的數據破壞嗎?
---上例已經回答了該問題,如果在new出來的不同對象上,並不能保證數據的原子性,只有監視對象為類級時才可以保證
5. 對方法加 synchronized 關鍵字與用 synchronized(xxx) 同步代碼塊兩種規避方法又有什麼分別和聯繫呢?
----給方法加個關鍵字 synchronized 其實就是相當於把方法中的所有代碼行框到了 synchronized(xxx) 塊中。同步肯定會影響到效率,這也是大家知道的,因為它會造成方法調用的等待。方法中有些代碼可能是線程安全的,所以可不用包裹在 synchronized(xxx) 中,提高線程安全的額外代碼執行效率。
---以上是自己對synchronized 關鍵字與用 synchronized(xxx) 同步代碼塊的一點理解~
閱讀更多 碼農宸娛樂淇 的文章