Увеличивая статическую переменную через 100 различных потоков без синхронизации, но получая конечный результат как 100

Я увеличиваю статическую переменную через 100 различных потоков без синхронизации, но получаю конечный результат как 100. Я выполнил этот код несколько раз и получил тот же результат. Разве мой код не требует синхронизации? Я использую BlueJ IDE для запуска кода

    public class Main {
        private final static int MAX_THREADS = 100;
        public static void main(String[] args) {
            Thread[] threads = new Thread[MAX_THREADS];

            for(int i=0; i<MAX_THREADS; i++) {
                threads[i] = new Thread(new Job(), "Thread-" + i);
                threads[i].start();
                try{
                    Thread.sleep((int)(Math.random() * 1000));
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }

            for(int i=0; i<MAX_THREADS; i++) {
                try {
                    threads[i].join();
                }catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.printf("Final Value: %d\n", Job.getSuccessCount());
        }

    }

    public class Job implements Runnable {
        private  static int successCount;

        public  static int getSuccessCount() {return successCount;}

        @Override
        public void run() {
            System.out.printf("%s: Incrementing successCount %d\n", Thread.currentThread().getName(), successCount);
            try{
                Thread.sleep((int)(Math.random() * 10000));
            }catch(InterruptedException e) {
                e.printStackTrace();
            }
            successCount++;
            System.out.printf("%s: Incrementing Complete %d\n", Thread.currentThread().getName(), successCount);
        }
    }

4 ответа

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

Ваш код в настоящее время не нуждается в синхронизации, так как никакие два шага не обращаются к одной и той же переменной одновременно. Другими словами, только 1 поток в вашем приложении увеличивает значение переменной.

В этом случае это связано с тем, что приращение переменной занимает меньше, чем Math.random() *1000. Почему это так? Давайте посмотрим на темы:

Основная тема:

  1. Запускает и запускает поток
  2. Выполняет как Math.random (), так и Thread.sleep().
  3. Снова петли

Пока основной поток выполняет шаг 2, новый поток:

  1. Инкрементная переменная
  2. Идти спать

В этом случае, как только новый поток переходит в спящий режим, он сразу после этого просто уничтожается, поэтому для нашей цели мы можем считать, что поток завершается сразу после шага 1, так как перестает влиять на переменную (он не влияет на переменная после шага 1).

Чтобы возникла проблема синхронизации, два новых потока должны получить доступ к переменной одновременно. Чтобы это произошло, основной поток должен запустить новый поток, прежде чем первый новый поток завершит увеличение. Чтобы это произошло, основной поток должен быть быстрее: выполнять Math.random(), Thread.sleep() и создавать новый поток, все до того, как другой поток завершит увеличение. Это, очевидно, не тот случай, и, таким образом, никакие 2 потока не будут увеличиваться сразу, и никакой ошибки синхронизации не произойдет.

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

Добавление к ответу Вомбата. Окончательный результат всегда будет равен 100, потому что вы выполняете унарную операцию после сна в Job учебный класс. По сути, команды чтения-изменения-записи могут выполняться последовательно для каждого задания, если планировщик Java не изменил состояние потока при выполнении следующего.

successCount++

Но если вы измените Job Исходный код для чтения-сна-изменения-записи, тогда вы обязательно увидите устаревшее значение следующим образом.

public class Job implements Runnable {
    private  static int successCount;

    public  static int getSuccessCount() {return successCount;}

    @Override
    public void run() {
        System.out.printf("%s: Incrementing successCount %d\n", Thread.currentThread().getName(), successCount);
        int sc = successCount; // Read
        try{
            Thread.sleep((int)(Math.random() * 10000)); // Sleep
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
        successCount = sc++; // Modify-Write
        System.out.printf("%s: Incrementing Complete %d\n", Thread.currentThread().getName(), successCount);
    }
}

При этом 2 потока могут читать, а затем спать, а затем проснуться и записать то же значение в successCount перезаписывая первоначальное значение

Другие вопросы по тегам