解析Java線程編程中的線程安全與synchronized的使用
一.什么時候會出現(xiàn)線程安全問題?
在單線程中不會出現(xiàn)線程安全問題,而在多線程編程中,有可能會出現(xiàn)同時訪問同一個資源的情況,這種資源可以是各種類型的的資源:一個變量、一個對象、一個文件、一個數(shù)據(jù)庫表等,而當(dāng)多個線程同時訪問同一個資源的時候,就會存在一個問題:
由于每個線程執(zhí)行的過程是不可控的,所以很可能導(dǎo)致最終的結(jié)果與實際上的愿望相違背或者直接導(dǎo)致程序出錯。
舉個簡單的例子:
現(xiàn)在有兩個線程分別從網(wǎng)絡(luò)上讀取數(shù)據(jù),然后插入一張數(shù)據(jù)庫表中,要求不能插入重復(fù)的數(shù)據(jù)。
那么必然在插入數(shù)據(jù)的過程中存在兩個操作:
1)檢查數(shù)據(jù)庫中是否存在該條數(shù)據(jù);
2)如果存在,則不插入;如果不存在,則插入到數(shù)據(jù)庫中。
假如兩個線程分別用thread-1和thread-2表示,某一時刻,thread-1和thread-2都讀取到了數(shù)據(jù)X,那么可能會發(fā)生這種情況:
thread-1去檢查數(shù)據(jù)庫中是否存在數(shù)據(jù)X,然后thread-2也接著去檢查數(shù)據(jù)庫中是否存在數(shù)據(jù)X。
結(jié)果兩個線程檢查的結(jié)果都是數(shù)據(jù)庫中不存在數(shù)據(jù)X,那么兩個線程都分別將數(shù)據(jù)X插入數(shù)據(jù)庫表當(dāng)中。
這個就是線程安全問題,即多個線程同時訪問一個資源時,會導(dǎo)致程序運行結(jié)果并不是想看到的結(jié)果。
這里面,這個資源被稱為:臨界資源(也有稱為共享資源)。
也就是說,當(dāng)多個線程同時訪問臨界資源(一個對象,對象中的屬性,一個文件,一個數(shù)據(jù)庫等)時,就可能會產(chǎn)生線程安全問題。
不過,當(dāng)多個線程執(zhí)行一個方法,方法內(nèi)部的局部變量并不是臨界資源,因為方法是在棧上執(zhí)行的,而Java棧是線程私有的,因此不會產(chǎn)生線程安全問題。
二.如何解決線程安全問題?
那么一般來說,是如何解決線程安全問題的呢?
基本上所有的并發(fā)模式在解決線程安全問題時,都采用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個線程訪問臨界資源,也稱作同步互斥訪問。
通常來說,是在訪問臨界資源的代碼前面加上一個鎖,當(dāng)訪問完臨界資源后釋放鎖,讓其他線程繼續(xù)訪問。
在Java中,提供了兩種方式來實現(xiàn)同步互斥訪問:synchronized和Lock。
本文主要講述synchronized的使用方法,Lock的使用方法在下一篇博文中講述。
三.synchronized同步方法或者同步塊
在了解synchronized關(guān)鍵字的使用方法之前,我們先來看一個概念:互斥鎖,顧名思義:能到達(dá)到互斥訪問目的的鎖。
舉個簡單的例子:如果對臨界資源加上互斥鎖,當(dāng)一個線程在訪問該臨界資源時,其他線程便只能等待。
在Java中,每一個對象都擁有一個鎖標(biāo)記(monitor),也稱為監(jiān)視器,多線程同時訪問某個對象時,線程只有獲取了該對象的鎖才能訪問。
在Java中,可以使用synchronized關(guān)鍵字來標(biāo)記一個方法或者代碼塊,當(dāng)某個線程調(diào)用該對象的synchronized方法或者訪問synchronized代碼塊時,這個線程便獲得了該對象的鎖,其他線程暫時無法訪問這個方法,只有等待這個方法執(zhí)行完畢或者代碼塊執(zhí)行完畢,這個線程才會釋放該對象的鎖,其他線程才能執(zhí)行這個方法或者代碼塊。
下面通過幾個簡單的例子來說明synchronized關(guān)鍵字的使用:
1.synchronized方法
下面這段代碼中兩個線程分別調(diào)用insertData對象插入數(shù)據(jù):
public class Test {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
new Thread() {
public void run() {
insertData.insert(Thread.currentThread());
};
}.start();
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入數(shù)據(jù)"+i);
arrayList.add(i);
}
}
}
此時程序的輸出結(jié)果為:

說明兩個線程在同時執(zhí)行insert方法。
而如果在insert方法前面加上關(guān)鍵字synchronized的話,運行結(jié)果為:
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public synchronized void insert(Thread thread){
for(int i=0;i<5;i++){
System.out.println(thread.getName()+"在插入數(shù)據(jù)"+i);
arrayList.add(i);
}
}
}

