Immutable Object不可变对象模式

为什么要不可变对象

不可变对象,指的是该对象一旦被创建后,对象的内部状态是对外隐藏的,其他线程没有办法修改对象的内部状态或数据,当然对象自己内部也不会对自己做修改,整个对象可以说像是“只读”的。你可能会觉得奇怪,创建一个不可修改的对象有什么意义。想一想你电脑上的一些属性为“只读”的文件,之所以它们设置为“只读”,是因为它们不希望自己的数据被别人修改,一直保持文件数据的一致性和正确性。

看到“一致性”和“正确性”两个特点,你应该想到,在并发环境下,线程同时访问共享资源,例如某一个对象或数据,保证这个对象或数据的一致性和正确性,不正是我们一直在努力做的么。在对共享资源做同步操作时,通常我们会选择加锁,但是加锁操作会使线程阻塞,降低了系统的性能。可能你会想到用无锁CAS,虽然使用CAS的线程,在执行修改操作失败后,不会被挂起,从而减少了上下文切换带来的性能损耗,但是,如果CAS长时间自旋等待,带来的开销也是不可忽视的。为了减少这些同步操作带来的问题和开销,使用不可变对象可以在多线程环境下,不使用同步操作,依然保持线程安全,原因正如上面所说,不可变对象一旦创建后,它的内部状态对外是不可见的,没有线程可以修改它的内部数据,这个对象自然就是一直安全的,所以多线程共同访问它时,不需要做同步处理。

当然,不可变对象的内部状态也不是一直不变的,意思是,你用一个不可变对象表示某种事物的状态,当事物的状态发生改变时,我们因为无法修改不可变对象,所以我们是直接新创建一个不可变对象,将变化数据初始化后,直接替换掉了旧的不可变对象,以这种新替换旧的方式来改变对象的内部状态或数据。

总的来说,就是不可变对象表示的事物状态并不是一成不变的,虽然不可变对象自身不可被修改,但它表示的事物的状态是可变的。为了用不可变对象表示出事物的变化,我们用创建新的对象并初始化新值的方式,完全替换掉旧的对象,来表示这个事物的状态变化。(表述的不好- -、见谅)

适用场景与创建规则

上面描述了那么多不可变对象的性质,总结它的特点就是,它是线程安全的,多线程共享同一个不可变对象时不需要同步控制来保证数据的一致性和正确性。它的使用场景有以下这些:

·• 对象表示的事物,其状态变化不频繁

  • 对象会被多线程共享访问,需要保证数据一致性和正确性
  • 实际场景中例如通信信息的发送,把信息数据创建为不可变对象

 

为了保证不可变对象创建后,其内部不发生任何改变,在创建时有以下规则:

  1. 该对象的类不提供修改自身数据的方法,如setter()方法。
  2. 把类中的所有属性字段设置为private访问控制,并加上final关键字标识。final可以保证数据在被其他线程访问时,它一定是已经初始化完成的。
  3. 为了保证类不会被子类继承后,修改里面的方法,把类用final关键字标识,或者把类的构造函数加上private访问控制。
  4. 如果类中的字段引用了其他外部的,状态可改变的对象,那么该字段要添加private访问控制。如果非要引用或返回这个字段,那么要返回它的副本,即做防御性复制。

 

来看一个简单的例子:

假设我们有一个学生信息管理程序,对每一个学生的信息进行录入,显示和修改,简单的实现方式如下:

package immutableobject;

public class Student {
	private String name;
	private String major;
	private int age;
	private String gender;
	
	public Student(String name, String major, int age, String gender) {
		this.name = name;
		this.major = major;
		this.age = age;
		this.gender = gender;
	}
	
	public void alterInfo(String name, String major, int age, String gender) {
		this.name = name;
		this.major = major;
		this.age = age;
		this.gender = gender;
	}
	
	public String getName() {
		return name;
	}
	
	public String getMajor() {
		return major;
	}
	
	public int getAage() {
		return age;
	}
	
	public String getGender() {
		return gender;
	}
}

每当录入一个新的学生时,实例化一个Student对象,并调用构造方法做初始化操作。若要修改学生的信息,可以调用alterInfo()方法,获取学生的某一信息,如年龄,性别等,调用相应的get()方法。

      由于任意的线程都可以调用其中的alterInfo()方法来修改这一对象的内部信息,使得这个程序明显是线程不安全的,有可能线程T1在对某一对象的major和age做修改,当线程T1完成major修改,准备对age修改时,另一个线程T2读取了同一对象的age数据,这就导致了线程T2拿到的是一个过期的数据。为了保证数据的一致性和正确性,必须做同步处理,例如加锁操作,使用synchronized关键字或是Lock显式锁。如果不想因为加锁而牺牲系统性能,可以尝试把上面的代码改成不可变对象的形式:

