“线程安全”一词是什么意思?

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

226713 次浏览

线程安全代码是指即使有多个线程同时执行也能正常工作的代码。

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

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

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

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

例如,Java有两个几乎相同的类,StringBufferStringBuilder。不同之处在于StringBuffer是线程安全的,因此StringBuffer的单个实例可以同时被多个线程使用。StringBuilder不是线程安全的,当String仅由一个线程构建时,它被设计为更高性能的替代品。

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

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

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

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

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

完成其他回答:

只有当方法中的代码做以下两件事之一时,同步才会令人担忧:

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

这意味着在方法中定义的变量总是线程安全的。对方法的每次调用都有自己版本的这些变量。如果方法是由另一个线程调用的,或者是由同一线程调用的,甚至是方法调用自身(递归),这些变量的值是不共享的。

线程调度不保证是round-robin . bref ="http://en.wikipedia.org/wiki/Round_robin" rel="noreferrer">round-robin。一个任务可能会以牺牲相同优先级的线程为代价完全占用CPU。你可以使用Thread.yield()来获得良心。你可以使用(java) thread . setpriority (thread . norm_priority -1)来降低线程的优先级

另外还要注意:

  • 迭代这些“线程安全”结构的应用程序的巨大运行时成本(已经被其他人提到)。
  • Thread.sleep(5000)应该休眠5秒。但是,如果有人更改了系统时间,您可能会睡很长时间或根本没有时间。操作系统记录唤醒时间是绝对的,而不是相对的。

是也不是。

线程安全不仅仅是确保共享数据一次只能被一个线程访问。你必须确保对共享数据的顺序访问,同时避免竞态条件死锁活动锁资源的饥饿

当多个线程运行时,不可预知的结果是线程安全代码的必要条件,但它通常是一个副产品。例如,你可以将生产国和消费国方案设置为一个共享队列、一个生产者线程和几个消费者线程,并且数据流可能是完全可预测的。如果你开始引入更多的消费者,你会看到更多随机的结果。

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

  • 不锁定访问共享变量
    该变量可以由另一个线程在执行函数时修改。你想用锁定机制来防止它,以确保你的函数的行为。一般的经验法则是保持锁尽可能短的时间

  • 共享变量相互依赖导致死锁
    如果你有两个共享变量A和B,在一个函数中,你先锁定A,然后再锁定B。在另一个函数中,你开始锁定B,过一会儿,你锁定A。这是一个潜在的死锁,当第二个函数等待A被解锁时,第一个函数将等待B被解锁。这个问题可能不会出现在您的开发环境中,只是偶尔出现。为了避免这种情况,所有的锁必须始终处于相同的顺序

我喜欢Brian Goetz的Java并发实践中的定义,因为它的全面性

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

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

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. 第二个条件是存在一个属性(通常称为不变的),它与这些共享内存位置相关联,该属性必须为真,或有效,以便程序正确运行。在上面的例子中,属性是" totalRequests必须准确地表示任意线程执行增量语句的任何部分的总次数 "。通常,这个不变属性需要在更新发生之前保持为真(在本例中,totalRequests必须保持准确的计数),以便更新是正确的。
  3. 第三个条件是不变属性在实际更新的某些部分不保持。(在处理的某些部分期间,它暂时无效或为假)。在这个特殊的情况下,从totalRequests被获取到更新的值被存储,totalRequests 满足不变量。
  4. 第四个也是最后一个发生竞争的条件(因此代码<强> < / >强不是“线程安全的”)是另一个线程必须能够访问共享内存,不变量被破坏,从而导致不一致或不正确的行为。

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

我喜欢Java并发性实践给出的定义:

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

通过正确,他们的意思是程序的行为符合其规范。

的例子

假设您实现了一个计数器。你可以说它的行为是正确的,如果:

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

线程安全计数器将根据这些规则进行操作,而不管并发有多少线程访问它(通常不是简单实现的情况)。

