Как на самом деле происходит взаимодействие потоков в классе примера Counter?

Я пытаюсь изучить концепцию взаимодействия потоков и столкнулся со следующим примером в Java Tutorial Oracle:

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

}

В руководстве по Oracle упоминалось, что если два потока пытаются получить доступ к переменной c, это может привести к интерференции потока, когда изменение, сделанное одним потоком, не замечено другим.

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

3 ответа

Решение

Вместо кода я предпочитаю объяснять, что произойдет. Предположим, что 2 потока, A и B обращаются к одному и тому же объекту счетчика, A вызывает приращение и B вызывает уменьшение. Любая из этих операций состоит как минимум из 3 шагов.

  1. Чтение С из памяти
  2. Увеличение или уменьшение C
  3. Запишите обратно С в память.

Когда A и B пытаются увеличить и уменьшить одновременно, один поток может прочитать C из памяти (шаг 1), в то время как другой поток находится на шаге 2. в таких случаях, если начальное значение c было 5, первый поток A считал и увеличил его до 6. Затем поток B прочитал и уменьшил его до 4. Помните, что B делает эти изменения до того, как A заканчивает запись c обратно в память. Поскольку шаги перекрываются, изменения, сделанные одним потоком, не будут видны другому, в результате чего конечное значение c будет равно 6 или 4. Но на самом деле мы ожидали 5.

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

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

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

class Counter {
private int c = 0;

public void increment() {c++;}

public void decrement() {c--;}

public int value() {
    return c;
}

public static void main(String[] args) {

    Counter x = new Counter();
    Runnable r1 = new Runnable() {
        @Override
        public void run() {
            x.increment();
        }
    };

    Runnable r2 = new Runnable() {
        @Override
        public void run() {
            x.decrement();
        }
    };

    Thread t1 = new Thread(r1);
    Thread t2 = new Thread(r2);     
    t1.start();
    t2.start();
    System.out.println(x.c);

}

}

Изменить: я решил добавить несколько потоков случае, я не мог устоять

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

Я использовал Thread.Sleep(). Вызывая Основной поток в спящий режим, который обязательно напечатает c после того, как оба потока завершат работу с ним.

class Counter {
private int c = 0;

public void increment() {
    for (int i = 0; i < 10000; i++) {
        c++;
    }

    }

public void decrement() {
    for (int i = 0; i < 5000; i++) {
        c--;
    }
    }

public int value() {
return c;
}

public static void main(String[] args) {

Counter x = new Counter();
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        x.increment();
    }
};

Runnable r2 = new Runnable() {
    @Override
    public void run() {
        x.decrement();
    }
};

Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);     
t1.start();
t2.start();
try {
    Thread.sleep(2000);
} catch (InterruptedException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}
if(!(t1.isAlive() && t2.isAlive()))
System.out.println(x.c);//expected answer 5000
}
}

Примечание. Синхронизированные методы увеличения / уменьшения дают правильный ответ. Попробуйте сами.

Создать несколько потоков и вызвать increment(), decrement() а также value() из этих тем.

Пример кода будет выглядеть так:

class Counter {
    private int c = 0;

    public void increment() {
        c++;
    }

    public void decrement() {
        c--;
    }

    public int value() {
        return c;
    }

    public static void main(String args[]){
        Counter c = new Counter();
        for ( int i=0; i<3; i++){
            Thread t = new Thread(new MyRunnable(c));
            t.start();
        }
    }
}
class MyRunnable implements Runnable{
    Counter counter;
    public MyRunnable(Counter c){
        counter = c;
    }
    public void run(){
        counter.increment();
        System.out.println("Counter value after increment:"+counter.value()+" from thread:"+  Thread.currentThread().getName());
        counter.decrement();
        System.out.println("Counter value after decrement:"+counter.value()+" from thread:"+  Thread.currentThread().getName());
    }
}

вывод: (Этот вывод будет меняться для каждого прогона)

Counter value after increment:1 from thread:Thread-0
Counter value after decrement:2 from thread:Thread-0
Counter value after increment:2 from thread:Thread-2
Counter value after decrement:1 from thread:Thread-2
Counter value after increment:3 from thread:Thread-1
Counter value after decrement:0 from thread:Thread-1

Теперь по выходным данным вы можете понять помехи потоков. Чтобы исправить эту проблему, вы можете использовать AtomicInteger.

Посмотрите на пост ниже для более подробной информации:

Почему синхронизация не работает должным образом?

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