首先同步是什麼呢?
同步即在多線程環境下保證對象的一致性,同時當一個線程進入到同步的代碼塊或者方法中的時候,都能夠看到由同一個鎖保護的前一個線程所有的修改效果。
java語言保證了讀或者寫一個變量是原子的,除了讀寫long、double類型的數據(爲什麼給讀寫long,double類型的數據時非原子的,使用AtomicLong/Double讀寫long/double可以保證原子性,同時保證內存的可見性),因此當我們讀取或者寫入變量的時候,我們的操作無需進行同步,就可以保證操作的原子性,但是由於JMM(java memory model)的限制,下圖爲JMM的示意圖:
圖片來自:https://juejin.im/post/5d5df4d7518825661a3c1dfe
每一個線程在工作的時候都會從主存中拷貝一份變量存儲在自己的工作內存中,因此一個線程對一個變量的更改對另一個線程來說不能保證是可見的,因此,即使是讀寫都爲原子的情況下,爲了在多線程環境下內存的可見性,我們也應該進行同步
- 不進行同步多線程下訪問共享變量的例子:
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
上面的代碼在運行之後並沒有停止,原因在於沒有同步,一個線程對該變量的修改,另一個線程沒有獲取到。
- 使用synchronized進行同步訪問共享變量的例子
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop(){
stopRequested=true;
}
private static synchronized boolean stopRequested(){
return stopRequested;
}
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread=new Thread(() -> {
int i=0;
while (!stopRequested()){
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
上面的代碼在運行之後大約在一秒之左右停止了,這個符合我們的預期
但是使用synchronized進行同步,我們爲什麼要在兩個方法上都加上synchronized呢,原因在於synchronized下面的兩條保證內存可見的通信規則,其實在這裏我們也僅僅利用了synchronized的通信規則,因爲方法中的讀取和寫入操作java本身就保證了他是原子的。
Synchronized的兩條規定保證了內存的可見性:
1、線程解鎖前,必須把共享變量的最新值刷新到主內存中;
2、線程加鎖時,講清空工作內存中共享變量的值,從而使用共享變量是需要從主內存中重新讀取最新的值(加鎖與解鎖需要統一把鎖)
線程執行互斥鎖代碼的過程:
1.獲得互斥鎖
2.清空工作內存
3.從主內存拷貝最新變量副本到工作內存
4.執行代碼塊
5.將更改後的共享變量的值刷新到主內存中
6.釋放互斥鎖
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#incorrectlySync
因此,如果我們只給一個加上synchronized進行同步的話,
只給stopRequested()加synchronized,那就沒有辦法保證requesttStop()將最新的值寫入到主存中,如果沒有寫到主存中,那麼stopRequested()沒有辦法獲取到最新的值。
只給requestStop()加synchronized的話,沒有辦法保證stopRequested()得到的值是最新的。
如果僅需要實現線程間通信效果的話,我們可以使用volatile關鍵字保證內存的可見性,這樣一個線程讀取該變量的時候得到的就是該變量最新的值。
- 使用volatile進行同步,多線程環境下訪問共享變量
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested) {
i++;
}
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
上面的代碼在運行1秒左右會自動停止,達到了我們預期的效果
但是在使用volatile進行同步的時候我們需要注意,volatile修飾的變量的++操作是非原子的因爲++分爲兩步,首先讀取舊值,然後進行加一,如果第二個線程在第一個線程讀取舊值和寫回新值之間讀取變量,那麼兩個線程最後會得到相同的值。
那麼如何避免上述的問題呢?
使用Atomic包下面的工具。
例子:使用AtomicLong在多線程環境下生成序列號:
private static AtomicInteger nextSerialNumber = new AtomicInteger();
public static long generateSerialNumber() {
return nextSerialNumber.getAndIncrement();
}
上面的代碼可以在不加鎖的情況下保證我們得到唯一的序列號。
我們使用java.util.concurrent.atomic包下面的AtomicLong類,該類在保證了在不加鎖的情況下,可以在單個變量上進行線程安全的編程,也就是該類既保證了通信效果,又保證了操作的原子性(使用AtomicLong/Double讀寫long/double可以保證原子性,同時保證內存的可見性)
總之非常重要的是在多個線程共享可變數據的時候,應保證多線程同步。
上面是自己對Java同步訪問共享可變數據的學習和理解。如果有錯誤請指正,歡迎大家多多交流。