【问题标题】:What is "non-blocking" concurrency and how is it different than normal concurrency?什么是“非阻塞”并发,它与普通并发有何不同?
【发布时间】:2011-02-18 22:25:55
【问题描述】:
  1. 什么是“非阻塞”并发,它与使用线程的普通并发有何不同?为什么我们不在所有需要并发的场景中都使用非阻塞并发呢?使用非阻塞并发是否有开销?
  2. 我听说在 Java 中可以使用非阻塞并发。是否存在我们应该使用此功能的特定场景?
  3. 将这些方法之一用于集合是否有区别或优势?有哪些取舍?

第三季度示例:

class List   
{  
    private final ArrayList<String> list = new ArrayList<String>();

    void add(String newValue) 
    {
        synchronized (list)
        {
            list.add(newValue);
        }
    }
}  

对比

private final ArrayList<String> list = Collections.synchronizedList(); 

这些问题更多是从学习/理解的角度出发的。感谢关注。

【问题讨论】:

  • 您的第一个问题是什么意思,您将非阻塞并发与什么进行比较?
  • private final ArrayList list = Collections.synchronizedMap() 无法编译有两个原因...

标签: java concurrency


【解决方案1】:

什么是非阻塞并发和 有什么不同。

正式:

在计算机科学中,非阻塞 同步确保线程 争夺共享资源不 无限期执行 因相互排斥而推迟。一种 非阻塞算法是无锁的,如果 有保证的系统范围 进步;如果也有,则无需等待 保证每个线程的进度。 (维基百科)

非正式:非阻塞与阻塞最有利的特性之一是,线程不必被操作系统挂起/唤醒。这样的开销可以达到 1ms 到几个 10ms,因此消除它可能会带来很大的性能提升。在java中,这也意味着你可以选择使用非公平锁,它可以比公平锁有更多的系统吞吐量。

听说有这个 在爪哇。有没有特别的 我们应该使用此功能的场景

是的,从 Java5 开始。实际上,在 Java 中,您基本上应该尽可能地尝试使用 java.util.concurrent 来满足您的需求(这恰好使用了很多非阻塞并发,但在大多数情况下您不必明确担心)。只有当您没有其他选择时,您才应该使用同步包装器(.synchronizedList() 等)或手动 synchronize 关键字。这样一来,您大部分时间都会使用更易于维护、性能更好的应用程序。

当存在大量争用时,非阻塞并发特别有利。当您需要阻塞(公平锁定、事件驱动的东西、具有最大长度的队列等)时,您不能使用它,但如果您不需要,非阻塞并发在大多数情况下往往会表现得更好。

是否有区别/优势 使用这些方法之一 收藏。有什么取舍

两者具有相同的行为(字节码应该相等)。但我建议使用Collections.synchronized,因为它更短=更小的空间来搞砸!