package immutableobject;

// final关键字修饰类,保证该类不会有其他子类
public final class ImmutableStudent {
	// private修饰变量保证变量不会被其他对象获取,final修饰变量保证变量只在初始化时被赋值一次
	private final String name;
	private final String major;
	private final int age;
	private final String gender;
	
	public ImmutableStudent(String name, String major, int age, String gender) {
        this.name = name;
        this.major = major;
        this.age = age;
        this.gender = gender;
	}
	
	public String getName() {
		return name;
	}
	
	public String getMajor() {
		return major;
	}
	
	public int getAage() {
		return age;
	}
	
	public String getGender() {
		return gender;
	}
}

可以看到,按照上面的不可变对象创建规则,我们先是把类加上了final关键字修饰,这样做的目的是保证这个类不会有子类,因为子类可以重写父类的方法,可能导致对象里面的数据被修改。然后就是每一个字段,也加上了final关键字修饰,目的是保证这些字段在对象被实例化时,由构造方法初始化后,不会再被修改,这很符合不可变对象的特征,对象一旦创建后,其内部状态不再被修改。

      之后就是对于对象(即学生信息)的修改操作了,假设我想对其中一个学生对象的内部字段做修改,做法是怎样?还记得上面说的吧,不可变对象的内部状态不可变,指的是,只是该对象自身不可改变,但是不可变对象表示的事物,它的状态是可变的,例如上面的例子,一个学生对象实例被设置为不可改变,因为它在构造方法初始化后,没有set()方法来对内部字段进行修改,且这些字段都是私有的,其他对象无法直接访问。但是这个实例对象表示的一个学生实体,他的状态一定是会改变的,例如年龄,转专业或者改名等情况。当学生的信息发生改变后,他不可变对象就不能再表示这个学生了,因为里面的信息过期了,为了表示这种状态的变化,我们直接新建一个不可变对象,把该名学生的最近信息填进去,最后把新的对象整个替换掉旧的对象,来完成信息数据的更新,这就是不可变对象表示状态变化的方式。所以,对于上面的学生信息修改操作,我们是这样做的:

package immutableobject;

import java.util.concurrent.ConcurrentHashMap;

public class StudentDemo {
	private ConcurrentHashMap<String, ImmutableStudent> studentForm;
	
	public StudentDemo() {
		studentForm = new ConcurrentHashMap<String, ImmutableStudent>();
	}
	
	// 修改学生信息
	public void alterInfo(String studentID,String name, String major, int age, String gender) {
		ImmutableStudent student = new ImmutableStudent(name, major, age, gender);
		studentForm.put(studentID, student); 
	}
}

可以看到,如果要修改一个学生的信息,我们会直接实例化一个新的对象来替换掉旧的对象,实现信息数据的更新。

 

不可变对象结构

总结一下,创建一个不可变对象,要包含两个核心的东西:

  1. Immutable Object数据结构:包含字段和方法。方法有
  • 构造方法。
  • 获取单个字段状态的方法。获取单个字段方法返回的数据是不可变对象实例化时通过构造方法初始化的数据。
  • 一个返回整个对象副本的方法。
  1. Change Object:根据不变对象所表示的事物,发生改变时,更新对应的,新的Immutable Object对象,替换原来旧的对象,来反映事物的状态变化。

对于不可变对象模式,我们重点关注它的特性是,适合于表示那些状态变化不频繁的事物。例如上面的学生信息管理例子,对于每一个学生,我们可以为他建立一张信息表,信息表上的有学生的个人信息,所学专业和联系方式等,这些信息通常都是不常改变的,例如姓名,所学专业,联系方式,而有一些信息如学生课程表,学生成绩等,一个学期才改变一次,频率非常低,这些特点都适合用不可变对象来存储表示。下面就来根据Immutable Object结构完善这个学生管理程序吧:

package immutableobject;

// final关键字修饰类,保证该类不会有其他子类
public final class ImmutableStudent {
	// private修饰变量保证变量不会被其他对象获取,final修饰变量保证变量只在初始化时被赋值一次
	private final String name;
	private final String major;
	private final int age;
	private final String gender;
	
	public ImmutableStudent(String name, String major, int age, String gender) {
        this.name = name;
        this.major = major;
        this.age = age;
        this.gender = gender;
	}
	
	public ImmutableStudent(ImmutableStudent student) {
		this.name = student.getName();
		this.major = student.getMajor();
		this.age = student.getAage();
		this.gender = student.getGender();
	}
	
	public String getName() {
		return name;
	}
	
	public String getMajor() {
		return major;
	}
	
