Иллюстрирует изменчивость: является ли этот код потокобезопасным?
Я пытаюсь проиллюстрировать использование и важность 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
книга.