424

这是否意味着两个线程不能同时更改底层数据?或者这是否意味着当多个线程正在执行该代码段时,给定的代码段将以可预测的结果运行?

4

17 回答 17

97

线程安全代码是即使许多线程同时执行它也能工作的代码。

http://mindprod.com/jgloss/threadsafe.html

于 2008-11-04T12:17:28.503 回答
64

一个更具信息性的问题是什么使代码不是线程安全的 - 答案是有四个条件必须为真......想象一下下面的代码(它是机器语言翻译)

totalRequests = totalRequests + 1
MOV EAX, [totalRequests]   // load memory for tot Requests into register
INC EAX                    // update register
MOV [totalRequests], EAX   // store updated value back to memory
  1. 第一个条件是可以从多个线程访问的内存位置。通常,这些位置是全局/静态变量,或者是可从全局/静态变量访问的堆内存。每个线程都为函数/方法范围的局部变量获取它自己的堆栈帧,因此这些局部函数/方法变量 otoh(位于堆栈上)只能从拥有该堆栈的一个线程访问。
  2. 第二个条件是存在与这些共享内存位置相关联的属性(通常称为不变量),该属性必须为真或有效,程序才能正常运行。在上面的示例中,属性是“<em>totalRequests 必须准确地表示任何线程执行增量语句的任何部分的总次数”。通常,此不变属性需要在更新发生之前保持为真(在这种情况下,totalRequests 必须保持准确的计数)才能使更新正确。
  3. 第三个条件是不变量属性在实际更新的某些部分不成立。(它在处理的某些部分暂时无效或错误)。在这种特殊情况下,从获取 totalRequests 到存储更新值的时间,totalRequests不满足变量。
  4. 竞争发生必须发生的第四个也是最后一个条件(因此代码不是“线程安全的”)是另一个线程必须能够在不变量被破坏访问共享内存,从而导致不一致或不正确的行为。
于 2008-11-04T16:59:08.840 回答
39

我喜欢 Brian Goetz 的 Java Concurrency in Practice 中的定义,因为它的全面性

“如果一个类在从多个线程访问时行为正确,则该类是线程安全的,无论运行时环境对这些线程执行的调度或交错如何,并且调用代码部分没有额外的同步或其他协调。 "

于 2008-11-04T14:32:11.663 回答
33

正如其他人指出的那样,线程安全意味着如果一段代码同时被多个线程使用,则它可以正常工作。

值得注意的是,这有时会以计算机时间和更复杂的编码为代价,因此并不总是可取的。如果一个类只能在一个线程上安全地使用,那么这样做可能会更好。

例如,Java 有两个几乎等价的类,StringBufferStringBuilder. 不同之处在于它是线程安全的,因此多个线程可以同时使用StringBuffera 的单个实例。不是线程安全的,并且被设计为当 String 仅由一个线程构建时的那些情况(绝大多数)的更高性能替代品。StringBufferStringBuilder

于 2008-11-04T12:30:25.933 回答
32

一种更容易理解的方法是使代码不是线程安全的。有两个主要问题会使线程应用程序具有不需要的行为。

  • 在不锁定的情况下访问共享变量
    此变量可以在执行函数时被另一个线程修改。您想通过锁定机制来防止它,以确保您的函数的行为。一般的经验法则是尽可能缩短锁定时间。

  • 由共享变量相互依赖引起的死锁
    如果你有两个共享变量 A 和 B。在一个函数中,你先锁定 A,然后再锁定 B。在另一个函数中,你开始锁定 B,一段时间后,你锁定 A。这是一个潜在的死锁,其中第一个函数将等待 B 被解锁,而第二个函数将等待 A 被解锁。此问题可能不会在您的开发环境中发生,并且只会不时发生。为避免这种情况,所有锁必须始终保持相同的顺序。

于 2008-11-04T13:46:37.137 回答
23

线程安全代码按规定工作,即使不同线程同时输入。这通常意味着,应该不间断地运行的内部数据结构或操作同时受到不同修改的保护。

