Дайте мне сценарий, где такой вывод может произойти, когда происходит многопоточность - PullRequest
1 голос
/ 23 октября 2019

Просто пытаюсь понять потоки и состояние гонки и то, как они влияют на ожидаемый результат. В приведенном ниже коде у меня когда-то был вывод, который начинался с «2 Thread-1», а затем «1 Thread-0» .... Как такой вывод мог произойти? Я понимаю следующее:

Шаг 1: Предполагая, что поток 0 запущен, счетчик увеличивается до 1,

Шаг 2: Перед печатью поток 1 увеличивает его до 2 и печатает,

Шаг 3: Счетчик нити 0 печатает счетчик, который должен быть 2, но печатает 1.

Как можно считать счетчик Нити 0 как 1, если поток 1 уже увеличил его до 2?

PS:Я знаю, что синхронизированный ключ мог бы справиться с такими условиями гонки, но я просто хочу сделать некоторые концепции раньше.

public class Counter {
    static int count=0;
    public void add(int value) {
            count=count+value;
            System.out.println(count+" "+ Thread.currentThread().getName());        
    }
}
public class CounterThread extends Thread {
    Counter counter;
    public CounterThread(Counter c) {
        counter=c;
    }
    public void run() {
        for(int i=0;i<5;i++) {
            counter.add(1);
        }
    }
}
public class Main {
    public static void main(String args[]) {
        Counter counter= new Counter();
        Thread t1= new CounterThread(counter);
        Thread t2= new CounterThread(counter);
        t1.start();
        t2.start();
    }
}

1 Ответ

1 голос
/ 23 октября 2019

Как мог поток 0 печатать счетчик как 1, когда поток 1 уже увеличил его до 2?

В этих двух строках происходит гораздо больше, чем кажется на первый взгляд:

count=count+value;
System.out.println(count+" "+ Thread.currentThread().getName());

Прежде всего, компилятор ничего не знает о потоках. Его работа состоит в том, чтобы выдавать код, который будет иметь тот же конечный результат при выполнении в одном потоке. То есть, когда все сказано и сделано, счетчик должен быть увеличен, и сообщение должно быть напечатано.

Компилятор имеет большую свободу для переупорядочения операций и сохранения значений во временных регистрах вчтобы гарантировать, что правильный конечный результат достигнут наиболее эффективным способом. Так, например, count в выражении count+" "+... не обязательно заставит компилятор получить последнее значение глобальной переменной count. Фактически, он, вероятно, не извлекает из глобальной переменной, потому что знает, что результат операции + все еще находится в регистре процессора. И, поскольку он не признает, что могут существовать другие потоки, он знает , что значение в регистре не может отличаться от значения, сохраненного в глобальной переменной после выполнения +.


Во-вторых, самому оборудованию разрешено хранить значения во временных местах и ​​переупорядочивать операции для повышения эффективности, а также допускать, что других потоков нет. Таким образом, даже когда компилятор выдает код, который говорит, что он фактически выбирает или сохраняет глобальную переменную вместо или из регистра, аппаратное обеспечение не обязательно сохраняет или выбирает фактический адрес в памяти.


Предполагая, что вашим примером кода является код Java, все это меняется, когда вы правильно используете блоки synchronized. Если вы добавите synchronized к объявлению вашего метода add, например:

public synchronized void add(int value) {
    count=count+value;
    System.out.println(count+" "+ Thread.currentThread().getName());        
}

Это заставит компилятор признать существование других потоков, и компилятор будет выдавать инструкции, которые заставляют аппаратное обеспечениераспознавать и другие потоки.

Добавляя synchronized к методу add, вы заставляете аппаратное обеспечение доставлять фактическое значение глобальной переменной при входе в метод, вы заставляете его фактически записыватьглобальный к моменту возврата метода, и вы предотвращаете одновременное нахождение в методе нескольких потоков.

...