举一个例子,对于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修饰,成为一个整体的代码块,这样就能够实现线程的安全了。修改后测试结果如下:
这样就不会出现上述的问题了。