于 2008-11-04T12:17:29.017 回答
15

至少在 C++ 中,我认为线程安全有点用词不当,因为它留下了很多名字。为了线程安全,代码通常必须主动处理它。这通常不是一种被动的品质。

对于线程安全的类,它必须具有增加开销的“额外”功能。这些特性是类实现的一部分,一般来说,对接口是隐藏的。也就是说,不同的线程可以访问任何类的成员,而不必担心与不同线程的并发访问发生冲突,并且可以以非常懒惰的方式这样做,使用一些普通的老式常规人类编码风格,而不必做所有已经卷入被调用代码内部的疯狂同步工作。

这就是为什么有些人更喜欢使用内部同步这个术语。

术语集

我遇到的这些想法主要有三组术语。第一个也是历史上更受欢迎(但最差)的是:

  1. 线程安全
  2. 不是线程安全的

第二个(更好的)是:

  1. 螺纹证明
  2. 线程兼容
  3. 线程敌对

第三个是(甚至更好)一个是:

  1. 内部同步
  2. 外部同步
  3. 不可同步

类比

线程安全~线程证明~内部同步

内部同步(又名线程安全线程证明)系统的一个示例是餐厅,主人在门口迎接您,并禁止您自己排队。主人是餐厅处理多个顾客的机制的一部分,并且可以使用一些相当棘手的技巧来优化等待顾客的座位,比如考虑他们的聚会规模,或者他们看起来有多少时间,甚至通过电话进行预订。餐厅是内部同步的,因为当您与之互动时,所有这些都包含在“幕后”。你,客户,不要做任何事情。房东会为你做这一切。

不是线程安全的(但很好)~线程兼容~外部同步~自由线程

假设你去银行。有一条线,即银行柜员的竞争。因为你不是野蛮人,所以你认识到在争夺资源时最好的办法就是像文明人一样排队。没有人在技术上让你这样做。我们希望你有必要的社交编程来自己做这件事。从这个意义上说,银行大厅是外部同步的。

我们应该说它是线程不安全的吗?如果您使用线程安全线程不安全的双极术语集,这就是含义。这不是一组很好的术语。更好的术语是外部同步,银行大厅对被多个客户访问并不怀有敌意,但它也不做同步他们的工作。客户自己这样做。

这也称为“free threaded”,其中“free”与“free from lice”一样——或者在这种情况下是锁。好吧,更准确地说,是同步原语。这并不意味着代码可以在没有这些原语的情况下在多个线程上运行。这只是意味着它没有随它们一起安装,它取决于你,代码的用户,你自己安装它们,但你认为合适。安装您自己的同步原语可能很困难,并且需要仔细考虑代码,但也可以通过允许您自定义程序在当今超线程 CPU 上的执行方式来实现最快的程序。

不是线程安全的(而且不好)~线程敌对~不可同步

一个线程敌对系统的日常类比示例是一个混蛋,一辆跑车拒绝使用他们的闪光灯并随意改变车道。他们的驾驶风格是线程敌对不可同步的,因为您无法与他们协调,这可能导致争用同一车道,没有解决方案,因此当两辆车试图占据同一空间时发生事故,没有任何协议防止这种情况。这种模式也可以更广泛地被认为是反社会的,尽管它不是特定于线程,而是更普遍地适用于编程的许多领域。

为什么线程安全/非线程安全是一个不好的术语集

第一个也是最古老的术语集未能在线程敌意线程兼容性之间做出更精细的区分。线程兼容性比所谓的线程安全更被动,但这并不意味着被调用的代码对于并发线程使用是不安全的。这只是意味着它对允许这样做的同步是被动的,将其推迟到调用代码中,而不是将其作为其内部实现的一部分提供。在大多数情况下,线程兼容是默认编写代码的方式,但遗憾的是,这也经常被错误地认为是线程不安全的,好像它本质上是反安全的,这对程序员来说是一个主要的困惑点。

