不可变对象设计模式
线程安全性:
所谓共享的资源,是指在多个线程同时对其进行访问的情况下,各线程都会使其发生变化,而线程安全性的主要目的就在于 在受控的并发访问中防止数据发生变化。除了使用synchronized关键字同步对资源的写操作之外,还可以在线程之间不共享 资源状态,甚至将资源的状态设置为不可变。在本章中,我们将讨论如何设计不可变对象,这样就可以不用依赖于synchronized 关键字的约束。
不可变对象的设计:
无论是synchronized关键字还是显示锁Lock,都会牺牲系统的性能,不可变对象的设计理念在这几年变的越来越受宠,其中Actor模型 (不可变对象在Akka、jActor、Kilim等Actor模型框架中得到了广泛的使用)以及函数式编程语言Clojure等都是依赖于不可变对象的 设计达到lock free(无锁)的。 Java核心类库中提供了大量的不可变对象范例,其中java.lang.String的每一个方法都没有同步修饰,可是其在多线程访问的情况下是安全的,Java8中通过Stream修饰的ArrayList在函数式方法并行访问的情况下也是线程安全的,所谓不可变对象是没有机会去修改它,每一次的修改都会导致一个新的对象产生,比如String s1="Hello";s1=s1+" world" 两者相加会产生新的字符串。
package MutilThreadModel.NotChangeModel; import java.util.Arrays; import java.util.List; /** * Created by JYM on 2019/1/10 * 有些非线程安全可变对象被不可变机制加以处理之后,照样也具备不可变性, * 比如ArrayList生成的stream在多线程的情况下也是线程安全的,同样是因为 * 其具备不可变性的结果。 * */ public class ArrayListStream { public static void main(String[] args) { //定义一个list并且使用Arrays的方式进行初始化 List<String> list = Arrays.asList("Java","Thread","Concurrency","Scala","Clojure"); //获取并行的stream,然后通过map函数对list中的数据进行加工,最后输出; list.parallelStream().map(String::toUpperCase).forEach(System.out::println); list.forEach(System.out::println); } } /** * list虽然是在并行的环境下运行的,但是在stream的每一个操作中都是一个全新的List, * 根本不会影响到最原始的list,这样也是符合不可变对象的最基本思想的。 * */
非线程安全的累加器:
package MutilThreadModel.NotChangeModel; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; /** * Created by JYM on 2019/1/10 * 不可变对象最核心的地方在于不给外部修改共享资源的机会,这样就会避免多线程情况下的数据冲突而导致的数据不一致的情况, * 又能避免因为对锁的依赖而带来的性能降低。 * */ //下面将模仿java.lang.String的方式实现一个不可变的int类型累加器,先来看看不加同步的累加器 public class IntegerAccumulator { private int init; //构造时传入初始值 public IntegerAccumulator(int init) { this.init = init; } //对初始值增加i public int add(int i) { this.init += i; return this.init; } //返回当前的初始值 public int getValue() { return this.init; } public static void main(String[] args) { //定义累加器,并且将设置初始值为0 IntegerAccumulator accumulator = new IntegerAccumulator(0); //定义三个线程,并且分别启动 IntStream.range(0,3).forEach(i->new Thread(()-> { int inc = 0; while (true) { //首先获得old value int oldValue = accumulator.getValue(); //然后调用add方法计算 int result = accumulator.add(inc); System.out.println(oldValue+"+"+inc+"="+result); //经过验证,如果不合理,则输出错误信息 if (inc+oldValue != result) { System.out.println("Error: "+oldValue+"+"+inc+"="+result); } inc++; //模拟延迟 slowly(); } }).start()); } private static void slowly() { try{ TimeUnit.MILLISECONDS.sleep(1); }catch (InterruptedException e) { e.printStackTrace(); } } } /** * 这段程序即没有对共享资源进行共享锁的保护,也没有进行不可变的设计,在程序的运行过程中偶尔会出现错误的情况。 * */
方法同步增加线程安全性:
package MutilThreadModel.NotChangeModel; import DuoThread.BingFaBianCheng.InterruptThreadTest; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; /** * Created by JYM on 2019/1/10 * 方法同步增加线程安全性 * */ public class IntegerAccumulatorTest { public static void main(String[] args) { IntegerAccumulator accumulator = new IntegerAccumulator(0); IntStream.range(0,3).forEach(i->new Thread(()->{ int inc = 0; while (true) { int oldValue; int result; //使用class实例作为同步锁 synchronized (InterruptThreadTest.class) { oldValue = accumulator.getValue(); result = accumulator.add(inc); } System.out.println(oldValue+"+"+inc+"="+result); if (inc+oldValue != result) { System.out.println("ERROR:"+oldValue+"+"+inc+"="+result); } inc++; slowly(); } }).start()); } private static void slowly() { try{ TimeUnit.MILLISECONDS.sleep(1); }catch (InterruptedException e) { e.printStackTrace(); } } } /** * 这里将数据同步的控制放在了线程的逻辑执行单元中,而在IntegerAccumulator中未增加任何同步 * 的控制,如果单纯对getValue方法和add方法增加同步控制,虽然保证了单个方法的原子性,但是两个 * 原子类型的操作在一起未必就是原子性的,因此在线程的逻辑执行单元中增加同步控制是最为合理的。 * */
不可变的累加器对象设计:
package MutilThreadModel.NotChangeModel; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; /** * Created by JYM on 2019/1/10 * 不可变的累加器对象设计 * */ //不可变对象不允许被继承 public final class IntegerAccumulator_NoChang { private final int init; //构造时传入初始值 public IntegerAccumulator_NoChang(int init) { this.init = init; } //构造新的累加器,需要用到另外一个accumulator和初始值 public IntegerAccumulator_NoChang(IntegerAccumulator_NoChang accumulator_noChang,int init) { this.init = accumulator_noChang.getValue()+init; } //每次相加都会产生一个新的IntegerAccumulator public IntegerAccumulator_NoChang add(int i) { return new IntegerAccumulator_NoChang(this,i); } public int getValue() { return this.init; } public static void main(String[] args) { //用同样的方式进行测试 IntegerAccumulator_NoChang accumulator_noChang = new IntegerAccumulator_NoChang(0); IntStream.range(0,3).forEach(i->new Thread(()-> { int inc = 0; while (true) { int oldValue = accumulator_noChang.getValue(); int result = accumulator_noChang.add(inc).getValue(); System.out.println(oldValue+"+"+inc+"="+result); if (inc+oldValue != result) { System.out.println("ERROR: "+oldValue+"+"+inc+"="+result); } inc++; slowly(); } }).start()); } private static void slowly() { try{ TimeUnit.MILLISECONDS.sleep(1); }catch (InterruptedException e) { e.printStackTrace(); } } } /** * 重构后的IntegerAccumulator,使用了final修饰其的目的是为了防止由于继承重写而导致失去线程安全性,另外 * init属性被final修饰不允许线程对其进行改变,在构造函数中赋值后将不会再改变。 * add方法并未在原有init的基础之上进行累加,而是创建了一个全新的IntegerAccumulator,并未提供任何修改原始 * IntegerAccumulator的机会,运行上面的程序不会出现ERROR的情况。 * */
总结:
设计一个不可变的类共享资源需要具备不可破坏性,比如使用final修饰,另外针对共享资源操作的方法是不允许被重写的,以防止由于继承而带来的安全性问题,但是单凭这两点也不足以保证一个类是不可变的,比如下面的的类用final修饰,并且其中的list也是final修饰的,只允许在构造时创建:
public final class Immutable { private final List<String> list; public Immutable(List<String> list) { this.list = list; } public List<String> getList() { return this.list; } }
Immutable类被final修饰因此不允许更改,同样list只能在构造时被指定,但是该类同样是可变的(mutable),因为getList方法返回的list是可被其他线程修改的,如果想要使其真正的不可变,则需要在返回list的时候增加不可修改的约束Collections.unmodifiableList(this.list)或者可控一个全新的list返回