多线程----Immutable VS Mutable (可变与不可变)

Posted 令仔很忙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程----Immutable VS Mutable (可变与不可变)相关的知识,希望对你有一定的参考价值。

文章目录

Immutable

    Immutable是什么意思?不变的、不发生改变的意思。在JDK中有很多的类被设计成不可变的,举个大家经常用到的类java.lang.StringString类被设计成不可变。String所表示的字符串的内容绝对不会发生变化。因此,在多线程的情况下,String类无需进行互斥处理,不用给方法进行synchronized或者lock等操作,进行上锁、争抢锁、解锁等流程也是有一定性能损耗的。因此,若能合理的利用Immutable,一定对性能的提升有很大帮助。

为什么String不可变?

    那么为什么String是不可变的呢?满足那些条件才可以变成不可变的类?大家可以打开String类的源码:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence 
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="@docRoot/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
    /**
     * Allocates a new @code String so that it represents the sequence of
     * characters currently contained in the character array argument. The
     * contents of the character array are copied; subsequent modification of
     * the character array does not affect the newly created string.
     *
     * @param  value
     *         The initial value of the string
     */
    public String(char value[]) 
        this.value = Arrays.copyOf(value, value.length);
    
    /**
     * Converts this string to a new character array.
     *
     * @return  a newly allocated character array whose length is the length
     *          of this string and whose contents are initialized to contain
     *          the character sequence represented by this string.
     */
    public char[] toCharArray() 
        // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    
  

通过上边的这段代码你会发现:

1、首先String类本身是被final修饰过的,表明该类无法进行扩展,无法创建子类。因此类中声明的方法则不会被重写。
2、String类中的成员变量都被final修饰,同时均为private,被final修饰则表示成员变量
不会被setter方法再次赋值,private则表示成员变量均为类私有,外部无法直接调用。
3、String类中的成员变量都没有setter方法,避免其他接口调用改变成员变量的值。
4、通过构造器初始化所有成员,同时在String中赋值是用的Arrays.copyOf等深拷贝方法。
5、在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝。

以上5点保证String类的不可变性(immutability)。

示例程序

    下面咱们通过一些简单的示例程序来实验Immutable,自己动手,丰衣足食,多动手会有好处的。下边定义三个类。

类名说明
People表示一个人的类
PeopleThread表示People实例的线程的类
Main测试程序行为的类

下边看下每个类的示例代码,People类,类以及成员变量均被final修饰,同时只能通过构造函数来对成员变量赋值,没有setter方法:

public final class People 
    private final String sex;
    private final int age;
    private final String address;

    public People(String sex, int age, String address) 
        this.sex = sex;
        this.age = age;
        this.address = address;
    

    public String getSex() 
        return sex;
    

    public int getAge() 
        return age;
    

    public String getAddress() 
        return address;
    

    @Override
    public String toString() 
        return "People" +
                "sex='" + sex + '\\'' +
                ", age=" + age +
                ", address='" + address + '\\'' +
                '';
    

PeopleThread类:

public class PeopleThread extends Thread

    private People people;

    public PeopleThread(People people)
        this.people = people;
    

    @Override
    public void run() 
        while (true)
            System.out.println(Thread.currentThread().getName() + " prints " + people);
        
    


Main类:

public class Main 

    public static void main(String[] args) 
        People people = new People("男",27,"北京");
        new PeopleThread(people).start();
        new PeopleThread(people).start();
        new PeopleThread(people).start();
    

下边是执行结果:

Thread-0 prints Peoplesex='男', age=27, address='北京'
Thread-0 prints Peoplesex='男', age=27, address='北京'
Thread-0 prints Peoplesex='男', age=27, address='北京'
Thread-0 prints Peoplesex='男', age=27, address='北京'
Thread-2 prints Peoplesex='男', age=27, address='北京'
Thread-2 prints Peoplesex='男', age=27, address='北京'
Thread-2 prints Peoplesex='男', age=27, address='北京'
Thread-2 prints Peoplesex='男', age=27, address='北京'
Thread-1 prints Peoplesex='男', age=27, address='北京'
Thread-1 prints Peoplesex='男', age=27, address='北京'
Thread-1 prints Peoplesex='男', age=27, address='北京'
Thread-1 prints Peoplesex='男', age=27, address='北京'

通过结果就可以发现,无论启动多少个线程,打印的结果其实都是一样的。因为People类本身在实例被创建且字段初始化之后,字段的值就不会再被修改,实例的状态在初始化之后就不会再发生改变,因此也不需要在进行加锁、解锁等操作。因为想破坏也破坏不了。但是有一点比较难的就是如果确保Immutability,因为在对类的创建过程中少个final,多个setter等,那么就无法保证类的Immutability