注:cross-post on Programmers

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

*适用条件

条件由其他答案提到,如 1. 如果你执行一个线程或多个线程,结果应该是相同的,等等

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

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

看看这个SE问题,了解更多细节:

threadsafe是什么意思?< / >

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

从oracle文档页面高级并发API:

内存一致性属性:

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

synchronizedvolatile构造,以及Thread.start()Thread.join()方法,可以形成之前关系。

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

  1. 在将对象放入任何并发集合之前,线程中的操作发生在另一个线程中从集合中访问或删除该元素之后的操作之前。
  2. Runnable提交给Executor之前,线程中的动作发生在它开始执行之前。类似地,提交给ExecutorService的Callables。
  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(及其变体)之前的操作发生在barrier操作执行的操作之前,以及barrier操作执行的操作发生在其他线程从相应的await成功返回之后的操作之前。

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

class NonThreadSafe {


private int count = 0;


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

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

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

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

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

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

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

至少在c++中,我认为线程安全的有点用词不当,因为它在名称中遗漏了很多内容。为了线程安全,代码通常必须是积极主动的。这通常不是一种被动的品质。

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

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

术语集

我遇到的这些概念主要有三组术语。第一个,历史上更受欢迎(但最糟糕)的是:

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

第二个(更好的)是:

  1. 线程的证据
  2. 线程兼容
  3. 线程敌意

第三个(甚至更好)是:

