Thread Safety
Thread Safety
Lab6中涉及到了并发编程的线程安全问题,我们在这里稍加讨论。
Recall race conditions: multiple threads sharing the same mutable variable without coordinating what they’re doing. This is unsafe, because the correctness of the program may depend on accidents of timing of their low-level operations.
There are basically four ways to make variable access safe in shared-memory concurrency:
Confinement(限制). Don’t share the variable between threads. This idea is called confinement, and we’ll explore it today.
Immutability(不变性). Make the shared data immutable. We’ve talked a lot about immutability already, but there are some additional constraints for concurrent programming that we’ll talk about in this reading.
Threadsafe data type(线程安全的数据结构). Encapsulate the shared data in an existing threadsafe data type that does the coordination for you. We’ll talk about that today.
Synchronization(同步). Use synchronization to keep the threads from accessing the variable at the same time. Synchronization is what you need to build your own threadsafe data type.
什么是线程安全
A data type or static method is threadsafe if it behaves correctly when used from multiple threads, regardless of how those threads are executed, and without demanding additional coordination from the calling code.
“behaves correctly” means satisfying its specification and preserving its rep invariant;
“regardless of how threads are executed” means threads might be on multiple processors or timesliced on the same processor;
“without additional coordination” means that the data type can’t put preconditions on its caller related to timing, like “you can’t call get() while set() is in progress.”
Confinement(限制)
我们实现线程安全的第一种方法是限制。线程限制是一个简单的想法:通过将数据限制在单个线程中来避免可变数据的竞争。不要让任何其他线程直接读取或写入数据。
由于共享可变数据是竞争条件的根本原因,因此限制通过不共享可变数据来解决它。
局部变量始终是线程限制的。局部变量存储在堆栈中,每个线程都有自己的堆栈。一次运行的方法可能有多次调用(在不同的线程中,甚至在单个线程的堆栈的不同级别,如果方法是递归的),但是每个调用都有自己的变量的私有副本,所以变量本身是有限的。
但要小心 - 变量是线程限制的,但如果它是一个对象引用,你还需要检查它指向的对象。如果对象是可变的,那么我们想要检查对象是否也受到限制 - 不能从任何其他线程可以访问它。
Immutability(不变性)
我们实现线程安全的第二种方法是使用不可变引用和数据类型。不变性解决了竞争条件的共享可变数据原因,并简单地通过使共享数据不可变来解决它。
最终变量是不可变引用,因此声明final的变量可以安全地从多个线程访问。您只能读取变量,而不能写入变量。要小心,因为这种安全性仅适用于变量本身,我们仍然必须争辩变量指向的对象是不可变的。
不可变对象通常也是线程安全的。我们在这里说“通常”是因为我们当前对不可变性的定义对于并发编程而言太松散了。我们已经说过,如果类型的对象在其整个生命周期中始终表示相同的抽象值,则类型是不可变的。但这实际上允许类型自由变异其代表,只要这些突变对客户是不可见的。我们已经看到了这个概念的几个例子,称为慈善突变。缓存,懒惰计算和数据结构重新平衡是典型的慈善变异。
Using Threadsafe Data Types(使用线程安全的数据结构)
我们实现线程安全的第三个主要策略是将共享可变数据存储在现有线程安全数据类型中。
当Java库中的数据类型是线程安全的时,其文档将明确说明该事实。
Synchronization(同步)
并发程序的正确性不应取决于计时事故。
由于共享可变数据的并发操作引起的竞争条件是灾难性的错误 - 难以发现,难以重现,难以调试 - 我们需要一种方法来共享内存以便彼此同步的并发模块。
锁是一种同步技术。锁是一种抽象,它允许一次最多一个线程拥有它。 拿着锁是一个线程告诉其他线程的方式:“我正在处理这个问题,现在不要触摸它。”
锁有两个操作:
acquire允许线程获取锁的所有权。如果一个线程试图获取另一个线程当前拥有的锁,它将阻塞,直到另一个线程释放锁。此时,它将与试图获取锁的任何其他线程竞争。一次最多只有一个线程拥有锁。
release 放弃锁的所有权,允许另一个线程获得它的所有权。
使用锁还会告诉编译器和处理器您正在同时使用共享内存,以便将寄存器和高速缓存刷新到共享存储。这避免了重新排序的问题,确保锁的所有者始终查看最新数据。
通常,阻塞意味着线程在事件发生之前等待(不做进一步的工作)。
一个acquire(l)如果另一个线程(线程说2)保持锁定在线程1将阻塞l。它等待的事件是线程2执行release(l)。此时,如果线程1可以获取l,它将继续运行其代码,并具有锁的所有权。有可能另一个线程(比如线程3)也被阻止了acquire(l)。线程1或3将锁定l并继续。另一个将继续阻止,release(l)再次等待。