Является ли println лучше, чем volatile?

Java задняя часть
Является ли println лучше, чем volatile?

Это первый день моего участия в Gengwen Challenge, чтобы узнать подробности о мероприятии, пожалуйста, проверьте:Обновить вызов

Ставь лайк и потом смотри, вырабатывай полезную привычку

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

Друзья: В главе о параллелизме книги «Эффективная JAVA» есть описание видимости. Следующий код будет иметь бесконечный цикл Я могу это понять Модель памяти JMM, JMM не гарантирует, что модификация stopRequested может быть соблюдена вовремя.

static boolean stopRequested = false;

public static void main(String[] args) throws InterruptedException {

    Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested) {
            i++;
        }
    }) ;
    backgroundThread.start();
    TimeUnit.MICROSECONDS.sleep(10);
    stopRequested = true ;
}

Но странно то, что после того, как я добавлю строку для печати, бесконечного цикла не будет! Может ли мой однострочный println быть лучше, чем volatile? Это тоже не имеет значения.

static boolean stopRequested = false;

public static void main(String[] args) throws InterruptedException {

    Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested) {
            
            // 加上一行打印,循环就能退出了!
        	System.out.println(i++);
        }
    }) ;
    backgroundThread.start();
    TimeUnit.MICROSECONDS.sleep(10);
    stopRequested = true ;
}

Я: Мальчик хорошо знаком с Багувеном, так что ДММ придет, когда он откроет рот. ​

Я: Это... на самом деле хорошая вещь, сделанная JIT, так что из вашего цикла нельзя выйти. JMM — это всего лишь логическая модель памяти, а некоторые внутренние механизмы связаны с JIT.

Например, в первом примере вы используете-XintОтключив JIT, можно выйти из бесконечного цикла.Если не верите, попробуйте?

Бадди: Блин, это действительно возможно, и цикл -Xint выйдет, это потрясающе! Что такое JIT? Может ли он все еще иметь этот эффект?

image.png

JIT-оптимизация (Just-in-Time)

Как мы все знаем, для достижения кроссплатформенности JAVA добавляет слой JVM, а JVM разных платформ отвечают за интерпретацию и выполнение файлов байт-кода. Хотя существует уровень интерпретации, влияющий на эффективность, преимущество заключается в том, что он кроссплатформенный, а файлы байт-кода не зависят от платформы.image.pngПосле JAVA 1.2 добавленоСвоевременная компиляция (JIT)Он может компилировать горячий код с большим временем выполнения в машинный код во время выполнения, поэтому ему не требуется JVM для его повторной интерпретации, и его можно выполнять напрямую для повышения эффективности работы.image.png

Но когда JIT-компилятор компилирует байт-код, это не просто прямой перевод байт-кода в машинный код., он также будет выполнять множество оптимизаций при компиляции, таких как развертывание цикла, встраивание методов и т. д. ​

Причина этой проблемы связана с одним из методов оптимизации компилятора JIT -поднятие выраженийвызванный.

поднятие выражений

Давайте сначала возьмем пример, в этомhoistingметод, переменная определяется каждый раз в цикле fory, затем сохраняя результат x*y в переменной результата и затем используя эту переменную для различных операций

public void hoisting(int x) {
	for (int i = 0; i < 1000; i = i + 1) {
		// 循环不变的计算 
		int y = 654;
		int result = x * y;
		
		// ...... 基于这个 result 变量的各种操作
	}
}

Но в этом примере результат результата фиксирован и не будет обновляться в цикле. Таким образом, вполне возможно извлечь вычисление результата за пределы цикла, чтобы вам не приходилось вычислять его каждый раз. После JIT-анализа этот код будет оптимизирован для выполнения операций продвижения выражений:

public void hoisting(int x) {
	int y = 654;
	int result = x * y;
    
	for (int i = 0; i < 1000; i = i + 1) {	
		// ...... 基于这个 result 变量的各种操作
	}
}

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

Обратите внимание, что компилятор предпочитает локальные переменные статическим переменным или переменным-членам; поскольку статические переменные «экранированы» и могут быть доступны нескольким потокам, локальные переменные являются частными потоками и не могут быть доступны другим потокам и Revise. ​

Когда компилятор обрабатывает статические переменные/переменные-члены, он будет консервативен и не будет легко оптимизироваться. ​

Как в этом примере в вашем вопросе,stopRequestedЭто статическая переменная, и компилятор не должен ее оптимизировать;

static boolean stopRequested = false;// 静态变量

public static void main(String[] args) throws InterruptedException {

    Thread backgroundThread = new Thread(() -> {
        int i = 0;
        while (!stopRequested) {
			// leaf method
            i++;
        }
    }) ;
    backgroundThread.start();
    TimeUnit.MICROSECONDS.sleep(10);
    stopRequested = true ;
}

