线程安全

线程安全:ADT或方法在多线程中要执行正确

什么是线程安全的?

线程之间的“竞争条件”:作用于同一个mutable数据上的多个线程, 彼此之间存在对该数据的访问竞争并导致互相交错存取,致使post condition可能被违反,这是不安全的。
保证线程安全的目的是为了让ADT或方法在多线程中要执行正确

  • 不违反spec、保持RI
  • 与多少处理器、 OS如何调度线程,均无关
  • 不需要在spec中强制要求client满足某种“线程 安全”的义务

线程不安全的例子:

还记得Iterator 类吗?你不能在集合上进行迭代遍历的同时修改集合
如果因为线程并行的缘故导致post condition可能被违反,Iterator类不能保证其行为的正确
所以这个类不是线程安全的

如何实现线程安全呢?

有四种方案:

  • 限制数据共享.
  • 共享不可变数据
  • 共享线程安全的可变数据
  • 同步机制:通过锁的机 制共享线程不安全的可变数据,变并行为串行

1. 限制数据共享

其核心思想是:
线程之间不共享mutable数据类型

  • 将可变数据限制在单一线程内部,避免竞争
  • 不允许任何线程直接读写该数据
  • 避免使用全局变量,全局变量不会自动进行线程限制。

但是如果一个ADT的rep中包含 mutable的属性且多线程之间 对其进行mutator操作,那么 就很难使用confinement策略 来确保该ADT是线程安全的,因此使用限制数据共享的限制很大。

除非你知道线程访问的所有数据,否则Confinement无法彻 底保证线程安全

2. 只共享 Immutabe类

使用不可变数据类型和不可变引用,避免 多线程之间的race condition
即使有竞争,但是数据是不可变的,因此还是线程安全的

不可变数据通常是线程安全的
如果类的对象在类的整个生命周期中始终一个不变的抽象数值,则该类型是Immutabe类。
但是实际上,Immutabe允许在该类型在内部更改其rep(内部表示),只要客户端看不到那些变化即可,比如beneficent mutation

举一个有理数的例子:
对于一个表示有理数的类,如果存放一个有理数—— 2 / 4 2/4 2/4,我们可以允许这个类将 2 / 4 2/4 2/4变化为 1 / 2 1/2 1/2,但是用户看到的数值都是不变的——0.5,因此这个类还是Immutabe类型,这个将 2 / 4 2/4 2/4变化为 1 / 2 1/2 1/2的优化便是beneficent mutation(有益的变化)
虽然这种beneficent mutation通常能给我们提供很多的优化,但是在多线程里,这种隐藏的变化是不安全的
如果Immutabe类中使用了beneficent mutation,必须 要通过“加锁”机制来保证线程安全

因此如果要保证一个immutable 在多线程情况下还是安全的,需要多不变类型进行更强的定义:

  • 没有mutator 方法
  • 所有属性都是private 且为final
  • 不存在表示泄露(不可以从外面更改)
  • No mutation whatsoever of mutable objects in the rep
  • 不存在 beneficent mutation

相比起策略1 Confinement,该策略2 Immutability允许有全局rep, 但是只能是immutable的。

如果一定需要mutable的ADT(List, Map,Set),怎么办?

3. 使用线程安全的数据类型

在JDK中的类,文档中明确指明了是否threadsafe
一般来说, JDK同时提供两个相同功能的类,一个是threadsafe,另一个不是。 原因:threadsafe的类一般性能上受影响

List , Set , Map 常见的集合类都是线程不安全的
但是Java API提供了进一步的 decorator ,对它们的每一个操作调用,都以原子方式执行 ,不会与其他操作interleaving
比如:

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)

使用委托功能委托给线程安全的类,利用装饰器进行装饰

private static Map<Integer,Boolean> cache = Collections.synchronizedMap(new HashMap<>());

在使用synchronizedMap(hashMap)之后,不要再把参数hashMap共享给其他线程,不要保留别名,一定要彻底销毁,一旦保留别名,可能被绕开synchronizedMap(hashMap),导致线程不安全

即使在线程安全的集合类上,使用iterator 也是不安全的
除非使用lock机制

即使是线程安全的collection类,仍可能产生竞争

执行其上某个操作是threadsafe的,比如执行多个 get(), 或者是 put() 之间不会产生竞争,但如果多个操作放在一起,仍旧不安全

例如对于:

if ( ! lst.isEmpty()) { 
String s = lst.get(0); ... 
} 

两行语句之间可能会产生交错执行,如果判断! lst.isEmpty()为非空后,再另一个线程种remove掉lst的最后一个元素,这时候lst变成空的,但是仍会执行String s = lst.get(0);,这显然不是我们想要的

还需要注意:线程安全数据类型!=真的可以做到线程安全
例如:

public class Building {
	private final String buildingName;
	private int numberOfFloors;
	private final int[] occupancyPerFloor;
	private final List<String> companyNames = Collections.synchronizedList(new ArrayList<>());
	private final Set<String> roomNumbers = new HashSet<>();
	private final Set<String> floorplan = Collections.synchronizedSet(roomNumbers);
	...
}

对于这片代码中的,
线程安全数据类型有:buildingName,numberOfFloors,occupancyPerFloor,companyNames ,floorplan
但是真的可以做到线程安全确只有:buildingName 和companyNames
因为 private int numberOfFloors; 并没有加final ,虽然int是线程安全的,但是在多个线程里交错执行,可能就会出现线程不安全
对于floorplan,因为其装饰的roomNumbers在别处留了索引,可以被绕过而访问,所以也不是线程安全的。

一些问题

问题1
以下关于ADT ThreadSafe的说法,不正确的是 AD

A 任何对外发布的ADT都必须要做到thread safe
B 不管怎么time slicing和interleaving,一个线程安全的ADT对象的RI要始终为真
C 在不同的机器上、不同的OS上执行关于该ADT的程序,都不应该出现RI被违反的情况
D 任何immutable的ADT都是threadsafe的
E 做到ThreadSafe与否,只与ADT自己的内部rep和内部方法实现有关,与client是否应遵循特定条件有关

A:在spec里写清楚即可,不一定必须保证线程安全
D:保证没有beneficent mutation
问题2.
关于Strategy 1: Confinement的说法,不正确的是BCD

A 多线程之间不要共享mutable的数据对象
B 各线程内部不应使用mutable的数据对象
C 只要ADT的rep里存在static类型的变量,该ADT在任何多线程场景下都无法做到threadafe
D 只要ADT的rep里存在mutable类型的变量,该ADT在任何多线程场景下都无法做到threadsafe

B:限定在单线程中使用即可
C:如果该static成员变量是immutable的,仍可以做到
D:如果mutable的属性通过其他策略确保的安全性(比如对集合进行装饰),仍可以做到

问题3
针对Strategy 2: Immutability,不正确的说法是:BC

A 该策略思想是:多线程之间共享数据时,使用不可变数据类型和不可变引用,以避免多线程之间的race condition
B 如果多线程共享的是mutable的数据类型,可以通过在线程中禁用其mutator方法来达到threadsafe
C 如果ADT的rep里所有属性都是final的,那么它在任何多线程场景下都可以做到threadafe
D 如果ADT的rep里存在public类型的属性,那么它就无法确保做到threadsafe

B:线程安全不能要求客户端
C: 需要immutable

问题4
针对Strategy 3: Using Threadsafe Data Types策略,不正确的是C

A 如果必须要用mutable的数据类型在多线程之间共享数据,要使用线程安全的数据类型
B 若在ADT的rep里使用集合类,最好使用SynchronizedMap/Syn…List/Syn…net
C 若能做到B选项中的说法,该ADT在任何多线程场景下都可以做到threadsafe
D 即使在线程安全的集合类上,使用iterator也是不安全的

C:如果未修饰的集合类的引用没有被销毁,就可能线程不安全

问题5

public class P{
	private static P p = null;
	// invariant: only one P object
	// should be created
	
	public static P getInstance() {
1)    	if(p == null) {
2)    		p = new P();
		}
3)  	return P;
	}
	...
}

有两个线程,其run()方法都是在调用getInstance()。
如果线程1正在执行语句1,线程2正在执行语句3,那么两个线程之间是否会产生race并导致invariant被违反?
答案:是的 因为p是全局变量,可能被其他线程修改

问题6

public String t = new String();
private List<String> ol = new ArrayList<>();
private final List<String> ls = Collections.synchronizedList(ol);

某ADT的Rep如上所示,该ADT在被用于多线程场景时的threadsafe,以下说法不正确的是AC

A t不存在线程安全风险,因为它是String类型,是immutable的
B ol存在线程安全风险,因为List<>不是线程安全的数据类型
C ls不存在线程安全风险,因为它用synchronizedList做了封装
D ls存在线程安全风险,可能在该ADT的某方法中存在对ls的多个方法的调用,可能产生race

A:不是final
C:未修饰的集合类的引用没有被销毁

问题7
线程1:

if ( ! lst.isEmpty()) {
	String s = lst.get(0);
	...
}

线程2:

if ( lst.size() >=1) {
	lst.remove(0);
	...
}

其中lst是利用Collections.synchronizedList()得到的List对象
以下说法正确的是CD
A 因为lst是线程安全的数据类型,故两个线程不存在race condition
B 线程1的isEmpty()和线程2的size()之间的race condition造成未期望的后果
C 线程1的isEmpty()和线程2的remove()产生race condition造成未期望的后果
D 线程1的get()和线程2的remove()产生race condition造成未期望的后果

A:存在race condition
C:线程1的isEmpty()和线程2的remove()产生race condition造成未期望的后果

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值