注意:许多软件手册实际上使用术语“线程安全”来指代“线程兼容”,这给已经一团糟的内容增添了更多混乱!出于这个原因,我不惜一切代价避免使用术语“线程安全”和“线程不安全”,因为一些消息来源会称其为“线程安全”,而另一些则称其为“线程不安全”,因为他们不能同意关于您是否必须满足一些额外的安全标准(预安装的同步原语),或者只是不敌对才能被认为是“安全的”。因此,请避免使用这些术语并改用更智能的术语,以避免与其他工程师发生危险的错误沟通。

提醒我们的目标

本质上,我们的目标是颠覆混乱。

我们通过创建我们可以依赖的半确定性系统来做到这一点。确定性是昂贵的,主要是由于失去并行性、流水线和重新排序的机会成本。我们试图最大限度地减少我们需要的确定性,以保持低成本,同时避免做出会进一步削弱我们能够承受的微不足道的确定性的决策。因此,半前缀。我们只希望代码状态的某些小部分是确定性的,而底层的计算机制不必完全如此。线程的同步是关于增加多线程系统中的顺序并减少混乱,因为拥有多个线程自然会导致大量的非确定性,必须以某种方式抑制这种不确定性。

总而言之,一些代码体可以在三个主要程度的努力上投入“杂耍刀”——即在多线程的上下文中正确工作。

最高程度(线程证明等)意味着即使您从多个线程中草率地调用它,系统也会以可预测的方式运行。它自己完成了实现这一目标所需的工作,因此您不必这样做。它为您(编写调用代码的程序员)提供了这个漂亮的界面,这样您就可以假装生活在一个没有同步原语的世界中。因为它已经在内部包含了它们。它也很昂贵,速度很慢,而且由于它正在执行同步而需要多长时间来完成任务,这也有点不可预测,这必须始终大于您特定程序所需的数量,因为它不知道您的代码会做。非常适合使用各种脚本语言进行科学或其他事情的临时编码人员,但他们自己并没有编写高效的接近金属的代码。他们不需要杂耍刀。

第二级(线程兼容等)意味着系统表现得足够好,调用代码可以及时可靠地检测到不可预测性,以便在运行时使用自己安装的同步原语正确处理它。DIY同步。BYOSP = 自带同步原语。至少你知道你调用的代码会很好地适应它们。这适用于更接近金属的专业程序员。

第三级(线程敌对等)意味着系统表现得不够好,无法与其他任何人一起玩,并且只能单线程运行而不会引起混乱。本质上,这是经典的 90 年代早期和更早的代码。它的编程缺乏对如何从多个线程调用或使用它的高度意识,以至于即使您尝试自己添加这些同步原语,它也无法正常工作,因为它做出了老式的假设,即这些日子似乎反社会和不专业。

但是,某些代码只有在称为单线程时才真正有意义,因此仍会故意以这种方式调用。对于已经具有高效流水线和内存访问序列的软件尤其如此,并且没有受益于多线程的主要目的:隐藏内存访问延迟。访问非高速缓存内存比大多数其他指令慢得多。因此,每当应用程序等待一些内存访问时,它应该同时切换到另一个任务线程以保持处理器工作。当然,这些天来,这可能意味着切换到另一个协程/光纤/等。在同一个线程中,如果可用,因为它们比线程上下文切换更有效。但是一旦暂时用尽了那些,它'

但有时,您的所有内存访问都被很好地打包和排序,而您最不想做的就是切换到另一个线程,因为您已经将代码流水线化以尽可能有效地处理这个问题。然后线程受伤无济于事。这是一个例子,但还有其他例子。

总的来说,我认为在编写要调用的代码时尽可能使用线程兼容是有意义的,特别是如果没有真正的理由不这样做,而且它只需要您在编写代码时的意识。

于 2019-10-26T19:28:14.617 回答
9

不要将线程安全与确定性混淆。线程安全代码也可以是非确定性的。鉴于调试线程代码问题的难度,这可能是正常情况。:-)

线程安全只是确保当一个线程正在修改或读取共享数据时,没有其他线程可以以更改数据的方式访问它。如果您的代码依赖于特定的执行顺序以确保正确性,那么您需要除了线程安全所需的其他同步机制来确保这一点。

