Статьи

Но это работает на моей машине …

Что происходит, когда мы запускаем main()фрагмент кода ниже? Конечно, эрудированные, технически подкованные люди Java могут ответить на это в одно мгновение.

public class TestVolatile implements Runnable {
private boolean stopRequested = false;

public void run() {
while(!stopRequested) {
// do something here...
}
}

public void stop() {
stopRequested = true;
}

public static void main(String[] args) throws InterruptedException {
TestVolatile tv = new TestVolatile();
new Thread(tv, "Neverending").start();
Thread.sleep(1000);
tv.stop();
}
}

Ответ в том, что мы не знаем наверняка, когда программа завершится, потому что stopRequestedпеременная не помечена volatile. Поэтому, когда мы вызываем stop()основной поток, Neverendingпоток, возможно, никогда не увидит, что stopRequestedон изменился на true, и он просто продолжает идти… и идет… и идет… точно так же, как кролик Energizer . Мы никогда не знаем, когда это остановится.

Но… на моей машине все работает отлично

Мой друг не был убежден, хотя, просто чтобы доказать это ему, я напечатал это в «Затмении» и запустил его, полностью ожидая, что он будет работать вечно.

Это остановилось примерно через одну секунду. Хммм. Weird.

Кажется , что изменения , сделанные из основного потока stopRequestedявляется сразу виден из Neverendingпотока, хотя stopRequestedне является volatile.

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

import java.util.concurrent.TimeUnit;

public class TestVolatile implements Runnable {
	private boolean stopRequested = false;

	private volatile long justAfterStopRequested;
	private volatile long afterNeverendingHasStopped;
	public void run() {
		while(!stopRequested) {
			// do something
		}
		afterNeverendingHasStopped = System.currentTimeMillis();
	}

	public void stop() {
		stopRequested = true;
		justAfterStopRequested = System.currentTimeMillis();
	}

	public static void main(String[] args) throws InterruptedException {
		TestVolatile tv = new TestVolatile();
		Thread t = new Thread(tv, "Neverending");
		t.start();
		// let main thread sleep for 1 second before requesting
		// Neverending to stop
		TimeUnit.SECONDS.sleep(1);
		tv.stop();
		// wait until Neverending has stopped
		t.join();
		System.out.println(tv.afterNeverendingHasStopped - tv.justAfterStopRequested);
	}
}

Я пытаюсь увидеть, сколько времени Neverendingпоток должен осознать, что stopRequestedизменился на true, и выйти из цикла. Результат 0 .

Но это не может быть мгновенным — должна быть какая-то разница во времени. Поэтому я изменил System.currentTimeMillis()звонки на System.nanoTime(). Результат практически тот же, в диапазоне от -300 до +300 наносекунд (мы не можем сказать, какой из них модифицируется первым, justAfterStopRequestedили afterNeverendingHasStopped— он отличается при каждом запуске). Но для всех практических целей мы можем сказать, что Neverendingпоток видит изменения stopRequestedпочти сразу.

Странный.

В эффективной Java, 2-е изд. , Джошуа Блох говорит в своей машине такой программы никогда не перестает работать. Не то чтобы у меня была проблема с самооценкой или что-то в этом роде, но когда дело доходит до Явы, если мне приходится выбирать, доверять ли мне или Джошуа Блоху, я выбираю первое. Извини меня.

На другой машине, однако …

Но вы знаете, что? Я обнаружил, что TestVolatile всегда работал на Solaris! (Ну, он работал до тех пор, пока я не вернулся через 15 минут и все равно его убил.) Это из-за различий между Windows и Solaris? Не совсем — речь идет о различиях между виртуальной машиной сервера и виртуальной машиной клиента. Моя тестовая платформа Solaris является машиной серверного класса , поэтому по умолчанию я использовал виртуальную машину сервера. В то время как на моей машине с Windows по умолчанию я запускал клиентскую виртуальную машину.

