前言
????上一節(jié)講了i++并不是線程安全的,我們需要用synchronized來保證其線程安全。
????這里我就介紹下synchronized的基本用法和簡單原理。
????便于說明,我寫了個(gè)i++的例子:
public class AddI {
public static volatile int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> add(1000000));
Thread t2 = new Thread(() -> add(1000000));
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
public static void add(int n) {
for (int m = 0; m < n; m++) {
i++;
}
}
}
1、什么時(shí)候加鎖呢?
????沒有共享就沒有傷害,比如上面的i++被2個(gè)線程同時(shí)修改,出現(xiàn)了并發(fā)問題。此時(shí)我們就需要進(jìn)行加鎖。
????如果一個(gè)變量沒有共享,且沒有并發(fā)問題,那加鎖只會(huì)降低程序的性能。
????線程安全是并發(fā)編程中的重要關(guān)注點(diǎn),應(yīng)該注意到的是,造成線程安全問題的主要誘因有兩點(diǎn),一是存在共享數(shù)據(jù)(也稱臨界資源),二是存在多條線程共同操作共享數(shù)據(jù)。
????因此為了解決這個(gè)問題,我們可能需要這樣一個(gè)方案,當(dāng)存在多個(gè)線程操作共享數(shù)據(jù)時(shí),需要保證同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理完數(shù)據(jù)后再進(jìn)行,這種方式有個(gè)高尚的名稱叫互斥鎖,即能達(dá)到互斥訪問目的的鎖,也就是說當(dāng)一個(gè)共享數(shù)據(jù)被當(dāng)前正在訪問的線程加上互斥鎖后,在同一個(gè)時(shí)刻,其他線程只能處于等待的狀態(tài),直到當(dāng)前線程處理完畢釋放該鎖。
????在 Java 中,關(guān)鍵字 synchronized可以保證在同一個(gè)時(shí)刻,只有一個(gè)線程可以執(zhí)行某個(gè)方法或者某個(gè)代碼塊(主要是對方法或者代碼塊中存在共享數(shù)據(jù)的操作),同時(shí)我們還應(yīng)該注意到synchronized另外一個(gè)重要的作用,synchronized可保證一個(gè)線程的變化(主要是共享數(shù)據(jù)的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點(diǎn)確實(shí)也是很重要的。
2、Synchronized三種應(yīng)用方式
常見的用法:
class X {
// 方式一:修飾非靜態(tài)方法
synchronized void foo() {
// 臨界區(qū)
}
// 方式二:修飾靜態(tài)方法
synchronized static void bar() {
// 臨界區(qū)
}
// 方式三:修飾代碼塊
Object obj = new Object();
void baz() {
synchronized(obj) {
// 臨界區(qū)
}
}
}
????在java里面使用synchronized,加鎖lock和解鎖unlock這2個(gè)操作是Java默認(rèn)加上的,Java 編譯器會(huì)在 synchronized 修飾的方法或代碼塊前后自動(dòng)加上加鎖 lock() 和解鎖 unlock(),這樣做的好處就是加鎖 lock() 和解鎖 unlock() 一定是成對出現(xiàn)的,畢竟忘記解鎖 unlock() 可是個(gè)致命的 Bug。
????那 synchronized 里的加鎖 lock() 和解鎖 unlock() 鎖定的對象在哪里呢?上面的代碼我們看到只有修飾代碼塊的時(shí)候,鎖定了一個(gè) obj 對象,那修飾方法的時(shí)候鎖定的是什么呢?這個(gè)也是 Java 的一條隱式規(guī)則:
- 當(dāng)修飾靜態(tài)方法的時(shí)候,鎖定的是當(dāng)前類的 Class 對象,在上面的例子中就是 Class X;
- 當(dāng)修飾非靜態(tài)方法的時(shí)候,鎖定的是當(dāng)前實(shí)例對象 this。
對于上面的例子,synchronized 修飾靜態(tài)方法相當(dāng)于:
class X {
// 修飾靜態(tài)方法
synchronized(X.class) static void bar() {
// 臨界區(qū)
}
}
修飾非靜態(tài)方法,相當(dāng)于:
class X {
// 修飾非靜態(tài)方法
synchronized(this) void foo() {
// 臨界區(qū)
}
}
這里把i++的例子改下就沒有并發(fā)問題了:
public class AddI {
public static volatile int i = 0;
Object object = new Object();// 單獨(dú)new一個(gè)對象 用于加鎖
public static void main(String[] args) throws InterruptedException {
// 方式一:修飾靜態(tài)方法
Thread t1 = new Thread(() -> add(1000000));
Thread t2 = new Thread(() -> add(1000000));
// 方式二:修飾普通方法
/*AddI addI = new AddI();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
addI.add2(1000000);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
addI.add2(1000000);
}
});*/
// 方式三
/*AddI addI = new AddI();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
addI.add3(1000000);
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
addI.add3(1000000);
}
});*/
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(i);
}
// 修飾靜態(tài)方法
public static synchronized void add1(int n) {
for (int m = 0; m < n; m++) {
i++;
}
}
// 修飾普通方法
public synchronized void add2(int n) {
for (int m = 0; m < n; m++) {
i++;
}
}
// 代碼塊加鎖
public void add3(int n) {
synchronized (object) {
for (int m = 0; m < n; m++) {
i++;
}
}
}
}
synchronized 是 Java 在語言層面提供的互斥原語,其實(shí) Java 里面還有很多其他類型的鎖,但作為互斥鎖,原理都是相通的:鎖,一定有一個(gè)要鎖定的對象,至于這個(gè)鎖定的對象要保護(hù)的資源以及在哪里加鎖 / 解鎖,就屬于設(shè)計(jì)層面的事情了。
加鎖本質(zhì)就是在鎖的對象的對象頭中寫入當(dāng)前線程id(這涉及到底層的東西,后面我也整理一篇)。
滬漂程序員一枚。
堅(jiān)持寫博客,如果覺得還可以的話,給個(gè)小星星哦,你的支持就是我創(chuàng)作的動(dòng)力。
個(gè)人微信公眾號:“Java尖子生”,閱讀更多干貨。
關(guān)注公眾號,領(lǐng)取學(xué)習(xí)、面試資料。加技術(shù)討論群。