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

时间:2016-08-02 21:03:21

标签: java multithreading

我最近试图围绕一些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_应为double而不是整数的事实。我知道原始类型的读取/赋值是原子的,除了双精度和长整数之外所以我选择了简单的整数

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

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

2 个答案:

答案 0 :(得分:2)

任何不在synchronized块内的代码都可以由多个线程统一执行,您的解决方案是在同步块之外创建新的余额,这样它就无法正常工作。让我们看一个例子:

int copy = balance_; // 1

copy += amount; //2

synchronized(this) {
   balance_ = copy; // 3
}
  1. 当程序启动时,我们有_balance = 10
  2. 然后我们开始尝试将10和15添加到余额中的2个线程
  3. 线程1将10分配给变量副本
  4. 线程2将10分配给变量副本
  5. 线程2添加15复制并将结果分配给_balance - &gt; 25
  6. 线程1添加10以复制并将结果分配给_balance - &gt; 20
  7. 最后,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_;
        }
    }
    

答案 1 :(得分:1)

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

考虑这一部分:

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将不起作用。