Java多线程设计模式之Immutable模式

     举一个例子,对于Java使用者,Java.lang.String类和StringBuilder类大家应该都熟悉,但是,我们也了解到String类中并没有修改字符串的内容的方法,也就是说String的实例所表示的字符串的内容不会发生变化,因此,其也不能被声明为synchronized,所以无论多少个线程进行访问,都是安全的。这有点Immutable模式的意思,没错,其实是Immutable的一种,如果能巧妙的利用这种方式,程序的性能能将提高很多。

    结合一个例子来说明一下这种设计方式:

首先设计一个Person类,其包含姓名和地址两个字段,Person类的字段值既可以通过构造函数来设置,类中没有setName()和setAddress()方法,但含有getName()和getAddress()方法,因此,当Person类一旦被创建则字段值就无法改变。这时候即便是多个线程同时访问一个实例erson类也是安全的,Person类中的方法也都可以被多个线程调用,且不会出现问题。有以下几个条件:1.将字段声明为private,则限制了类外部其它的类及方法等不能够直接对Person类的字段进行修改。2.将字段值设置为final修饰,这时,字段值只能够被赋值一次,且赋值后就不能够再去改变。

public class Person {
	private final String name;
	private final String address;
	public Person(String name,String address){
		this.name=name;
		this.address=address;
	}
	
	public String getName(){
		return name;
	}
	
	public String getAddress(){
		return address;
	}
	
	public String toString(){
		return "Person : name = "+ name +", address = " + address + "]";
	}

}
   之后设计PrintPersonThread类,这个类用于创建多个线程并打印当前线程的名称以及当前字段内容:

public class PrintPersonThread extends Thread{
	private Person person;
	public PrintPersonThread(Person person){
		this.person=person;
	}
	public void run(){
		while(true){
			System.out.println(Thread.currentThread().getName()+" prints "+ person);
			
		}
	}

}
  最后,通过一个测试类Person类进行测试:

 

public class ImmuableMain {
	public static void main(String[] args){
		Person alice = new Person("Alice","Alaska");
		new PrintPersonThread(alice).start();
		new PrintPersonThread(alice).start();
		new PrintPersonThread(alice).start();

		
	}

}

测试结果如下所示:

结果显示,不同的进程会不停的打印测试类中所添加的姓名和地址,会交替的进行打印过程,这样也就说明了多个线程调用toString()方法时病没用产生异常,也就可以说明Person类是线程安全的。所以在这个模式中,Person类的实例的状态不会改变,所以也就不必使用synchronized修饰。虽说优点明显,但是确保这个类时Immutabilty是一个比较困难的工作。

何时使用Immutable模式呢?

1.当实例创建后,实例的状态不在发生改变时,可以使用这个模式,然而,实例的状态是有字段的值决定的,所以将字段声明为final,并不提供setter方法市重点所在。

2.即便是字段设置为final也不代表这个字段一定是状态不变的,即时字段的值不发生变化,字段引用的实例也有可能发生变化。例如:

public class UserInfo {
	private final StringBuffer info;
	public UserInfo(String name,String address){
		this.info= new StringBuffer("<info name =\""+name+"\" address=\""+
	address +"\" />");
	}
	
	public StringBuffer getInfo(){
		return info;
	}
	
	public String toString(){
		return "[ UserInfo: "+ info +"]";
	}

}
构造一个UserInfo类,用于显示用户的姓名和地址,我们为了检验这个类是否是线程安全的,为此设计了一个检测的类:

public class UserInfoTest {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		UserInfo userinfo = new UserInfo("Alice","Alaska");
		
		System.out.println("userinfo = "+ userinfo);
		
		//修改状态
		StringBuffer info = userinfo.getInfo();
		info.replace(12, 17, "Bobby");
		
		System.out.println("userinfo = "+ userinfo);
		

	}

}

中间通过StringBuffer的实例info来修改人的名称,在字符串的位置为12-17,并将再次显示信息,测试结果如下:

  原因是因为getInfo()方法获取的info字段中保存的实例并不是String类型的实例,而是StringBuffer类型的实例,StringBuffer和String类型不同,前者包含修改内部状态的方法,所以info字段的内容可以被外部修改。String类型的replace方法并不是修改字符串本身,但是StringBuffer类的replace方法则是修改实例的本身,这是因为StringBuffer类是可变的,由于info字段声明为final,所以info字段的值本身并不会改变,但是info字段所指向的实例的状态却有可能改变。

3.实例是共享的,且被频繁的访问时可以使用Immutable模式来设计。在线程安全的前提下,不适用synchronized修饰保护会提高程序的性能,当多个线程共同频繁的访问实例时,这个模式的优点就会显现出来。例如下面的测试:

public class testMain {
	
	private static final long CALL_COUNT=1000000000L;

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		trial("NotSynch",CALL_COUNT,new NotSynch());
		trial("Synch",CALL_COUNT,new Synch());

	}
	
	private static void trial(String msg,long count,Object obj){
		System.out.println(msg + ": BEGIN");
		long start_time = System.currentTimeMillis();
		
		for(long i = 0;i<count;i++){
			obj.toString();
		}
		System.out.println(msg + ": END");
		System.out.println("Clapsed time = "+(System.currentTimeMillis()-start_time)+"msec.");
		
	}
}
class NotSynch{
	private final String name = "NoSynch";
	public String toString(){
		return "["+name+"]";
		
	}
}

