1.线程安全
所谓共享的资源,是指在多个线程同时对其进行访问的情况下,各线程都会使其发生变化,而线程安全性的主要目的就在于在受控的并发访问中防止数据发生变化。除了使用synchronized关键字同步对资源的写操作之外, 还可以在线程之间不共享资源状态, 甚至将资源的状态设置为不可变。在本章中,我们将讨论如何设计不可变对象,这样就可以不用依赖于synchronized关键字的约束。
2.不可变对象的设计
Java核心类库中提供了大量的不可变对象范例, 其中java.lang.String的每一个方法都没有同步修饰, 可是其在多线程访问的情况下是安全的, Java 8中通过Stream修饰的ArrayList在函数式方法并行访问的情况下也是线程安全的, 所谓不可变对象是没有机会去修改它, 每一次的修改都会导致一个新的对象产生, 比如Strings 1="Hello";sl=s1+" world"两者相加会产生新的字符串。
有些非线程安全可变对象被不可变机制加以处理之后,照样也具备不可变性,比如ArrayList生成的stream在多线程的情况下也是线程安全的, 同样是因为其具备不可变性的结果,示例代码所示。
java
import java.util.Arrays;
import java.util.List;
public class ArrayListStream {
public static void main(String[] args) {
// 定义一个list并且使用Arrays的方式进行初始化
List<String> list = Arrays.asList("Java","Thread", "Concurrency","Scala","Co");
// 获取并行的stream,然后通过map函数对list中的数据进行加工,最后输出
list.parallelStream().map(String::toUpperCase).forEach(System.out::println);
list.forEach(System.out::println);
}
}
2.1 非线程安全的累加器
不可变对象最核心的地方在于不给外部修改共享资源的机会,这样就会避免多线程情况下的数据冲突而导致的数据不一致的情况,又能避免因为对锁的依赖而带来的性能降低,好了, 在本节中我们将模仿java.lang.String的方式实现一个不可变的int类型累加器, 先来看看不加同步的累加器,代码如所示。
java
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class IntegerAccumulator {
private int init;
public IntegerAccumulator(int init) {
this.init = init;
}
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) {
//
int oldvalue = accumulator.getValue();
int result = accumulator.add(inc);
System.out.println("result:" + result);
if(inc + oldvalue != result ) {
System.out.println("ERROR:" + oldvalue + "+" + inc + "=" + result);
}
inc++;
slowly();
}
}
).start());
}
// 简单模拟操作的耗时
private static void slowly() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.2 方法同步增加线程安全性
2.3 不可变的累加器对象设计
2.2节中通过同步的方式解决了线程安全性的问题,正确的加锁方式固然能使得一个类变成线程安全的, 比如java.utils.Vector, 但是我们需要的是设计出类似于java.lang.String的不可变类,示例代码如所示。
java
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
public class IntegerAccumulator1 {
private int init;
public IntegerAccumulator1(int init) {
this.init = init;
}
// 构造新的累加器,需要用到另一个累加
public IntegerAccumulator1(IntegerAccumulator1 integerAccumulator, int init) {
this.init = integerAccumulator.getValue() + init;
}
// public int add(int i ) {
// this.init += i;
// return this.init;
// }
public IntegerAccumulator1 add(int init) {
return new IntegerAccumulator1(this, init);
}
public int getValue() {
return this.init;
}
public static void main(String[] args) {
// 定义累加器,并且设计初始值为0
IntegerAccumulator1 accumulator = new IntegerAccumulator1(0);
// 定义三个线程,并且分别启动
IntStream.range(0,3).forEach(i->new Thread(
() -> {
int inc = 0;
while(true) {
//
int oldvalue = accumulator.getValue();
int result = accumulator.add(inc).getValue();
System.out.println("result:" + result);
if(inc + oldvalue != result ) {
System.out.println("ERROR:" + oldvalue + "+" + inc + "=" + result);
}
inc++;
slowly();
}
}
).start());
}
// 简单模拟操作的耗时
private static void slowly() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
重构后的Integer Accumulator, 使用了final修饰其的目的是为了防止由于继承重写而导致失去线程安全性, 另外init属性被final修饰不允许线程对其进行改变, 在构造函数中赋值后将不会再改变。
add方法并未在原有in it的基础之上进行累加, 而是创建了一个全新的IntegerAccumulator, 并未提供任何修改原始IntegerAccumulator的机会,运行上面的程序不会出现ERROR的情况。