一、解決什么問(wèn)題
多線程訪問(wèn)同一對(duì)象的實(shí)例變量時(shí)饺蔑,避不開(kāi)線程安全的問(wèn)題锌介。如果處理不當(dāng),很可能會(huì)出現(xiàn)臟讀、死鎖等問(wèn)題孔祸。所以我們應(yīng)該合理地使用synchronized來(lái)保證線程安全隆敢。
什么場(chǎng)景下不用考慮線程安全問(wèn)題?
-
變量是在方法內(nèi)部聲明的局部變量崔慧;
public class Calculator { public int add(int num){ // 局部變量沒(méi)有線程安全問(wèn)題 int result = 0; result += num; return result; } }
@Slf4j public class MyThread implements Runnable { private Calculator calculator; private int num; public MyThread(Calculator calculator, int num) { this.calculator = calculator; this.num = num; } @Override public void run() { int result = calculator.add(num); log.info("線程{}的計(jì)算結(jié)果為:{}", Thread.currentThread().getName(), result); } }
@Slf4j public class Test001 { private static int result = 0; public static void main(String[] args) throws InterruptedException { // 實(shí)例化一個(gè)計(jì)算器對(duì)象 Calculator calculator = new Calculator(); // 兩個(gè)線程訪問(wèn)同一個(gè)計(jì)算器對(duì)象 Thread thread1 = new Thread(new MyThread(calculator,100)); Thread thread2 = new Thread(new MyThread(calculator,100)); thread1.start(); thread2.start(); } }
-
多個(gè)線程分別訪問(wèn)不同對(duì)象的實(shí)例變量拂蝎;
@Slf4j public class Test001 { private static int result = 0; public static void main(String[] args) throws InterruptedException { // 實(shí)例化兩個(gè)計(jì)算器對(duì)象 Calculator calculator1 = new Calculator(); Calculator calculator2 = new Calculator(); // 兩個(gè)線程分別訪問(wèn)各自的計(jì)算器對(duì)象 Thread thread1 = new Thread(new MyThread(calculator1,100)); Thread thread2 = new Thread(new MyThread(calculator2,100)); thread1.start(); thread2.start(); } }
此時(shí)每個(gè)線程對(duì)應(yīng)一個(gè)計(jì)算器對(duì)象,無(wú)論變量為局部變量還是實(shí)例變量温自,都不存在線程安全的問(wèn)題夹界。
-
變量為線程獨(dú)享變量的時(shí)候;
@Slf4j public class MyThread implements Runnable { // 線程獨(dú)享的變量 private int count = 200; @Override public void run() { count--; log.info("當(dāng)前線程名稱(chēng):{},計(jì)數(shù)器為:{}", Thread.currentThread().getName(), count); } }
或者使用ThreadLocal的時(shí)候,關(guān)于ThreadLocal的使用,參考另外一篇文章编振;
剩下的場(chǎng)景中瓢阴,多線程共享變量就必須要考慮線程安全的問(wèn)題了累贤。
二示损、對(duì)方法的使用
先來(lái)看下一個(gè)線程不安全的例子:
public class Calculator {
private int result = 0;
public int add(int num){
result += num;
return result;
}
}
@Slf4j
public class MyThread implements Runnable {
private Calculator calculator;
private int num;
public MyThread(Calculator calculator, int num) {
this.calculator = calculator;
this.num = num;
}
@Override
public void run() {
int result = calculator.add(num);
log.info("線程{}的計(jì)算結(jié)果為:{}", Thread.currentThread().getName(), result);
}
}
@Slf4j
public class Test001 {
public static void main(String[] args) throws InterruptedException {
Calculator calculator = new Calculator();
Thread thread1 = new Thread(new MyThread(calculator,100));
Thread thread2 = new Thread(new MyThread(calculator,100));
thread1.start();
thread2.start();
}
}
此時(shí)thread1和thread2共享了Calculator中的result變量嘉汰,執(zhí)行后可能會(huì)得到如下線程不安全的結(jié)果,注意,這里是可能的結(jié)果村斟,并不是每次運(yùn)行都會(huì)得到這個(gè)結(jié)果逾滥。如果想要模擬線程不安全的結(jié)果,可以使用多線程調(diào)試技術(shù)《IDEA調(diào)試技巧進(jìn)階》。
線程Thread-1的計(jì)算結(jié)果為:200
線程Thread-0的計(jì)算結(jié)果為:200
如何解決這個(gè)線程不安全問(wèn)題呢?很簡(jiǎn)單,在Calculator的add方法中加上synchronized關(guān)鍵字即可。
此時(shí)使用多線程調(diào)試技術(shù)可以發(fā)現(xiàn)芙委,某個(gè)線程先進(jìn)入add方法后侧啼,另外一個(gè)線程壓根就沒(méi)有停止在斷點(diǎn)處哪审,運(yùn)行的結(jié)果是線程安全的舌狗。
關(guān)于加在方法上的synchronized需要注意如下事項(xiàng):
-
鎖定的是對(duì)象實(shí)例恋日,而不是這個(gè)方法筷屡;
因此扼倘,對(duì)于這個(gè)實(shí)例中其它的synchronized方法其實(shí)也是處于鎖定狀態(tài)的秉剑,其它線程都不能進(jìn)入岗仑,因?yàn)樵搶?duì)象的鎖還未被釋放既鞠;對(duì)于這個(gè)實(shí)例中其它的非synchronized方法,其它線程則是可以進(jìn)入的它碎。
需要注意的是踊谋,要避免在非synchronized的方法中讀取會(huì)在synchronized方法中修改的變量害驹,這會(huì)造成臟讀,一定要這樣操作的話圣勒,那么請(qǐng)將非synchronized的方法改為synchronized。
-
synchronized鎖重入;
一個(gè)已經(jīng)獲得對(duì)象鎖的線程夏跷,再次請(qǐng)求獲取該對(duì)象的鎖是沒(méi)問(wèn)題的佣蓉,這就叫做synchronized的鎖重入。具體表現(xiàn)就是基显,一個(gè)線程可以在synchronized的方法中調(diào)用該實(shí)例其它的synchronized方法舅桩,不會(huì)存在等待鎖的情況恢暖。
-
出現(xiàn)異常時(shí)釋放鎖挨队;
當(dāng)一個(gè)線程進(jìn)入某個(gè)實(shí)例的synchronized方法執(zhí)行出現(xiàn)異常時(shí)蔬充,是會(huì)自動(dòng)釋放該對(duì)象鎖的;
三如输、對(duì)語(yǔ)句塊的使用
先前synchronized都是在方法級(jí)別上使用的,當(dāng)方法邏輯很簡(jiǎn)單酪惭,耗時(shí)不多時(shí)纺铭,沒(méi)啥問(wèn)題锥累。但是如果方法邏輯是分步驟的姑蓝,且有些步驟并不需要同步執(zhí)行,那么方法級(jí)別的synchronized就顯得不太合適了蜻拨。我們應(yīng)該只鎖定那些需要同步執(zhí)行的代碼塊厘灼。
@Slf4j
public class Calculator {
private int result = 0;
public int add(int num) {
// 不需要同步執(zhí)行
if (num == 100) {
log.info("不執(zhí)行計(jì)算,返回0萍悴!");
return 0;
} else {
// 需要同步執(zhí)行
synchronized (this) {
result += num;
return result;
}
}
}
}
如此嗅蔬,對(duì)于執(zhí)行非同步代碼塊的線程盒延,并不受同步代碼塊中鎖的影響票罐,可以提高多線程的執(zhí)行效率。
關(guān)于加在代碼塊上的synchronized需要注意如下事項(xiàng):
- 使用synchronized(this)玖翅,鎖定的是對(duì)象,所以其它線程都是無(wú)法直接進(jìn)入該對(duì)象的任何synchronized(this)代碼塊和synchronized方法的勘高,需要等待當(dāng)前線程釋放對(duì)象鎖建蹄;
- 還可以使用synchronized(任意監(jiān)視對(duì)象)來(lái)實(shí)現(xiàn)同步鎖定代碼塊的效果焦人,但是相較于synchronized(this)有一些區(qū)別。
- 因?yàn)榇藭r(shí)鎖定的不是對(duì)象前塔,而是這個(gè)“任意監(jiān)視對(duì)象”顶燕,多個(gè)線程只有在"任意監(jiān)視對(duì)象"為同一個(gè)對(duì)象的前提下,才可以同步執(zhí)行當(dāng)前對(duì)象中的任何synchronized(任意監(jiān)視對(duì)象)代碼塊憋肖。
- 如果當(dāng)前對(duì)象中多個(gè)同步代碼塊"任意監(jiān)視對(duì)象"不是同一個(gè),或者synchronized(this)代碼塊和synchronized方法怎炊,那么多個(gè)線程都是不能同步執(zhí)行的。
- 特別的歹垫,對(duì)于這個(gè)“任意監(jiān)視對(duì)象”中的synchronized(this)代碼塊和synchronized方法是可以同步執(zhí)行的。
- 避免在非同步代碼塊中讀取會(huì)在同步代碼塊中修改的變量,容易造成臟讀;同樣的纱控,避免在非同一個(gè)監(jiān)視對(duì)象中讀取另外一個(gè)監(jiān)視對(duì)象中會(huì)修改的變量辆毡;
總之,就是注意多個(gè)線程執(zhí)行同步代碼時(shí)甜害,關(guān)注鎖定的是否是同一個(gè)對(duì)象舶掖。
四、對(duì)類(lèi)的使用
在功能和效果上唾那,對(duì)靜態(tài)方法使用synchronized和對(duì)類(lèi)加鎖的代碼塊是一樣的访锻。
@Slf4j
public class Calculator {
private static int result = 0;
public static synchronized int add(int num) {
result += num;
return result;
}
}
等同于
@Slf4j
public class Calculator {
private int result = 0;
public int add(int num) {
synchronized (Calculator.class) {
result += num;
return result;
}
}
}
它們都是把當(dāng)前類(lèi)鎖定了褪尝,對(duì)于該類(lèi)的所有對(duì)象闹获,競(jìng)爭(zhēng)同一個(gè)類(lèi)鎖是同步的。
@Slf4j
public class Test001 {
public static void main(String[] args) throws InterruptedException {
// 兩個(gè)對(duì)象競(jìng)爭(zhēng)的是同一個(gè)類(lèi)鎖河哑,而不是各自的對(duì)象鎖
Calculator calculator1 = new Calculator();
Calculator calculator2 = new Calculator();
Thread thread1 = new Thread(new MyThread(calculator1, 100));
Thread thread2 = new Thread(new MyThread(calculator2,200));
thread1.start();
thread2.start();
}
}
同時(shí)避诽,類(lèi)鎖并不影響對(duì)象鎖的運(yùn)行,它們不是同步的璃谨。
@Slf4j
public class Calculator {
private int result = 0;
public int add(int num) {
// 類(lèi)鎖
synchronized (Calculator.class) {
result += num;
return result;
}
}
public int minus(int num) {
// 對(duì)象鎖
synchronized (this) {
result -= num;
return result;
}
}
}
五沙庐、其它
- 持有字符串類(lèi)型的鎖容易因?yàn)镾tring常量池緩存導(dǎo)致多線程并發(fā)問(wèn)題。
- 同步代碼塊獲取的鎖有可能是會(huì)改變的佳吞,多線程等待的只要同一個(gè)對(duì)象拱雏,那么就可以同步,不是同一個(gè)對(duì)象底扳,就不同步铸抑。
- 在對(duì)象的屬性發(fā)生變化時(shí)并不代表對(duì)象發(fā)生變化,此時(shí)等待該對(duì)象鎖的所有線程是可以同步的衷模。