共享模型之不可变

一.不可变

理论

1.什么是不可变

如果一个对象他里面的所有成员变量都是不可变的,那么这个对象可以认为他是线程安全的,因为多个线程没办法修改共享变量所以肯定是安全的。这个就是不可变保障线程安全的一种思维。

2.目前已知的线程安全的几种处理方案

  • 最安全也是最粗暴的方式就是直接加锁,直接在方法或者类上面加对象锁,一个线程获取锁就可以执行临界区的代码,其它线程都要等待获取锁,可以解决原子性,可见性,一致性等问题。但是也有缺点,就是增加了线程的上下文切换执行效率并不高。
  • 第二种方式是通过cas交换的方式来保障线程安全,他的实现是基于乐观锁,比较并且设置值,每次对成员变量赋值时都要判断现在的成员变量是不是我线程本地保存的期望值,如果是期望值则把计算得到的值赋值给成员变量,如果不是期望值则不停的循环。常见的cas实现有AtomicInteger、AtomicIntegerArray,AtomicReference等,java.util.concurrent.atomic包下面的类。通过cas保障线程安全的优势就是在线程少cpu核多的情况下效率要比直接加锁的效率高,但是如果线程过多的时候是不适合使用cas的,因为会频繁的上下文切换,效率可能还没直接加锁的效率好。
  • 第三种方式也是我今天重点要介绍的那就是不可变对象的设计,不可变对象的class一般是由final修饰的,并且对象内部的成员变量也基本都是用final修饰或者private修饰,用final修饰class是为了保障这个对象是没有子类的,防止子类对成员变量进行修改,final修饰成员变量是为了不让线程去修改对象的成员变量,如果不存在修改那么线程安全问题也就不存在了。java中常见的不可变类有String,BigDecimal,所有的基础类型的包装类如Integer,Boolean等都是不可变对象,也都是线程安全的。这里有一点需要注意,这些不可变对象提供的方法一般都是线程安全的,但是这个线程安全只是说单独调用一个方法是线程安全的,如果存在多个方法组合调用时还是不安全的。因为没办法阻止线程交互执行时指令的执行时间和执行顺序。

代码分析

