所谓共享的资源,是指在多个线程同时对其进行访问的情况下,各线程都会使其发生变化,而线程安全性的主要目的就在于在受控的并发访问中防止数据发生变化。除了使用synchronized关键字同步对资源的写操作之外,还可以在线程之间不共享资源状态,甚至将资源的状态设置为不可变。在本章中,我们将讨论如何设计不可变对象,这样就可以不用依赖于synchronized关键字的约束。
1 不可变对象认识
Java核心类库中提供了大量的不可变对象范例,其中java.lang.String的每一个方法都没有同步修饰,可是其在多线程访问的情况下是安全的,Java 8中通过Stream修饰的ArrayList在函数式方法并行访问的情况下也是线程安全的,所谓不可变对象是没有机会去修改它,每一次的修改都会导致一个新的对象产生
比如String s1=“Hello”;s1=s1+”world”两者相加会产生新的字符串。
有些非线程安全可变对象被不可变机制加以处理之后,照样也具备不可变性,比如ArrayList生成的stream在多线程的情况下也是线程安全的,同样是因为其具备不可变性的结果:
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);
System.out.println("===================");
list.forEach(System.out::println);
}
CONCURRENCY
CLOJURE
SCALA
THREAD
JAVA
===================
Java
Thread
Concurrency
Scala
Clojure
list虽然是在并行的环境下运行的,但是在stream的每一个操作中都是一个全新的List,根本不会影响到最原始的list,这样也是符合不可变对象的最基本思想。
2 累加器
2.1 非安全累加器
public class IntAccumulator {
/**
* 初始值
*/
private int init;
/**
* 构造函数,传入初始值
*
* @param init
*/
public IntAccumulator(int init) {
this.init = init;
}
/**
* 增加指定delta
*
* @param delta
*/
public int add(int delta) {
this.init += delta;
return this.init;
}
public int getValue() {
return this.init;
}
}
测试一下:
public static void main(String[] args) {
// 定义累加器,并且将设置初始值为0
IntAccumulator intAccumulator = new IntAccumulator(0);
// 定义三个线程,操作这个intAccumulator
IntStream.range(0, 3).forEach(i -> {
new Thread(() -> {
int delta = 0;
while (true) {
// 调用add之前老的值
int oldValue = intAccumulator.getValue();
// 调用add
int result = intAccumulator.add(delta);
System.out.println(oldValue + "+" + delta + "=" + result);
//验证,如果不合理,则输出错误信息
if (delta + oldValue != result) {
System.err.println("ERROR:" + oldValue + "+" + delta + "=" + result);
}
delta++;
// 模拟延迟
slowly();
}
}).start();
});
}
private static void slowly() {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
共享资源被多个线程操作未加任何同步控制,出现数据不一致的问题是情理之中的事情:
3078+52=3130
3078+52=3130
ERROR:1815+40=1895
ERROR:1935+41=2017
3130+52=3182
3235+53=3288
2.2 安全累加
通过加锁,让上面的测试变为线程安全的:
public static void main(String[] args) {
// 定义累加器,并且将设置初始值为0
IntAccumulator intAccumulator = new IntAccumulator(0);
// 定义三个线程,操作这个intAccumulator
IntStream.range(0, 3).forEach(i -> {
new Thread(() -> {
int delta = 0;
while (true) {
int oldValue;
int result;
// 在对共享数据操作的地方进行加锁
synchronized (IntAccumulator.class){
// 调用add之前老的值
oldValue = intAccumulator.getValue();
// 调用add
result = intAccumulator.add(delta);
}
System.out.println(oldValue + "+" + delta + "=" + result);
//验证,如果不合理,则输出错误信息
if (delta + oldValue != result) {
System.err.println("ERROR:" + oldValue + "+" + delta + "=" + result);
}
delta++;
// 模拟延迟
slowly();
}
}).start();
});
}
这里将数据同步的控制放在了线程的逻辑执行单元中,而在IntegerAccumulator中未增加任何同步的控制,如果单纯对getValue方法和add方法增加同步控制,虽然保证了单个方法的原子性,但是两个原子类型的操作在一起未必就是原子性的,因此在线程的逻辑执行单元中增加同步控制是最为合理的。
3 不可变的累加器对象设计
- 不可变类需要使用final修饰,防止他人继承该类,可对其进行修改
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
/**
* @author wyaoyao
* @date 2021/4/10 11:26
*/
// 使用final修饰不许被继继承
public final class FinalIntAccumulator {
private final int init;
public FinalIntAccumulator(int init) {
this.init = init;
}
/**
* 构造新的累加器
*
* @param intAccumulator
* @param init
*/
public FinalIntAccumulator(FinalIntAccumulator intAccumulator, int init) {
this.init = intAccumulator.getValue() + init;
}
/**
* 每次累加都会返回一个新的FinalIntAccumulator
*
* @param delta
* @return
*/
public FinalIntAccumulator add(int delta) {
return new FinalIntAccumulator(this, delta);
}
public int getValue() {
return this.init;
}
// 测试方法
public static void main(String[] args) {
// 定义累加器,并且将设置初始值为0
FinalIntAccumulator intAccumulator = new FinalIntAccumulator(0);
// 定义三个线程,操作这个intAccumulator
IntStream.range(0, 3).forEach(i -> {
new Thread(() -> {
int delta = 0;
while (true) {
// 调用add之前老的值
int oldValue = intAccumulator.getValue();
// 调用add
int result = intAccumulator.add(delta).getValue();
System.out.println(oldValue + "+" + delta + "=" + result);
//验证,如果不合理,则输出错误信息
if (delta + oldValue != result) {
System.err.println("ERROR:" + oldValue + "+" + delta + "=" + result);
}
delta++;
// 模拟延迟
slowly();
}
}).start();
});
}
private static void slowly() {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4 总结
设计一个不可变的类共享资源需要具备不可破坏性,比如使用final修饰,另外针对共享资源操作的方法是不允许被重写的,以防止由于继承而带来的安全性问题,但是单凭这两点也不足以保证一个类是不可变的:
public final class Immutable
{
private final List<String> list;
public Immutable(List<String> list)
{
this.list = list;
}
public List<String> getList()
{
Collections.unmodifiableList(this.list)
//return this.list;
}
}
Immutable类被final修饰因此不允许更改,同样list只能在构造时被指定,但是该类同样是可变的(mutable),因为getList方法返回的list是可被其他线程修改的,如果想要使其真正的不可变,则需要在返回list的时候增加不可修改的约束Collections.unmodifiableList(this.list)或者克隆一个全新的list返回