Поток небезопасного уменьшения / увеличения - почему в основном положительный?

Я задаюсь вопросом о результате небезопасного уменьшения / увеличения в потоках Java, поэтому есть моя программа:

Основной класс:

public class Start {

    public static void main(String[] args) {

        int count = 10000000, pos = 0, neg = 0, zero = 0;

        for (int x=0; x<10000; x++) {

            Magic.counter = 0;

            Thread dec = new Thread(new Magic(false, count));
            Thread inc = new Thread(new Magic(true, count));

            dec.start();
            inc.start();

            try {
                inc.join();
                dec.join();
            } catch (InterruptedException e) {
                System.out.println("Error");
            }

            if (Magic.counter == 0)
                zero++;
            else if (Magic.counter > 0)
                pos++;
            else
                neg++;
        }

        System.out.println(Integer.toString(neg) + "\t\t\t" + Integer.toString(pos) + "\t\t\t" + Integer.toString(zero));
    }
}

Класс темы:

public class Magic implements Runnable {

    public static int counter = 0;

    private boolean inc;
    private int countTo;

    public Magic(boolean inc, int countTo) {
        this.inc = inc;
        this.countTo = countTo;
    }

    @Override
    public void run() {

        for (int i=0;i<this.countTo;i++) {

            if (this.inc)
                Magic.counter++;
            else
                Magic.counter--;
        }

    }
}

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

Number of results < 0 | Number of results > 0 | Number of results = 0

1103                8893                4
3159                6838                3
2639                7359                2
3240                6755                5
3264                6728                8
2883                7112                5
2973                7021                6
3123                6873                4
2882                7113                5
3098                6896                6

2 ответа

Решение

Бьюсь об заклад, вы увидите совершенно противоположное поведение со следующим изменением (то есть, переверните ветви, не меняя ничего другого):

if (this.inc)
   Magic.counter--; // note change, and lie about `this.inc`
else
   Magic.counter++;

Если это правда, что это может указывать на это, указывает на взаимодействие потоков?

Теперь, для удовольствия, сделать Magic.counter volatile - [как] меняются результаты?

Как насчет удаления volatile и окружающие if/else с lock? (A lock обеспечивает полный забор памяти и устанавливает критическую область. Это всегда должно давать идеальные результаты.)

Удачного кодирования.


Что нужно учитывать:

  1. Код только смотрит на значение меньше или больше нуля, а не на общее отклонение / отклонение: все, что нужно, чтобы склонить чашу весов, - это +1 или -1. (Это может быть более полезным для расширения собранных данных.)
  2. Требуется немного больше времени для выполнения ветви "else", поскольку требуется переход; обычно это не проблема, но более 10 миллионов циклов... один или два не так много.
  3. Отсутствие летучих заборов / ограждений оставляет значительную слабость в видимости Magic.counter переменная. (Я считаю, что соответствующая JVM может дать гораздо худшие результаты...)
  4. ++ а также -- операторы по своей природе не атомарны.
  5. Чередование потоков обычно "недетерминировано"; меньше, если выполняется на нескольких ядрах.

Вообще говоря, это связано с тем, как работает модель памяти Java. Вы получаете доступ к общим переменным в двух разных потоках без синхронизации. Ни переменная не объявлена ​​как volatile, ни вы не выполняете операции Atomar. Отсутствие координационных и атомарных или изменчивых переменных приведет к внутренней оптимизации многопоточного кода, выполняемого JVM при выполнении. Кроме того, энергонезависимые переменные, которые не пересекли барьер памяти (т.е. synchronized) приведет к кэшируемым значениям для каждого потока и, следовательно, к двум конфликтующим локальным копиям потоков внутри их кэшей.

Принимая во внимание отсутствие последовательной модели согласованности в Java, сложные оптимизации во время выполнения и особенности используемой JVM и базовой системы (одноядерная или многоядерная, гиперпоточность), невозможно предсказать результат детерминистически - только потому, что он нарушает несколько -предметные соглашения для модели языка Java. Выполнение точно такого же кода на той же машине может привести к аналогичным результатам, но из-за эффектов планирования потоков, использования ЦП другими процессами ОС и т. Д. Они вряд ли будут точно такими же.

Вот некоторые ресурсы о JMM: http://www.cs.umd.edu/~pugh/java/memoryModel/

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