于 2008-11-04T12:31:51.767 回答
9

简单地说 - 如果许多线程同时执行此代码,代码将运行良好。

于 2008-11-04T12:35:14.083 回答
9

从本质上讲,在多线程环境中,很多事情都可能出错(指令重新排序、部分构造的对象、由于 CPU 级别的缓存,相同的变量在不同的线程中具有不同的值等)。

我喜欢Java Concurrency in Practice给出的定义:

如果一个[代码部分]在从多个线程访问时行为正确,则它是线程安全的,而不管运行时环境对这些线程执行的调度或交错,并且没有额外的同步或其他协调调用代码。

正确的意思是程序的行为符合其规范。

人为的例子

想象一下,您实现了一个计数器。您可以说它在以下情况下表现正确:

  • counter.next()从不返回之前已经返回的值(为简单起见,我们假设没有溢出等)
  • 从 0 到当前值的所有值都已在某个阶段返回(没有跳过任何值)

线程安全计数器将根据这些规则运行,无论有多少线程同时访问它(这通常不是简单实现的情况)。

注意:程序员的交叉帖子

于 2013-02-07T10:57:14.097 回答
9

是和不是。

线程安全不仅仅是确保您的共享数据一次只能由一个线程访问。您必须确保对共享数据的顺序访问,同时避免竞争条件死锁活锁资源匮乏

多线程运行时的不可预测结果不是线程安全代码的必需条件,但它通常是副产品。例如,您可以设置一个生产者-消费者方案,其中包含一个共享队列、一个生产者线程和几个消费者线程,并且数据流可能是完全可预测的。如果您开始介绍更多的消费者,您会看到更多随机的结果。

于 2008-11-04T12:59:36.733 回答
7

让我们通过例子来回答这个问题:

class NonThreadSafe {

    private int count = 0;

    public boolean countTo10() {
        count = count + 1;
        return (count == 10);
    }

countTo10方法将计数器加一,然后如果计数达到 10,则返回 true。它应该只返回 true 一次。

只要只有一个线程在运行代码,这将起作用。如果两个线程同时运行代码,可能会出现各种问题。

例如,如果 count 从 9 开始,一个线程可以将 1 加到 count (使 10),但随后第二个线程可以进入该方法并在第一个线程有机会执行与 10 的比较之前再次加 1(使 11) . 然后两个线程都做比较,发现count都是11,都没有返回true。

所以这段代码不是线程安全的。

本质上,所有的多线程问题都是由这类问题的某种变体引起的。

解决方案是确保加法和比较不能分开(例如,通过某种同步代码将两条语句包围起来)或设计一种不需要两次操作的解决方案。这样的代码将是线程安全的。

于 2019-05-10T09:07:03.547 回答
6

我想在其他好的答案之上添加更多信息。

线程安全意味着多个线程可以在同一个对象中写入/读取数据而不会出现内存不一致错误。在高度多线程的程序中,线程安全的程序不会对共享数据造成副作用

查看此 SE 问题以了解更多详细信息:

线程安全是什么意思?

线程安全程序保证内存一致性

来自高级并发 API 的oracle 文档页面:

内存一致性属性:

Java™ 语言规范的第 17 章定义了内存操作(例如共享变量的读取和写入)的发生前关系。只有当写操作发生在读操作之前,一个线程写的结果才能保证对另一个线程的读可见

synchronizedandvolatile结构以及Thread.start()andThread.join()方法可以形成先发生的关系。

所有类java.util.concurrent及其子包中的方法都将这些保证扩展到更高级别的同步。尤其是:

  1. 在将对象放入任何并发集合之前的线程中的操作发生在另一个线程中从集合中访问或删除该元素之后的操作。
  2. 线程中的操作在提交RunnableExecutor发生之前执行之前。对于提交给ExecutorService.
  3. 在通过另一个线程Future检索结果之后,异步计算所采取的操作由发生前的操作表示。Future.get()
  4. 在“释放”同步器方法之前的操作,例如Lock.unlock, Semaphore.release, and CountDownLatch.countDown在成功“获取”方法之后的操作之前发生的操作,例如Lock.lock, Semaphore.acquire, Condition.await, and CountDownLatch.await在另一个线程中的相同同步器对象上。
  5. 对于通过 成功交换对象的每对线程,每个线程中的 之前Exchanger的动作exchange()发生在另一个线程中对应的 exchange() 之后的动作之前。
  6. 调用之前的动作CyclicBarrier.awaitPhaser.awaitAdvance(及其变体)由屏障动作执行的动作发生之前,以及屏障动作执行的动作发生在从其他线程中的相应等待成功返回之后的动作。
于 2016-05-20T17:42:20.883 回答
5

要完成其他答案:

仅当您的方法中的代码执行以下两种操作之一时,同步才令人担忧:

  1. 与一些不是线程安全的外部资源一起使用。
  2. 读取或更改持久对象或类字段

这意味着在您的方法中定义的变量始终是线程安全的。对方法的每次调用都有自己的这些变量版本。如果该方法被另一个线程调用,或者被同一个线程调用,或者即使该方法调用自身(递归),这些变量的值也不会共享。

线程调度不保证是循环的。一个任务可能以相同优先级的线程为代价完全占用 CPU。你可以使用 Thread.yield() 有良心。您可以使用(在 java 中)Thread.setPriority(Thread.NORM_PRIORITY-1) 来降低线程的优先级

另外要注意:

  • 迭代这些“线程安全”结构的应用程序的大量运行时成本(其他人已经提到过)。
  • Thread.sleep(5000) 应该休眠 5 秒。但是,如果有人更改系统时间,您可能会睡很长时间或根本没有时间。操作系统以绝对形式记录唤醒时间,而不是相对形式。
于 2008-11-04T12:41:38.127 回答
2

是的,是的。这意味着数据不会被多个线程同时修改。但是,您的程序可能会按预期工作,并且看起来是线程安全的,即使它根本不是。

请注意,结果的不可预测性是“竞争条件”的结果,这可能导致数据以不同于预期的顺序被修改。

于 2008-11-04T12:36:15.500 回答
1

与其将代码视为线程安全与否,我认为将动作视为线程安全更有帮助。如果两个动作在从任意线程上下文运行时按照指定的方式运行,则它们是线程安全的。在许多情况下,类将支持线程安全方式的某些操作组合,而其他则不支持。

例如,许多集合(如数组列表和哈希集)将保证,如果它们最初仅由一个线程访问,并且在引用对任何其他线程可见后它们永远不会被修改,则它们可以通过任何组合以任意方式读取线程无干扰。

更有趣的是,一些哈希集集合,例如 .NET 中的原始非泛型集合,可以保证只要没有项目被删除,并且只要一个线程向它们写入,任何试图读取集合的行为就像访问一个集合,其中更新可能会延迟并以任意顺序发生,但否则会正常运行。如果线程#1 添加X,然后添加Y,线程#2 查找并看到Y,然后是X,线程#2 可能会看到Y 存在但X 不存在;这种行为是否是“线程安全的”取决于线程#2 是否准备好处理这种可能性。

最后一点,一些类——尤其是阻塞通信库——可能有一个“close”或“Dispose”方法,相对于所有其他方法是线程安全的,但没有其他方法是线程安全的彼此。如果线程执行阻塞读取请求并且程序的用户单击“取消”,则尝试执行读取的线程将无法发出关闭请求。然而,关闭/释放请求可能会异步设置一个标志,这将导致读取请求尽快被取消。一旦在任何线程上执行关闭,该对象将变得无用,并且所有未来操作的尝试都会立即失败,

于 2020-05-22T15:19:44.420 回答
0

用最简单的话来说:P 如果在一个代码块上执行多个线程是安全的,那么它就是线程安全的*

*适用条件

其他答案(例如1)提到了条件。如果您在其上执行一个线程或多个线程等,结果应该是相同的。

于 2013-12-15T09:19:52.143 回答