何时使用呢?

上边简单的讲解了下Immutable模式,那么在那些情况下考虑使用Immutability不可变性呢?

实例创建后,状态不再发生变化时

这个可以参考上边的示例,一个人的属性被赋值之后就不会发生改变。这种情况下就可以考虑不可变性来实现。

实例是共享的,且被频繁访问时

Immutable模式的优点是“不需要使用synchronized来保护”,这就意味着能够在不是去安全性和生存性的前提下提高性能。当实例被多个线程共享,且有可能被频繁访问时,Immutable模式的优点就会凸显出来。关于不适用synchronized能提高多少性能?下边做个实现:

public class NoSynchronized 

    private static final long CALL_COUNT = 1000000000L;

    public static void main(String[] args) 
        trial("NotSynch",CALL_COUNT,new NotSynch());
        trial("Synch",CALL_COUNT,new Synch());
    

    private static void trial(String msg,long count,Object object)
        System.out.println(msg + ": BEGIN");
        long start_time = System.currentTimeMillis();
         for (long i = 0; i< count; i++)
             object.toString();
         

         System.out.println(msg + ": END");
         System.out.println("Elapsed time = " + (System.currentTimeMillis() - start_time) + "msec");
    

    static class NotSynch
        private final String name = "NotSynch";

        @Override
        public String toString() 
            return "NotSynch" +
                    "name='" + name + '\\'' +
                    '';
        
    

    static class Synch
        private final String name = "Synch";

        @Override
        public synchronized String toString() 
            return "Synch" +
                    "name='" + name + '\\'' +
                    '';
        
    


查看执行结果,差了44倍,这是在完全没有发生线程冲突的情况下测试的,所以测试的时间就是获取和释放实例锁所花费的时间。当然也跟本地的环境也会对时间差有一定的影响,因此仅供参考:

NotSynch: BEGIN
NotSynch: END
Elapsed time = 693msec

Synch: BEGIN
Synch: END
Elapsed time = 31162msec

哪些情况会破坏不可变性?

  • getter返回的不是不可变的类
    举个例子,比如变量为StringBuffer类型:
public final class UserDetail 
    private final StringBuffer detail;
    public UserDetail(String sex,int age,String address)
        this.detail = new StringBuffer(sex + ":" + age + ":" + address);
    

    public StringBuffer getDetail() 
        return detail;
    

    @Override
    public String toString() 
        return "UserDetail" +
                "detail=" + detail +
                '';
    


    public static void main(String[] args) 
        UserDetail userDetail = new UserDetail("男",27,"北京");
        //显示
        System.out.println(userDetail);

        StringBuffer detail = userDetail.getDetail();
        detail.append(":::").append("test");
        //再次显示
        System.out.println(userDetail);
    


运行结果:
UserDetaildetail=:27:北京
UserDetaildetail=:27:北京:::test

get结果之后重新进行append操作,StringBuffer包含修改内部状态的方法,所以detail字段的内容也是可以被外部修改的。

  • 在一个类中使用了其他的类,其他的类是可变的
    当一个不可变的类中使用了其他的可变类之后,那么受影响不可变的类也会变成可变的类。

扩展

Java的标准类库中,有些类也用到了Immutable模式

  • java.lang.String
  • java.math.BigInteger && java.math.BigDecimal
  • java.util.regex.Pattern
  • java.lang.Integer && java.lang.Short等基本数据类型包装类

在一开始的时候说到了String是不可变的,那么是否真的不可变呢?有一种方式是可以更改其状态的,反射机制。

//创建字符串"Hello World", 并赋给引用s
    String s = "Hello World"; 
    System.out.println("s = " + s); //Hello World

    //获取String类中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");
    //改变value属性的访问权限
    valueFieldOfString.setAccessible(true);

    //获取s对象上的value属性的值
    char[] value = (char[]) valueFieldOfString.get(s);
    //改变value所引用的数组中的第5个字符
    value[5] = '#';
    System.out.println("s = " + s);  //Hello#World

运行结果
s = Hello World
s = Hello#World

发现String的值已经发生了改变。也就是说,通过反射是可以修改所谓的“不可变”对象的。这些在使用的时候都是需要注意的地方。

以上是关于多线程----Immutable VS Mutable (可变与不可变)的主要内容,如果未能解决你的问题,请参考以下文章

JAVA多线程模式-Immutable

多线程不可变对象设计模式immutable

Java多线程编程模式实战指南:Immutable Object模式

[Python] Understand Mutable vs. Immutable objects in Python

Python多进程vs多线程

多进程 vs 多线程