Java как работает volatile

Java как работает volatile

Вот четыре основных правила «happens-before» в Java: Правило захвата монитора (Monitor Lock Rule): Если поток A захватывает монитор объекта X, а затем выполняет операцию, а поток B должен захватить тот же монитор X, чтобы увидеть результаты изменений, выполненных потоком A. Другими словами, все операции, выполненные потоком A до освобождения монитора X, будут видны потоку B после захвата того же монитора X.

 // Поток A synchronized (lock) < sharedVariable = 10; >// Поток B synchronized (lock) < int value = sharedVariable; // Гарантируется, что значение 10 будет видно потоку B >

Даже на 64-битных платформах, использование volatile с 64-битными переменными, такими как long и double, может быть недостаточным для обеспечения атомарности сложных операций. Например, если два потока пытаются одновременно выполнить операцию инкремента на volatile long переменной, может возникнуть состояние гонки, потому что инкремент состоит из нескольких операций чтения, модификации и записи. В таких случаях для обеспечения атомарности операций или синхронизации между потоками следует использовать средства синхронизации, такие как synchronized блоки или классы из пакета java.util.concurrent.atomic, которые предоставляют атомарные операции над переменными, включая 64-битные переменные.

volatile в априори не может создавать атомарное представление переменной, он лишь отменяет ее кэширование, что косвенно делает ее атомарной, но volatile != 100% атомарность

Правило 4 «Запись в volatile переменную happens-before чтение из той же переменной» Само собой это не происходит, мы сами это регулируем. Если мы запустим чтение/запись с разных потоков, какой поток и когда прочитает переменную c volatile зависит от самих потоков, и их конкуренции. Сдесь вывод будет разный, но в основном по первому потоку который был запущен.

 public class Main < public volatile static String message = "No changes"; public static void main(String[] args) throws InterruptedException < new FreeThread().start(); new MyThread().start(); >public static class MyThread extends Thread < @Override public void run() < message = "Message was changed"; >> public static class FreeThread extends Thread < @Override public void run() < System.out.println(message); >> > 
 public class Solution < public static volatile int proposal = 0; public static void main(String[] args) throws InterruptedException < Thread mt1 = new Mt1(); Thread mt2 = new Mt2(); mt1.start(); mt2.start(); Thread.sleep(100); System.out.println(proposal + " " +Thread.currentThread().getName()); >public static class Mt1 extends Thread < @Override public void run() < proposal = 1; try < Thread.sleep(100); >catch (InterruptedException e) < throw new RuntimeException(e); >System.out.println(proposal + " " +Thread.currentThread().getName()); > > public static class Mt2 extends Thread < @Override public void run() < proposal = 2; try < Thread.sleep(100); >catch (InterruptedException e) < throw new RuntimeException(e); >System.out.println(proposal + " " +Thread.currentThread().getName()); > > 

А в этом же коде без слипов выводит значения, соответствующие установленным в каждом треде, и никто не ждет никаких записей от других потоков. Выходит, что последний пункт вот вообще не работает с точки зрения синхронизации, потому что результат зависит от скорости выполнения тредов, и в зависимости от нее результаты будут совершенно разными. Это уже не говоря о планировщике. Да, можно починить при помощи join, но в таком случае получается поделка на тему синхронизации и не более. Бред какой-то..

Читайте также:  Html input tag events

Источник

Применение volatile

— Хочу рассказать тебе о модификаторе volatile. Знаешь, что это такое?

— Вроде что-то связанное с нитями. Не помню точно.

— Тогда слушай. Вот тебе немного технических деталей:

В компьютере есть два вида памяти – глобальная (обычная) и встроенная в процессор. Встроенная в процессор делится на регистры, затем кэш первого уровня (L1), кэш второго уровня (L2) и третьего уровня (L3).

Эти виды памяти отличаются по скорости работы. Самая быстрая и самая маленькая память – это регистры, затем идет кэш процессора (L1, L2, L3) и, наконец, глобальная память (самая медленная).

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

— А можно как-то управлять этим процессом?

— Практически никак – всю работу делает Java-машина, она очень интеллектуальная в плане оптимизации скорости работы.

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

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

public volatile int count = 0;

— О, вспомнил. Ты же уже про это рассказывала. Я же это уже знаю.

Читайте также:  Java parameter name to string

— Ага, знаешь. А вспомнил, только когда я рассказала.

Вот тебе несколько новых фактов работы модификатора volatile. Модификатор volatile гарантирует только безопасное чтение/запись переменной, но не ее изменение.

— Вот смотри. Как изменяется переменная:

register = count; register = register+1; count = register;

Этап 2
Внутри процессора регистровая переменная увеличивается на 1.

— Ого! Так что, изменение любой переменной происходит только в процессоре?

— И значения копируются туда-сюда: из памяти в процессор и обратно?

Так вот, модификатор volatile, гарантирует, что при обращении к переменной count она будет прочитана из памяти (этап 1). А если какая-то нить захочет присвоить ей новое значение, то оно обязательно окажется в глобальной памяти (этап 3).

Но Java-машина не гарантирует, что не будет переключения нитей между этапами 1 и 3.

— Т.е. увеличение переменной на 1 – это фактически три операции?

— И если две нити одновременно захотят исполнить count++, то они могут помешать друг другу?

register1 = count; register1++; count = register1;
register2 = count; register2++; count = register2;
register1 = count; register2 = count; register2++; count = register2; register1++; count = register1;

— Т.е. обращаться к переменной можно, а изменять рискованно все равно?

— Ну, изменять можно, только осторожно ☺

synchronized наше все.

Источник

Оцените статью