软件构造 7-2 Thread Safety

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<>();

  如果一个 ADTrep 中包含 mutable 的属性且多线程之间对其进行 mutator 操作,那么就很难使用 confinement 策略来确保该 ADT 是线程安全的。
  这种严格的限制,使得 Confinement 变得难以使用。

策略 2 . 共享不可变数据(Immutability)

  使用不可变数据类型不可变引用,避免多线程之间的 race condition

  • 添加 final 是一种方法

  不可变数据通常(使用通常是因为定义可能不同,这里要求无论是不是 beneficent mutation 都不能做任何的改变)是线程安全的。;如果 ADT 中使用了 beneficent mutation ,必须要通过“加锁”机制来保证线程安全

  线程安全的角度重新定义 immutable (前三为原有的,第四个为新增的):

  • 无可变方法
  • 所有属性为 privatefinal
  • 无表示暴露(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 的类一般性能上受影响
在这里插入图片描述
  如上图 StringBuilderStringBuffer后者线程安全的,前者需程序员自己手动保证线程安全。


  集合类ListMapSet)都是线程不安全的。于是 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 内部创建的线程,可以清楚得知访问数据有哪些。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值