從上輸出結(jié)果說明,Thread-1插入數(shù)據(jù)是等Thread-0插入完數(shù)據(jù)之后才進(jìn)行的。說明Thread-0和Thread-1是順序執(zhí)行insert方法的。
這就是synchronized方法。
不過有幾點需要注意:
1)當(dāng)一個線程正在訪問一個對象的synchronized方法,那么其他線程不能訪問該對象的其他synchronized方法。這個原因很簡單,因為一個對象只有一把鎖,當(dāng)一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized方法。
2)當(dāng)一個線程正在訪問一個對象的synchronized方法,那么其他線程能訪問該對象的非synchronized方法。這個原因很簡單,訪問非synchronized方法不需要獲得該對象的鎖,假如一個方法沒用synchronized關(guān)鍵字修飾,說明它不會使用到臨界資源,那么其他線程是可以訪問這個方法的,
3)如果一個線程A需要訪問對象object1的synchronized方法fun1,另外一個線程B需要訪問對象object2的synchronized方法fun1,即使object1和object2是同一類型),也不會產(chǎn)生線程安全問題,因為他們訪問的是不同的對象,所以不存在互斥問題。
2.synchronized代碼塊
synchronized代碼塊類似于以下這種形式:
synchronized(synObject) {
}
當(dāng)在某個線程中執(zhí)行這段代碼塊,該線程會獲取對象synObject的鎖,從而使得其他線程無法同時訪問該代碼塊。
synObject可以是this,代表獲取當(dāng)前對象的鎖,也可以是類中的一個屬性,代表獲取該屬性的鎖。
比如上面的insert方法可以改成以下兩種形式:
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
public void insert(Thread thread){
synchronized (this) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入數(shù)據(jù)"+i);
arrayList.add(i);
}
}
}
}
class InsertData {
private ArrayList<Integer> arrayList = new ArrayList<Integer>();
private Object object = new Object();
public void insert(Thread thread){
synchronized (object) {
for(int i=0;i<100;i++){
System.out.println(thread.getName()+"在插入數(shù)據(jù)"+i);
arrayList.add(i);
}
}
}
}
從上面可以看出,synchronized代碼塊使用起來比synchronized方法要靈活得多。因為也許一個方法中只有一部分代碼只需要同步,如果此時對整個方法用synchronized進(jìn)行同步,會影響程序執(zhí)行效率。而使用synchronized代碼塊就可以避免這個問題,synchronized代碼塊可以實現(xiàn)只對需要同步的地方進(jìn)行同步。
另外,每個類也會有一個鎖,它可以用來控制對static數(shù)據(jù)成員的并發(fā)訪問。
并且如果一個線程執(zhí)行一個對象的非static synchronized方法,另外一個線程需要執(zhí)行這個對象所屬類的static synchronized方法,此時不會發(fā)生互斥現(xiàn)象,因為訪問static synchronized方法占用的是類鎖,而訪問非static synchronized方法占用的是對象鎖,所以不存在互斥現(xiàn)象。
看下面這段代碼就明白了:
public class Test {
public static void main(String[] args) {
final InsertData insertData = new InsertData();
new Thread(){
@Override
public void run() {
insertData.insert();
}
}.start();
new Thread(){
@Override
public void run() {
insertData.insert1();
}
}.start();
}
}
class InsertData {
public synchronized void insert(){
System.out.println("執(zhí)行insert");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執(zhí)行insert完畢");
}
public synchronized static void insert1() {
System.out.println("執(zhí)行insert1");
System.out.println("執(zhí)行insert1完畢");
}
}
執(zhí)行結(jié)果;

第一個線程里面執(zhí)行的是insert方法,不會導(dǎo)致第二個線程執(zhí)行insert1方法發(fā)生阻塞現(xiàn)象。
下面我們看一下synchronized關(guān)鍵字到底做了什么事情,我們來反編譯它的字節(jié)碼看一下,下面這段代碼反編譯后的字節(jié)碼為:
public class InsertData {
private Object object = new Object();
public void insert(Thread thread){
synchronized (object) {
}
}
public synchronized void insert1(Thread thread){
}
public void insert2(Thread thread){
}
}

