Java多线程--设计模式(二、Immutable Object(不可变对象)模式)

一、Immutable Object 模式简介

多线程共享变量的情况下,为了保证数据的一致性,往往需要对这些变量的访问进行加锁。而锁本身又会带来一些问题和开销。Immutable Object 模式使得我们可以在不使用锁的情况下,既保证共享变量访问的线程安全,又能避免引入锁可能带来的问题和开销。
这里采用的方法是状态不可变,从而保证了数据的一致性,又避免了同步访问控制所产生的额外开销和问题,也简化了编程。
下例2.1展示了非线程安全状态的售票站模型:

public class Ticket{
    
    
	
	private int ticket;

	public Ticket(int ticket){
    
    
		this.ticket = ticket;
	}

	public int getTicket(){
    
    
		return ticket;
	}

	public void setTicket(int ticket){
    
    
		this.ticket = ticket;
	}
}

当系统更新票数时,需要调用 setTicket 方法来更新,显然,这是非线程安全的,因为对票数的写操作不是一个原子操作。这时我们可以将门票建模为状态不可变的对象。
如下例2.2:

public final class TIcket{
    
    
	public final int ticket;

	public Ticket(int ticket){
    
    
		this.ticket = ticket;
	}
}

使用状态不可变的门票模型时,如果票数发生改动,则通过替换整个门票对象来实现的。

因此,所谓状态不可变的对象并非指被建模的现实世界实体的状态不可变,而是我们在建模的时候的一种决策;现实世界实体的状态总是在变化的,但我们可以用状态不可变的对象来对这些实体进行建模。

二、Immutable Object 模式的架构

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

图2.1展示了该模式的几个主要参与者:
在这里插入图片描述

  • ImmutableObject:负责存储一组不可变状态。该参与者不对外暴露任何可以修改其状态的方法,其主要方法及职责如下:
    1. getStateX, getStateY:这些 getter方法返回其所属 ImmutableObject 实例所维护的状态相关变量的值。这些变量在对象实例化时通过其构造器的参数获得值。
    2. getStateSnapshot:负责维护 ImmutableObject 所建模的现实世界实体状态的改变。当相应的现实实体状态改变时,该参与者负责生成新的 ImmutableObject 的实例,以反映新的状态。
  • Manipulator:负责维护 ImmutableObject 所建模的现实世界实体状态的变更。当相应的现实实体状态变更时,该参与者负责生成新的 ImmutableObject 的实例,以反映新的状态。
    1. changeStateTo:根据新的状态值生成新的 ImmutableObject 的实例。
  • 获取单个状态的值:调用不可变对象的相关 getter 方法即可实现。
  • 获取一组状态的快照:不可变对象可以提供一个 getter 方法,该方法需要对其返回值做防御性复制或者返回一个只读对象,以避免其状态对外泄露而被改变。
  • 生成新的不可变对象实例:当建模对象的状态发生改变时,创建新的不可变对象实例来反映这种变化。

图2.2展示了该模式典型交互场景的序列图:
在这里插入图片描述
注意,一个严格意义上不可变对象要满足以下所有条件:

  1. 类本身使用 final 修饰:防止其子类改变其定义的行为。
  2. 所有字段都是用final修饰的:final 修饰的字段在其他线程可见时,它必定是初始化完成的,而非 final 修饰的字段由于缺少这种保证,可能导致一个线程“看到”一个字段的时候,它还没有被初始化完成,从而导致一些不可预料的后果。
  3. 在对象的创建过程中,this 关键字没有泄露给其他类:防止其他类(如该类的内部匿名类)在对象创建的过程中修改其状态。
  4. 任何字段,若其引用了其他状态可变的对象(如集合、数组等),这这些字段必须是 private 修饰的,并且这些字段值不能对外暴露。若有相关方法要返回这些字段值,应该进行防御性复制(Defensive Copy)。

三、Immutable Object 模式案例分析

在多线程的情况下,模拟一个线程安全的卖票系统。

创建一个门票信息的类

/**
 * 门票信息
 *
 * 模式角色: ImmutableObject.ImmutableObject
 */
