了解Java多线程中的内存可见性



我最近试图了解一些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_应该是双精度而不是整数这一事实。我知道除了doubles和long之外,基元类型的读取/赋值都是原子的,所以为了简单起见,我选择了int。

我试着在函数内部写评论来描述我的想法。编写此类是为了获得正确的同步,并最大限度地减少被锁定的代码量。以下是我的问题:

  1. 这个代码正确吗?它会遇到任何数据/比赛条件吗?所有更新对其他线程可见吗
  2. 这段代码是否和进行方法级同步一样有效?我可以想象,随着我们所做的工作量的增加(这里,它只是一个加法/减法),它可能会导致doe方法级同步的显著性能问题
  3. 这个代码能提高效率吗

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

int copy = balance_; // 1
copy += amount; //2
synchronized(this) {
   balance_ = copy; // 3
}
  1. 当程序启动时,我们有_balance=10
  2. 然后我们启动两个线程,试图将10和15添加到平衡中
  3. 线程1为变量副本指定10
  4. 线程2为变量副本指定10
  5. 线程2添加15以复制并将结果分配给_balance->25
  6. 线程1添加10以复制并将结果分配给_balance->20

最后,银行账户有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_;
    }
}

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

考虑这部分:

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

如果有人在"此处"部分呼叫withdrawdeposit,会发生什么?第二种方法将更改_balance,但这种更改不会反映在本地copy中。然后将copy写入共享变量时,它将简单地覆盖该值。

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

最新更新