线程安全是什么意思?

最近我试图从一个线程(UI 线程除外)访问一个文本框,但是抛出了一个异常。它提到了“代码不是线程安全的”,因此我最终编写了一个委托(来自 MSDN 的示例有所帮助) ,并调用了它。

但即便如此,我还是不太明白为什么所有额外的代码都是必要的。

更新: 如果我检查一下,会不会遇到什么严重的问题

Controls.CheckForIllegalCrossThread..blah =true
99667 次浏览

Wikipedia 有一篇关于线程安全的文章。

这个 定义页(你必须跳过一个广告-对不起)这样定义它:

在计算机编程中,线程安全描述可以从多个编程线程调用的程序部分或例程,这些线程之间没有不必要的交互。

线程是程序的执行路径。一个单线程程序将只有一个线程,所以这个问题不会出现。几乎所有的 GUI 程序都有多个执行路径,因此线程-至少有两个,一个用于处理 GUI 的显示和处理用户输入,另一个用于实际执行程序的操作。

这样做是为了在程序工作时 UI 仍然能够响应,方法是将任何长时间运行的进程卸载给任何非 UI 线程。这些线程可以创建一次并在程序的生命周期内存在,或者只在需要时创建,完成后销毁。

由于这些线程通常需要执行常见的操作——磁盘 i/o、将结果输出到屏幕等等——这些代码部分需要以这样的方式编写,即它们能够处理来自多个线程的调用,通常是同时进行。这将涉及诸如:

  • 正在处理数据副本
  • 在关键代码周围添加锁
  • 以适当的模式打开文件-所以如果正在读取,不要打开文件进行写操作。
  • 处理无法访问资源的问题,因为资源被其他线程/进程锁定。

您显然是在 WinForms 环境中工作。WinForms 控件具有线程关联性,这意味着创建它们的线程是唯一可用于访问和更新它们的线程。这就是为什么您将在 MSDN 和其他地方找到演示如何将调用封送回主线程的示例。

通常的 WinForms 实践是有一个专门用于所有 UI 工作的线程。

在最简单的术语中,线程安全意味着从多个线程访问它是安全的。当您在一个程序中使用多个线程,并且每个线程都试图访问内存中的一个公共数据结构或位置时,可能会发生一些糟糕的事情。因此,您需要添加一些额外的代码来防止这些不好的事情发生。例如,如果两个人在同一时间写同一个文档,要保存的第二个人将覆盖第一个人的工作。为了让它线程安全,你必须强制人2等待人1完成他们的任务,然后才允许人2编辑文档。

Eric Lippert 发表了一篇题为 你说的“线程安全”是什么?的博客文章,内容是关于 Wikipedia 中线程安全的定义。

从这些链接中提取的3个重要信息:

”如果一段代码在下列情况下正确运行,则该代码是线程安全的 多线程同时执行”

”特别是,它必须满足多个线程的需要 访问相同的共享数据...”

只有一个人可以访问共享的数据 线程在任何给定的时间。“

绝对值得一读!

一个模块是线程安全的,如果它保证它能够在面对多线程和并发使用时保持其不变量。

在这里,模块可以是数据结构、类、对象、方法/过程或函数。基本上是作用域范围内的代码和相关数据。

这种保证可能仅限于某些环境,例如特定的 CPU 体系结构,但必须适用于这些环境。如果没有对环境进行明确的分界,那么通常会被认为暗示可以对所有环境编译和执行代码。

线程不安全的模块 在多线程和并发使用情况下可以正常工作,但这往往是运气和巧合,而不是精心设计的结果。即使有些模块在下面没有中断,它也可能在移动到其他环境时中断。

多线程 bug 通常很难调试。其中一些只是偶尔发生,而另一些则表现得咄咄逼人——这也可能是特定于环境的。它们可能表现为微妙的错误结果或死锁。它们会以不可预测的方式扰乱数据结构,并导致其他看似不可能的错误出现在代码的其他远程部分。它可以是非常具体的应用程序,因此很难给出一般性的描述。

我发现 http://en.wikipedia.org/wiki/Reentrancy_%28computing%29的概念是我通常认为的不安全线程,即当一个方法具有并依赖于全局变量等副作用时。

例如,我曾经看到过将浮点数字格式化为字符串的代码,如果其中两个在不同的线程中运行,则 decalPartiator 的全局值可以永久地更改为’

//built in global set to locale specific value (here a comma)
decimalSeparator = ','


function FormatDot(value : real):
//save the current decimal character
temp = decimalSeparator


//set the global value to be
decimalSeparator = '.'


//format() uses decimalSeparator behind the scenes
result = format(value)


//Put the original value back
decimalSeparator = temp

简单地说,线程安全意味着一个方法或类实例可以由多个线程同时使用,而不会出现任何问题。

考虑以下方法:

