Понимание взаимодействия потоков Java

Я изучаю Java, работаю с Python и пытаюсь понять взаимодействие потоков, начиная с кода и пояснений на этой странице: http://docs.oracle.com/javase/tutorial/essential/concurrency/interfere.html

Чтобы воспроизвести помехи, у меня есть еще один класс, который запускает три потока, каждый из которых случайным образом вызывает либо увеличение, либо уменьшение 10 раз.

Я ожидаю, что с 3 потоками и 30 приращениями или уменьшениями некоторые из них будут перекрываться, и, таким образом, окончательное значение Counter не будет равно (# increments) - (# decrements).

Но каждый раз, когда я запускаю код и анализирую результат, я нахожу окончательное значение равным (# increments) - (# decrements). Хотя возможно, что после 5 прогонов я каким-то образом не получил никаких помех, гораздо более вероятно, что я неправильно понял эффект помех или непреднамеренно реализовал код, который избегает помех.

Вот мой код:

// file: CounterThreads.java
public class CounterThreads {
    private static class CounterThread implements Runnable {
        private Counter c;

        CounterThread(Counter c)
        {
            this.c = c;
        }

        public void run()
        {
            String threadName = Thread.currentThread().getName();
            for (int i=0; i<10; i++) {
                try {

                    if (((int)(Math.random() * 10) % 2) == 0) {
                        System.out.format("%s - Decrementing...\n", threadName);
                        c.decrement();
                    } else {
                        System.out.format("%s - Incrementing...\n", threadName);
                        c.increment();
                    }
                    System.out.format("%s - The internal counter is at %s\n", threadName, c.value());
                    Thread.sleep(1000);

                } catch (InterruptedException e) {
                    System.out.format("Thread %s interrupted\n", threadName);
                }
            }
        }
    }

    public static void main(String[] args)
    {
        Counter c = new Counter();
        for (int  i=0; i<3; i++) {
            Thread t = new Thread(new CounterThread(c));
            System.out.format("Starting Thread: %s\n", t.getName());
            t.start();
        }
    }
}

Файл Counter.java содержит код, скопированный из приведенной выше документации Oracle, воспроизведенный здесь для удобства.

// file: Counter.java
public class Counter {
    private int c = 0;

    void increment ()
    {
        c++;
    }

    void decrement()
    {
        c--;
    }

    int value()
    {
        return c;
    }
}

person Haleemur Ali    schedule 09.10.2016    source источник
comment
Предложение: начните с запуска одного потока и посмотрите, получите ли вы что-нибудь еще, кроме 0 - если вы этого не сделаете, значит, что-то не так с вашим подходом к обнаружению.   -  person Adrian Colomitchi    schedule 10.10.2016
comment
Чтобы увидеть помехи, вы должны создать фактическое параллельное выполнение. Написанный вами код выполняет увеличение или уменьшение за наносекунды, а затем засыпает на целую секунду. Вероятность того, что два потока будут отправлены в одну и ту же наносекунду, ничтожно мала. Вам нужно написать код, который выполняет увеличение и уменьшение миллионы раз, в тесном цикле, в нескольких потоках одновременно, и проверяет неожиданное значение после каждой итерации. т.е. получить локальную копию значения потока, увеличить/уменьшить копию, увеличить/уменьшить значение, сравнить.   -  person Jim Garrison    schedule 10.10.2016
comment
пока сложение и вычитание являются атомарными операциями, значение Counter всегда будет равно (# increments) - (# decrements), так что в этом нет ничего удивительного   -  person mangusta    schedule 10.10.2016
comment
однако, если вы обновите счетчик в каком-то определенном потоке (скажем, в потоке1), а затем попытаетесь вывести его значение, он может быть обновлен другим потоком прямо перед операцией вывода, поэтому вывод будет несогласованным в рамках потока1.   -  person mangusta    schedule 10.10.2016
comment
То же самое они сказали (выше): Избавьтесь от sleep() звонков. Кроме того, вместо того, чтобы каждый поток выполнял десять операций, пусть каждый поток выполняет десять миллионов.   -  person Solomon Slow    schedule 10.10.2016
comment
Я думаю, что материала по этому поводу предостаточно.   -  person Derrops    schedule 10.10.2016
comment
Всем спасибо за комментарии. @mangusta, спасибо за то, что должно было быть очевидным в отношении # inc - # dec == final counter value и в чем могло бы проявиться вмешательство.   -  person Haleemur Ali    schedule 10.10.2016
comment
Если вы хотите увидеть только ошибку ACID, убедитесь, что атомарная операция занимает некоторое время. Подобно копированию массива из нескольких миллионов записей, увеличивайте каждую запись и записывайте ее обратно, а затем проверяйте, соответствует ли количество приращений количеству потоков. То, что вы обнаружили, является самой большой проблемой из всех, ошибки, которые могут не возникать в тесте.   -  person Aziuth    schedule 10.10.2016


Ответы (1)


Для воспроизведения необходимо максимизировать вероятность увеличения и/или уменьшения счетчика одновременно (примечание: это непростая задача, поскольку увеличение/уменьшение счетчика — очень быстрая работа), что не относится к вашему текущему коду, потому что:

  1. Вы не используете какой-либо механизм для синхронизации потоков перед увеличением/уменьшением счетчика.
  2. Вы печатаете свои сообщения в стандартном потоке вывода слишком часто и в неправильном месте, что является проблемой, когда вы знаете, что PrintStream является потокобезопасным и использует встроенную блокировку для предотвращения одновременного доступа, что снижает вероятность для одновременного увеличения и/или уменьшения вашего счетчика.
  3. Вы добавляете бесполезный долгий сон, который еще раз снижает вероятность получения одновременных модификаций вашего счетчика.
  4. Вы не используете столько потоков, сколько можете.

Итак, ваш код должен быть немного переписан, чтобы исправить предыдущие проблемы.

Чтобы исправить № 1, вы можете использовать CyclicBarrier< /a> чтобы убедиться, что все ваши потоки достигают одной и той же точки барьера (расположенной непосредственно перед увеличением/уменьшением вашего счетчика), прежде чем идти дальше.

Чтобы исправить № 2, я бы рекомендовал сохранить только одно сообщение после увеличения/уменьшения вашего счетчика.

Чтобы исправить № 3, я бы просто удалил его, так как он все равно бесполезен.

Чтобы исправить № 4, я бы использовал Runtime.getRuntime().availableProcessors() в качестве количества используемых потоков, поскольку он будет использовать столько процессоров, сколько у вас есть на вашей локальной машине, чего должно быть достаточно для такой задачи.

Таким образом, окончательный код может быть таким:

Счетчик

public class Counter {
    private final CyclicBarrier barrier;
    private int c;

    public Counter(int threads) {
        this.barrier = new CyclicBarrier(threads);
    }

    void await() throws BrokenBarrierException, InterruptedException {
        barrier.await();
    }
    ...
}

Метод main

public static void main(String[] args) {
    int threads = Runtime.getRuntime().availableProcessors();
    Counter c = new Counter(threads);
    for (int  i=0; i<threads; i++) {
        ...
    }
}

Цикл for метода run

try {
    // Boolean used to know if the counter has been decremented or not
    // It has been moved before the await to avoid doing anything before
    // incrementing/decrementing the counter
    boolean decrementing = (int)(Math.random() * 10) % 2 == 0;
    // Wait until all threads reach this point
    c.await();
    if (decrementing) {
        c.decrement();
    } else {
        c.increment();
    }
    // Print the message
    System.out.format(
        "%s - The internal counter is at %d %s\n", 
        threadName, c.value(), decrementing ? "Decrementing" : "Incrementing"
    );

} catch (Exception e) {
    System.out.format("Thread %s in error\n", threadName);
}
person Nicolas Filotto    schedule 10.10.2016
comment
Спасибо, я не совсем понял CyclicBarrier, но я реализовал ваши фрагменты и ясно вижу помехи. - person Haleemur Ali; 10.10.2016
comment
Я не вижу, как вызывается Counter.await, то есть где используется барьер. Я попытался добавить эту строку в Counter.await непосредственно перед barrier.await();, System.out.println("Calling Barrier");, но ничего не выводится на консоль. - person Haleemur Ali; 11.10.2016