在多线程操作共享资源的情况下,大多数情况下都需要对资源进行加锁操作,多线程设计模式中的一个方法,可以不依赖加锁操作,直接将资源的状态设置为不可变,一个不可变的对象,无论任何时候都是线程安全的,就像java.lang.String一样。
那么String类是如何保证线程安全的呢?做过String s1 = “hello”;s1 = s1+”world”;的就会知道,对string类的每一次修改都会产生一个新的对象,这样就不会修改之前的对象数据了。
另外有些非线程安全可变对象被不可变机制处理后,也是具有了不可变性。比如ArrayList生成的stream在多线程的情况下也是线程安全的,同样是因为其具备不可变性的结果。Stream的每一个操作中都是一个全新的list,不会影响到最原始的list。
public class ArrayListStream {
public static void main(String[] args) {
List list = Arrays.asList("java", "thead", "scala", "clojure");
//获取并行的stream,map函数对list中的数据进行加工,
list.parallelStream().map(String::toUpperCase).forEach(System.out::println);
//stream的每一个操作都是一个全新的list,不会影响到最原始的list
list.forEach(System.out::println);
}
}
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 init;
}
public int getValue(){
return this.init;
}
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 = accumulator.getValue();
int result = accumulator.add(inc);
System.out.println(oldValue+"+"+inc+"="+result);
if(inc + oldValue != result){
System.err.println("error:"+oldValue+"+"+inc+"="+result);
}
inc ++;
slowly();
}
}).start());
}
private static void slowly() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
可能出现的错误:
public static void main(String[] args) {
IntegerAccumulator accumulator = new IntegerAccumulator(0);
IntStream.range(0,3).forEach(i -> new Thread(() ->{
int inc = 0;
while (true){
// TODO: 2019/11/22 加同步锁 实现线程安全
int oldValue ;
int result ;
synchronized (IntegerAccumulator.class){
oldValue = accumulator.getValue();
result = accumulator.add(inc);
}
System.out.println(oldValue+"+"+inc+"="+result);
if(inc + oldValue != result){
System.err.println("error:"+oldValue+"+"+inc+"="+result);
}
inc ++;
slowly();
}
}).start());
}
这里将同步的控制放在了线程的逻辑执行单元中,而在IntegerAccumulator中未增加任何同步的控制,如果单纯地对getValue 方法和add方法增加同步控制,虽然保证了单个方法的原子性,但是两个原子性的操作在一起未必就是原子性的,因为在线程的逻辑执行单元中增加同步控制是最合理的。
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* 不可变对象不允许被继承
*/
public final class SafeIntegerAccumulator {
private final int init;
public SafeIntegerAccumulator(int init){
this.init = init;
}
//构造新的累加器,需要用到另外一个accumulator和初始值
public SafeIntegerAccumulator(SafeIntegerAccumulator accumulator,int init){
this.init = accumulator.getValue() + init;
}
//每次相加都会产生一个新的SafeIntegerAccumulator
public SafeIntegerAccumulator add(int i){
return new SafeIntegerAccumulator(this,i);
}
public int getValue(){
return this.init;
}
public static void main(String[] args) {
SafeIntegerAccumulator accumulator = new SafeIntegerAccumulator(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(oldValue+"+"+inc+"="+result);
if(inc + oldValue != result){
System.err.println("error:"+oldValue+"+"+inc+"="+result);
}
inc ++;
slowly();
}
}).start());
}
private static void slowly() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
重构后的IntegerAccumulator,使用了final修饰,防止由于继承重写而导致失去线程安全性,init属性被final修饰不允许线程对其进行改变,在构造函数中赋值后将不会在改变。Add方法并未在原有init的基础之进行累加,而是创建了一个全新的IntegerAccumulator,并未提过任何修改原始IntegerAccumulator的机会。
设计一个不可变的类的共享资源需要具备不可破坏性,比如使用final修饰,另外针对共享资源操作的方法是不允许被重写的,以防止由于继承而带来的安全性问题,但是单凭这两点也不足以保证一个类是不可变的,比如下面的类用final修饰,其中的list也是final修饰,只允许在构造时创建。
import java.util.Collections;
import java.util.List;
public final class Immutable {
private final List list;
public Immutable(List list) {
this.list = list;
}
public List getList() {
// Collections.unmodifiableCollection(this.list);
return this.list;
}
}
Immutable类中,getList方法返回的list是可被其他线程修改的,如果想要使其真正的不可变,则需要在返回list的时候增加不可修改的约束Collections.unmodifiableList(this.list)或者克隆一个全新的list返回。