简体   繁体   English

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

[英]Understanding Memory Visibility in Java Multi-threading

I was recently trying to wrap my head around some Java multi-threading concepts and was writing a small piece of code to help me understand memory visibility and getting synchronization as correct as possible. 我最近试图围绕一些Java多线程概念,并编写一小段代码来帮助我理解内存可见性并尽可能正确地进行同步。 Based on what I've read, it seems that the smaller the amount of code we hold a lock around, the more efficient our program will be (in general). 根据我所读到的内容,似乎我们锁定的代码量越小,我们的程序就越有效(通常)。 I've written a small class to help me understand some of the synchronization issues I might run into: 我写了一个小类来帮助我理解可能遇到的一些同步问题:

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_;
    }
}

Please ignore the fact that balance_ should be a double instead of an integer. 请忽略balance_应该是double而不是整数的事实。 I know that primitive type reads/assignments are atomic with the exception of doubles and longs so I opted for ints for simplicity 我知道原始类型的读取/赋值是原子的,除了双精度和长数,所以我选择了简单的int

I've tried to write comments inside of the functions to describe my thinking. 我试图在函数内部写评论来描述我的想法。 This class was written to get correct synchronization as well as minimize the amount of code that's under locking. 编写此类是为了获得正确的同步以及最小化锁定下的代码量。 Here's my questions: 这是我的问题:

  1. Is this code correct? 这段代码是否正确? Will it ever run into any data / race conditions? 它会遇到任何数据/竞争条件吗? Are all of the updates visible to other threads? 其他线程是否可以看到所有更新?
  2. Is this code just as efficient as just putting a method-level synchronization? 这个代码和方法级同步一样高效吗? I can imagine that as the amount of work we do increases (here, it's just one addition/subtraction), it can cause significant performance issues doe method-level synchronization. 我可以想象,随着我们工作量的增加(这里,它只是一个加法/减法),它可能会导致显着的性能问题,方法级同步。
  3. Can this code be made more efficient? 这段代码可以提高效率吗?

Any code that is not inside the synchronized block can be concurrenlty executed by multiple threads, your solution is creating the new balance outside of a synchronized block so it won't work properly. 任何不在synchronized块内的代码都可以由多个线程统一执行,您的解决方案是在同步块之外创建新的余额,这样它就无法正常工作。 Let's see an example: 我们来看一个例子:

int copy = balance_; // 1

copy += amount; //2

synchronized(this) {
   balance_ = copy; // 3
}
  1. When the program starts we have _balance = 10 当程序启动时,我们有_balance = 10
  2. Then we start 2 threads that are trying to add 10 and 15 to the balance 然后我们开始尝试将10和15添加到余额的2个线程
  3. Thread 1 assigns 10 to the variable copy 线程1将10分配给变量副本
  4. Thread 2 assigns 10 to the variable copy 线程2将10分配给变量副本
  5. Thread 2 adds 15 to copy and assign the result to _balance -> 25 线程2添加15复制并将结果分配给_balance - > 25
  6. Thread 1 adds 10 to copy and assign the result to _balance ->20 线程1添加10复制并将结果分配给_balance - > 20

At the end the BankAccount has 20 but it should be 35 最后,BankAccount有20个,但应该是35个

This is the correct way to make it: 这是正确的方法:

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_;
    }
}

This code is prone to race conditions. 此代码容易出现竞争条件。

Consider this part: 考虑这部分:

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

What happens if someone calls withdraw or deposit during the "here" section? 如果有人在“此处”部分呼叫withdrawdeposit ,会发生什么? That second method will change _balance , but that change won't be reflected in your local copy . 第二种方法将改变_balance ,但该更改不会反映在本地copy When you then write copy to the shared variable, it will simply overwrite the value. 然后,当您将copy写入共享变量时,它将简单地覆盖该值。

The way to handle this would be to do the whole operation — the read, modification, and write — under the exclusive lock. 处理这个问题的方法是在独占锁下执行整个操作 - 读取,修改和写入。 Alternatively, you could use an AtomicInteger , which provides an atomic incrementAndGet method. 或者,您可以使用AtomicInteger ,它提供原子incrementAndGet方法。 This can often compile down to hardware primitives called "compare and swap" and is thus very efficient. 这通常可以编译为称为“比较和交换”的硬件原语,因此非常有效。 The disadvantage is that it only provides atomicity for that one operation; 缺点是它只为那一个操作提供原子性; if you need some other operation to also be atomic (maybe you also want to increment a depositCounts field?), then AtomicInteger won't work. 如果你还需要一些其他操作也是原子的(也许你还想增加depositCounts字段?),那么AtomicInteger将不起作用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM