【问题标题】:Understanding Memory Visibility in Java Multi-threading了解 Java 多线程中的内存可见性
【发布时间】:2016-12-08 09:31:13
【问题描述】:

我最近试图围绕一些 Java 多线程概念进行思考,并且正在编写一小段代码来帮助我理解内存可见性并尽可能正确地获得同步。根据我所阅读的内容,似乎我们持有的锁的代码量越少,我们的程序就会越高效(通常)。我写了一个小类来帮助我理解我可能遇到的一些同步问题:

public class BankAccount {
    private int balance_;

    public BankAccount(int initialBalance) {
        if (initialBalance < 300) {
            throw new IllegalArgumentException("Balance needs to be at least 300");
        }
        balance_ = initialBalance;
    }

    public void deposit(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit has to be positive");
        }
        // should be atomic assignment
        // copy should also be non-shared as it's on each thread's stack
        int copy = balance_;

        // do the work on the thread-local copy of the balance. This work should
        // not be visible to other threads till below synchronization
        copy += amount;

        synchronized(this) {
            balance_ = copy; // make the new balance visible to other threads
        }
    }

    public void withdraw(int amount) {
        // should be atomic assignment
        // copy should also be non-shared as it's on each thread's stack
        int copy = balance_;

        if (amount > copy) {
            throw new IllegalArgumentException("Withdrawal has to be <= current balance");
        }

        copy -= amount;

        synchronized (this) {
            balance_ = copy; // update the balance and make it visible to other threads.
        }
    }

    public synchronized getBalance() {
        return balance_;
    }
}

请忽略 balance_ 应该是双精度而不是整数的事实。我知道原始类型的读取/赋值是原子的,除了双精度和长整数,所以为了简单起见,我选择了整数

我尝试在函数内部编写 cmets 来描述我的想法。编写此类是为了获得正确的同步以及最小化处于锁定状态的代码量。这是我的问题:

  1. 此代码正确吗?它会遇到任何数据/竞争条件吗?其他线程是否可以看到所有更新?
  2. 此代码是否与放置方法级同步一样有效?我可以想象,随着我们所做工作量的增加(这里只是一个加法/减法),它可能会导致方法级同步出现严重的性能问题。
  3. 能否让这段代码更高效?

【问题讨论】:

  • "balance_应该是double而不是整数" 不,建议不要使用浮点类型来表示货币。
  • 始终使用 long 或 BigInteger 作为货币,从不加倍。 1.20 欧元 = 120。
  • @dit 我会说 long 或 BigDecimal。如果您使用 long 来表示美分,则 2^63 将 more 绰绰有余。例如,以美分计算的世界 GDP 仅为 2^46 左右。如果你的客户账户里的钱多于他们的账户,我会少担心 BigInteger,而多担心猖獗的通货膨胀会破坏全球经济。 ;)

标签: java multithreading


【解决方案1】:

任何不在同步块内的代码都可以由多个线程并发执行,您的解决方案是在同步块之外创建新的平衡,因此它无法正常工作。我们来看一个例子:

int copy = balance_; // 1

copy += amount; //2

synchronized(this) {
   balance_ = copy; // 3
}
  1. 程序启动时,我们有 _balance = 10
  2. 然后我们启动 2 个线程,尝试将 10 和 15 添加到余额中
  3. 线程 1 将 10 分配给变量 copy
  4. 线程 2 将 10 分配给变量 copy
  5. 线程 2 加 15 复制并将结果分配给 _balance -> 25
  6. 线程1加10复制并赋值给_balance ->20

最后 BankAccount 有 20 但应该是 35

这是正确的制作方法:

public class BankAccount {
    private int balance_;

    public BankAccount(int initialBalance) {
        if (initialBalance < 300) {
            throw new IllegalArgumentException("Balance needs to be at least 300");
        }
        balance_ = initialBalance;
    }

    public void deposit(int amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("Deposit has to be positive");
        }

        synchronized(this) {
            balance_ += amount;
        }
    }

    public void withdraw(int amount) {
        synchronized (this) {
            if (amount > balance_) {
                throw new IllegalArgumentException("Withdrawal has to be <= current balance");
            }

            balance_ -= amount;
        }
    }

    public synchronized int getBalance() {
        return balance_;
    }
}

【讨论】:

    【解决方案2】:

    此代码容易出现竞争条件。

    考虑这部分:

    int copy = balance_;
    copy += amount;
    // here!
    synchronized(this) {
        balance_ = copy; // make the new balance visible to other threads
    }
    

    如果有人在“这里”部分致电withdrawdeposit,会发生什么情况?第二种方法将更改_balance,但该更改不会反映在您本地的copy 中。然后,当您将copy 写入共享变量时,它只会覆盖该值。

    处理这个问题的方法是在排他锁下完成整个操作——读取、修改和写入。或者,您可以使用AtomicInteger,它提供了一个原子incrementAndGet 方法。这通常可以编译为称为"compare and swap" 的硬件原语,因此非常有效。缺点是它只为那个操作提供原子性;如果您需要其他一些操作也是原子的(也许您还想增加 depositCounts 字段?),那么 AtomicInteger 将不起作用。

    【讨论】:

    • 换句话说。将您的公共方法标记为synchronized
    猜你喜欢
    • 2017-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-16
    相关资源
    最近更新 更多