從反編譯獲得的字節(jié)碼可以看出,synchronized代碼塊實際上多了monitorenter和monitorexit兩條指令。monitorenter指令執(zhí)行時會讓對象的鎖計數(shù)加1,而monitorexit指令執(zhí)行時會讓對象的鎖計數(shù)減1,其實這個與操作系統(tǒng)里面的PV操作很像,操作系統(tǒng)里面的PV操作就是用來控制多個線程對臨界資源的訪問。對于synchronized方法,執(zhí)行中的線程識別該方法的 method_info 結(jié)構(gòu)是否有 ACC_SYNCHRONIZED 標(biāo)記設(shè)置,然后它自動獲取對象的鎖,調(diào)用方法,最后釋放鎖。如果有異常發(fā)生,線程自動釋放鎖。
有一點要注意:對于synchronized方法或者synchronized代碼塊,當(dāng)出現(xiàn)異常時,JVM會自動釋放當(dāng)前線程占用的鎖,因此不會由于異常導(dǎo)致出現(xiàn)死鎖現(xiàn)象。
三.關(guān)于synchronized的其他一些值得注意的地方
1.synchronized與static synchronized 的區(qū)別
synchronized是對類的當(dāng)前實例進(jìn)行加鎖,防止其他線程同時訪問該類的該實例的所有synchronized塊,注意這里是“類的當(dāng)前實例”,類的兩個不同實例就沒有這種約束了。那么static synchronized恰好就是要控制類的所有實例的訪問了,static synchronized是限制線程同時訪問jvm中該類的所有實例同時訪問對應(yīng)的代碼快。實際上,在類中某方法或某代碼塊中有synchronized,那么在生成一個該類實例后,該類也就有一個監(jiān)視快,放置線程并發(fā)訪問改實例synchronized保護(hù)快,而static synchronized則是所有該類的實例公用一個監(jiān)視快了,也就是兩個的區(qū)別了,也就是synchronized相當(dāng)于this.synchronized,而
static synchronized相當(dāng)于Something.synchronized.
一個日本作者-結(jié)成浩的《java多線程設(shè)計模式》有這樣的一個列子:
pulbic class Something(){
public synchronized void isSyncA(){}
public synchronized void isSyncB(){}
public static synchronized void cSyncA(){}
public static synchronized void cSyncB(){}
}
那么,加入有Something類的兩個實例a與b,那么下列組方法何以被1個以上線程同時訪問呢
a. x.isSyncA()與x.isSyncB()
b. x.isSyncA()與y.isSyncA()
c. x.cSyncA()與y.cSyncB()
d. x.isSyncA()與Something.cSyncA()
這里,很清楚的可以判斷:
a,都是對同一個實例的synchronized域訪問,因此不能被同時訪問
b,是針對不同實例的,因此可以同時被訪問
c,因為是static synchronized,所以不同實例之間仍然會被限制,相當(dāng)于Something.isSyncA()與 Something.isSyncB()了,因此不能被同時訪問。
那么,第d呢?,書上的 答案是可以被同時訪問的,答案理由是synchronzied的是實例方法與synchronzied的類方法由于鎖定(lock)不同的原因。
個人分析也就是synchronized 與static synchronized 相當(dāng)于兩幫派,各自管各自,相互之間就無約束了,可以被同時訪問。目前還不是分清楚java內(nèi)部設(shè)計synchronzied是怎么樣實現(xiàn)的。
結(jié)論:A: synchronized static是某個類的范圍,synchronized static cSync{}防止多個線程同時訪問這個 類中的synchronized static 方法。它可以對類的所有對象實例起作用。
B: synchronized 是某實例的范圍,synchronized isSync(){}防止多個線程同時訪問這個實例中的synchronized 方法。
2.synchronized方法與synchronized代碼快的區(qū)別
synchronized methods(){} 與synchronized(this){}之間沒有什么區(qū)別,只是 synchronized methods(){} 便于閱讀理解,而synchronized(this){}可以更精確的控制沖突限制訪問區(qū)域,有時候表現(xiàn)更高效率。
3.synchronized關(guān)鍵字是不能繼承的
相關(guān)文章
將Java(SpringBoot)項目打包為Docker鏡像的三種方法
這篇文章主要介紹了將Java(SpringBoot)項目打包為Docker鏡像的三種方法,分別是手動構(gòu)建、使用Dockerfile和使用SpringBootMaven插件,每種方法都有其特點和適用場景,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-03-03
Mybatis動態(tài)SQL foreach標(biāo)簽用法實例
這篇文章主要介紹了Mybatis動態(tài)SQL foreach標(biāo)簽用法實例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-10-10
Java實現(xiàn)導(dǎo)出pdf格式文件的示例代碼
這篇文章主要為大家詳細(xì)介紹了Java實現(xiàn)導(dǎo)出pdf格式文件的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-02-02
mybatis-plus雪花算法自動生成機(jī)器id原理及源碼
Mybatis-Plus是一個Mybatis的增強(qiáng)工具,它在Mybatis的基礎(chǔ)上做了增強(qiáng),卻不做改變,Mybatis-Plus是為簡化開發(fā)、提高開發(fā)效率而生,但它也提供了一些很有意思的插件,比如SQL性能監(jiān)控、樂觀鎖、執(zhí)行分析等,下面一起看看mybatis-plus雪花算法自動生成機(jī)器id原理解析2021-06-06
Spring?Cloud負(fù)載均衡組件Ribbon原理解析
本文主要講述了微服務(wù)體系下的?Spring?Cloud?Netflix?套件中?Ribbon?的使用,并結(jié)合部分源碼講述了?Ribbon?的底層原理,重點講述了?Ribbon?中是如何獲取服務(wù)以及如何判定一個服務(wù)是否可用,最后也介紹了?Ribbon?中默認(rèn)提供的?7?種負(fù)載均衡策略,感興趣的朋友一起看看吧2022-04-04
Java基礎(chǔ)高級綜合練習(xí)題撲克牌的創(chuàng)建
今天小編就為大家分享一篇關(guān)于Java基礎(chǔ)高級綜合練習(xí)題撲克牌的創(chuàng)建,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-01-01

