Атомарные операции volatile java

Применение volatile

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

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

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

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

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

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

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

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

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

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

public volatile int count = 0;

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

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

Вот тебе несколько новых фактов работы модификатора 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 наше все.

Источник

Atomic Access

In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn’t happen at all. No side effects of an atomic action are visible until the action is complete.

We have already seen that an increment expression, such as c++ , does not describe an atomic action. Even very simple expressions can define complex actions that can decompose into other actions. However, there are actions you can specify that are atomic:

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double ).
  • Reads and writes are atomic for all variables declared volatile (including long and double variables).

Atomic actions cannot be interleaved, so they can be used without fear of thread interference. However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible. Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What’s more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile , but also the side effects of the code that led up the change.

Using simple atomic variable access is more efficient than accessing these variables through synchronized code, but requires more care by the programmer to avoid memory consistency errors. Whether the extra effort is worthwhile depends on the size and complexity of the application.

Some of the classes in the java.util.concurrent package provide atomic methods that do not rely on synchronization. We’ll discuss them in the section on High Level Concurrency Objects.

Источник

Атомарные операции volatile java

Вот четыре основных правила «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, но в таком случае получается поделка на тему синхронизации и не более. Бред какой-то..

Источник

Читайте также:  Число степень двойки java
Оцените статью