线程安全是什么意思?

最近我试图从一个线程(除了UI线程)访问一个文本框,并引发一个exception。 它说了一些关于“代码不是线程安全的”,所以我最终写了一个委托(来自MSDN的示例帮助),并调用它。

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

更新:如果我检查,我会遇到任何严重的问题

Controls.CheckForIllegalCrossThread..blah =true 

Eric Lippert有一个很好的博客文章,题目叫做“线程安全”是什么? 关于维基百科发现的线程安全的定义。

从链接中提取3个重要的东西:

如果在multithreading同时执行的过程中它能够正常工作,则一段代码是线程安全的。“

“特别是,它必须满足multithreading访问相同的共享数据的需要,…”

“…而且在任何时候只需要一个线程就可以访问共享的数据。”

绝对值得一读!

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

维基百科有一篇关于线程安全的文章。

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

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

线程是程序的执行path。 单线程程序只有一个线程,所以这个问题不会出现。 几乎所有的GUI程序都有多个执行path,因此有多个线程 – 一个用于处理GUI的显示和处理用户input,另一个用于实际执行程序的操作。 这是为了使用户界面在程序运行时仍能够响应。

您可以从“实践中的Java并发”一书中获得更多的解释:

如果一个类在从多个线程进行访问时行为正确,则类是线程安全的,而不pipe运行时环境对这些线程执行的调度或交错,以及调用代码没有额外的同步或其他协调。

如果一个模块保证在multithreading和并发使用的情况下可以保持不变式,则模块是线程安全的。

在这里,模块可以是数据结构,类,对象,方法/过程或function。 基本范围的一段代码和相关的数据。

这种担保可能会限制在特定的环境中,例如特定的CPU架构,但是必须适用于这些环境。 如果没有明确的环境定义,那么通常就暗示它适用于所有环境,代码可以被编译和执行。

线程不安全的模块可以在multithreading和并发使用下正常工作,但这往往更多的是运气和巧合,而不是精心的devise。 即使某些模块不能为你所用,在移动到其他环境时也会中断。

multithreading错误往往很难debugging。 其中一些只是偶尔发生,而另一些performance出积极的态度 – 这也可以是环境特定的。 他们可以performance为微妙的错误结果或僵局。 他们可以以不可预知的方式搞乱数据结构,并导致其他看起来不可能的错误出现在代码的其他远程部分。 它可以是非常具体的应用程序,所以很难给出一个一般的描述。

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

考虑以下方法:

 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)的值读入到它自己的variablestmp中。 线程B完成整个方法,所以最后myInt = 1,并返回1。 现在轮到A轮了。 线程A继续。 并添加1到tmp(线程A的tmp为0)。 然后将这个值保存在myInt中。 myInt又是1。

所以在这种情况下,方法AddOne被调用了两次,但是因为该方法没有以线程安全的方式实现,所以myInt的值不是2,如预期的那样,而是因为第二个线程在第一个线程完成之前读取variablesmyInt更新它。

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

你显然在WinForms环境中工作。 WinForms控件显示线程关联,这意味着创build它们的线程是唯一可用于访问和更新它们的线程。 这就是为什么你会findMSDN和其他地方的例子,演示如何将调用callback到主线程。

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

我发现http://en.wikipedia.org/wiki/Reentrancy_%28computing%29这个概念就是我通常认为的不安全线程,当一个方法具有并且依赖副作用(比如全局variables)的时候。;

例如,我已经看到了将浮点数格式化为string的代码,如果其中两个运行在不同线程中,decimalSeparator的全局值可以永久更改为“。”。

 //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 

线程安全 :线程安全的程序保护其数据免于内存一致性错误。 在一个高度multithreading的程序中,一个线程安全的程序不会对同一对象上的多个线程的多个读/写操作产生任何副作用。 不同的线程可以共享和修改对象数据而没有一致性错误

您可以使用高级并发API来实现线程安全。 这个文档页面提供了很好的编程结构来实现线程安全。

锁对象支持简化许多并发应用程序的locking习惯用法。

执行者定义了一个用于启动和pipe理线程的高级API。 由java.util.concurrent提供的执行器实现提供了适用于大规模应用程序的线程池pipe理。

并发集合可以更容易地pipe理大量的数据集合,并且可以大大减less同步的需要。

primefacesvariables具有最小化同步的function,有助于避免内存一致性错误。

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

对于其他编程构造,也参考java.util.concurrent和java.util.concurrent.atomic包。

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

假设你有一个银行账户,有互联网和手机银行,你的账户只有10美元。 您使用手机银行向另一个账户转账,同时您使用同一个银行账户进行网上购物。 如果这个BankAccount不是“THREAD SAFE”,那么银行允许你执行两笔交易,然后银行就会破产。

ThreadSafe的意思是,如果同时尝试访问Object的多个线程,Object状态不会改变。

要了解线程安全性,请阅读以下部分 :

4.3.1。 示例:车辆跟踪器使用委派

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

清单4.6。 DelegatingVehicleTracker使用的不可变点类。

  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不使用任何明确的同步。 所有对状态的访问都由ConcurrentHashMappipe理,并且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。 独立状态variables

只要这些基础状态variables是独立的,也就是说,复合类不强加任何涉及多个状态variables的不variables,我们也可以将线程安全委派给多个基础状态variables。

清单4.9中的VisualComponent是一个graphics组件,它允许客户端为鼠标和击键事件注册侦听器。 它维护每种types的注册侦听器的列表,以便在事件发生时可以调用适当的侦听器。 但是鼠标听众和关键听众之间没有任何关系, 两者是独立的,因此VisualComponent可以将其线程安全责任委托给两个底层线程安全列表。

清单4.9。 将线程安全委托给多个基础状态variables。

 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实现,特别适用于pipe理监听器列表(见第5.2.3节)。 每个List都是线程安全的,并且因为没有约束条件将状态耦合到另一个的状态,所以VisualComponent可以将其线程安全责任委托给底层的mouseListenerskeyListeners对象。

4.3.3。 授权失败时

大多数组合类并不像VisualComponent那么简单:它们具有与组件状态variables相关的不variables。 清单4.10中的NumberRange使用两个AtomicIntegers来pipe理它的状态,但是强加了一个额外的约束 – 第一个数字小于或等于第二个数字。

代码清单4.10 数量范围类没有充分保护其不variables。 不要这样做。

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

NumberRange 不是线程安全的 ; 它不保留约束更低和更高的不variables。 setLowersetUpper方法试图尊重这个不variables,但做得不好。 setLowersetUpper都是check-then-act序列,但是它们没有使用足够的locking来使它们成为primefaces。 如果数字范围成立( setLower(5) ),一个线程调用setLower(5)而另一个线程调用setUpper(4) ,有一些不幸的时机将通过setters中的检查,并将应用这两个修改。 结果是范围现在成立(5,4) – 一个无效的状态 。 所以虽然底层的AtomicIntegers是线程安全的,但是复合类不是 。 因为底层状态variableslowerupper不是独立的,所以NumberRange不能简单地将线程安全委托给它的线程安全状态variables。

NumberRange可以通过使用锁来维护其不variables,例如使用普通锁来保护lower和upper。 它也必须避免公开发布,以防止客户破坏其不variables。

如果一个类具有复合动作,如NumberRange所做的那样,单独委派对于线程安全来说再次不是一个合适的方法。 在这些情况下,类必须提供自己的锁,以确保复合操作是primefaces的,除非整个复合操作也可以委托给基础状态variables。

如果一个类由多个独立的线程安全状态variables组成,并且没有任何操作具有任何无效的状态转换,则可以将线程安全性委托给基础状态variables。