Var*_*jan 339 language-agnostic concurrency multithreading programming-languages
这是否意味着两个线程不能同时更改基础数据?或者它是否意味着当多个线程运行时,给定的代码段将以可预测的结果运行?
Bla*_*ohr 252
来自维基百科:
线程安全是适用于多线程程序的计算机编程概念.如果一段代码在多个线程同时执行期间正常运行,则它是线程安全的.特别是,它必须满足多线程访问相同共享数据的需要,并且在任何给定时间只需要一个线程访问共享数据块.
有几种方法可以实现线程安全:
重入:
以这样的方式编写代码,使其可以由一个任务部分执行,由另一个任务重新输入,然后从原始任务恢复.这需要将状态信息保存在每个任务的本地变量中,通常在其堆栈中,而不是静态或全局变量中.
相互排斥:
使用确保只有一个线程可以随时读取或写入共享数据的机制来序列化对共享数据的访问.如果一段代码访问多个共享数据片段,则需要非常小心 - 问题包括竞争条件,死锁,活锁,饥饿以及许多操作系统教科书中列举的各种其他问题.
线程本地存储:
变量已本地化,因此每个线程都有自己的私有副本.这些变量在子例程和其他代码边界中保留其值,并且是线程安全的,因为它们对于每个线程都是本地的,即使访问它们的代码可能是可重入的.
原子操作:
通过使用不能被其他线程中断的原子操作来访问共享数据.这通常需要使用特殊的机器语言指令,这些指令可能在运行时库中可用.由于操作是原子操作,因此无论其他线程访问它,共享数据始终保持有效状态.原子操作构成了许多线程锁定机制的基础.
阅读更多:
http://en.wikipedia.org/wiki/Thread_safety
Mar*_*tny 79
线程安全代码是即使许多线程同时执行它也能工作的代码.
http://mindprod.com/jgloss/threadsafe.html
Cha*_*ana 49
一个更具信息性的问题是什么使代码不是线程安全的 - 答案是有四个条件必须是真的...想象一下下面的代码(和它的机器语言翻译)
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
Run Code Online (Sandbox Code Playgroud)
Buu*_*yen 34
我喜欢Brian Goetz的Java Concurrency in Practice中的定义,因为它的全面性
"如果一个类在从多个线程访问时行为正确,则无论运行时环境对这些线程的执行进行调度或交错,并且调用代码没有额外的同步或其他协调,它都是线程安全的. "
Mar*_*ing 27
正如其他人所指出的那样,线程安全意味着如果一个代码一次被多个线程使用,那么一段代码就可以正常工作.
值得注意的是,这有时会带来成本,计算机时间和更复杂的编码,因此并不总是令人满意.如果一个类只能在一个线程上安全使用,那么这样做可能会更好.
例如,Java有两个几乎相同的类,StringBuffer和StringBuilder.不同之处在于它StringBuffer是线程安全的,因此StringBuffer多个线程可以同时使用a的单个实例.StringBuilder它不是线程安全的,并且当String仅由一个线程构建时,被设计为更高性能的替换那些情况(绝大多数).
Hap*_*ido 21
理解它的一种更简单的方法是使代码不是线程安全的.有两个主要问题会导致线程应用程序出现不需要的行为.
无锁定地访问共享变量
执行该函数时,另一个线程可以修改此变量.您希望使用锁定机制来阻止它,以确保您的函数的行为.一般的经验法则是尽可能在最短的时间内保持锁定.
共享变量相互依赖造成的死锁
如果你有两个共享变量A和B.在一个函数中,你先锁定A然后再锁定B.在另一个函数中,你开始锁定B,一段时间后,你锁定A.是一个潜在的死锁,当第二个函数等待A解锁时,第一个函数将等待B解锁.此问题可能不会在您的开发环境中发生,并且只会不时发生.为避免这种情况,所有锁必须始终处于相同的顺序.
实质上,在多线程环境中很多事情都可能出错(指令重新排序,部分构造的对象,由于CPU级别的缓存等,在不同线程中具有不同值的相同变量等).
我喜欢Java Concurrency in Practice提供的定义:
如果代码的一部分在从多个线程访问时行为正确,则它是线程安全的,无论运行时环境是否调度或交错执行这些线程,并且没有额外的同步或其他协调部分.调用代码.
通过正确,它们意味着程序的行为符合其规范.
举例说明
想象一下,你实现了一个计数器.如果出现以下情况,您可以说它的行为正确:
counter.next() 永远不会返回之前已经返回的值(为简单起见,我们假设没有溢出等)线程安全计数器将根据这些规则运行,无论有多少线程同时访问它(通常不是天真实现的情况).
注意:在程序员上交叉发布
至少在 C++ 中,我认为线程安全有点用词不当,因为它在名称之外留下了很多东西。为了线程安全,代码通常必须积极主动。这通常不是一种被动的品质。
要使类成为线程安全的,它必须具有增加开销的“额外”功能。这些特性是类实现的一部分,一般来说,对接口隐藏。也就是说,不同的线程可以访问类的任何成员,而不必担心与不同线程的并发访问发生冲突,并且可以以非常懒惰的方式这样做,使用一些普通的常规人类编码风格,而不必做所有那些已经融入被调用代码的疯狂同步的东西。
这就是为什么有些人更喜欢使用内部同步这个术语的原因。
我遇到的这些想法有三组主要术语。第一个也是历史上更受欢迎(但最糟糕)的是:
第二个(更好)是:
第三个是(甚至更好)一个是:
线程安全~线程证明~内部同步
内部同步(又名线程安全或线程证明)的示例)系统的是餐厅,主人在门口迎接你,不允许你自己排队。主人是餐厅处理多个顾客的机制的一部分,可以使用一些相当棘手的技巧来优化等待顾客的座位,比如考虑他们聚会的规模,或者他们看起来有多少时间,甚至可以通过电话进行预订。餐厅是内部同步的,因为当您与其互动时,所有这些都包含在“幕后”中。你,客户,不要做任何事情。主人为您做所有这一切。
不是线程安全的(但很好)~线程兼容~外部同步~自由线程
假设你去银行。有一条线,即银行出纳员的争用。因为您不是野蛮人,所以您认识到,在争夺资源的过程中,最好的做法是像文明人一样排队。从技术上讲,没有人让你这样做。我们希望您拥有必要的社交程序来自己完成。从这个意义上说,银行大厅是外部同步的。
我们应该说它是线程不安全的吗?如果您使用线程安全、线程不安全的双极术语集,这就是含义。这不是一套很好的术语。更好的术语是外部同步,银行大厅对被多个客户访问没有敌意,但它也不做同步它们的工作。客户自己做。
这也称为“自由线程”,其中“自由”就像“免于虱子”一样——或者在这种情况下,是锁。好吧,更准确地说,是同步原语。这并不意味着代码可以在没有这些原语的情况下在多个线程上运行。这只是意味着它并没有随它们一起安装,而是由您(代码的用户)自行安装,但您认为合适。安装您自己的同步原语可能很困难,需要仔细考虑代码,但也可以通过允许您自定义程序在当今超线程 CPU 上的执行方式来获得尽可能快的程序。
不是线程安全的(和坏的)~线程敌对~不可同步
线程敌对系统的一个日常类比示例是跑车拒绝使用闪光灯和随意改变车道的一些混蛋。他们的驾驶风格是线程敌对或不同步的,因为你无法与他们协调,这可能导致同一车道的争用,无法解决,从而导致两辆车试图占据同一空间而没有任何协议的事故防止这种情况。这种模式也可以更广泛地被认为是反社会的,尽管它不是针对线程的,而是更普遍地适用于许多编程领域。
第一个和最古老的术语集未能更好地区分线程敌意和线程兼容性。线程兼容性比所谓的线程安全更被动,但这并不意味着被调用的代码对于并发线程使用是不安全的。这只是意味着它对允许这样做的同步是被动的,将其推迟到调用代码中,而不是将其作为其内部实现的一部分提供。线程兼容是在大多数情况下默认情况下应该如何编写代码的方式,但遗憾的是,这也经常被错误地认为是线程不安全的,好像它本质上是反安全的,这是程序员的一个主要困惑点。
注意:许多软件手册实际上使用术语“线程安全”来指代“线程兼容”,这让已经一团糟的东西更加混乱!正是出于这个原因,我不惜一切代价避免使用术语“线程安全”和“线程不安全”,因为有些消息来源会称其为“线程安全”,而另一些则将其称为“线程不安全”,因为他们不能同意关于您是否必须满足一些额外的安全标准(同步原语),或者只是不要怀有敌意才能被视为“安全”。因此,请避免使用这些术语并使用更智能的术语,以避免与其他工程师进行危险的误传。
本质上,我们的目标是颠覆混乱。
我们通过创建我们可以依赖的半确定性系统来做到这一点。确定性是昂贵的,主要是由于失去并行性、流水线和重新排序的机会成本。我们尽量减少我们需要的决定论数量,以保持低成本,同时也避免做出会进一步侵蚀我们所能承受的决定论的决定。因此,半前缀。我们只希望代码状态的某些小部分是确定性的,而底层的计算机制不必完全如此。线程同步是在多线程中增加顺序并减少混乱
总之,一些代码体可以在三个主要程度上投入到“杂耍刀”中——即在多线程的上下文中正确工作。
最高级别(thread-proof等)意味着即使您从多个线程草率地调用系统,系统也会以可预测的方式运行。它自己完成了实现这一目标所必需的工作,因此您不必这样做。它为您(编写调用代码的程序员)提供了一个很好的界面,这样您就可以假装生活在没有同步原语的世界中。因为它已经在内部包含了它们。它也很昂贵、很慢,并且在涉及到由于同步而完成任务需要多长时间时也有些不可预测,这必须始终大于特定程序所需的数量,因为它不知道是什么您的代码会做。非常适合使用各种脚本语言编写代码来做科学或其他事情的临时编码人员,但他们自己并不能编写高效的接近金属的代码。他们不需要玩弄刀具。
第二级(线程兼容等)意味着系统表现得足够好,调用代码可以可靠地及时检测到不可预测性,以便在运行时使用自己安装的同步原语正确处理它。DIY同步。BYOSP = 自带同步原语。至少你知道你正在调用的代码会很好地与他们合作。这适用于更接近金属的专业程序员。
第三级(thread-hostile等)意味着系统表现得不够好,无法与其他人一起玩,并且只能单线程运行而不会引起混乱。本质上,这是经典的 90 年代早期和更早的代码。它的编程缺乏对如何从多个线程调用或使用它的高度了解,以至于即使您尝试自己添加这些同步原语,它也不会起作用,因为它做出了老式的假设,这些日子似乎是反社会和不专业的。
然而,一些代码只有真正有意义称为单线程,因此仍然被有意地以这种方式调用。对于已经拥有高效管道和内存访问序列,并且没有从多线程的主要目的中受益的软件尤其如此:隐藏内存访问延迟。访问非缓存内存比大多数其他指令慢得多。所以每当一个应用程序正在等待一些内存访问时,它应该同时切换到另一个任务线程以保持处理器工作。当然,现在,这可能意味着切换到另一个协程/光纤/等。在同一个线程中,如果可用,因为它们比线程上下文切换更有效。但是一旦那些暂时筋疲力尽,它'
但有时,您的所有内存访问都被很好地打包和排序,而您最不想做的就是切换到另一个线程,因为您已经将代码流水线化以尽可能有效地处理这个问题。然后线程伤害无济于事。这是一个例子,但还有其他例子。
总的来说,我认为在编写要调用的代码时尽可能使用线程兼容是有意义的,特别是如果没有真正的理由不这样做,并且它只需要您在编写代码时注意。
不要将线程安全与确定性混淆。线程安全代码也可以是不确定的。鉴于调试线程代码问题的难度,这可能是正常情况。:-)
线程安全只是确保当一个线程正在修改或读取共享数据时,没有其他线程可以以更改数据的方式访问它。如果您的代码依赖于特定的执行顺序以确保正确性,那么您需要除线程安全所需的同步机制之外的其他同步机制来确保这一点。
让我们通过例子来回答这个问题:
class NonThreadSafe {
private int count = 0;
public boolean countTo10() {
count = count + 1;
return (count == 10);
}
Run Code Online (Sandbox Code Playgroud)
该countTo10方法将计数器加 1,如果计数达到 10,则返回 true。它应该只返回 true 一次。
只要只有一个线程在运行代码,这就会起作用。如果两个线程同时运行代码,就会出现各种问题。
例如,如果计数从 9 开始,一个线程可以将计数加 1(使 10),但是第二个线程可以进入该方法并在第一个线程有机会执行与 10 的比较之前再次加 1(使 11) . 然后两个线程做比较,发现count是11,都没有返回true。
所以这段代码不是线程安全的。
本质上,所有的多线程问题都是由这种问题的某种变异引起的。
解决方案是确保加法和比较不能分开(例如,通过某种同步代码将两个语句包围起来)或设计一个不需要两次操作的解决方案。这样的代码将是线程安全的。
要完成其他答案:
只有当您的方法中的代码执行以下两种操作之一时,才需要担心同步:
这意味着在您的方法中定义的变量始终是线程安全的。对方法的每次调用都有其自己版本的这些变量。如果该方法被另一个线程调用,或者被同一个线程调用,或者即使该方法调用自身(递归),这些变量的值也不会共享。
线程调度不保证是循环的。一个任务可能会以相同优先级的线程为代价完全占用 CPU。您可以使用 Thread.yield() 来获得良心。您可以使用(在 Java 中)Thread.setPriority(Thread.NORM_PRIORITY-1) 来降低线程的优先级
另外要注意:
我想在其他好的答案之上添加更多信息.
线程安全意味着多个线程可以在同一对象中写入/读取数据而不会出现内存不一致错误 在高度多线程程序中,线程安全程序不会对共享数据造成副作用.
有关详细信息,请查看此SE问题:
线程安全程序保证内存一致性.
从高级并发API的oracle文档页面:
内存一致性属性:
Java™语言规范的第17章定义了内存操作的先发生关系,例如共享变量的读写.只有在读取操作之前发生写入操作时,一个线程的写入结果才能保证对另一个线程的读取可见.
该synchronized和volatile结构,以及在Thread.start()和Thread.join()方法,可以形成之前发生关系.
所有类java.util.concurrent及其子包的方法将这些保证扩展到更高级别的同步.特别是:
Runnable到Executor执行之前的线程中的操作- 在执行开始之前.同样,Callables提交给了ExecutorService.Future检索结果之后的发生前动作表示Future.get().Lock.unlock, Semaphore.release, and CountDownLatch.countDown之前的操作,例如在成功的"获取"方法之后的发生之前的操作,例如Lock.lock, Semaphore.acquire, Condition.await, and CountDownLatch.await在另一个线程中的同一同步器对象上.Exchanger之前的动作exchange()发生 - 在另一个线程中相应的exchange()之后的动作之前.CyclicBarrier.await和Phaser.awaitAdvance(以及其变体),以从相应的成功返回随后发生-前由阻挡动作执行通过屏障操作执行的动作和操作发生-之前动作等待其他线程.