关于多线程:“线程安全”代码是什么意思?

What is meant by “thread-safe” code?

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


维基百科:

线程安全是一个适用于多线程程序环境的计算机编程概念。如果一段代码在多个线程同时执行期间运行正确,那么它就是线程安全的。特别是,它必须满足多个线程访问同一共享数据的需要,以及在任何给定时间只有一个线程访问共享数据块的需要。

实现线程安全有几种方法:

重新进入:

以这样的方式编写代码:一个任务可以部分执行代码,另一个任务可以重新输入代码,然后从原始任务中恢复。这需要将状态信息保存在每个任务的局部变量中,通常保存在其堆栈中,而不是静态或全局变量中。

相互排斥:

对共享数据的访问是使用确保在任何时候只有一个线程读取或写入共享数据的机制序列化的。如果一段代码访问多个共享数据段,则需要非常小心,这些问题包括争用条件、死锁、LiveLocks、饥饿以及许多操作系统教科书中列举的各种其他问题。

线程本地存储:

变量是本地化的,因此每个线程都有自己的私有副本。这些变量通过子例程和其他代码边界保留其值,并且是线程安全的,因为它们是每个线程的本地变量,即使访问它们的代码可能是可重入的。

原子操作:

共享数据是通过使用原子操作访问的,而原子操作不能被其他线程中断。这通常需要使用特殊的机器语言指令,这些指令可能在运行库中可用。由于操作是原子操作,所以不管其他线程访问共享数据,共享数据都始终保持有效状态。原子操作构成了许多线程锁定机制的基础。

阅读更多:

http://en.wikipedia.org/wiki/thread_安全

  • 德语:网址:http://de.wikipedia.org/wiki/threadsicherheit

  • 法语:网址:http://fr.wikipedia.org/wiki/threasafe(非常短)


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

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


一个更具信息性的问题是什么使得代码不安全——答案是有四个条件必须是正确的……想象一下下面的代码(它是机器语言翻译)

1
2
3
4
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
  • 第一个条件是存在可以从多个线程访问的内存位置。通常,这些位置是全局/静态变量,或者可以从全局/静态变量访问堆内存。每个线程都为函数/方法范围内的局部变量获得自己的堆栈框架,因此这些局部函数/方法变量otoh(位于堆栈上)只能从拥有该堆栈的一个线程访问。
  • 第二个条件是有一个属性(通常称为不变量),它与这些共享内存位置相关联,必须为真或有效,程序才能正常运行。在上面的示例中,属性是"totalrequests必须准确地表示任何线程执行increment语句任何部分的总次数"。通常,此不变属性需要保持为真(在本例中,totalRequests必须保持准确的计数),然后才能进行更新以使更新正确。
  • 第三个条件是,在实际更新的某些部分中,不变量属性不保持不变。(在处理的某些部分中,它暂时无效或错误)。在这种特殊情况下,从获取totalrequests到存储更新值,totalrequests不满足不变量。
  • 第四个也是最后一个必须发生的竞争条件(以及代码因此而不是"线程安全")是另一个线程必须能够在不变量被破坏时访问共享内存,从而导致不一致或不正确的行为。

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

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


    正如其他人所指出的,线程安全意味着如果一个以上的线程同时使用一段代码,那么它将不会出错。

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

    例如,Java有两个几乎相等的类,EDCOX1,0,EDCX1,1。区别在于StringBuffer是线程安全的,因此多个线程可以同时使用StringBuffer的单个实例。StringBuilder不是线程安全的,当字符串仅由一个线程构建时,它被设计为对这些情况(绝大多数情况)的更高性能替换。


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


    理解它的一个简单方法是使代码不具有线程安全性。有两个主要问题会使线程应用程序具有不需要的行为。

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

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


    是和不是。

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

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


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

    在实践中,我喜欢Java并发给出的定义:

    A [portion of code] is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordination on the part of the calling code.

    正确地说,它们意味着程序的行为符合其规范。

    人为的例子

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

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

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

    注:Cross Post on Programmers


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


    我想在其他好答案的基础上再添加一些信息。

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

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

    螺纹安全是什么意思?

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

    从高级并发API上的Oracle文档页面:

    内存一致性属性:

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

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

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

  • 在将对象放入任何并发集合之前,线程中的操作发生在从另一线程的集合中访问或删除该元素之后的操作之前。
  • Runnable提交给Executor之前线程中的操作发生在执行开始之前。同样,对于提交给ExecutorService的可调用文件。
  • Future表示的异步计算所采取的操作发生在通过另一个线程中的Future.get()检索结果之后的操作之前。
  • "释放"同步器方法(如Lock.unlock, Semaphore.release, and CountDownLatch.countDown)之前的操作发生在成功"获取"方法(如Lock.lock, Semaphore.acquire, Condition.await, and CountDownLatch.await)之后的操作之前,该方法位于另一线程中同一同步器对象上。
  • 对于通过Exchanger成功交换对象的每对线程,每个线程中exchange()之前的操作发生在另一个线程中相应的exchange()之后的操作之前。
  • 调用CyclicBarrier.awaitPhaser.awaitAdvance之前的操作(及其变体)发生在屏障操作执行的操作之前,屏障操作执行的操作发生在其他线程中相应等待成功返回之后的操作之前。

  • 不要混淆线程安全和确定性。线程安全代码也可以是非确定性的。考虑到用线程代码调试问题的困难,这可能是正常情况。-)

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


    完成其他答案:

    当方法中的代码执行以下两项操作之一时,同步只是一个问题:

  • 使用一些非线程安全的外部资源。
  • 读取或更改持久对象或类字段
  • 这意味着方法中定义的变量始终是线程安全的。对方法的每个调用都有其自己的变量版本。如果该方法被另一个线程或同一个线程调用,或者即使该方法本身调用(递归),这些变量的值也不会共享。

    线程调度不能保证是循环调度。一个任务可能会以牺牲相同优先级的线程为代价完全占用CPU。您可以使用thread.yield()来问心无愧。可以使用(在Java中)线程。StestPosits(Trime.NoMyPrRoRITY-1)来降低线程的优先级。

    谨防:

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

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

    请注意,结果的不可预测性是"竞态条件"的结果,这可能导致数据按预期顺序之外的顺序进行修改。


    让我们用示例来回答这个问题:

    1
    2
    3
    4
    5
    6
    7
    8
    class NonThreadSafe {

        private int counter = 0;

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

    countTo10方法向计数器中添加一个,如果计数达到10,则返回true。它应该只返回一次真值。

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

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

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

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


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

    *条件适用

    其他回答如1。如果您在它上面执行一个线程或多个线程等,结果应该是相同的。