【讨论】:

    【解决方案2】:

    什么是非阻塞并发,它与使用线程的普通并发有何不同。

    非阻塞并发是协调线程间访问与阻塞并发不同的一种方式。那里有很多背景(理论)材料,但最简单的解释(您似乎正在寻找一个简单的动手答案)是非阻塞并发不使用锁。

    为什么我们不在所有需要并发的场景中使用“非阻塞”并发。

    我们愿意。我稍后会告诉你。但确实,对于每个并发问题,并不总是有高效的非阻塞算法。

    “非阻塞”是否有任何开销

    嗯,线程之间任何类型的共享信息都会产生开销,一直到 CPU 的结构,尤其是当您遇到我们所谓的“争用”时,即同步多个尝试写入的线程同时到同一个内存位置。 但总的来说,在许多情况下,非阻塞比阻塞(基于锁的)并发更快,尤其是所有已知的、简单的、给定算法/数据结构的无锁实现的情况。 Java 提供的正是这些优秀的解决方案。

    我听说这在 Java 中可用。

    当然。首先,java.util.concurrent.atomic 中的所有类都提供共享变量的无锁维护。此外,java.util.concurrent 中所有以 ConcurrentLinked 或 ConcurrentSkipList 开头的类都提供了列表、映射和集合的无锁实现。

    是否有我们应该使用此功能的特定场景。

    在所有情况下(在 JDK 1.5 之前)使用 Collections.synchronizedlist 的情况下,您都希望使用无锁队列和双端队列,因为它们在大多数情况下都能提供更好的性能。即,当多个线程同时修改集合时,或者当一个线程正在修改集合而其他线程试图读取它时,您将使用它们。 请注意,非常流行的 ConcurrentHashMap 实际上在内部使用了锁,但它比 ConcurrentSkipListMap 更流行,因为我认为它在大多数情况下提供了更好的性能。但是,我认为 Java 8 将包含 ConcurrentHashMap 的无锁实现。

    将这些方法之一用于集合是否有区别/优势。有什么取舍

    嗯,在这个简短的例子中,它们是完全相同的。但是请注意,当您有并发的读取器和写入器时,您必须同步读取和写入,而 Collections.synchronizedList() 会执行此操作。 您可能想尝试使用无锁 ConcurrentLinkedQueue 作为替代方案。在某些情况下,它可能会为您提供更好的性能。

    一般说明

    虽然并发是一个非常重要的学习主题,但请记住,它也是一个非常棘手的主题,即使是非常有经验的开发人员也经常会犯错。更糟糕的是,只有在系统负载过重时,您才可能发现并发错误。所以我总是建议尽可能多地使用现成的并发类和库,而不是推出自己的。

    【讨论】:

      【解决方案3】:

      1) 什么是非阻塞并发,它有何不同。

      当一个任务(线程)不会导致其他任务(线程)等待任务完成时,它是非阻塞的。

      2) 我听说这在 Java 中可用。有什么特殊的场景我们应该使用这个功能

      Java 支持自己的多线程。您可以利用它同时运行多个任务。如果编写和实现得当,当在具有多个逻辑 CPU 的机器上运行时,这可能会加快程序的运行速度。首先,有java.lang.Runnablejava.lang.Thread 作为低级并发实现。然后是 java.util.concurrent API 的高级并发。

      3) 将这些方法之一用于集合是否有区别/优势。有什么取舍

      我会使用Collections#synchronizedList(),不仅因为它更强大、更方便,还因为Map 不是List。如果 API 已经提供了该功能,则无需尝试自行开发。


      也就是说,有一个Sun tutorial about Concurrency。我建议你自己完成它。

      【讨论】:

        【解决方案4】:

        1]什么是非阻塞并发和 有什么不同。

        正如其他人所提到的,非阻塞是一种说无死锁的方式(这意味着我们不应该出现在线程被阻塞、等待访问时进度完全停止的情况)。

        “并发”的含义只是同时(同时)发生多个计算。

        2] 听说有这个 在爪哇。有没有特别的 我们应该使用此功能的场景

        当多个线程可以同时访问相同资源很重要时,您希望使用非阻塞算法,但我们并不关心访问顺序或交错操作的可能后果(更多内容见下文) .

        3] 是否有区别/优势 使用这些方法之一 收藏。有什么取舍

        .

        使用 synchronized(list) 块可确保在块内执行的所有操作都被视为原子操作。也就是说,只要我们只从 synchronized(list) 块中访问列表,所有对列表的更新都会出现在块内同时发生的情况。

        同步列表(或同步映射)对象仅确保单个操作是线程安全的。这意味着两个插入不会同时发生。考虑以下循环:

        for(int i=0; i < 4; i++){
            list.add(Integer.toString(i));
        }
        

        如果正在使用的列表是一个同步列表,并且此循环在两个不同的线程上执行,那么我们的列表中可能会出现 {0,0,1,2,1,3,2,3},或者一些其他排列。

        为什么?好吧,我们保证线程 1 将按该顺序添加 0-3,并且我们保证线程 2 相同,但是我们不能保证它们将如何交错。

        但是,如果我们将此列表包装在 synchronized(list) 块中:

        synchronized(list){
            for(int i=0; i < 4; i++){
                list.add(Integer.toString(i));
            }
        }
        

        我们保证来自线程 1 和线程 2 的插入不会交错,但它们会同时发生。我们的列表将包含 {0,1,2,3,0,1,2,3}。另一个线程将阻塞,在列表中等待,直到第一个线程完成。我们无法保证哪个线程会先执行,但我们保证它会在另一个线程开始之前完成。

        因此,一些权衡是:

        • 使用 syncrhonizedList,您可以 插入而不显式使用 同步块。
        • syncrhonizedList 可能会给你一个 虚假的安全感,因为你 可能天真地相信连续 一个线程上的操作 原子的,当只有个别 操作是原子的
        • 必须小心使用 syncrhonized(list) 块,因为我们可能会造成死锁(更多内容见下文)。

        当两个(或更多)线程都在等待另一个线程持有的资源子集时,我们可以创建死锁。例如,如果您有两个列表:userList 和 movieList。

        如果线程1首先获取userList的锁,然后是movieList,但是线程2反过来执行这些步骤(在userList之前获取movieList的锁),那么我们已经打开了自己的死锁。考虑以下事件过程:

        1. 线程 1 锁定到 userList
        2. 线程 2 锁定到 movieList
        3. 线程 1 尝试锁定 movieList,等待线程 2 释放
        4. 线程 2 尝试锁定 userList,等待线程 1 释放

        两个线程都在等待另一个线程,都不能前进。这是一个阻塞的场景,因为两者都不会放弃它的资源,所以我们陷入了僵局。

        【讨论】:

        • 您对两个列表中的事件枚举不准确。应该是#1 userlist, #2 movieList, #1 movieList (block), #2 userList (block)
        • 感谢 Ed 的回答。我特别假设在同步块内只有一个添加操作。如果我们将同步链接与可以在一个块中同步添加多个元素的列表进行比较,那么它不是苹果对苹果。一般来说,如果两者都执行与我的问题相同的一次添加一个元素的功能,你会更喜欢一个吗?
        • 我可能更喜欢 synchronized(list) 块而不是 synchronizedList,只是因为我觉得它使我们使用它的意图更加明确。
        【解决方案5】:

        1) 非阻塞同步表示消除deadlocks的风险。没有一个线程会等待“永远”获得另一个线程持有的锁。

        2) 更多关于 java 中的非阻塞同步algorithms

        【讨论】:

          【解决方案6】:
          1. 维基百科对于任何计算机科学专业的学生来说都是一个很好的资源。这是一篇关于非阻塞同步的文章 - http://en.wikipedia.org/wiki/Non-blocking_synchronization

          2. 任何语言都可以使用非阻塞同步,这是程序员定义多线程应用程序的方式。

          3. 您应该只在必要时使用锁定(即 synchronized (list) ),因为获取锁定需要时间。在 Java 中,Vector 是一种线程安全的数据结构,与 Java 的 ArrayList 非常相似。

          【讨论】:

            【解决方案7】:

            1:什么是“非阻塞”并发,它与使用线程的普通并发有何不同?为什么我们不在所有需要并发的场景中都使用非阻塞并发呢?使用非阻塞并发有开销吗?

            非阻塞算法不使用特定的对象锁定方案来控制对内存的并发访问(同步和标准对象锁是使用对象/函数级别锁来减少 Java 中的并发访问问题的示例。相反,这些使用某种形式的低级别在内存位置执行(在某种程度上)同时比较和交换的指令;如果失败,它只会返回 false 并且不会出错,如果它有效,那么它是成功的,你继续前进。通常,这是在一个循环直到它工作,因为只有一小段时间(希望)它会失败,它只是额外循环几次,直到它可以设置它需要的内存。

            这并不总是被使用,因为从代码的角度来看它比标准的 Java 同步要复杂得多,即使对于相对琐碎的用例也是如此。此外,对于大多数用途而言,与系统中的其他来源相比,锁定对性能的影响是微不足道的。在大多数情况下,性能要求还不够高,甚至不足以保证看到这一点。

            最后,随着 JDK/JRE 的发展,核心设计人员正在改进内部语言实现,以尝试将实现这些目标的最有效方法整合到核心结构中。当您远离核心构造时,您将失去这些改进的自动实现,因为您使用的标准实现较少(例如 jaxb/jibx;jaxb 过去的性能严重低于 jibx,但现在在大多数情况下如果不是更快的话,我从 java 7 开始测试),当你升级你的 java 版本时。

            如果您查看下面的代码示例,您可以看到“开销”位置。它本身并不是真正的开销,但代码必须非常高效才能工作非锁定并且由于循环而实际上比标准同步版本执行得更好。即使是轻微的修改也可能导致代码从比标准性能好几倍到差几倍的代码(例如不需要在那里的对象实例化,甚至是快速的条件检查;你说的是节省周期在这里,所以成功和失败之间的差别很小)。

            2:我听说在 Java 中可以使用非阻塞并发。是否有我们应该使用此功能的特定场景?

            在我看来,只有在以下情况下,您才应该使用它 A) 在您的生产运行系统中,在其生产硬件上存在已证明的性能问题; B)如果你能证明关键部分剩下的唯一低效率是锁定相关的; C)你从你的利益相关者那里得到了坚定的支持,他们愿意使用非标准的不易维护的代码来换取你必须的性能改进 D)在你的生产硬件上进行数字证明,以确保它甚至会有所帮助。

            3:将这些方法之一与集合一起使用是否有区别或优势?有哪些取舍?

            优势在于性能,首先要权衡的是它是更专业的代码(因此许多开发人员不知道该怎么做;让新团队或新员工更难跟上进度,请记住软件的大部分成本是人工成本,因此您必须注意通过设计决策强加的总拥有成本),并且应该再次测试任何修改以确保构造实际上仍然更快。通常,在需要此功能的系统中,任何更改都需要进行一些性能或负载和吞吐量测试。如果您不进行这些测试,那么我认为您几乎可以肯定甚至不需要考虑这些方法,并且几乎肯定不会看到增加的复杂性有任何价值(如果您一切正常)。

            再一次,我只需要重申所有针对优化的标准警告,因为其中许多论点与我在设计中使用的相同。这样做的许多缺点与任何优化相同,例如,每当您更改代码时,您必须确保您的“修复”不会在某些仅用于提高性能的构造中引入低效率,并处理如果修复很关键并且会降低性能,那么(意味着重构整个部分以可能删除优化)。

            以非常难以调试的方式将其搞砸真的非常容易,所以如果你不必这样做(我只发现了一些你曾经做过的场景;对我来说那些非常值得怀疑,我宁愿不这样做)不要这样做。使用标准的东西,每个人都会更快乐!

            讨论/代码

            非阻塞或无锁并发避免使用特定对象锁来控制共享内存访问(如同步块或特定锁)。代码段非锁定时有性能优势;但是,CAS 循环中的代码(如果您采用这种方式,Java 中还有其他方法)必须非常、非常高效,否则最终会花费您更多的性能而不是获得的收益。

            与所有性能优化一样,对于大多数用例而言,额外的复杂性并不值得。如果不比大多数优化更好,使用标准结构编写干净的 Java 也能正常工作(实际上,一旦您离开,您的组织可以更轻松地维护软件)。在我看来,这仅在具有已证明性能问题的高性能部分才有意义,其中锁定是低效率的唯一来源。如果您确实没有已知和量化的性能问题,我会避免使用任何类似的技术,直到您证明问题实际上是由于锁定而存在的,而不是对代码效率的其他问题。一旦你发现了一个基于锁定的性能问题,我会确保你有某种类型的指标,以确保这种类型的设置实际上比仅使用标准 Java 并发运行得更快。

            我为此完成的实现使用 CAS 操作和 Atomic 系列变量。在这个用例中(用于从高吞吐量翻译系统进行离线测试的随机采样输入和输出),这个基本代码从未锁定或引发任何错误。它基本上是这样工作的:

            您有一些在线程之间共享的对象,它被声明为 AtomicXXX 或 AtomicReference(对于大多数重要的用例,您将使用 AtomicReference 版本运行)。

            当引用给定的值/对象时,您从 Atomic 包装器中检索它,这将为您提供一个本地副本,您可以在该副本上执行一些修改。从这里你使用 compareAndSwap 作为 while 循环的条件来尝试从你的线程中设置这个 Atomic,如果失败它返回 false 而不是锁定。这将迭代直到它工作(这个循环中的代码必须非常高效和简单)。

            您可以查看 CAS 操作以了解它们是如何工作的,它基本上旨在实现为单个指令集,并在末尾进行比较以查看该值是否是您尝试设置的值。

            如果 compareAndSwap 失败,您从 Atomic 包装器中再次获取您的对象,再次执行任何修改,然后再次尝试比较和交换,直到它工作。没有特定的锁,您只是尝试将对象设置回内存,如果失败,您只需在线程再次获得控制权时重试。

            下面是一个带有列表的简单案例的代码:

            /* field declaration*/
            //Note that I have an initialization block which ensures that the object in this
            //reference is never null, this was required to remove null checks and ensure the CAS
            //loop was efficient enough to improve performance in my use case
            private AtomicReference<List<SampleRuleMessage>> specialSamplingRulesAtomic = new AtomicReference<List<SampleRuleMessage>>();
            
            
            /*start of interesting code section*/
            
                List<SampleRuleMessage> list = specialSamplingRulesAtomic.get();
                list.add(message);
                while(!specialSamplingRulesAtomic.compareAndSet(specialSamplingRulesAtomic.get(), list)){
                    list = specialSamplingRulesAtomic.get();
                    list.add(message);
                };
             /* end of interesting code section*/
            

            【讨论】:

              【解决方案8】:

              非阻塞同步和阻塞同步一样,都是同步的一种,唯一的区别是非阻塞同步整体速度更快。

              对于初学者,您只想在多个线程访问 RAM 中的同一资源时使用同步。尝试访问磁盘上的内容时不能使用同步,或者更确切地说,您需要在磁盘上使用锁。

              也就是说,如果没有线程阻塞,你怎么能同步?

              答案是乐观锁定。这个想法至少存在了20年。也许更多。

              您可能听说过 Lisp 语言。事实证明,函数式语言从不修改其参数,只返回新值,因此它们永远不需要同步。

              在 Lisp 中,您可以拥有共享状态,但这很棘手。所以大多数程序可以并行运行,不用担心同步问题。

              乐观锁的思想是所有线程都愿意修改共享值,但它们有一个局部区域来修改值,并且只在最后应用修改,使用一条指令,原子地,使用 CAS。 Cas 代表 Compare And Swap,它只在一个 CPU 周期内执行,并且在 CPU 中实现了至少 20 年。

              什么是 CAS 的精彩解释:https://www.cs.umd.edu/class/fall2010/cmsc433/lectures/nonBlocking.pdf

              所以如果修改中出现冲突,只会影响到其中一个写者,其余的都由它来处理。

              此外,如果没有任何争用,非阻塞算法的执行速度要比阻塞算法快得多。

              Java 中的非阻塞算法教程以及您可以在现实生活中使用的代码示例:http://tutorials.jenkov.com/java-concurrency/non-blocking-algorithms.html

              【讨论】:

                猜你喜欢
                • 2018-11-15
                • 1970-01-01
                • 2011-05-07
                • 1970-01-01
                • 2021-05-21
                • 2011-04-29
                • 2014-10-24
                • 2012-01-15
                相关资源
                最近更新 更多