  1. 内部同步
  2. 外部同步
  3. unsynchronizable

类比

__abc0 ~ __abc1 ~ __abc2

内部同步(又名。线程安全的线程的证据)系统是这样的餐厅,主人在门口迎接你,并且不允许你自己排队。主人是餐馆处理多名顾客的机制的一部分,可以使用一些相当棘手的技巧来优化等待顾客的座位,比如考虑他们的聚会规模,或者他们看起来有多少时间,甚至通过电话预订。餐厅内部是同步的,因为所有这些都包括在“幕后”。当你与它互动时。你,作为顾客,什么都不用做。主人为你做了所有的事情。

非线程安全(但是不错)~ 线程兼容 ~ 外部同步 ~ 自由线程

假设你去银行。有一条线,即争夺银行出纳员。因为你不是野蛮人,所以你知道在争夺资源的过程中,最好的办法就是像文明人一样排队。严格来说没人逼你这么做。我们希望你有必要的社会规划来自己做这件事。在这个意义上,银行大厅是外部同步。

我们是否可以说它是线程不安全的?这就是你使用线程安全的线程不安全的双极术语集的含义。这不是一个很好的术语集合。更好的术语是外部同步,银行大厅对多个客户访问并不敌对,但它也不做同步他们的工作。这些都是客户自己做的。

这也被称为“自由螺纹,”;在“free"就像“免受奴役”一样——或者在这种情况下,是锁。更准确地说,是同步原语。这并不意味着代码可以在没有这些原语的情况下在多个线程上运行。这只是意味着它不附带它们已经安装,这取决于您,代码的用户,以您认为合适的方式自己安装它们。安装自己的同步原语可能很困难,需要认真考虑代码,但也可以通过允许您自定义程序在当今超线程cpu上的执行方式来实现尽可能快的程序。

不线程安全(和坏)~ 线程敌意 ~ unsynchronizable

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

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

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

注意:许多软件手册实际上使用术语“线程安全”。提到“线程兼容”,“线程兼容”;在已经一团糟的情况下增加更多的混乱!我避免使用“线程安全”这个词。和“;thread-unsafe"为了这个原因,不惜一切代价,因为一些资源将某些东西称为“线程安全”;而其他人则称之为“线程不安全”;因为他们无法就你是否必须满足一些额外的安全标准(预安装的同步原语)达成一致,或者只是对“安全”不抱敌意。因此,避免使用这些术语,而是使用更聪明的术语,以避免与其他工程师产生危险的误解。

提醒我们的目标

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

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

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

最高级(thread-proof等)意味着即使从多个线程草率地调用它,系统也以可预测的方式运行。它自己做了必要的工作来实现这个目标,所以你不需要这样做。它为编写调用代码的程序员提供了一个很好的接口,这样您就可以假装生活在一个没有同步原语的世界中。因为它已经在内部包含了它们。当涉及到由于它正在进行的同步而需要多长时间来完成任务时,它也很昂贵、缓慢,而且有些不可预测,因为它不知道你的代码将做什么,所以它必须总是大于你特定程序所需的时间。非常适合那些用各种脚本语言编写代码来进行科学研究或其他工作的临时程序员,但他们自己并没有编写高效的接近金属的代码。他们不需要变戏法。

第二个程度(thread-compatible等)意味着系统的行为足够好,以至于调用代码能够可靠地及时检测到不可预测性,并在运行时使用自己安装的同步原语正确地处理它。D-I-Y同步。BYOSP =自带同步原语。至少你知道你调用的代码会很好地处理它们。这是为接近金属的专业程序员准备的。

第三度(thread-hostile等)意味着系统不能很好地与其他任何人一起运行,并且只能在不引起混乱的情况下单线程运行。本质上,这是90年代早期的经典代码。它的编程缺乏对如何在多线程中调用或使用它的意识,以至于即使您尝试自己添加这些同步原语,它也无法工作,因为它做出了过时的假设,这些假设在今天看起来是反社会和不专业的。

然而,有些代码只有被称为单线程才真正有意义,所以仍然被故意写成这样调用。对于那些已经拥有高效的管道和内存访问序列的软件来说尤其如此,并且不能从多线程的主要目的中受益:隐藏内存访问延迟。访问非缓存内存比大多数其他指令慢得可笑。因此,当应用程序在等待内存访问时,它应该同时切换到另一个任务线程以保持处理器工作。当然,这些天,这可能意味着切换到另一个协程/光纤/等等。在同一线程中,如果可用,因为这些比线程上下文切换更有效。但是,一旦这些线程暂时耗尽,就该切换内核上执行的线程了。

但有时,你已经很好地打包和排序了所有的内存访问,你最不想做的就是切换到另一个线程,因为你已经流水线化了你的代码来尽可能有效地处理这个问题。那么,丝线不会带来伤害。这是一个例子,但还有其他例子。

一般来说,我认为在编程要调用的代码时,尽可能使用thread-compatible是有意义的,特别是如果没有真正的理由不这样做,并且它只是需要你在编码时意识到这一点。

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

例如,许多像数组列表和散列集这样的集合可以保证,如果它们最初只由一个线程访问,并且在引用对任何其他线程可见后永远不会被修改,那么它们可以被任何线程组合以任意方式读取而不受干扰。

更有趣的是,一些哈希集集合,比如。net中原始的非泛型集合,可以提供一种保证,只要没有任何项被删除,并且只有一个线程写入它们,任何试图读取集合的线程都将像访问一个更新可能被延迟并以任意顺序发生的集合一样,但在其他情况下将表现正常。如果线程#1添加了X和Y,线程#2寻找并看到Y和X,线程#2可能会看到Y存在而X不存在;这种行为是否“线程安全”取决于线程#2是否准备好处理这种可能性。

最后需要注意的是,一些类——特别是阻塞通信库——可能具有相对于所有其他方法是线程安全的“close”或“Dispose”方法,但没有其他方法彼此是线程安全的。如果一个线程执行一个阻塞读请求,而程序的用户单击“取消”,那么试图执行读的线程将无法发出关闭请求。然而,close/dispose请求可以异步设置一个标志,使读请求尽快被取消。一旦在任何线程上执行了close,对象将变得无用,并且所有对未来操作的尝试都将立即失败,但是能够异步终止任何尝试的I/O操作比要求关闭请求与读同步要好(因为如果读永远阻塞,同步请求也将同样被阻塞)。