public final class TicInfo {
    
    

    /**
     * 剩余门票数量
     */
    private final int ticNum;

    public TicInfo(int ticNum) {
    
    
        this.ticNum = ticNum;
    }

    public int getTicNum() {
    
    
        return ticNum;
    }
}

用线程来处理门票售卖

/**
 * 变更门票信息
 *
 * 模式角色:ImmutableObject.Manipulator
 */

public class TicChange implements Runnable{
    
    
    private final TicInfo test;

    public TicChange(TicInfo test) {
    
    
        this.test = test;
    }

    @Override
    public void run() {
    
    
        System.out.println("剩余门票数为:" + test.getTicNum() + "张,已成功为您购买!");
    }
}

创建一个测试类

public class Test {
    
    
    public static void main(String[] args){
    
    

        /**
         * 给定初始门票数
         */
        final int ticNum = 100;

        for (int i = ticNum;i > 0;i--){
    
    
            TicInfo test = new TicInfo(i);
            TicChange thread = new TicChange(test);
            new Thread(thread).start();
        }
    }
}

结果预览:
在这里插入图片描述

四、Immutable Object 模式的评价与实现考量

不可变对象具有天生的安全性,多个线程共享一个不可变对象的时候无须使用额外的并发访问控制,这使得我们可以避免显式锁等并发访问控制的开销和问题,简化了多线程编程。

Immutable Object 模式特别适用于以下场景

  • 被建模对象的状态变化不频繁
  • 同时对一组相关的数据进行写操作,因此需要保证原子性
    此场景为了保证操作的原子性,通常的做法是使用显式锁。但若采用 Immutable Object 模式,将这一组相关的数据“组合”成一个不可变对象,则对这一组数据的操作就可以无需加显式锁也能保证原子性,既简化了编程,又提高了代码运行效率。
  • 使用某个对象作为安全的 HashMap 的 Key
    我们知道,一个对象作为 HashMap 的 Key 被“放入 HashMap 之后,若该对象状态变化导致了其 Hash Code 的变化,则会导致后面在用同样的对象作为 Key 去 get 的时候无法获取关联的值,尽管该 HashMap 中的确存在以该对象为 Key 的条目。相反,由于不可变对象的状态不变,因此其 Hash Code 也不变。这使得不可变对象非常适于用作 HashMap 的 Key 。

Immutable Object 模式实现时需要注意以下几个问题

  • 被建模对象的状态变更比较频繁
    此时也不见得不能使用 Immutable Object 模式。只是这意味着频繁创建新的不可变对象,因此会增加GC(Garbage Collection)的负担和CPU消耗,我们需要综合考虑:被建模对象的规模、代码目标运行环境的JVM内存分配情况、系统对吞吐率和响应性的要求。若这几个方面因素综合考虑都能满足要求,那么使用不可变对象建模也未尝不可。
  • 使用等效或者近似的不可变对象
    有时创建严格意义上的不可变对象比较难,但是尽量向严格意义上的不可变对象靠拢也有利于发挥不可变对象的好处。
  • 防御性复制
    如果不可变对象本身包含一些状态需要对外暴露,而相应的字段本身又是可变的(如 HashMap),那么在返回这些字段的方法还是需要做防御性拷贝,以避免外部代码修改了其内部状态。

总结

本文介绍了Immutable Object模式的意图及架构。并结合笔者经验提供了一个实际的案例用于展示使用该模式的典型场景,在此基础上对该模式进行了评价并分享在实际运用该模式时需要注意的事项。

参考资源

[1]黄文海. Java多线程编程指南.北京:电子工业出版社,2015.10.
[2]冲锅煮酒. Immutable Object模式 - 多线程. https://www.cnblogs.com/pingyun/p/11451078.html.
[3]Brian Goetz. Java theory and practice:To mutate or not to mutate?. http://www.ibm.com/developworks/java/library/j-jtp02183/index.html.
[4]Brian Goetz et al. Java Concurrency In Practice. Addison Wesley, 2006.

猜你喜欢

转载自blog.csdn.net/qq_34533266/article/details/105196363