共享可變性設計中存在風險以及解決方法(四)

本篇是《Java虛擬機併發編程》第五章的閱讀筆記

在(三)中,我們在代碼裏沒有使用任何顯示的同步操作,直接作用在可變變量上,當然是因爲在程序中只有一個可變字段。如果程序中不止一個與可變狀態相關或依賴的變量,那麼我們就無可避免地要使用顯示的同步操作。


到目前爲止重構都達到了預想的效果,但我們還要想更高要求的目標邁進:

  1. 追蹤並記錄電源的使用情況。即每次電源電量消耗完畢的時候,我們度需要把電源的使用次數進行累加

當然這也意味着,我們必須保證對於變量level和useage的改動是原子的。(在同一個線程裏修改這個兩個變量,要麼全部修改成功,要麼全部都不變)

我們這裏選擇使用ReentrantReadWriteLock。該類同時提供兩把鎖(即讀鎖和寫鎖)。由於使用了顯示的鎖,所以就可以將level字段由AtomicLong改回long類型。

package com.ensureatomicity;

import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class EnergySource {
    private final long MAXLEVEL = 100;
    private long level = MAXLEVEL;
    private long usage = 0;
    private final ReadWriteLock monitor = new ReentrantReadWriteLock();
    private static final ScheduledThreadPoolExecutor replenishTimer = 
            new ScheduledThreadPoolExecutor(10);
    private ScheduledFuture<?> replenishTask;

    private EnergySource(){}

    private void init(){
        replenishTask = replenishTimer.scheduleAtFixedRate(new Runnable(){
            public void run(){
                System.out.println(System.nanoTime()/1.0e9);
                replenish();
            }
        }, 0, 1, TimeUnit.SECONDS);
    }

    public static EnergySource create(){
        final EnergySource energySource = new EnergySource();
        energySource.init();
        return energySource;
    }

    public long getUnitsAvailable(){
        monitor.readLock().lock();
        try{
            return level;
        }finally{
            monitor.readLock().unlock();
        }
    }

    public long getUsageCount(){
        monitor.readLock().lock();
        try{
            return usage;
        }finally{
            monitor.readLock().unlock();
        }
    }

    public boolean useEnergy(final long units){
        monitor.writeLock().lock();
        try{
            if(units>0 && level>=units){
                level -= units;
                usage++;
                return true;
            }else{
                return false;
            }
        }finally{
            monitor.writeLock().lock();
        }
    }

    public void stopEnergySource(){
        replenishTask.cancel(false);
    }

    private void replenish(){
        monitor.writeLock().lock();
        try{
            if(level < MAXLEVEL){
                level++;
            }
        }finally{
            monitor.writeLock().unlock();
        }
    }
}
  1. 在上面的代碼中,引入兩個新字段:usage和monitor。其中usage的作用是記錄電源被使用的次數。
  2. 在useEnergy函數中,我們先獲得了寫鎖,如果可以的話還可以在獲得鎖的時候指定一個超時時間。一旦獲得寫鎖的操作完成,就可以更改兩個變量的值。最後在finally塊中,安全地將鎖釋放。

    遺憾的是,這一版本的代碼比之前的版本複雜了不少,而這錯誤正是由複雜性所產生的。當然還有方法可以避免使用顯示的同步操作。


ReentrantReadWriteLock簡單介紹

  1. 讀鎖,可以使得讀操作併發執行,但是如果巧合有線程在進行寫操作,那麼該讀操作會被阻塞。
  2. 寫線程獲取寫入鎖後可以再次獲取讀取鎖,但是讀線程獲取讀取鎖後卻不能獲取寫入鎖,要獲取寫入鎖,只能先釋放讀取鎖。

    可以參考別人寫的ReentrantReadWriteLock 可重入讀寫鎖的理解ReentrantReadWriteLock可重入讀寫鎖分析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章