Immutable Object(不可变对象)模式
标签:并发模式

Immutable Object(不可变对象)模式

Immutable Object模式使得我们可以在不使用锁的情况下,即保证共享变量访问的线程安全,又能避免引入锁带来的问题和开销。

如果多个线程并发的修改某个对象的状态或者一个线程修改,一个线程访问的情况下,我们得利用加锁或者CAS的方式来同步控制数据的一致性。但是他们存在上下文切换、等待时间以及CAS的ABA问题等。

Immutable Object模式的方法是采用对外可见的是状态不可变的对象,使得共享的状态本身就具有线程安全性,这样可以避免了额外的同步访问控制。

注意不变模式和只读属性有一定区别的,不变模式比只读属性具有更强的一致性和不变性,对只读属性的对象而言,对象本身不能被其他线程修改,但是对象自身的状态却可能自行修改的。

1. 架构

Immutable Object模式将现实可变的物体建模为状态不可变的对象,将现实中实体的状态变化通过创建不同的状态不可变的对象来反映。

下面是相关类图:


2. 要求

一个严格意义上的不可变对象应满足:

  1. 类本身要用final修饰,防止子类修改其行为
  2. 所有字段用final修饰,一是防止字段被修改,二是在多线程环境下由JMM保证被修饰的字段是初始化安全的(即final字段在对其他线程可见时,必定是初始化完成的)。
  3. 在创建该类时,this关键字没有泄露,防止其他类(比如匿名内部类),防止在对象创建过程中修改其状态。
  4. 任何字段,若其引用了其他状态可变的对象(如集合,数组),则这些字段必须是private修饰的,并且这些字段值不能对外暴露,如果某些方法要对外返回这些这些字段值,必须要进行防御性复制

3. 实例

下面用一个电话号码对应一个信息中心,并且有一个更新类来负责检测是否有路由表有变化来采取修改

信息中心 MMSCInfo

/**
 * 信息中心
 * @author liuyao
 * @date 2018/08/23
 */
public class MMSCInfo {
    private final String deviceID;

    private final String url;

    private final int maxAttachmentSizeInBytes;

    public MMSCInfo(String deviceID, String url, int maxAttachmentSizeInBytes) {
        this.deviceID = deviceID;
        this.url = url;
        this.maxAttachmentSizeInBytes = maxAttachmentSizeInBytes;
    }

    public MMSCInfo(MMSCInfo prototype){
        this.deviceID=prototype.deviceID;
        this.url=prototype.url;
        this.maxAttachmentSizeInBytes=prototype.maxAttachmentSizeInBytes;
    }

    public String getDeviceID() {
        return deviceID;
    }

    public String getUrl() {
        return url;
    }

    public int getMaxAttachmentSizeInBytes() {
        return maxAttachmentSizeInBytes;
    }
}

路由表 MMSCRouter

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * 路由表
 * @author liuyao
 * @date 2018/08/23
 */
public class MMSCRouter {
    // volatile修饰,保证多线程环境下的可见性
    private static volatile MMSCRouter instance = new MMSCRouter();
//    维护一个手机号码到信息中心的路由表
    private final Map<String, MMSCInfo> routeMap;

    public MMSCRouter() {
//        将映射关系从数据库取出,存到内存
        this.routeMap = MMSCRouter.retrieveRouteMapFromDB();
    }

    private static Map<String, MMSCInfo> retrieveRouteMapFromDB() {
        Map<String, MMSCInfo> map = new HashMap<String, MMSCInfo>();
        //....
        return map;
    }

    public static MMSCRouter getInstance() {
        return instance;
    }

    public MMSCInfo getMMSC(String msisdnPrefix) {
        return routeMap.get(msisdnPrefix);
    }

    /**
     * 将当前的MMSCRouter实例更新为指定的新实例
     *
     * @param newInstance
     */
    public static void setInstance(MMSCRouter newInstance) {
        instance = newInstance;
    }

    /**
     * 进行深拷贝
     * @param m
     * @return
     */
    public static Map<String, MMSCInfo> deepCopy(Map<String, MMSCInfo> m) {
        Map<String, MMSCInfo> result = new HashMap<String, MMSCInfo>();
        for (String key : m.keySet()) {
            result.put(key, new MMSCInfo(m.get(key)));
        }
        return result;
    }

    public Map<String, MMSCInfo> getRouteMap() {
        //防御性复制
        return Collections.unmodifiableMap(deepCopy(routeMap));
    }
}

上面这两个都可以看出ImmutableObject,可见当要返回可变对象的时候,需要进行防御复制,生成不可变对象。

接下来是操作变更类 OMCAgent

/**
 * 处理路由表变更类
 * @author liuyao
 * @date 2018/08/23
 */
public class OMCAgent extends Thread {
    @Override
    public void run() {
        boolean isTableModificationMsg=false;
        String updatedTableName=null;
        while (true){
            //...
            //读取到新的路由表信息,进行更新
            if (isTableModificationMsg){
                if ("MMSCInfo".equals(updatedTableName)){
                    MMSCRouter.setInstance(new MMSCRouter());
                }
            }
            //....
        }
    }
}

4. 适用场景

  1. 被建模的对象的状态变化不频繁,该模式采用了volatile关键字保证了可见性,根据happens-before原则,对volatile变量的写先于volatile变量的读,省去了加锁操作,但是如果对象的状态变化比较频繁的话,那么频繁的创建的新对象,可能会增加JVM垃圾回收的负担。
  2. 同时对一组相关的数据进行写操作,因此需要保证原子性,平时对于一组数据进行写操作,一般都是要通过加锁的方式,而我们可以通过final语义进行控制,保证了变量的初始化是完整的。
  3. 使用某个对象作为安全的HashMap的key,由于HashMap的key涉及到了hashcode,如果对象改变了则相应的hashcode会进行改变,不可变的对象保证了hashcode计算出的值始终唯一。

5. JDK实例

CopyOnWriteArrayList中,内部使用的是一个array来保存集合的元素,该array被声明成volatile类型。

来看一下add方法:

final void setArray(Object[] a) {
    array = a;
}
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        //复制原数组,将新元素添加到末尾
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        //  直接将新的newElements设置为新的array
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

而对于其他的遍历操作,是不需要加锁的

 public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
    }

还有一个 CopyOnWriteArraySet它是内部使用了一个 CopyOnWriteArrayList,可见套路一样的。

  • 6 min read

CONTRIBUTORS


  • 6 min read