【问题标题】:Synchronize on value, not object [duplicate]同步值,而不是对象[重复]
【发布时间】:2011-11-25 05:32:00
【问题描述】:

我想在 Java 中做这样的事情

  public void giveMoney(String userId, int money) {
    synchronized (userId) {

        Profile p = fetchProfileFromDB(userId);
        p.setMoney(p.getMoney() + userId);
        saveProfileToDB(p);

    }
   }

当然,在字符串上同步是不正确的。这样做的正确方法是什么?

【问题讨论】:

  • 是否有无法在Profile 上同步的原因?
  • 我很想知道为什么在字符串上同步不正确..
  • @Jim 它是为每个请求新创建的。
  • @mrkhrts synchronized 使用对象标识。一个字符串可以有多个副本,其标识不同但内容相同。

标签: java thread-safety synchronized


【解决方案1】:

您可以为字符串使用代理对象。

Object userIdMutex = new Object();

synchronized (userIdMutex) {
    Profile p = getProfile(userId);
    p.setMoney(p.getMoney() + p);
    saveProfile(p);
}

在您访问userId 时使用此互斥锁。

【讨论】:

  • 但是我在哪里可以得到这个对象?如果我像您的示例中那样为每个请求创建一个新请求,则没有实际锁定。
  • 这将是类的静态字段。
  • userIdMutex 当然不能是局部变量,因为那样做就不行了。
  • "这将是类的静态字段。" - 如果您的示例代码显示了该细节,那就太好了...
【解决方案2】:

如果用户 ID 集有限,您可以在 String 的实习版本上进行同步。

如果您需要对实习进行更多控制,请使用 String.intern()(有一些缺点)或 Guava Interners 之类的东西。

【讨论】:

  • 我们有大约 20 万用户,并且预计会更多,所以它并没有真正受到限制。另请参阅我对 stevevis 关于intern的评论
  • @BartvanHeukelom:自定义 Interner 可以解决这两个问题:因为你有一个 custom 内部人员,所以你不会运行与其他代码冲突的风险。
  • 一个简单的Interners.<String>newWeakInterner() 会做我想做的事吗?
  • @BartvanHeukelom:我想会的。
  • 太好了,这很简单:)
【解决方案3】:

这个呢:

String userId = ...;
Object userIdLock = new Object();
synchronized (userIdLock) {
    Profile p = getProfile(userId);
    p.setMoney(p.getMoney() + p);
    saveProfile(p);
}

这很简单,最重要的是显而易见

【讨论】:

  • 这如何让您拥有互斥性?
  • 哇...那是...适合您的名字。如果userIdLock 在局部变量之外“转义”,那么synchronized完全没用
  • 这种方法有什么问题?
  • @mrkhrts 每个调用此代码的线程都有自己的对象,因此它自己的锁。
  • @Jim,如果它是类的一个字段,对吧?
【解决方案4】:

我想有几个选择。

最简单的方法是您可以将userId 映射到线程安全映射中的锁定对象。其他人提到了实习,但我认为这不是一个可行的选择。

但是,更常见的选项是在 p(配置文件)上进行同步。如果getProfile() 是线程安全的,这是合适的,而且我怀疑它可能是。

【讨论】:

  • 配置文件的问题是每个请求都会创建一个新实例。我会稍微澄清一下示例代码。
【解决方案5】:

原则上,您可以在 Java 中的任何对象上进行同步。在 String 对象上同步本身并不是“不正确的”;这取决于你到底在做什么。

但是如果userId 是方法中的局部变量,那么这将不起作用。每个执行该方法的线程都有自己的变量副本(大概是指每个线程的不同String 对象);线程之间的同步当然只有当你让多个线程在同一个对象上同步时才有效。

您必须在对象的成员变量上创建您正在同步的对象,该对象包含您拥有synchronized 块的方法。如果多个线程在同一个对象上调用该方法,您将实现互斥。

class Something {
    private Object lock = new Object();

    public void someMethod() {
        synchronized (lock) {
            // ...
        }
    }
}

您还可以使用 java.util.concurrent.locks 包中的显式锁,如果您需要,它可以为您提供更多控制:

class Something {
    private Lock lock = new ReentrantLock();

    public void someMethod() {
        lock.lock();
        try {
            // ...
        } finally {
            lock.unlock();
        }
    }
}

特别是如果你想要一个独占锁来写入,但你不希望线程在读取时必须相互等待,你可能想要使用ReadWriteLock

【讨论】:

  • 这正是我在回答中所指的,虽然我没有那么优雅。
  • 是的,你的第二段解释了为什么我称之为不正确。
  • 关于你的扩展,是的,我可以在每次更新任何人的钱时锁定,这绝对是第二好的解决方案。不过,这不允许我同时更新多个用户。
  • 如果这只是关于进行原子数据库更新(获取、更新、存储),那么您应该使用数据库事务并让数据库处理行的锁定。
  • @Jesper 是的,我知道,但数据库目前不支持它,它不是我的。
【解决方案6】:

根据您的示例,我假设您想要获得对配置文件类的锁定,对其进行更改,然后释放锁定。在我看来,同步并不是你所需要的。您需要一个类来管理这些记录,并允许您在需要对其进行更改时锁定和解锁记录,也就是源代码控制样式。

看看这个:Lock class Java 5

【讨论】:

    【解决方案7】:

    从理论上讲,由于可以对实习对象进行 GC,因此可以在不同时间对不同对象(具有相同值)进行同步。仍然可以保证互斥性,因为不可能在同时时间对不同的对象进行同步。

    然而,如果我们在不同的对象上同步,happens-before 关系是有疑问的。我们必须检查实现以找出答案。而且由于它涉及到 GC,Java 内存模型没有解决这个问题,因此推理可能相当困难。

    这是理论上的反对意见;实际上我认为这不会造成任何问题。

    不过,对于您的问题,仍然可以找到简单、直接且理论上正确的解决方案。例如Simple Java name based locks?

    【讨论】:

    • 我不介意实习 ID 是否经过 GC,只要保证互斥性即可。事实上,我希望它们被 GC 处理,因为会有许多不同的 ID。感谢您的链接,这也是一个不错的选择。
    猜你喜欢
    • 1970-01-01
    • 2018-08-01
    • 2012-07-01
    • 2016-05-08
    • 1970-01-01
    • 2017-07-12
    • 2011-02-07
    • 2015-07-25
    • 2021-09-18
    相关资源
    最近更新 更多