class Synch{
	private final String name = "Synch";
	public synchronized String toString(){
		return "["+name+"]";
	}
}
利用系统函数currentTimeMills()来获取系统的时间,然后记录所花费的时间,这个测试例子中,我们东需要执行obj对象的toString()方法1000000000次,测试结果如下:



可以发现不使用synchronized修饰的immutable模式用时为359毫秒,而使用synchronized修饰的方法则用时很长,为25423毫秒,这是在完全没有线程冲突的情况下测出来的,所以测得时间也就是获取和释放实例锁的时间。测试结果也与Java的运行环境有关,所以该结果仅供参考。

  下面介绍一个例子,模拟从matable类实例创建immutable实例:

首先先建立mutablePerson类:

public final class MutablePerson {
	private String name;
	private String address;
	public MutablePerson(String name,String address){
		this.name=name;
		this.address=address;
	}
	//调用immutable的ImmutablePerson来实例化此类
	public MutablePerson(ImmutablePerson person){
		this.name=person.getName();
		this.address=person.getAddress();
	}
	
	public synchronized void setPerson(String newName,String newAddress){
		name=newName;
		address=newAddress;
	}
	
	public synchronized ImmutablePerson getImmutablePerson(){
		return new ImmutablePerson(this);
	}
	
	//这是为了方便这个方法只允被ImmutablePerson类的实例访问getName()和getAddress()方法。
	String getName(){
		return name;
	}
	String getAddress(){
		return address;
	}

	public synchronized String toString(){
		return "MutablePerson: [name = "+name +", address = "+address+"].";
	}

}

其次建立对应的ImmutablePerson类:

public class ImmutablePerson {
	private final String name;
	private final String address;
	
	public ImmutablePerson(String name,String address){
		this.name=name;
		this.address=address;
	}
	
	public ImmutablePerson(MutablePerson person){
		//保证在赋值的时候是同一的,不被修改的
		//挡在修改MutablePerson类的字段的值的时候,由于字段name和字段address是不断变化的
		//所以可能会出现当修改name后,未修改address之前可能就调用了MutablePerson类
		//的get方法导致获取的person的字段的值与预期不同
			this.name=person.getName();
			this.address=person.getAddress();		
	}
	
	public MutablePerson getMutablePerson(){
		return new MutablePerson(this);
	}
	
	public String getName(){
		return name;
	}
	public String getAddress(){
		return address;
	}
	
	public String toString(){
		return "ImmutablePerson: [name = "+name +", address = "+address+"].";
	}

}
创建测试类,使用不同的线程去修改MutablePerson类的对应的实例,观察ImmutablePerson类是否有异常:

public class TestMain {
	public static void main(String[] args){
		MutablePerson mutable = new MutablePerson("Start","Start");
		//启动线程后,会对ImmutablePerson类的字段进行修改
		new CrackThread(mutable).start();
		new CrackThread(mutable).start();
		new CrackThread(mutable).start();
		
		for(int i =0; true;i++){
			mutable.setPerson(""+i,""+i);
		}
	}

}


class CrackThread extends Thread{
	private final MutablePerson mutable;
	
	//constructors
	public CrackThread(MutablePerson mutable){
		this.mutable=mutable;
	}

	public void run(){
	while(true){
		
		//使用MutablePerson类的实例来初始化ImmutablePerson类
		ImmutablePerson immutable = new ImmutablePerson(mutable);
		
		if(!immutable.getName().equals(immutable.getAddress())){
			System.out.println(Thread.currentThread().getName()+"*****BROKEN*****"+immutable);
				}else{
					System.out.println("test is successful!");
				}
			}
		}
	}
测试结果如下:


可以看到在中间伊欧失败的一个警告,这就说明ImmutablePerson类不是线程安全的,所以我们需要分析一下,在哪里做出修改:

首先主函数线程中启动了3个线程,同时通过for循环不断的修改MutablePerson类的实例mutable。再CrackThread类中的重写的run()方法中,在while判断中不断的使用mutable实例创建新的ImmutablePerson类的实例,当地址和姓名字段的首字母不相同时则输出BROKEN,所以我们在往下分析借用mutable类来构造ImmutablePerson类的构造方法,可以发现,ImmutablePerson类的赋值是通过调用mutable实例的get方法来实现的,但是,这里状态的改变是分离的,因为immutablePersong类字段都设置为final字段,在程序运行时极有可能出现当getName()方法调用后,还未调用getAddress()方法for循环就把MutablePerson类的实例mutable的状态已经修改了。这就造成了结果与预期结果不同,所以将代码中借用mutable实例初始化ImmutablePerson类的构造方法中的两个通过调用get方法的获取字段值的过程用synchronized修饰,成为一个整体的代码块,这样就能够实现线程的安全了。修改后测试结果如下:


这样就不会出现上述的问题了。



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值