本篇是《Java虛擬機併發編程》第六章的閱讀筆記
在(四)中,因爲程序中不止一個與可變狀態相關或依賴的變量,所以我們使用顯示鎖的來進行同步操作。
雖然是用了同步鎖成功執行了代碼,但同時也會產生許多的問題,因爲同步本身就有缺陷。例如可能會產生死鎖,活鎖;可能會因爲是人工加鎖,所以會導致錯誤的概率增加,你需要一個個確認是否每個地方都做了適當的同步,開發效率低,等等。
這裏我們通過使用軟件事務內存STM模型來使得線程安全的處理共享可變變量
簡單介紹STM
STM是一種多線程之間數據共享的同步機制。它是模擬數據庫事務的併發控制機制來控制在並行工作時對共享內存的訪問控制。它是鎖的一種代替機制。它擁有了ACID中的ACI的特性。
最大的好處就是它提高了開發效率,靈活和擴展性。對於並行編程而言,只需將線程中需要訪問共享內存的關鍵邏輯部分劃分出來封裝到一個事務中即可,不再需要關心相關的同步,鎖產生的問題,全部交給事務內存系統來處理。
對於事務就不多說了。
在Java中使用STM有如下的選擇:
- 直接在Java中使用Clojure STM
- 使用Multiverse的STM的註解形式
- 使用Akka,它既支持STM又支持角色的模型
在這裏使用Akka中提供的Scala的包來使用STM
package com.stm;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import scala.concurrent.stm.Ref;
import scala.concurrent.stm.japi.STM;
public class EnergySource {
private final long MAXLEVEL = 100;
final Ref.View<Long> level = STM.newRef(MAXLEVEL);
final Ref.View<Long> usageCount = STM.newRef(0L);
final Ref.View<Boolean> keepRunning = STM.newRef(true);
private static final ScheduledExecutorService replenishTimer =
Executors.newScheduledThreadPool(10, new ThreadFactory(){
@Override
public Thread newThread(Runnable runnable) {
Thread t = new Thread(runnable);
t.setDaemon(true);
return t;
}
});
private EnergySource(){}
private void init(){
replenishTimer.schedule(new Runnable(){
public void run(){
replenish();
if(keepRunning.get()){
replenishTimer.schedule(this, 1, TimeUnit.SECONDS);
}
}
}, 1, TimeUnit.SECONDS);
}
public static EnergySource create(){
final EnergySource energySource = new EnergySource();
energySource.init();
return energySource;
}
public void stopEnergySource(){
keepRunning.swap(false);
}
public Long getUnitsAvailable(){
return level.get();
}
public Long getUsageCount(){
return usageCount.get();
}
public boolean useEnergy(final long units){
return STM.atomic(new Callable<Boolean>(){
@Override
public Boolean call() throws Exception {
long currentLevel = level.get();
if(units>0 && currentLevel >= units){
level.swap(currentLevel - units);
usageCount.set(usageCount.get() + 1);
return true;
}else{
return false;
}
}
});
}
private void replenish(){
STM.atomic(new Runnable(){
@Override
public void run() {
long currentLevel = level.get();
if(currentLevel < MAXLEVEL){
level.swap(currentLevel + 1);
}
}
});
}
private static final EnergySource es = EnergySource.create();
public static void main(String[] args) throws InterruptedException{
List<Callable<Object>> tasks = new ArrayList<>();
System.out.println("Energy level at start: " + es.getUnitsAvailable());
for(int i=0; i<10; i++){
tasks.add(new Callable<Object>(){
@Override
public Object call() throws Exception {
for(int j=0; j<7; j++)
es.useEnergy(1);
return null;
}
});
}
final ExecutorService service = Executors.newFixedThreadPool(10);
service.invokeAll(tasks);
System.out.println("Energy level at end: " + es.getUnitsAvailable());
System.out.println("Usage: " + es.getUsageCount());
es.stopEnergySource();
service.shutdown();
}
}
- 首先level和usageCount都被聲明爲託管引用(後面會介紹),並各自持有一個不可變的Long類型的值,雖然我們不能直接更改它的值,但是我們仍然可以通過更改託管引用使其安全指向新值。
- 由於我們會在useEnergy函數中同時修改電量和使用次數,所以useEnergy函數需要使用一個顯示的事務來完成這些操作,以確保對這兩個字段變更是原子的。用Callable接口將邏輯代碼封裝到一個事務裏面。同理replenish函數也是一樣。
- 在EnergySource上一個版本中,ScheduledExecutorService會週期性地(每秒鐘一次)調用replenish()函數,直至整個任務結束,這就要求stopEnergySource()必須是同步的,因爲有可能會有多個線程調用這個函數(假設有可能)
- 在這個版本中,我們不用在週期性地調用replenish函數,而只會在對象實例初始化的時候執行一下調度操作。在每次調用replenish函數時,我們都會根據keepRunning的值來決定函數是否應該在1s之後被再度執行
- 這一變化消除了stopEnergySource函數和調度器/計時器(timer)之間的耦合。現在,stopEnergySource函數只依賴於keepRunning這個標誌。而該標誌可以很容易地通過STM事務來管理
該版本和上一版本相比就簡潔明瞭許多。
託管引用和不可變的值
在上面的例子中,最初託管引用是level,不可變的值是100。你可以想象level中有一個指針並指向100。
這樣就相當於將可變實體(託管引用)和狀態(不可變的值)分離了。爲什麼這麼說,因爲
- 對於普通變量來說,以前聲明普通對象的時候是通過分配在棧上reference指向在堆中爲該對象實例分配的那塊內存。所以如果你要修改對象中的數據就可以直接訪問該對象的的內存進行修改。
- 對於託管引用和不可變來說,雖然也是通過指針指向100這個數據分配的內存,但是100是不可變的,換句話說你不可以在這塊內存中進行修改,要想修改數據,只能重新分配新的內存創建一個新的值然後將指針指向這個新的值。當然,修改指針的操作規定了是原子的,所以要麼修改成功對所有線程可見,要麼修改失敗數據不變。這也是爲什麼在多線程的條件下,對於單獨託管引用的訪問和值的更新操作它是線程安全的。