【问题标题】:Nested synchronized blocks on interned Strings内部字符串上的嵌套同步块
【发布时间】:2009-12-05 12:54:52
【问题描述】:

标题听起来前面有很多问题。这是我的具体情况:

这是一个旅游票销售系统。每条路线的车票数量有限,因此购买给定路线的最后一张车票不应该让两个人访问(标准场景)。但是,有“回程票”选项。所以,我使用唯一的路线 ID(数据库提供)来执行以下操作:

synchronized(bothRoutesUniqueString.intern()) {
    synchronized (routeId.intern()) {
        if (returnRouteId != null) {
            synchronized (returnRouteId.intern()) {
                return doPurchase(selectedRoute, selectedReturnRoute);
            }
        }
        return doPurchase(selectedRoute, selectedReturnRoute);
    }
}

两个内部synchronized 块是为了使线程仅在两个人同时购买此特定路线的票时才停止在那里,而不是在同时购买两条不同路线的票时.第二次同步当然是因为有人可能同时尝试购买返回路由作为出站路由。

最外面的synchronized 块是为了说明当两个人购买相同组合的票时,颠倒的情况。例如,一个订单伦敦-曼彻斯特,另一个订单曼彻斯特-伦敦。如果没有外部同步块,这种情况可能会导致死锁。

(如果没有可用的票证,doPurchase() 方法要么返回 Ticket 对象,要么抛出异常)

现在,我完全知道这是一个非常尴尬的解决方案,但是,如果它按预期工作,它会给出:

  • 10 行代码来处理整个复杂场景(并且使用适当的 cmets,它不会那么难理解)
  • 没有不必要的锁定 - 一切都只有在必须阻塞时才会阻塞。
  • 数据库不可知论

我也知道这种情况是由悲观或乐观的数据库锁来处理的,而且由于我使用的是 Hibernate,所以这些也不难实现。

我认为可以通过上述代码使用 VM 集群来实现水平扩展。根据Teracotta documentation,它允许将单节点多线程应用程序转换为多节点,并且:

Terracotta 跟踪 String.intern() 调用并保证这些显式实习字符串的引用相等。由于对内部 String 对象的所有引用都指向规范值,因此即使对于分布式应用程序,引用相等检查也将按预期工作。

所以,现在开始讨论问题本身:

  • 您是否发现上述代码的任何缺点(除了笨拙之外)?
  • 是否有来自java.util.concurrent API 的适用类来帮助解决这种情况?
  • 为什么数据库锁定比这更可取?

更新: 由于大部分答案都与OutOfMemoryError 相关,所以我为intern() 做了一个基准测试,内存并没有被吃掉。也许正在清除字符串表,但这在我的情况下并不重要,因为我需要对象在竞争条件下相等,并且此时不应清除最近的字符串:

System.out.println(Runtime.getRuntime().freeMemory());
for (int i = 0; i < 10000000; i ++) {
    String.valueOf(i).intern();
}
System.out.println(Runtime.getRuntime().freeMemory());

附:环境是 JRE 1.6