Но так как ваша петляleaf method, то есть никакой метод не вызывается, поэтому ни один другой поток в цикле не будет наблюдатьstopRequestedизменение стоимости. Тогда компилятор будет действовать опрометчивоПродвижение экспрессииоперация, будетstopRequestedПоднимается вне выражения, обрабатывается как инвариант цикла:

int i = 0;

boolean hoistedStopRequested = stopRequested;// 将stopRequested 提升为局部变量
while (!hoistedStopRequested) {    
	i++;
}

Таким образом, окончательныйstopRequestedОперация, назначенная как true, не влияет на продвигаемыйhoistedStopRequestedЗначение , естественно, не может повлиять на выполнение цикла и в конечном итоге приводит к невозможности выхода. ​

Как вы добавилиprintlnПосле этого цикл может выйти из задачи. Это потому, что ваша строка кода println влияет на оптимизацию компилятора.Метод println нельзя встроить, так как в конечном итоге он вызовет собственный метод FileOutputStream.writeBytes.. И вызов метода, который не ограничен, является «полным уничтожением памяти» с точки зрения компилятора, то естьНеизвестные побочные эффекты, должен выполнять операции чтения и записи в памятиКонсервативное лечение. ​

В этом примере следующий циклstopRequestedОперация чтения происходит по порядку после println предыдущего раунда цикла. «Консервативное лечение» здесь таково: даже если я прочитал предыдущий раундstopRequestedзначение, так как послеНеизвестные побочные эффектыгде его необходимо прочитать снова при следующем посещении. ​

Поэтому после того, как вы добавите prinltln, JIT не сможет сделать это из-за консервативной обработки и повторного чтения.Продвижение экспрессииулучшен. ​

вышеуказанная параПродвижение экспрессииобъяснение, обобщая выдержки изR большойизЗнать ответ. Big R, ходячая JVM Wiki! ​

Я: «Теперь я понимаю, это все хорошо, что делает JIT, если вы отключите JIT, эта проблема не будет проблемой»

Маленький друг: "Бля 🐂🍺, слишком много механизмов для простого цикла for. Я не ожидал, что JIT будет таким умным, и я не ожидал, что R будет таким большим 🐂🍺"

Маленький друг: "Тогда в JIT должно быть много механизмов оптимизации, что еще есть, кроме улучшения этого выражения?"

Я: Я не компилятор... Я так много знаю, я знаю некоторые часто используемые, позвольте мне кратко рассказать вам

выражение лица

Подобно расширению выражений, существует также оптимизация приема выражений, например следующий код:

public void sinking(int i) {
	int result = 543 * i;

	if (i % 2 == 0) {
		// 使用 result 值的一些逻辑代码
	} else {
		// 一些不使用 result 的值的逻辑代码
	}
}

Поскольку значение результата не используется в ветке else, результат будет вычисляться первым независимо от ветки, что необязательно. JIT переместит выражение вычисления результата в ветвь if, таким образом избегая вычисления результата каждый раз.Эта операция называется погружением выражения:

public void sinking(int i) {
	if (i % 2 == 0) {
		int result = 543 * i;
		// 使用 result 值的一些逻辑代码
	} else {
		// 一些不使用 result 的值的逻辑代码
	}
}

Какие еще общие оптимизации есть в JIT?

В дополнение к описанным выше подъему/снижению выражения, существуют некоторые распространенные механизмы оптимизации компилятора.

Разматывание/разматывание цикла

Следующий цикл for должен выполняться в общей сложности 10 раз, и каждый раз необходимо проверять условие.

for (int i = 0; i < 100000; i++) {
    delete(i);
}

После оптимизации компилятора будет удалено определенное количество циклов, что уменьшит накладные расходы, вызванные операциями увеличения индекса и проверки условий:

for (int i = 0; i < 20000; i+=5) {
    delete(i);
    delete(i + 1);
    delete(i + 2);
    delete(i + 3);
    delete(i + 4);
}

В дополнение к развертыванию циклов, циклы также имеют некоторые механизмы оптимизации, такие как удаление циклов, обмен циклами, разделение циклов, слияние циклов...

Встроенная оптимизация (Inling)

Вызов метода JVM представляет собой модель стека. Каждый вызов метода требует операции push и pop. Компилятор также оптимизирует модель вызова и встроит некоторые вызовы методов. ​

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

public  void inline(){
	int a = 5;
    int b = 10;
    int c = calculate(a, b);
    
    // 使用 c 处理……
}

public int calculate(int a, int b){
	return a + b;
}

После встроенной оптимизации компилятора онcalculateТело метода извлекается вinlineметод, выполняемый напрямую без вызова метода:

public  void inline(){
	int a = 5;
    int b = 10;
    int c = a + b;
    
    // 使用 c 处理……
}

Однако эта встроенная оптимизация имеет некоторые ограничения.Например, встроенные методы нельзя оптимизировать.

пустой раньше времени

