7.2 线程安全
一. 线程安全的定义
线程之间的“竞争条件”(race condition
):作用于同一个 mutable
数据上的多个线程,彼此之间存在对该数据的访问竞争并导致 interleaving
,导致 post condition
可能被违反,这是不安全的。
线程安全: ADT
或方法在多线程中要执行正确
- 不违反
spec
、保持RI
- 与多少处理器、
OS
如何调度线程,均无关 - 不需要在
spec
中强制要求client
满足某种“线程安全”的义务
迭代器 Iterator
不是线程安全的。
public static void dropCourse6(ArrayList<String> subjects) {
MyIterator iter = new MyIterator(subjects);
while (iter.hasNext()) {
String subject = iter.next();
if (subject.startsWith("6.")) {
subjects.remove(subject);
}
}
}
四种保证线程安全的方式:
- 限制数据共享(
Confinement
) - 共享不可变数据(
Immutability
) - 共享线程安全的可变数据(
Threadsafe data type
) - 同步机制:通过锁的机制共享线程不安全的可变数据,变并行为串行(
Synchronization
)
策略 1 . 限制数据共享(Confinement
)
核心思想:线程之间不共享 mutable
数据类型。其只能使用局部变量,避免使用全局或静态变量。
- 将可变数据限制在单一线程内部,避免竞争
- 不允许任何线程直接读写该数据
以下程序可能出现 Interleaving
,使得线程不安全,违背了单例模式:
// This class has a race condition in it.
public class PinballSimulator {
private static PinballSimulator simulator = null;
// invariant: there should never be more than one PinballSimulator
// object created
private PinvallSimulator() {
system.out.println("created a PinballSimulator object");
}
// factory method that returns the sole PinballSimulator object,
// creating it if it doesn't exist
public static PinballSimulator getInstance() {
if (simulator == null) {
simulator = new PinballSimulator();
}
return simulator;
}
}
静态变量造成的错误:
/**
* @param x integer to test for primeness; requires x > 1
* @return true if x is prime with high probability
*/
public static boolean isPrime(int x) {
if (cache.containsKey(x)) return cache.get(x);
boolean answer = BigInteger.valueOf(x).isProbablePrime(100);
cache.put(x, answer);
return answer;
}
private static Map<Integer, Boolean> cache = new HashMap<>();
如果一个 ADT
的 rep
中包含 mutable
的属性且多线程之间对其进行 mutator
操作,那么就很难使用 confinement
策略来确保该 ADT
是线程安全的。
这种严格的限制,使得 Confinement
变得难以使用。
策略 2 . 共享不可变数据(Immutability
)
使用不可变数据类型和不可变引用,避免多线程之间的 race condition
。
- 添加
final
是一种方法
不可变数据通常(使用通常是因为定义可能不同,这里要求无论是不是 beneficent mutation
都不能做任何的改变)是线程安全的。;如果 ADT
中使用了 beneficent mutation
,必须要通过“加锁”机制来保证线程安全
线程安全的角度重新定义 immutable
(前三为原有的,第四个为新增的):
- 无可变方法
- 所有属性为
private
和final
- 无表示暴露(
RE
) - 不能存在任何包括
beneficent mutation
在内的对属性等的修改
设计 ADT
需关注以下内容使得线程安全:
Field
属性Creator implementations
Producer implementations
Observer implementations
Mutator implementations
(不能出现)
无需关注(这是因为无需限制用户使用方式):
Client calls to creators
Client calls to produces
Client calls to observers
Client calls to mutators
相比起策略 1 Confinement
,该策略 2 Immutability
允许有全局 rep
但是只能是 immutable
的。
策略 3 . 共享线程安全的可变数据(Threadsafe data type
)
如果必须要用 mutable
的数据类型在多线程之间共享数据,要使用线程安全的数据类型(均是原子操作即不可切片的操作)。
- 在
JDK
中的类,文档中明确指明了是否threadsafe
一般来说,JDK
同时提供两个相同功能的类,一个是 threadsafe
,另一个不是。这是因为: threadsafe
的类一般性能上受影响。
如上图 StringBuilder
和 StringBuffer
,后者是线程安全的,前者需程序员自己手动保证线程安全。
集合类(List
、Map
、Set
)都是线程不安全的。于是 Java API
提供了进一步的 decorator
,使得线程安全:
- 对它们的每一个操作调用,都以原子方式执行
- 不会与其他操作
interleaving
方法如下:
private static Map<Integer,Boolean> cache = Collections.synchronizedMap(new HashMap<>());
Conclusion
:
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
这使用了装饰模式 decorator pattern
。(注:这些 mutable
变为 immutable
,线程安全变为线程不安全都使用了装饰模式。装饰模式使用静态工厂方法构建新类型。)
/**
* @param x integer to test for primeness; requires x > 1
* @return true if x is prime with high probability
*/
public static boolean isPrime(int x) {
if (cache.containsKey(x)) return cache.get(x);
boolean answer = BigInteger.valueOf(x).isProbablePrime(100);
cache.put(x, answer);
return answer;
}
private static Map<Integer, Boolean> cache = new HashMap<>();
但即使将 cache
变为线程安全的类,但 isPrime(x)
仍然线程不安全。这是因为虽然转换后 get()
, put()
为原子操作,但多个操作间可能出现 interleaving
。
private static Map<Integer,Boolean> cache =
Collections.synchronizedMap(new HashMap<>());
在使用 synchronizedMap(hashMap)
之后,不要再把参数 hashMap
(上面的 new HashMap<>()
)共享给其他线程,不要保留别名,一定要彻底销毁,这是防止通过 HashMap
绕过线程安全的对象访问 Map
对象。
即使在线程安全的集合类上,使用 iterator
也是不安全的,除非使用 lock
机制。
List<Type> c = Collections.synchronizedList (new ArrayList<Type>());
synchronized(c) { // to be introduced later (the 4 th threadsafe way)
for (Type e : c)
foo(e);
}
即使是线程安全的 collection
类,仍可能产生竞争。这是因为尽管执行其上某个操作是 threadsafe
的,但如果多个操作放在一起,仍旧不安全。
2. 如何写 Safety Argument
在代码中以注释的形式增加说明:该 ADT
采取了什么设计决策来保证线程安全。
- 使用了四种方法的一种
- 如果是后两种,还需考虑对数据的访问都是原子的,不存在
interleaving
除非你知道线程访问的所有数据,否则 Confinement
无法彻底保证线程安全,因为你只能使用局部变量。除非是在 ADT
内部创建的线程,可以清楚得知访问数据有哪些。