1.SimpleDateFormat和DateTimeFormatter线程安全分析

  1. 开启10个线程执行testTime1()方法可以查看到结果抛出了异常这是因为因为SimpleDateFormat不是线程安全对象也不是不可变对象所以多线程执行parse方法时就会发生错误,sdf1虽然是局部变量但是多个线程可以共享sdf1并且调用他的parse()方法所以会报错并且存在线程安全问题。
  2. 开启10个线程执行testTime2()方法,可以看到运行结果是正常的没有报错,因为testTime2()方法中使用了DateTimeFormatter他是不可变对象,所以时线程安全的。
    public static void testTime1() {
        SimpleDateFormat sdf1 = new SimpleDateFormat("YYYY-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    //多线程执行sdf1.parse时会报错,因为SimpleDateFormat不是线程安全对象也不是不可变对象所以
                    // 多线程执行parse方法时就会发生错误,
                    // sdf1虽然是局部变量但是多个线程可以共享sdf1并且调用他的parse()方法所以会报错并且存在线程
                    // 安全问题,线程安全问题通常都是由多线程同时运行,cpu交替执行指令共同修改同一个共享变量引起的。
                    System.out.println(Thread.currentThread().getName()+" : "+sdf1.parse("2017-05-05"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
    
    //testTime1()执行结果
//======================================================================================================
    Exception in thread "Thread-5" Exception in thread "Thread-2" Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.shiyiwei.test.deepStudy.Final.Test1.lambda$testTime1$1(Test1.java:63)
    at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.shiyiwei.test.deepStudy.Final.Test1.lambda$testTime1$1(Test1.java:63)
    at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: empty String
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.shiyiwei.test.deepStudy.Final.Test1.lambda$testTime1$1(Test1.java:63)
    at java.lang.Thread.run(Thread.java:748)
    Thread-8 : Sun Jan 01 00:00:00 GMT+08:00 2017
    Thread-9 : Sun Jan 01 00:00:00 GMT+08:00 2017
    Thread-6 : Sun Jan 01 00:00:00 GMT+08:00 2017
    Thread-7 : Sun Jan 01 00:00:00 GMT+08:00 2017
    Thread-4 : Sun Jan 01 00:00:00 GMT+08:00 2017
    Thread-1 : Sun Jan 01 00:00:00 GMT+08:00 2017
    Thread-3 : Sun Dec 27 00:00:00 GMT+08:00 2009
//======================================================================================================
    

    public static void testTime2() {
        DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                //DateTimeFormatter 是不可变类,所以他是线程安全的。
                LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
//                log.debug("{}", date);
                System.out.println(date);
            }).start();
        }
    }

//testTime2()执行结果
//======================================================================================================
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
//======================================================================================================

String不可变对象代码分析

String类型内部是由final char value[]不可变数组来存储字符串数据的,也就是说String只能通过构造函数赋值,他是不可变的。同时他的class也是用final修饰的所以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;

String类的substring方法是线程安全的,从方法1分析,每次调用String的substring方法都会创建一个新的String所以原来的String对象就不会发生改变,从方法2分析新建的String对象会把传递过来的数组拷贝一份然后赋值给value,这样做的目的是为了防止value引用指向的数组同时也被其它引用指向,很有可能会改变数组。这样就违背了不可变对象的设计原则,这种模式叫做保护性拷贝。

    //方法1
    public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
    
    //方法2
    public String(char value[], int offset, int count) {
        if (offset < 0) {
            throw new StringIndexOutOfBoundsException(offset);
        }
        if (count <= 0) {
            if (count < 0) {
                throw new StringIndexOutOfBoundsException(count);
            }
            if (offset <= value.length) {
                this.value = "".value;
                return;
            }
        }
        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {
            throw new StringIndexOutOfBoundsException(offset + count);
        }
        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }

BigDecimal代码分析

结果是打印10次26.75,为啥多线程执行add方法不存在线程安全问题呢?是因为每次调用add方法都会创建一个新的BigDecimal对象,线程操作新的对象就不会对共享对象进行修改事实上BigDecimal被设计时里面的成员是用final修饰的通过构造函数赋值后就不再可变,所以他的对象本身也是不可变的,这种不可变的对象是线程安全的。

    public static void testBigDecimal() {
        BigDecimal bigDecimal = new BigDecimal("25.25");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                // 结果是打印10次26.75,
                // 为啥多线程执行add方法不存在线程安全问题呢?
                // 是因为每次调用add方法都会创建一个新的BigDecimal对象,线程操作新的对象就不会对共享对象进行修改事实上BigDecimal
                // 被设计时里面的成员是用final修饰的通过构造函数赋值后就不再可变,所以他的对象本身也是不可变的。这种不可变的对象是
                // 线程安全的
                BigDecimal addCount = bigDecimal.add(new BigDecimal("1.5"));
                System.out.println(addCount);
            }).start();
        }
    }

//testBigDecimal()执行结果
//======================================================================================================
26.75
26.75
26.75
26.75
26.75
26.75
26.75
26.75
26.75
26.75
//======================================================================================================

二、享元模式

理论

享元模式是23种设计模式之一,并不是并发编程设计模式。享元模式就是提前实例化同一类对象的几种常用并且会被复用的对象,把他们缓存起来等到需要用的时候直接从缓存中获取这些常用对象,尽量避免不停的创建重复的对象可以节省内存空间。常见的基础类型的包装类如Integer,Long等都使用了享元模式。

源码理解

Integer源码

Integer内部类IntegerCache里面有一个static代码块,jvm加载内部类IntegerCache时会运行static中的代码创建int对象并且往cache里面塞值,初始化时一共会创建256个int对象,范围是 -128~127 。

    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

调用Integer的valueOf方法创建Integer对象时可以看到会先判断传入的参数是否在缓存中,如果在缓存中则直接从缓存中获取,如果不在缓存中则创建一个新的Integer对象。

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容