Сначала рассмотрим пример, в которомwas finalized!Будет вdone.Предыдущий вывод, это также вызвано оптимизацией JIT.

class A {
    // 对象被回收前,会触发 finalize
    @Override protected void finalize() {
        System.out.println(this + " was finalized!");
    }

    public static void main(String[] args) throws InterruptedException {
        A a = new A();
        System.out.println("Created " + a);
        for (int i = 0; i < 1_000_000_000; i++) {
            if (i % 1_000_00 == 0)
                System.gc();
        }
        System.out.println("done.");
    }
}

//打印结果
Created A@1be6f5c3
A@1be6f5c3 was finalized!//finalize方法输出
done.

Как видно из примера, еслиaПосле завершения цикла он больше не используется, а сначала будет выполняться finalize; хотя из области видимости объекта метод не был выполнен и кадр стека не выталкивался, но он все равно будет выполняться заранее. ​

Это потому, что JIT думаетaОбъект не будет использоваться внутри или после цикла, поэтому очистите его заранее, чтобы помочь сборщику мусора перезапустить; если JIT отключен, этой проблемы не возникнет... ​

Этот ранний механизм повторного использования все еще немного рискован, и он может вызвать ошибки в некоторых сценариях, таких как "Думая о механизме GC, вызванном ОШИБКОЙ пула потоков JDK

Различные оптимизации для HotSpot VM JIT

Выше представлено только несколько простых и часто используемых механизмов оптимизации компиляции.Дополнительные механизмы оптимизации JVM JIT см. на следующем рисунке. Это материал в формате pdf, представленный в документации OpenJDK, в котором перечислены различные механизмы оптимизации HotSpot JVM, довольно много...image.png

Как избежать проблем, вызванных JIT?

Маленький друг: «В JIT так много механизмов оптимизации, что легко ошибиться. Как мне избежать их, когда я обычно пишу код?»

Обычно при написании кода не нужно обращать внимание на оптимизацию JIT.Так же, как и проблема println выше, JMM не гарантирует, что модификация будет видна другим потокам. ​

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

Я: Значит, это не травка JIT, а твоя...

Маленький друг: "Понял, ты говоришь о моей еде, говоришь, что мой код написал дерьмо..."

Суммировать

В повседневном процессе кодирования нет необходимости намеренно угадывать механизм оптимизации JIT, а JVM не расскажет вам все оптимизации полностью. И разные версии такого рода вещей имеют разные эффекты, даже если вы разберетесь с механизмом, он может быть совершенно другим в следующей версии. ​

Поэтому, если вы не занимаетесь разработкой компиляторов, знания JIT-компиляции хороши как запас знаний.

Также не нужно гадать, как JIT оптимизирует ваш код, вы (вероятно) не можете догадаться...

Ссылаться на

Нелегко быть оригинальным, и несанкционированная перепечатка запрещена. Если моя статья полезна для вас, пожалуйста, поставьте лайк/добавьте в избранное/подпишитесь, чтобы поддержать и поддержать ее ❤❤❤❤❤❤

небольшое дополнение

Некоторые читатели могут подумать, что проблема вызвана синхронизацией.Ниже приведен пример синхронизации после небольшой модификации.В результате выйти из бесконечного цикла по-прежнему невозможно...

public class HoistingTest {
	static boolean stopRequested = false;

	public static void main(String[] args) throws InterruptedException {
		Thread backgroundThread = new Thread(() -> {
			int i = 0;
			while (!stopRequested) {

				// 加上一行打印,循环就能退出了!
//				System.out.println(i++);
				new HoistingTest().test();
			}
		}) ;
		backgroundThread.start();
		TimeUnit.SECONDS.sleep(5);
		stopRequested = true ;
	}

	Object lock = new Object();

	private  void test(){

		synchronized (lock){}
	}
}

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

Object lock = new Object();

private synchronized void test(){

        synchronized (lock){}
}

Но я просто хочу сказать, что ключом к этой проблеме является оптимизация jitвызвать проблемы. Механизм оптимизации jit также является частью jmm, jmm — это просто спецификация, а jit — это механизм в реализации vm, он также будет следовать спецификации jmm.

Но jmm не говорит, что синхронизация повлияет на jit и тому подобное, но даже если синхронизация повлияет на это, ну и что... не ключевой момент

В сочетании с пояснением R компилятор более чувствителен к статическим переменным.Если объект блокировки выше изменен, чтобы быть статическим, цикл может выйти снова...

Так что, если вы не добавите static и замените sync на unsafe.pageSize()? Является ли результат циклом или из него можно выйти...

Итак, в этой статье основное внимание уделяется описанию эффектов jit, а не различных действий, влияющих на jit. Возможностей воздействовать на jit много, и разные вмс и даже разные версии ведут себя по-разному.Нам не нужно выяснять этот механизм, да и не узнать (ведь это не компилятор, это устройство компиляции, не обязательно HotSpot...)