有 Java 编程相关的问题?

你可以在下面搜索框中键入要查询的问题!

多线程理解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_uu应该是双精度而不是整数这一事实。我知道除了double和long之外,基元类型的读取/分配是原子的,因此为了简单起见,我选择了int

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

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

共 (2) 个答案

  1. # 1 楼答案

    此代码容易出现竞争条件

    考虑这部分:

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

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

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

  2. # 2 楼答案

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

    int copy = balance_; // 1
    
    copy += amount; //2
    
    synchronized(this) {
       balance_ = copy; // 3
    }
    
    1. 当程序启动时,我们有_balance=10
    2. 然后我们开始两个线程,它们试图向余额中添加10和15
    3. 线程1为变量copy赋值10
    4. 线程2将10分配给变量副本
    5. 线程2添加15以复制结果并将结果分配给_balance->;二十五
    6. 线程1添加10以复制结果并将其分配给_balance->;二十

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