	public int getAage() {
		return age;
	}
	
	public String getGender() {
		return gender;
	}
}

学生信息设计为不可变对象,第二个构造方法,也就是传入ImmutableStudent对象作为参数的构造方法,为了是更新不可变对象时更方便,下面你会看到。

package immutableobject;

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Collections;
import java.util.HashMap;

public class ManagementSystem {
	private static volatile ManagementSystem managementSystem= new ManagementSystem();
	// 学号与学生对象实例之间的对应关系
	private final HashMap<String, ImmutableStudent> studentForm = new HashMap<String, ImmutableStudent>();
	public void Initial() {
		// 数据库操作
		Connection con = null;
    	String selectSql = "select * from StudentForm";
    	
        try {
        	con = new StudentDB().connectDataBase();
        	Statement statement = con.createStatement();
        	ResultSet rs = statement.executeQuery(selectSql);
        	
        	while(rs.next()) {
        		String studentID = rs.getString("studentID");
        		String name = rs.getString("name");
        		String major = rs.getString("major");
        		int age = rs.getInt("age");
        		String gender = rs.getString("gender");
        		studentForm.put(studentID, new ImmutableStudent(name, major, age, gender));
        	}

        	rs.close();
        	con.close();
        } catch(SQLException e) {
        	e.printStackTrace();
        }
        
	}
	
	// 根据学号获得学生信息
	public ImmutableStudent getStudent(String studentID) {
		return studentForm.get(studentID);
	}
	
	private static HashMap<String, ImmutableStudent> Copy(HashMap<String, ImmutableStudent> oldMap) {
		HashMap<String, ImmutableStudent> newMap = new HashMap<String, ImmutableStudent>();
		
		for(String Key : oldMap.keySet()) {
			newMap.put(Key, new ImmutableStudent(oldMap.get(Key)));
		}
		
		return newMap;
	}
	
	public static void updateSystem(ManagementSystem newManagementSystem) {
		managementSystem = newManagementSystem;
	}
	
	//JDK提供的创建不可变对象集合,不过它不是真正意义上的不可变对象集合
	public HashMap<String, ImmutableStudent> getImmutableMap() {
		return (HashMap<String, ImmutableStudent>) Collections.unmodifiableMap(Copy(studentForm));
	}
	
}

在学生信息管理系统类中,用一个HashMap维护学生对象集合,第14行到39行是初始化方法,从数据库获取学生信息,可忽略。关键看第46行,Copy()方法用来更新维护不可变对象的HashMap,它的做法是把新的也就是修改后的HashMap复制一份,返回出去后,调用第56行的updateSystem()方法来覆盖自己旧的HashMap。

      在第60行你看到,JDK提供了一种创建不可变对象集合的方法,例如Collections.unmodifiableMap,还有Collections.unmodifiableList,

Collections.unmodifiableSet等,不过,它们不是真正意义上的不可变对象,为什么这么说?上面的不可变对象创建规则中有一条是:“该对象的类不提供修改自身数据的方法,如setter()方法。”

我们来看Collections.unmodifiableMap的源码中有这么一个方法:

public V put(K key, V value) {
	throw new UnsupportedOperationException();
}

Collections.unmodifiableMap中提供了修改自身状态的方法,只不过让方法抛出UnsupportedOperationException异常。同样的Collections.unmodifiableList源码中是这样处理的:

public void add(int index, E element) {
	throw new UnsupportedOperationException();
}

 

CopyOnWriteArrayList中的不可变对象模式

在JDK中,CopyOnWriteArrayList线程安全的ArrayList,它使用就使用了类似的不可变对象模式,为什么说是类似的,先来看看它的部分源码:

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	// 只截取部分源码
	private volatile transient Object[] array;
	
	final Object[] getArray() {
		return array;
	}
	
	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;
			
			setArray(newElements);
			return true
		} finally {
			lock.unlock();
		}
	}
}

可以看到,类中有一个实例数组array,用来存储元素。然后重点关注add()方法,往CopyOnWriteArrayList中添加一个元素e,它的做法是获得先获得旧数组,然后把旧数组中的元素全部复制一遍到新数组中,最后在新数组的最后一位把元素e添加进去,再调用setArray()方法(相当于上面的updateSystem()方法)更新这个array数组,CopyOnWriteArrayList中的array数组就是一个类似的不可变对象,为什么说是类似的?因为这个array数组中的元素是可以被更新的,所以,它并不是完全真正意义上的不可变对象。

 

完整代码已上传:

https://github.com/justinzengtm/Java-Multithreading/tree/master/%E5%B9%B6%E8%A1%8C%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/ImmutableObject

 

发布了97 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/justinzengTM/article/details/94397485