文章目錄
概念
在上篇文章介紹Volatile關鍵字的時候提到,synchronized 可以保障原子性和可見性。因爲 synchronized 無論是同步的方法還是同步的代碼塊,都會先把主內存的數據拷貝到工作內存中,同步代碼塊結束,會把工作內存中的數據更新到主內存中,這樣主內存中的數據一定是最新的。更重要的是禁用了亂序重組以及保證了值對存儲器的寫入,這樣就可以保證可見性。
背景
現在可以多個線程對同一片存儲空間進行訪問,這時存儲空間裏面的數據叫做共享數據。線程併發給我們帶來效率的同時,也帶了一些數據安全性的問題,數據安全性是一個很嚴重的問題,多個線程同時訪問同一片數據區,很有可能把裏面的數據弄的混亂。 所以Java語言提供了專門機制以解決這種數據安全性問題,有效避免了同一個數據對象被多個線程同時訪問,從而導致數據的錯亂的問題。
synchronized關鍵字用法
- synchronized關鍵字可以作爲函數的修飾符(也就是常說的同步方法)
- synchronized關鍵字可以作爲函數內的語句(也就是常說的同步代碼塊)
示例
同步方法的寫法
public synchronized void test(){}
同步代碼塊的寫法
public void test(){
synchronized(this){
System.out.println("Test");
}
}
代碼synchronized(this)中的this的含義會在後面詳解。
synchronized關鍵字的作用域
- 對象實例: 可以防止多個線程同時訪問這個對象的synchronized方法,如果一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程就不能同時訪問這個對象中任何一個synchronized方法。這時,不同的對象實例的
synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。 - 類: 可以防止多個線程同時訪問這個類所創建的對象中的synchronized方法。它可以對這個類創建的所有對象實例起作用。
synchronized關鍵字用法及含義
synchronized 方法
它的作用域默認是當前對象,這時鎖就是對象,誰拿到這個鎖誰就可以運行它所控制的那段代碼。如果這個對象有多個synchronized方法,其它線程就不能同時訪問這個對象中任何一個synchronized方法。
示例
public class SynchronizedTest {
/**
* 同步方法1
*/
public synchronized void printA(){
System.out.println("AAAAAAAAAAAAAAAAA");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 同步方法2
*/
public synchronized void printB(){
System.out.println("BBBBBBBBBBBBBBBBB");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 創建一個對象實例
SynchronizedTest synchronizedTest = new SynchronizedTest();
/**
* 線程1,執行該實例的printA方法
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.printA();
}
}).start();
/**
* 線程2,執行該實例的printB方法
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.printB();
}
}).start();
}
}
這個示例代碼很簡單,在一個類裏面有兩個打印字符串的方法,然後在main函數裏面啓動兩個線程去分別調用這個兩個方法。
代碼執行後會出現兩種種打印情況
第一種:
AAAAAAAAAAAAAAAAA
(這裏會等待三秒)
BBBBBBBBBBBBBBBBB
(這裏會等待三秒,然後進程退出)
第二種:
BBBBBBBBBBBBBBBBB
(這裏會等待三秒)
AAAAAAAAAAAAAAAAA
(這裏會等待三秒,然後進程退出)
爲什麼會這樣呢?
因爲在main函數裏面的兩個線程都調用了start()方法後,並不是按照誰先調用start()方法,就先執行哪個線程,而是需要等待CPU的調度,那麼CPU先調度誰呢?這我也不知道,因爲CPU是隨機的。
如果CPU先調用了線程1,因爲printA()方法是synchronized修飾的,所以線程1在執行printA()方法前,先看看有沒有誰把synchronizedTest對象鎖住了。目前來看沒有鎖,那線程1就把該對象鎖起來,並執行printA()方法,然後睡眠3秒鐘,如果這時候,CPU又調度了線程2,那麼線程2去執行的synchronized修改的printB()方法前,看到synchronizedTest對象已經被鎖住了,拿不到鎖,於是就只能等到synchronizedTest對象鎖被釋放後才能執行printB()方法了。
再假設,此時等待synchronizedTest對象鎖的線程有很多,有線程1、2…10,這麼多線程在等待,那麼synchronizedTest對象鎖被釋放後,下一次對象鎖會被誰拿到,也是要看CPU的心情了,不知道誰纔是那個天選之子呢…
如果CPU先調用了線程2,後面的等待流程是一樣的。
這個例子說明了synchronized關鍵字在對象實例的作用域,防止多個線程同時訪問這個對象的synchronized方法,如果一個對象有多個synchronized方法,只要一個線程訪問了其中的某一個synchronized方法,其它線程就不能同時訪問這個對象中其他任何一個synchronized方法了。
那在對象實例的作用域概念後面還有一句話“這時,不同的對象實例的 synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。”
現在來驗證一下這句話,只需要修改上面代碼中main函數的兩句話,修改後如下:
public static void main(String[] args) {
/**
* 線程1,執行該實例的printA方法
*/
new Thread(new Runnable() {
@Override
public void run() {
// 創建一個對象實例
SynchronizedTest synchronizedTest1 = new SynchronizedTest();
synchronizedTest1.printA();
}
}).start();
/**
* 線程2,執行該實例的printB方法
*/
new Thread(new Runnable() {
@Override
public void run() {
// 創建一個對象實例
SynchronizedTest synchronizedTest2 = new SynchronizedTest();
synchronizedTest2.printB();
}
}).start();
}
修改的地方是把創建對象實例的地方,放在線程裏面去了,此時就有兩個不同的對象實例了,現在來看看執行結果呢。
也會有兩種打印情況
第一種:
AAAAAAAAAAAAAAAAA
BBBBBBBBBBBBBBBBB
(等待睡眠時間結束,退出進程)
第二種:
BBBBBBBBBBBBBBBBB
AAAAAAAAAAAAAAAAA
(等待睡眠時間結束,退出進程)
兩個線程並行發生,這就印證了上面這句話:不同的對象實例的 synchronized方法是不相干擾的,也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法。
思考時間?
如果我們在示例1的基礎上,增加一個普通成員方法的打印方法:
/**
* 普通成員方法1
*/
public void printC(){
System.out.println("CCCCCCCCCCCCCCCCC");
}
在main函數裏面增加一個線程去執行這個方法:
/**
* 線程3,執行該實例的printC方法
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.printC();
}
}).start();
看看現在的程序執行結果會是什麼樣的呢?可能會有6中不同的打印哦,自己試試吧,想想爲什麼。
synchronized 代碼塊
鎖對象
synchronized關鍵字還可以用於方法中的某個代碼塊中,表示只對這個代碼塊裏的資源實行互斥訪問。
示例
public class SynchronizedObjTest {
/**
* 同步方法1
*/
public void printA(){
synchronized (this){
System.out.println("AAAAAAAAAAAAAAAAA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 同步方法2
*/
public void printB(){
synchronized (this){
System.out.println("BBBBBBBBBBBBBBBBB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizedObjTest synchronizedObjTest = new SynchronizedObjTest();
/**
* 線程1,執行該實例的printA方法
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronizedObjTest.printA();
}
}).start();
/**
* 線程2,執行該實例的printB方法
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronizedObjTest.printB();
}
}).start();
}
}
這段代碼和示例1極爲相似,不同的地方在於鎖的寫法,synchronized (this),中的this代表着當前對象,那麼它的作用域就是當前對象,這時鎖就是對象,誰拿到這個鎖誰就可以運行它所控制的那段代碼。如果這個對象有多個synchronized方法,其它線程就不能同時訪問這個對象中任何一個synchronized方法。
那麼synchronized 代碼塊這種做法對於同一個類的不同的對象實例的 synchronized 代碼塊會不會相互干擾呢?答案是不會的,就像synchronized方法一樣。不同的對象實例是不同的鎖,也就不會相互干擾。
鎖class
可以防止多個線程同時訪問這個類所創建的對象中的synchronized方法。它可以對這個類創建的所有對象實例起作用。
鎖class只需要將上面代碼中的this,換成“類名.class”就行了
示例
public class SynchronizedClassTest {
/**
* 同步方法1
*/
public void printA(){
synchronized (SynchronizedClassTest.class){
System.out.println("AAAAAAAAAAAAAAAAA");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 同步方法2
*/
public void printB(){
synchronized (SynchronizedClassTest.class){
System.out.println("BBBBBBBBBBBBBBBBB");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchronizedClassTest synchronizedClassTest = new SynchronizedClassTest();
/**
* 線程1,執行該實例的printA方法
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronizedClassTest.printA();
}
}).start();
/**
* 線程2,執行該實例的printB方法
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronizedClassTest.printB();
}
}).start();
}
}
程序會出現兩種種打印情況
第一種:
AAAAAAAAAAAAAAAAA
(這裏會等待三秒)
BBBBBBBBBBBBBBBBB
(這裏會等待三秒,然後進程退出)
第二種:
BBBBBBBBBBBBBBBBB
(這裏會等待三秒)
AAAAAAAAAAAAAAAAA
(這裏會等待三秒,然後進程退出)
現在將對象實例放在線程中去創建,使其生成兩個不同的對象實例
public static void main(String[] args) {
/**
* 線程1,執行該實例的printA方法
*/
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedClassTest synchronizedClassTest1 = new SynchronizedClassTest();
synchronizedClassTest1.printA();
}
}).start();
/**
* 線程2,執行該實例的printB方法
*/
new Thread(new Runnable() {
@Override
public void run() {
SynchronizedClassTest synchronizedClassTest2 = new SynchronizedClassTest();
synchronizedClassTest2.printB();
}
}).start();
}
修改後程序會出現兩種種打印情況
第一種:
AAAAAAAAAAAAAAAAA
(這裏會等待三秒)
BBBBBBBBBBBBBBBBB
(這裏會等待三秒,然後進程退出)
第二種:
BBBBBBBBBBBBBBBBB
(這裏會等待三秒)
AAAAAAAAAAAAAAAAA
(這裏會等待三秒,然後進程退出)
修改前和修改後的打印情況是一致的。這就印證了這句話:synchronized鎖class可以防止多個線程同時訪問這個類所創建的對象中的synchronized方法。它可以對這個類創建的所有對象實例起作用。
在Java中還有一條隱式規則
- 當修飾靜態方法的時候,鎖定的是當前類的Class對象。
- 當修飾非靜態方法的時候,鎖定的是當前實例對象this。
技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。