线程安全是什么意思?


123

最近,我尝试从线程(UI线程除外)访问文本框,并引发了异常。它说了一些有关“代码不是线程安全的”的内容,因此我最终编写了一个委托(MSDN的示例有所帮助)并改为调用它。

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

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

Controls.CheckForIllegalCrossThread..blah =true

5
通常,“线程安全”是指使用该术语的人认为至少对那个人而言意味着什么。因此,它不是一种非常有用的语言结构-在谈论线程代码的行为时,您需要更加具体。


@戴夫对不起,我试图寻找,但放弃了......我还是谢谢你..
维韦克伯纳德

1
不会出现的代码Race-Condition
Muhammad Babar 2014年

Answers:


121

埃里克·利珀特(Eric Lippert)的博客文章不错,您称其为“线程安全”是什么?有关Wikipedia中关于线程安全性的定义。

从链接中提取的3个重要事项:

“如果一段代码在多个线程同时执行期间正常运行,那么它就是线程安全的。”

“尤其是,它必须满足多个线程访问同一共享数据的需求……”

“……并且需要在任何给定时间只能由一个线程访问共享数据。”

绝对值得一读!


24
请避免仅链接答案,因为将来可能会变差。
akhil_mittal


106

用最简单的术语来说,线程安全是指可以安全地从多个线程进行访问。当您在程序中使用多个线程,并且每个线程都试图访问内存中的通用数据结构或位置时,可能会发生一些不良情况。因此,您添加了一些额外的代码来防止这些不好的事情。例如,如果两个人同时编写同一文档,则要保存的第二个人将覆盖第一个人的工作。为了使线程安全,您必须强迫2号人员等待1号人员完成其任务,然后再允许2号人员编辑文档。


11
这称为同步。对?
JavaTechnical

3
是。可以通过同步来强制各个线程等待对共享资源的访问。
Vincent Ramdhanie 2014年

在格雷戈里的公认答案中,他说:“如果一段代码在多个线程同时执行期间正常运行,那么一段代码是线程安全的。” 当您说“要使其线程安全然后,必须迫使第1个人等待”时;他不是在说不可以的同时说可以接受吗?您能解释一下吗?
Honey

这是同一件事。我只是建议一种简单的机制作为使代码线程安全的示例。不管使用哪种机制,尽管运行相同代码的多个线程不应相互干扰。
Vincent Ramdhanie

那么,这仅适用于使用全局变量和静态变量的代码吗?以您的人员编辑文档为例,我认为阻止人员2在另一个文档上运行文档编写代码没有任何意义。
亚伦·弗兰克

18

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

定义页(您必须跳过广告-很抱歉)对它的定义如下:

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

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

这样做是为了通过在程序运行时通过将任何长时间运行的进程卸载到任何非UI线程来使UI仍然响应。这些线程可以创建一次并在程序的整个生命周期内存在,也可以在需要时创建并在完成时销毁。

由于这些线程通常需要执行共同的操作-磁盘I / O,将结果输出到屏幕等-这些代码的这些部分将需要以可以处理多个线程调用的方式编写,通常是在同时。这将涉及以下内容:

  • 处理数据副本
  • 在关键代码周围添加锁

8

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

请考虑以下方法:

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。然后返回1。现在轮到线程A了。线程A继续。并将1加到tmp(线程A的tmp为0)。然后将此值保存在myInt中。myInt又是1。

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

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


如果方法AddOne被调用两次
Sujith PS

6

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

假设您在互联网和手机银行有一个银行帐户,而您的帐户只有10美元。您使用移动银行向其他帐户执行了转帐余额,同时,您使用同一银行帐户进行了在线购物。如果此银行帐户不是线程安全的,则该银行允许您同时执行两个交易,然后该银行将破产。

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


5

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

如果一个类在从多个线程访问时能正确运行,则该线程是线程安全的,而与运行时环境对这些线程的执行进行调度或交织无关,并且在调用代码部分没有其他同步或其他协调。


4

如果模块保证了它可以在面对多线程和并发使用时保持其不变性,则它是线程安全的。

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

该保证可能会限于某些环境,例如特定的CPU体系结构,但必须对那些环境有效。如果没有明确的环境定界,则通常认为它对所有环境都具有可以编译和执行代码的条件。

线程不安全的模块在多线程和并发使用下可能可以正常运行,但是,与精心设计相比,这往往要靠运气和巧合。即使某个模块不会在您的帮助下损坏,但在移至其他环境时也可能会损坏。

多线程错误通常很难调试。其中一些仅偶尔发生,而其他一些则主动出现-这也可能是特定于环境的。它们可能表现为微妙的错误结果或僵局。它们可能以无法预测的方式弄乱数据结构,并导致其他看似不可能的错误出现在代码的其他远程部分。它可能是非常特定于应用程序的,因此很难给出一般描述。


3

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

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

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

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

并发收集使管理大型数据收集更加容易,并且可以大大减少同步需求。

原子变量具有最大程度地减少同步并有助于避免内存一致性错误的功能。

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

对于其他编程结构,也请参考java.util.concurrentjava.util.concurrent.atomic包。


1

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

WinForms的常规做法是只有一个线程专用于您的所有UI工作。


1

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

例如,我见过将浮点数格式化为字符串的代码,如果其中两个在不同的线程中运行,则可以将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

-2

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

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

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

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

 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是线程安全的,因为它是不可变的。不变值可以自由共享和发布,因此我们在返回它们时不再需要复制位置。

DelegatingVehicleTracker清单4.7中没有使用任何显式同步;所有对状态的访问都由管理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。独立状态变量

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

VisualComponent清单4.9中的图形组件是允许客户端注册鼠标和击键事件的侦听器的图形组件。它维护每种类型的已注册侦听器列表,以便在事件发生时可以调用适当的侦听器。但是,鼠标侦听器和按键侦听器的集合之间没有任何关系。两者是独立的,因此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节)。每个列表都是线程安全的,并且由于没有将一个状态与另一个状态耦合的约束,因此VisualComponent可以将其线程安全职责委派给基础对象mouseListenerskeyListeners对象。

4.3.3。委托失败时

大多数复合类都不像VisualComponent:它们具有将其组件状态变量关联的不变式。NumberRange清单4.10中的示例使用两个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());
    }

}

NumberRange不是线程安全的 ; 它不保留约束上下限的不变式。该setLowersetUpper方法试图尊重这一不变的,但这样做不好。这两个setLowersetUpper是检查当时的行为序列,但他们不使用足够的锁定,使它们的原子。如果数字范围保持(0,10),并且一个线程调用,setLower(5)而另一个线程调用setUpper(4),则在一些不幸的时间安排下,两个线程都将通过设置器中的检查,并且将应用两个修改。结果是该范围现在保持(5,4)- 无效状态。因此,虽然底层的AtomicIntegers是线程安全的,但复合类不是。因为基础状态变量lowerupper它们不是独立的,NumberRange不能简单地将线程安全委托给其线程安全状态变量。

NumberRange通过使用锁来保持其不变性,例如使用通用锁保护下部和上部,可以使线程安全。它还必须避免上下发布,以防止客户端颠覆其不变式。

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.