private int myInt = 0;
public int AddOne()
{
int tmp = myInt;
tmp = tmp + 1;
myInt = tmp;
return tmp;
}

现在,线程 A 和线程 B 都希望执行 AddOne()。但是 A 首先开始并将 myInt (0)的值读入 tmp。现在,由于某种原因,调度程序决定暂停线程 A 并将执行推迟到线程 B。线程 B 现在也将 myInt(仍然是0)的值读入它自己的变量 tmp 中。线程 B 完成了整个方法,所以在最后 myInt = 1。一个回来了。现在轮到 A 线程了。线程 A 继续。并将1添加到 tmp (对于线程 A,tmp 为0)。然后在 myInt中保存这个值。myInt又是1。

因此,在这个例子中,方法 AddOne()被调用了两次,但是因为方法没有以线程安全的方式实现,所以 myInt的值不是预期的2,而是1,因为第二个线程在第一个线程完成更新之前读取了变量 myInt

在非平凡的情况下,创建线程安全的方法是非常困难的。还有很多技巧。在 Java 中,您可以将一个方法标记为 synchronized,这意味着在给定的时间内只有一个线程可以执行该方法。其他线程排队等待。这使得一个方法是线程安全的,但是如果在一个方法中有很多工作要做,那么这就会浪费很多空间。另一种技术是通过创建一个锁或信号量来锁定 ‘只将方法的一小部分标记为同步’,并锁定这个小部分(通常称为临界部分)。甚至还有一些方法被实现为无锁线程安全的,这意味着它们的构建方式使得多个线程可以同时通过它们而不会造成任何问题,当一个方法只执行一个原子调用时就会出现这种情况。原子调用是不能被中断的调用,一次只能由一个线程完成。

您可以从《 Java 并发实践》一书中获得更多解释:

如果一个类在从多个线程访问时运行正确,那么它就是线程安全的,不管执行期函式库对这些线程的执行是调度还是交错执行,也不需要调用代码进行额外的同步或其他协调。

线程安全 : 线程安全程序保护其数据不受内存一致性错误的影响。在高度多线程的程序中,线程安全的程序不会对来自同一对象上的多个线程的多个读/写操作造成任何副作用。不同的线程可以共享和修改对象数据,而不会出现一致性错误。

可以通过使用高级并发 API 来实现线程安全。这个文档 呼叫提供了良好的编程构造来实现线程安全。

Lock Objects 支持简化许多并发应用程序的锁定习惯用法。

执行程序 定义了一个用于启动和管理线程的高级 API。并发提供的执行器实现提供了适合大型应用程序的线程池管理。

并发集合 使得管理大量数据集合变得更加容易,并且可以大大减少对同步的需求。

原子变量 具有最小化同步和帮助避免内存一致性错误的特性。

ThreadLocalRandom (在 JDK 7中)提供了从多个线程高效生成伪随机数生成器。

有关其他编程构造,请参考 并发原子包。

在现实世界中,外行人的例子是

让我们假设你有一个银行帐户与互联网和移动银行,你的帐户只有10美元。 您使用移动银行将余额转移到另一个账户,与此同时,您使用相同的银行账户进行网上购物。 如果这个银行账户不是线程安全的,那么银行允许你同时进行两笔交易,然后银行就会破产。

线程安全意味着当多个线程同时尝试访问对象时,对象的状态不会改变。

要理解线程安全性,请阅读以下 :

4.3.1示例: 使用委托的车辆跟踪器

作为一个更实质性的委托示例,让我们构造一个车辆跟踪器的版本,该版本将委托给一个线程安全的类。我们将位置存储在一个 Map 中,因此我们从一个线程安全的 Map 实现 ConcurrentHashMap开始。我们还使用不可变的 Point 类而不是 MutablePoint来存储位置,如清单4.6所示。

清单4.6。

 class Point{
public final int x, y;


public Point() {
this.x=0; this.y=0;
}


public Point(int x, int y) {
this.x = x;
this.y = y;
}


}

Point是线程安全的,因为它是不可变的。可以自由地共享和发布不可变的值,因此在返回这些值时,我们不再需要复制这些位置。

清单4.7中的 DelegatingVehicleTracker不使用任何显式同步; 对状态的所有访问都由 ConcurrentHashMap管理,Map 的所有键和值都是不可变的。

清单4.7. 将线程安全委托给 ConcurrentHashMap。

  public class DelegatingVehicleTracker {


private final ConcurrentMap<String, Point> locations;
private final Map<String, Point> unmodifiableMap;


public DelegatingVehicleTracker(Map<String, Point> points) {
this.locations = new ConcurrentHashMap<String, Point>(points);
this.unmodifiableMap = Collections.unmodifiableMap(locations);
}


public Map<String, Point> getLocations(){
return this.unmodifiableMap; // User cannot update point(x,y) as Point is immutable
}


public Point getLocation(String id) {
return locations.get(id);
}


public void setLocation(String id, int x, int y) {
if(locations.replace(id, new Point(x, y)) == null) {
throw new IllegalArgumentException("invalid vehicle name: " + id);
}
}

}

