一、什么是synchronized
在Java當中synchronized通常是用來標記一個方法或者代碼塊。在Java當中被synchronized標記的代碼或者方法在同一個時刻只能夠有一個線程執行被synchronized修飾的方法或者代碼塊。因此被synchronized修飾的方法或者代碼塊不會出現數據競爭的情況,也就是說被synchronized修飾的代碼塊是并發安全的。synchronized是java內置關鍵字,是內置鎖,JVM中內置了,顆粒度比較大
二、synchronized的四種用法
2.1、修飾一個代碼塊
被修飾的代碼塊稱為同步語句塊,其作用的范圍是大括號{} 括起來的代碼,作用的對象是調用這個代碼塊的對象;
2.2、修飾一個方法
?被修飾的方法稱為同步方法,其作用的范圍是整個方法,作用的對象是調用這個方法的對象;
非靜態方法使用 synchronized 修飾的寫法,修飾實例方法時,鎖定的是當前實例對象:
2.3、修飾一個靜態的方法
其作用的范圍是整個靜態方法,作用的對象是這個類的所有對象;
2.4、修飾一個類
其作用的范圍是synchronized后面括號括起來的部分,作用對象是這個類的所有對象。
三、使用案例分析
3.1、修飾一個方法
class SyncDemo {private int count;public void add() {count++;}public static void main(String[] args) throws InterruptedException {SyncDemo syncDemo = new SyncDemo();Thread t1 = new Thread(()->{for (int i = 0; i <1000 ; i++) {syncDemo.add();}});Thread t2 = new Thread(()->{for (int i = 0; i <1000 ; i++) {syncDemo.add();}});t1.start();t2.start();t1.join(); //線程的阻塞方法,線程t1執行完畢,再執行main主線程打印語句t2.join(); //線程的阻塞方法,線程t2執行完畢,再執行main主線程打印語句System.out.println(syncDemo.count);}
}
由于add
方法沒有使用synchronized
修飾,線程t1和線程t2可能同時去執行add
方法,那么就會導致最終count
的結果小于20000,因為count++
操作不具備原子性。?
將上述add方法被synchronized修飾
public synchronized void add() {count++;}
由于add方法被
synchronized
修飾,因此每一個時刻只能有一個線程執行add
方法,因此上面打印的結果是20000
總結:?
synchronized
修飾的add
方法一個時刻只能有一個線程執行的意思是對于一個SyncDemo
類的對象來說一個時刻只能有一個線程進入。比如現在有兩個SyncDemo
的對象s1
和s2
,一個時刻只能有一個線程進行s1
的add
方法,一個時刻只能有一個線程進入s2
的add
方法,但是同一個時刻可以有兩個不同的線程執行s1
和s2
的add
方法,也就說s1
的add
方法和s2
的add
是沒有關系的,一個線程進入s1
的add
方法并不會阻止另外的線程進入s2
的add
方法,也就是說synchronized
在修飾一個非靜態方法的時候“鎖”住的只是一個實例對象,并不會“鎖”住其它的對象。其實這也很容易理解,一個實例對象是一個獨立的個體別的對象不會影響他,他也不會影響別的對象。
3.2、修飾一個靜態的方法
class SyncDemo {private static int count; //靜態變量public static synchronized void add() { //靜態方法count++;}public static void main(String[] args) throws InterruptedException {SyncDemo syncDemo = new SyncDemo();Thread t1 = new Thread(()->{for (int i = 0; i <1000 ; i++) {syncDemo.add();}});Thread t2 = new Thread(()->{for (int i = 0; i <1000 ; i++) {syncDemo.add();}});t1.start();t2.start();t1.join(); //線程的阻塞方法,線程t1執行完畢,再執行main主線程打印語句t2.join(); //線程的阻塞方法,線程t2執行完畢,再執行main主線程打印語句System.out.println(syncDemo.count); //輸出結果為2000}
}
上面的代碼最終輸出的結果也是20000,但是與前一個程序不同的是。這里的
add
方法用static
修飾的,在這種情況下真正只能有一個線程進入到add方法
,因為用static
修飾的add方法是靜態方法,靜態方法所有對象公共的方法,因此和前面的那種情況不同,不存在兩個不同的線程同一時刻執行不同實例對象的add
方法。你仔細想想如果能夠讓兩個不同的線程執行
add
代碼塊,那么count++
的執行就不是原子的了。那為什么沒有用static
修飾的代碼為什么可以呢?因為當沒有用static
修飾時,每一個對象的count
都是不同的,內存地址不一樣,因此在這種情況下count++
這個操作仍然是原子的!
3.3、修飾一個代碼塊
class SyncDemo {private int count; //靜態變量public void add() {System.out.println("進入了add方法");synchronized (this){count++;}}public void minus() {System.out.println("進入了minus方法");synchronized (this){count--;}}public static void main(String[] args) throws InterruptedException {SyncDemo syncDemo = new SyncDemo();Thread t1 = new Thread(()->{for (int i = 0; i <10 ; i++) {syncDemo.add();}});Thread t2 = new Thread(()->{for (int i = 0; i <10 ; i++) {syncDemo.minus();}});t1.start();t2.start();t1.join(); //線程的阻塞方法,線程t1執行完畢,再執行main主線程打印語句t2.join(); //線程的阻塞方法,線程t2執行完畢,再執行main主線程打印語句System.out.println(syncDemo.count); //輸出結果為0}
}
有時候我們并不需要用
synchronized
去修飾一個方法,因為這樣并發度就比較低了,一個方法一個時刻只能有一個線程在執行。因此我們可以選擇用synchronized
去修飾代碼塊,只讓某個代碼塊一個時刻只能有一個線程執行,除了這個代碼塊之外的代碼還是可以并行的。比如上面的代碼當中
add
和minus
方法沒有使用synchronized
進行修飾,因此一個時刻可以有多個線程執行這個兩個方法。在上面的synchronized
代碼塊當中我們使用了this
對象作為鎖對象,只有拿到這個鎖對象的線程才能夠進入代碼塊執行,而在同一個時刻只能有一個線程能夠獲得鎖對象。也就是說add
函數和minus
函數用synchronized
修飾的兩個代碼塊同一個時刻只能有一個代碼塊的代碼能夠被一個線程執行,因此上面的結果同樣是0。這里說的鎖對象是
this
也就SyncDemo
類的一個實例對象,因為它鎖住的是一個實例對象,因此當實例對象不一樣的時候他們之間是沒有關系的,也就是說不同實例用synchronized
修飾的代碼塊是沒有關系的,他們之間是可以并發的。
3.4、修飾一個類
class SyncDemo {private int count; //靜態變量public void add() {System.out.println("進入了add方法");synchronized (SyncDemo.class){count++;}}public void minus() {System.out.println("進入了minus方法");synchronized (SyncDemo.class){count--;}}public static void main(String[] args) throws InterruptedException {SyncDemo syncDemo = new SyncDemo();Thread t1 = new Thread(()->{for (int i = 0; i <10 ; i++) {syncDemo.add();}});Thread t2 = new Thread(()->{for (int i = 0; i <10 ; i++) {syncDemo.minus();}});t1.start();t2.start();t1.join(); //線程的阻塞方法,線程t1執行完畢,再執行main主線程打印語句t2.join(); //線程的阻塞方法,線程t2執行完畢,再執行main主線程打印語句System.out.println(syncDemo.count); //輸出結果為0}
}
上面的代碼是使用
synchronized
修飾類,鎖對象是SyncDemo.class,這個時候他不再是鎖住一個對象了,而是一個類了,這個時候的并發度就變小了,上一份代碼當鎖對象是SyncDemo的實例對象時并發度更大一些,因為當鎖對象是實例對象的時候,只有實例對象內部是不能夠并發的,實例之間是可以并發的。但是當鎖對象是SyncDemo.class的時候,實例對象之間時不能夠并發的,因為這個時候的鎖對象是一個類。
四、Synchronized與可見性和重排序
4.1互斥性/排他性(非公平鎖)
synchronized(非公平鎖)會起到互斥效果,某個線程執?到某個對象的 synchronized 中時,其他線程如果也執?到同?個對象 synchronized 就會阻塞等待。
- 進? synchronized 修飾的代碼塊, 相當于加鎖。
- 退出 synchronized 修飾的代碼塊, 相當于解鎖。
PS:公平鎖 VS 非公平鎖
公平鎖:按資排輩,先到先得。需要“喚醒”這一步操作,會犧牲一定的性能。(線程發現鎖占用,嘗試獲取鎖一段時間后,進入休眠狀態,進入排隊隊列中等待。上一個線程釋放鎖后,會喚醒排隊等候的其他線程中最前面的線程從阻塞狀態又切換至運行狀態)
非公平鎖:來得早不如來得巧,不需要“喚醒”,性能高。(線程1發現鎖占用,嘗試獲取鎖一段時間后,進入休眠狀態。此時線程2來了,處于運行狀態,嘗試獲取鎖,此時剛好上一個線程釋放了鎖,那么線程2直接得到了鎖并去運行它的任務了。排隊等待的其他線程獲取鎖的順序不是按照訪問的順序先來先到的,而是由線程自己競爭,隨機獲取到鎖)Java里所有的鎖默認是非公平鎖。
4.2可見性
-
當一個線程進入到
synchronized
同步代碼塊的時候,將會刷新所有對該線程的可見的變量,也就是說如果其他線程修改了某個變量,而且線程需要在Synchronized
代碼塊當中使用,那就會重新刷新這個變量到內存當中,保證這個變量對于執行同步代碼塊的線程是可見的。 -
當一個線程從同步代碼塊退出的時候,也會將線程的工作內存同步到內存當中,保證在同步代碼塊當中修改的變量對其他線程可見。
4.3可重入性(可重入性鎖)
synchronized 同步塊對同?條線程來說是可重?的,不會出現??把??鎖死的問題。
/*** synchronized 可重入性測試*/
public class ThreadSynchronized4 {public static void main(String[] args) {synchronized (ThreadSynchronized4.class) {System.out.println("當前主線程已經得到了鎖"); //當執行到此行代碼時,表示已經獲得鎖synchronized (ThreadSynchronized4.class) { //同一個線程獲取鎖兩次System.out.println("當前主線程再次得到了鎖"); //若兩行代碼都能打印,說明具備可重入性}}}
}
?
五、synchronized實現原理
(面試必問)synchronized是如何實現的?
①在Java代碼層面:synchronized加鎖的對象里有一個的隱藏的對象頭,這個對象頭(可看作一個類)里有很多屬性,其中比較關注的兩個屬性是:是否加鎖的標識和擁有當前鎖的線程id。
每次進? synchronized 修飾的代碼塊時,會去對象頭中先判斷加鎖的標識,再判斷擁有當前鎖的線程id,從而決定當前線程能否往下繼續執行。
判斷加鎖標識為false->對象頭未加鎖,當前線程可以進入synchronized 修飾的代碼塊,并設置加鎖標識為true,設置擁有當前鎖的線程id為此線程id。
判斷加鎖標識為true->對象頭已加鎖,需進一步判斷擁有當前鎖的線程id是否為此線程id,若是,則繼續往下執行;否則,不能往下執行,需要等待鎖資源釋放后重新競爭再獲取鎖。
②在JVM層面和操作系統層面:synchronized同步鎖是通過JVM內置的Monitor監視器實現的,而監視器又是依賴操作系統的互斥鎖Mutex實現的。↓
————————————————
原文鏈接:https://blog.csdn.net/WWXDwrn/article/details/129115774
六、synchronized歷史發展進程
- 在JDK1.6之前(多使用Lock)synchronized使用很少,那時synchronized默認使用重量級鎖實現,所以性能較差。
- 在JDK1.6時,synchronized做了優化。鎖升級流程如下:
1,無鎖:沒有線程訪問時默認是無鎖狀態,加了synchronized也是無鎖狀態。有線程訪問時才加鎖。更大程度上減少鎖帶來的程序上的開銷。
2,偏向鎖:當有一個線程訪問時會升級為偏向鎖。(在對象頭里存了這樣一把鎖,后面再來線程時會判斷,如果線程id和擁有鎖的線程id相同,會讓它進去,只偏向某一個線程,其他線程來了之后要繼續等)
3,輕量級鎖:當有多個線程訪問時會升級為輕量級鎖。
4,重量級鎖:當大量線程訪問同時競爭鎖資源的時候會升級為重量級鎖。
原文鏈接:https://baijiahao.baidu.com/s?id=1740505389877266267&wfr=spider&for=pc