Иллюстрирует изменчивость: является ли этот код потокобезопасным?

Я пытаюсь проиллюстрировать использование и важность volatile с примером, который действительно не даст хорошего результата, если volatile был опущен.

Но я не очень привык к volatile, Идея следующего кода состоит в том, чтобы вызвать бесконечный цикл, если volatile опущен, и быть полностью поточно-ориентированным, если volatile настоящее. Является ли следующий код потокобезопасным? У вас есть какой-нибудь другой реалистичный и короткий пример кода, который использует volatile и дал бы заведомо неверный результат без него?

Вот код:

public class VolatileTest implements Runnable {

    private int count;
    private volatile boolean stopped;

    @Override
    public void run() {
        while (!stopped) {
            count++;
        }
        System.out.println("Count 1 = " + count);
    }

    public void stopCounting() {
        stopped = true;
    }

    public int getCount() {
        if (!stopped) {
            throw new IllegalStateException("not stopped yet.");
        }
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileTest vt = new VolatileTest();
        Thread t = new Thread(vt);
        t.start();
        Thread.sleep(1000L);
        vt.stopCounting();
        System.out.println("Count 2 = " + vt.getCount());
    }
}

6 ответов

Решение

Виктор прав, есть проблемы с вашим кодом: атомарность и видимость.

Вот мое издание:

    private int count;
    private volatile boolean stop;
    private volatile boolean stopped;

    @Override
    public void run() {
        while (!stop) {
            count++; // the work
        }
        stopped = true;
        System.out.println("Count 1 = " + count);
    }

    public void stopCounting() {
        stop = true;
        while(!stopped)
           ; //busy wait; ok in this example
    }

    public int getCount() {
        if (!stopped) {
            throw new IllegalStateException("not stopped yet.");
        }
        return count;
    }

}

Если поток замечает, что stopped==trueГарантируется, что работа завершена и результат виден.

Существует отношение "происходит до" из volatile-записи в volatile-чтение (по одной и той же переменной), поэтому, если есть два потока

   thread 1              thread 2

   action A
       |
 volatile write  
                  \
                     volatile read
                          |  
                       action B

действие A происходит до действия B; записи в A видны B.

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

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

Итак, вот версия, которая действительно показывает разницу:

public class VolatileExample implements Runnable {
    public static boolean flag = true; // do not try this at home

    public void run() {
        long i = 0;
        while (flag) {
            if (i++ % 10000000000L == 0)
                System.out.println("Waiting  " + System.currentTimeMillis());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new VolatileExample());
        thread.start();
        Thread.sleep(10000L);
        flag = false;
        long start = System.currentTimeMillis();
        System.out.println("stopping " + start);
        thread.join();
        long end = System.currentTimeMillis();
        System.out.println("stopped  " + end);
        System.out.println("Delay: " + ((end - start) / 1000L));
    }
}

Простой прогон показывает:

Waiting  1319229217263
stopping 1319229227263
Waiting  1319229242728
stopped  1319229242728
Delay: 15

То есть работающему потоку требуется более десяти секунд (15 здесь), чтобы заметить какие- либо изменения.

С volatile, у тебя есть:

Waiting  1319229288280
stopping 1319229298281
stopped  1319229298281
Delay: 0

то есть, выход (почти) немедленно. Разрешение currentTimeMillis составляет около 10 мс, поэтому разница составляет более 1000 раз.

Обратите внимание, что это была версия Apple (бывшая)Sun JDK, с -server вариант. Было добавлено 10-секундное ожидание, чтобы JIT-компилятор узнал, что цикл достаточно горячий, и оптимизировал его.

Надеюсь, это поможет.

Упрощение примера @Elf далее, где другой поток никогда не получит значение, которое было обновлено другим потоком. Удаление System.out.println, поскольку внутри println и out есть синхронизированный код, является статическим, что помогает другому потоку получить последнее значение переменной-флага.

public class VolatileExample implements Runnable {
   public static boolean flag = true; 


  public void run() {
     while (flag);
  }

  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(new VolatileExample());
    thread.start();
    Thread.sleep(1000L);
    flag = false;
    thread.join();
  }
}

Чтобы проиллюстрировать важность volatile ключевое слово, когда дело доходит до параллелизма, все, что вам нужно сделать, это убедиться, что volatile Поле модифицируется и читается в отдельных темах.

Неверный код, с которым мы не можем принять x = 1 также, если y уже равен 2:

Class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}

Пример использования изменяемого ключевого слова:

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }

  public void reader() {
    if (v == true) {
      //uses x - guaranteed to see 42.
    }
  }
}

Источник: http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html

ОБНОВЛЕНИЕ Мой ответ неверный, см. Ответ от неопровержимого.


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

Видимость count значение для основного потока обеспечивается проверкой stopped летучий внутри getCount метод. Это то, что называется piggybacking on synchronization в Concurrency in practice книга.

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