我們知道,在java中,每個對象都持有一把對象鎖,這是在java語言層實現的機制(不同於java.util.concurrent包的lock),因此不需要顯式去釋放鎖。
當我們談到synchronized這個關鍵字,大夥第一時間想到的,可能就是:當資源共享時,訪問(修改)該資源的所有方法都要加上鎖(synchronized),才能保證數據不會出錯(數據不一致);
Java提供了專門的機制去避免了同一個數據對象被多個線程同時訪問,這套機制就是 synchronized 關鍵字,它包括兩種用法:synchronized 方法和 synchronized 塊。
1. synchronized 方法:通過在方法聲明中加入 synchronized關鍵字來聲明 synchronized 方法。如:
public synchronized void foo(int i);
2. synchronized 塊:通過 synchronized關鍵字來聲明synchronized 塊。語法如下:
synchronized(syncObject) {
// 允許訪問控制的代碼
// 其中的代碼塊必須獲得對象syncObject的鎖才能執行
}
如果方法A和方法B都對同一個對象加了synchronized,並且在方法A中調用了方法B;假設有某個線程T調用方法A,線程T也不需要在A調用方法B時等待該 對象鎖 釋放從而在方法B中又持有該對象的鎖,因爲synchronized 不是方法級的,而是線程級的;因此,線程T在A方法的時候,就已經獲得了那個對象的鎖了。
而我們這裏所要探討的,就是synchronized塊的參數;而synchronized 塊裏的代碼必須要獲得對象 syncObject (可以是類實例或類)的鎖方能執行。
首先我們來模擬兩個線程,逐個字母輸出某個字符串:
package com.gdut.thread;
public class SynchronizedTest {
public static void main(String[] args) {
new SynchronizedTest().init();
}
public void init() {
final Outputer outputer = new Outputer();
// 第一個線程
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
try {
Thread.sleep(5);
} catch (Exception e) {
e.printStackTrace();
}
// 輸出某個字符串
// 在這個匿名類裏,我們需要把outputer變量定義爲final
outputer.output("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
}
}).start();
// 第二個線程
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
try {
Thread.sleep(50);
} catch (Exception e) {
e.printStackTrace();
}
// 輸出某個字符串
// 在這個匿名類裏,我們需要把outputer變量定義爲final
outputer.output("5555555555555555555");
}
}
}).start();
} // end of init
class Outputer {
public void output(String name) {
int length = name.length();
for(int i=0; i<length; i++) {
// 逐個字母輸出
System.out.print(name.charAt(i));
}
System.out.println();
}
}
}
上面這段程序是線程不安全的,我們來看看它的部分輸出:
對此,我們有一種比較省事的做法,直接在Output類的output方法加上關鍵字synchronized,即:public synchronized void output(String name) { }
我們也還有另外一種方法,就是synchronized塊,而這裏便涉及到synchronized塊裏面的參數;
通俗一點的說法就是:
當A線程執行到synchronized塊的時候,A會去檢查參數對象的鎖有沒有給其他線程拿走,如果沒拿走,則線程A拿走該對象的鎖,並且在線程A執行unlock前不允許其他線程執行該對象鎖的所有synchronied塊。
但是下面這段代碼同樣是線程不安全的,原因就在synchronized塊的參數name是一個局部變量,也就是說,當A,B兩個線程執行到這的時候,都會去執行synchronized塊裏面的代碼!
class Outputer {
public void output(String name) {
int length = name.length();
synchronized(name) {
for(int i=0; i<length; i++) {
// 逐個字母輸出
System.out.print(name.charAt(i));
}
System.out.println();
}
}
}
運行結果:
把參數對象改成" this " 或者是Outputer類的成員變量str,都可以解決這個問題:
class Outputer {
// String str = "";
public void output(String name) {
int length = name.length();
// synchronized(str) {
// 這裏的"this"表示調用該方法的那個對象,即init方法的 final Outputer outputer = new Outputer();
// 如果A,B線程各自new Outputer()對象,這樣又是不同步的了!
synchronized(this) {
for(int i=0; i<length; i++) {
// 逐個字母輸出
System.out.print(name.charAt(i));
}
System.out.println();
}
}
}
===============================
接下來,我們繼續看一下下面的這個例子:
package com.gdut.thread;
public class SynchronizedTest {
public static void main(String[] args) {
new SynchronizedTest().init();
}
public void init() {
final Outputer outputer = new Outputer();
// 第一個線程
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
// 輸出某個字符串
// 調用的是output_1
outputer.output_1("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
}
}
}).start();
// 第二個線程
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
// 輸出某個字符串
// 調用的是output_2
outputer.output_2("5555555555555555555");
}
}
}).start();
} // end of init
static class Outputer {
public void output_1(String name) {
int length = name.length();
synchronized (this) {
for(int i=0; i<length; i++) {
// 逐個字母輸出
System.out.print(name.charAt(i));
}
System.out.println();
}
}
public static synchronized void output_2(String name) {
int length = name.length();
for(int i=0; i<length; i++) {
// 逐個字母輸出
System.out.print(name.charAt(i));
}
System.out.println();
}
}
}
output_1和output_2的區別是:前者是synchronized塊,而後者是static方法!輸出的結果同樣是有問題的,如下:
也就是說,和static方法關聯的對象,並不是this,而是某個Class類!(這裏的的Class爲:java.lang.Class)把output_1方法稍做修改,即可:
public void output_1(String name) {
int length = name.length();
// 參數對象爲Outputer.class
synchronized (Outputer.class) {
for(int i=0; i<length; i++) {
// 逐個字母輸出
System.out.print(name.charAt(i));
}
System.out.println();
}
}
這樣同步的問題又解決了!!
在最後,建議大夥都學習一下java.util.concurrent包裏面的相關知識!