如果我们使用的是原始的 MutablePoint类而不是 Point,那么我们将通过让 getLocations发布对不是线程安全的可变状态的引用来破坏封装。注意,我们稍微改变了车辆跟踪器类的行为; 当监视器版本返回位置的快照时,委托版本返回车辆位置的一个不可修改但“实时”的视图。这意味着,如果线程 A 调用 getLocations,而线程 B 稍后修改了一些点的位置,那么这些更改将反映在返回给线程 A 的 Map 中。

4.3.2. 独立状态变量

我们还可以将线程安全性委托给多个底层状态变量,只要这些底层状态变量是独立的,这意味着组合类不强加涉及多个状态变量的任何不变量。

清单4.9中的 VisualComponent是一个图形组件,它允许客户机为鼠标和击键事件注册侦听器。它维护每种类型的已注册侦听器列表,以便在发生事件时可以调用适当的侦听器。但是鼠标侦听器集和键侦听器集之间没有关系; 它们是独立的,因此 VisualComponent可以将其线程安全义务委托给两个底层线程安全列表。

清单4.9. 将线程安全委托给多个底层状态变量。

public class VisualComponent {
private final List<KeyListener> keyListeners
= new CopyOnWriteArrayList<KeyListener>();
private final List<MouseListener> mouseListeners
= new CopyOnWriteArrayList<MouseListener>();


public void addKeyListener(KeyListener listener) {
keyListeners.add(listener);
}


public void addMouseListener(MouseListener listener) {
mouseListeners.add(listener);
}


public void removeKeyListener(KeyListener listener) {
keyListeners.remove(listener);
}


public void removeMouseListener(MouseListener listener) {
mouseListeners.remove(listener);
}


}

VisualComponent使用一个 CopyOnWriteArrayList来存储每个侦听器列表; 这是一个线程安全的 List 实现,特别适合于管理侦听器列表(参见5.2.3节)。每个 List 都是线程安全的,并且因为没有将一个 List 的状态与另一个 List 的状态耦合起来的约束,所以 VisualComponent可以将其线程安全责任委托给底层的 mouseListenerskeyListeners对象。

4.3.3授权失败

大多数复合类并不像 VisualComponent那样简单: 它们具有关联其组件状态变量的不变量。清单4.10中的 NumberRange使用两个 AtomicIntegers来管理它的状态,但是增加了一个额外的约束ーー第一个数字小于或等于第二个数字。

清单4.10. 没有充分保护其不变量的数值范围类。

public class NumberRange {


// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);


public void setLower(int i) {
//Warning - unsafe check-then-act
if(i > upper.get()) {
throw new IllegalArgumentException(
"Can't set lower to " + i + " > upper ");
}
lower.set(i);
}


public void setUpper(int i) {
//Warning - unsafe check-then-act
if(i < lower.get()) {
throw new IllegalArgumentException(
"Can't set upper to " + i + " < lower ");
}
upper.set(i);
}


public boolean isInRange(int i){
return (i >= lower.get() && i <= upper.get());
}


}

NumberRangesetLower0; 它不保留约束下限和上限的不变量。setLowersetUpper方法试图尊重这个不变量,但效果很差。setLowersetUpper都是 check-then-act 序列,但是它们没有使用足够的锁使它们成为原子序列。如果数字范围保持(0,10) ,一个线程调用 setLower(5),而另一个线程调用 setUpper(4),有一些不幸的时间都将通过设置器的检查,两个修改都将被应用。结果是现在的范围保持(5,4)ー setLower1。所以是 setLower2。因为底层状态变量 lowerupper不是独立的,所以 NumberRange不能简单地将线程安全性委托给它的线程安全状态变量。

通过使用锁定来保持 NumberRange的不变性,例如使用一个公共锁定来保护上下两层,可以使 NumberRange成为线程安全的。它还必须避免发布越来越低的版本,以防止客户端破坏其不变量。

如果一个类具有复合操作(如 NumberRange) ,那么单独的委托也不是一种适合线程安全的方法。在这些情况下,类必须提供自己的锁,以确保复合操作是原子的,除非整个复合操作也可以委托给底层状态变量。

如果一个类由多个独立的线程安全状态变量组成,并且没有具有任何无效状态转换的操作,那么它可以将线程安全委托给底层状态变量。

生成线程安全的代码完全是关于管理对共享可变状态的访问。当可变状态在线程之间发布或共享时,需要对它们进行同步,以避免像 比赛条件内存一致性错误这样的 bug。

我最近写了一个 关于线程安全的博客。你可以阅读它获得更多的信息。