【问题讨论】:

    标签: java multithreading concurrency synchronization


    【解决方案1】:

    为什么数据库锁定比这更可取?

    只有当您的数据库只有一个前端时,您的代码内解决方案才有效,这意味着您只能垂直扩展(在单个前端盒上拥有更多处理能力)。一旦您开始使用网络农场或类似的(水平扩展),您的解决方案就不再有效。

    【讨论】:

    • 在这种情况下虚拟机集群不会有用吗?
    • @Bozho - 没有。关键是三个字符串(锁)必须存在于一个且只有一个 VM 中才能使该方案工作。
    • 好吧,Teracotta VM 集群似乎可以解决问题。我用详细信息更新了我的问题(这里没有空格)
    • @Bozho:如果 Terracotta VM 集群能做到这一点,那就太好了。但是您提供的文档引用仅说引用将是相等的,而不是在它们上同步会跨集群虚拟机同步。如果确实如此,我也会担心运行时成本(例如,每次同步都必须跨 VM 进行 = 大量运行时开销)。我希望synchronized 不会这样做,而是让 Terracotta 提供一个 API 调用。但我对兵马俑一无所知。 :-)
    • 是的,我认为 Teracotta 提供了集群同步。这就是“广告”所说的。
    【解决方案2】:

    实习是一项成本适中的操作,与其他可能的替代方案相比,它消耗更多 CPU 时间的可能性很小。但可以肯定的是,我看不出它在墙上的时间比对数据库的查询要长。我可以想象基于 DB 的实现获胜的唯一情况是,如果您有这么多线程并行执行此操作,那么您很乐意让 DB 完成一些工作,因此您的 CPU 将等待但不会研磨同时。

    对于您的预期应用程序的公认范围有限,您的解决方案对我来说看起来很棒。

    【讨论】:

      【解决方案3】:

      我会说主要缺点是您的interning 的同步对象。

      我认为intern 映射的大小是有限的,所以在某些时候,唯一的字符串会开始被推出,所以如果你的程序运行足够长的时间,你就不会锁定相同的对象.

      另一方面,如果在某些实现中intern 映射不受大小限制,则可能内存不足。

      我不会依赖intern 的内部逻辑,而是创建了我自己的对象来保存stings 和locks。

      除此之外,使用嵌套锁并没有错。只是不要尝试在代码中的其他地方以相反的顺序锁定。

      【讨论】:

      • 来自mindprod.com/jgloss/interned.html: java.lang.OutOfMemoryError: 字符串实习表溢出意味着你有太多的实习字符串。一些较旧的 JVM 可能会将您限制为 64K 字符串,这可能会为您的应用程序留下 50,000 个字符串。 IBM Java 1.1.8 JRE 有这个限制。如果您想捕获它,这是一个错误而不是异常。
      • 实习生消耗永久空间。最好有自己的实习集。
      【解决方案4】:

      我可以提供两条建议。

      1) 避免死锁的标准方法是对您的锁获取进行排序,以便以众所周知的顺序获取它们。这将避免在只需要两个锁时使用三个锁的一些尴尬。

      2) 您是否明确地提出这个问题,并且只在 Terracotta 面前提出?如果是这样,那么没有必要实习你的字符串。这可能不明显,但是当将同步(字符串)转换为 Terracotta 锁时,Terracotta 锁定在字符串的 VALUE 上,而不是身份上。显然,如果您依赖这种行为,那么您应该对其进行评论,因为除了出现 Terracotta 之外,任何情况下都需要像您所做的那样进行实习,因此任何查看您的代码的标准 Java 程序员都会有理由感到害怕:)

      【讨论】:

      • 谢谢。当我需要扩展应用程序时,Teracotta 是一种选择。这一次可能永远不会到来:)
      【解决方案5】:

      要走的路最好是单例,RouteProvider,它只给一个 Route 一次,如果该路由已被使用,则阻塞或返回 null。我正在考虑一个类似通用 GenericObjectPool 的构造,每条路线只有一次拍摄。

      Route 会有一个 Origin 位置和一个 Destination,并且有一个适当的 equals 和 HashCode。

      您需要 take() 和 release() 一条路线,这样 RouteProvider 才知道它再次可用。不要忘记在 finally 子句中考虑异常和 release() :)

      无论如何,我永远不会将实现依赖的东西作为实习生字符串。

      【讨论】:

        【解决方案6】:

        我对您实现此方法的另一个想法是您使用的是悲观锁定方案。

        这取决于碰撞的总体概率,哪种方案更好 - 乐观或悲观。

        但是考虑到问题域,我怀疑在给定的时间窗口内发生冲突的可能性很小,因此与乐观锁定方案相比,使用悲观锁定方案会非常慢。

        乐观锁定方案是典型数据库场景中默认使用的方案,例如使用 Hibernate。

        这意味着您只需尝试进行购买而不用担心锁定,并且只有当购买尝试提交时,您才会检查以确保没有其他人进行了该购买。

        这种行为在 Hibernate 中很容易表达 - 并且会比您的提议更好地扩展。此外,由于您可以使用 Hibernate 以非常标准的方式非常容易地做到这一点,您会发现您的代码更易于调试、维护和扩展,因为此类问题的复杂非标准解决方案在困难中更容易出错识别方式,更难维护,通常更难以扩展。

        Read this page(尤其是第 11.3 节)了解有关 Hibernate 并发和锁定支持的更多详细信息。

        【讨论】:

        • hm,实际上我不太确定如何在这种情况下实现乐观锁定 - 这些是插入,它们以不透明的方式相互依赖。因此,如果我在“seatsRemaining”路线上没有要更新(然后验证)的列,我就不能使用乐观锁定?
        • 我确定你可以在某处介绍一个专栏?
        猜你喜欢
        • 2012-05-09
        • 2018-04-15
        • 1970-01-01
        • 1970-01-01
        • 2018-11-01
        • 1970-01-01
        • 2011-09-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多