Действительно, когда я снова запустил программу на Solaris с клиентской виртуальной машиной (с -clientвозможностью), она остановилась примерно через секунду, не делая stopRequestedэнергозависимой. И наоборот, когда я запускал программу в Windows с этой -serverопцией, она никогда не прекращалась и остановилась только в том случае, если я установил stopRequestedэнергозависимость.

Это показывает, что клиентская виртуальная машина может обмануть нас, заставляя думать, что мы не нуждаемся в энергозависимом устройстве (пока мы не запустим нашу программу на компьютере серверного класса и все начнёт ломаться странным образом). Поверхностно, клиент VM и сервер VM может звучать как они не что разные. Но некоторые различия имеют значение: то, что мы видим здесь, является реальным примером того, как различия между клиентской виртуальной машиной и серверной виртуальной машиной могут создавать или разрушать ваше приложение.

И это еще не все. Есть еще одна неочевидная вещь, которая может маскировать потребность в энергозависимости в наших программах. Как System.out.println(), например.

Скрытые синхронизированные блоки иногда скрывают потребность в энергозависимых

Если мы хотим напечатать переменную счетчика в цикле следующим образом:

public class TestVolatile implements Runnable {
	private boolean stopRequested = false;
	private int i = 0;

	public void run() {
		while(!stopRequested) {
			System.out.println(i++);
		}
	}

	public void stop() {
		stopRequested = true;
	}

	// the rest
}

Тогда программа будет всегда завершаться, даже если мы не помечаем stopRequestedкак volatile. Зачем? Потому что внутри есть синхронизированный блок System.out.println(). Когда поток, который выполняет run()метод, вызывает System.out.println(), его копия stopRequestedобновляется с последним значением, и цикл while завершается.

Обратите внимание, что это НЕ гарантируется моделью памяти Java . JMM гарантирует видимость между двумя потоками, которые входят и выходят из синхронизированных блоков, защищенных одной и той же блокировкой . Если синхронизированные блоки защищены различными блокировками, то единственное безопасное предположение — предположить, что синхронизации вообще нет.

Эта программа ниже останавливается,

public class TestVolatile implements Runnable {
	private boolean stopRequested = false;
	private int i = 0;

	private final Object lock1 = new Object();
	private final Object lock2 = new Object();

	public void run() {
		while(!stopRequested) {
			synchronized(lock1) {}
			i++;
		}
	}

	public void stop() {
		stopRequested = true;
		synchronized(lock2) {}
	}

	// the rest
}

даже если два потока входят в синхронизированные блоки, защищенные различными блокировками. Мы даже можем удалить синхронизированный блок, охраняемый lock2in stop(), и программа все равно останавливается. Но не наоборот (то есть, если мы удалим синхронизированный блок lock1 и оставим там lock2, снова программа будет работать вечно).

Поэтому кажется, что поток, который читает переменную, получает актуальное значение при выполнении синхронизированного блока, независимо от блокировки, используемой для защиты блока. Даже если переменная не является изменчивой. Это означает, что возможно иметь код, который работал, когда вы System.out.println()разбрасывали вызовы, внезапно перестает работать правильно, когда вы удаляете эти вызовы!

Влияет ли изменчивый каскад на переменные-члены? Элементы массива? Предметы в коллекциях?

Теперь, скажем, вместо примитивного логического значения, stopRequestedэто переменная-член другого класса, например:

import java.util.concurrent.TimeUnit;

public class TestVolatile implements Runnable {
	private A wrapper = new A();

	public void run() {
		while(!wrapper.stopRequested) {
			// do something
		}
	}

	public void stop() {
		wrapper.stopRequested = true;
	}

	public static void main(String[] args) throws InterruptedException {
		TestVolatile tv = new TestVolatile();
		new Thread(tv, "Neverending").start();
		// let main thread sleep for 1 second before requesting
		// Neverending to stop
		TimeUnit.SECONDS.sleep(1);
		tv.stop();
	}
}

class A {
	boolean stopRequested = false;
}

Запуск с -serverфлагом, этот никогда не останавливается либо. Но что, если мы пометим обертку как volatile:

	private volatile A wrapper = new A();

Does the “volatility” of the reference cascades to the member variable stopRequested as well? Turned out, looks like the answer is yes. We can either mark wrapper as volatile, or stopRequested as volatile, and the program will terminate in about a second.

I’m not surprised that the program terminates when I mark stopRequested as volatile. But why wrapper as volatile works as well? The same thing happens when we use an ArrayList, like this:

import java.util.ArrayList;
import java.util.List;

public class TestVolatile implements Runnable {
    List<Boolean> wrapper = new ArrayList<Boolean>();
    public TestVolatile() {
        wrapper.add(Boolean.FALSE);
    }

    public void run() {
        while(wrapper.get(0) == Boolean.FALSE) {
            // do something
        }
    }

    public void stop() {
        wrapper.set(0, Boolean.TRUE);
    }

    // the rest of the code...
}

Run as it is, it never stops. If we mark the List<Boolean> wrapper as volatile, it stops pretty fast.

I wonder why? The object reference to that List instance itself never changes. We’re only changing an element within the List, and not the List itself. There’s no hidden synchronized block. Why does the Neverending thread sees the up-to-date value of stopRequested?

Above and beyond the call of duty

Before JSR 133, if thread A writes to volatile field f, thread B is guaranteed to see the new value of f only. Nothing else is guaranteed. With JSR 133 though, volatile is closer to synchronization than it was.  Reading/writing a volatile field now is like acquiring/releasing a monitor in terms of visibility. As the excellent FAQ says: “… anything that was visible to thread A when it writes to volatile field f becomes visible to thread B when it reads f.”

But still, all these new guarantees for volatile doesn’t answer the question:

public class TestVolatile implements Runnable {
	private volatile A wrapper = new A();

	public void run() {
		while(!wrapper.stopRequested) {
			// do something
		}
	}

	public void stop() {
		wrapper.stopRequested = true;
	}

	// the rest
}

Why does this work? In stop() we’re not exactly writing to wrapper. We’re just using it to change the value of stopRequested. So why does the other thread see the change?

Unfortunately, in the examples I’ve seen so far in books and countless articles, a volatile field is always a primitive, so it’s kinda hard to find the answer to my question. So I did the only remaining way I know to proceed: asking The Concurrency Expert himself. And I was pleasantly surprised to find that he replied very quickly! Here’s what Brian Goetz said:

The Java Memory Model sets the minimum requirements for visibility, but all VMs and CPUs generally provide more visibility than the minimum.  There’s a difference between “what do I observe on machine XYZ in casual use” and “what is guaranteed.”

So there. The VM in this case just goes above and beyond what it is supposed to do. But there’s no guarantee that on another VM and another CPU, the same thing will happen.

Conclusion

So here’s what I’ve learned from this little experiment:

  1. If your application is a server application, or will run on a server-class machine, remember to use the -server flag during development and testing as well to uncover potential problems as early as possible. The Server VM performs some optimizations that can expose bugs that do not manifest on the Client VM.
  2. Just because it works on your machine, doesn’t mean that it’ll work on other machines running other VMs too. It’s important to know what are exactly guaranteed, and code with those minimal guarantees in mind, instead of assuming that other VMs and OSes will be as forgiving as the ones you’re using for development.
  3. (This is closely related to #2 above.) Because VMs and CPUs generally provide more visibility than the minimum guaranteed by JSR 133, it’s good to know the extra things that they do that may mask a potential bug. For example, at least in some VMs, calling System.out.println() forces the change to a non-volatile variable to be visible to other threads because it has a synchronized block inside. This can explain a bug that appears after you’ve made a seemingly unrelated change (that removes a synchronized block from the execution path, for instance).

From http://rayfd.wordpress.com/