Когда использовать летучие и синхронизированные
Я знаю, что есть много вопросов об этом, но я до сих пор не совсем понимаю. Я знаю, что делают оба этих слова, но я не могу определить, что использовать в определенных сценариях. Вот несколько примеров, которые я пытаюсь определить, что лучше всего использовать.
Пример 1:
import java.net.ServerSocket;
public class Something extends Thread {
private ServerSocket serverSocket;
public void run() {
while (true) {
if (serverSocket.isClosed()) {
...
} else { //Should this block use synchronized (serverSocket)?
//Do stuff with serverSocket
}
}
}
public ServerSocket getServerSocket() {
return serverSocket;
}
}
public class SomethingElse {
Something something = new Something();
public void doSomething() {
something.getServerSocket().close();
}
}
Пример 2:
public class Server {
private int port;//Should it be volatile or the threads accessing it use synchronized (server)?
//getPort() and setPort(int) are accessed from multiple threads
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
Любая помощь очень ценится.
Ответы
Ответ 1
Неустойчивое ключевое слово:
Если вы работаете с многопоточным программированием, ключевое слово volatile будет более полезным. Когда несколько потоков используют одну и ту же переменную, каждый поток будет иметь свою собственную копию локального кеша для этой переменной.
Итак, когда он обновляет значение, он фактически обновляется в локальном кеше не в основной переменной. Другой поток, который использует одну и ту же переменную, ничего не знает о значениях, измененных другим потоком.
Чтобы избежать этой проблемы, если вы объявляете переменную как изменчивую, то она не будет храниться в локальном кеше. Всякий раз, когда поток обновляет значения, он обновляется до основной памяти. Таким образом, другие потоки могут получить доступ к обновленному значению.
Летучий пример:
class ExampleThread extends Thread {
private volatile int testValue;
public ExampleThread(String str){
super(str);
}
public void run() {
for (int i = 0; i < 3; i++) {
try {
System.out.println(getName() + " : "+i);
if (getName().equals("Thread 1"))
{
testValue = 10;
}
if (getName().equals("Thread 2"))
{
System.out.println( "Test Value : " + testValue);
}
Thread.sleep(1000);
} catch (InterruptedException exception) {
exception.printStackTrace();
}
}
}
}
public class VolatileExample {
public static void main(String args[]) {
new ExampleThread("Thread 1").start();
new ExampleThread("Thread 2").start();
}
}
Синхронизированное ключевое слово:
Синхронизированное ключевое слово может быть применено к блоку оператора или к методу. Синхронизированное ключевое слово обеспечивает защиту для важных разделов, которые требуются только для выполнения одним потоком один раз за раз. Синхронизированное ключевое слово позволяет избежать критического кода, выполняемого более чем одним потоком за раз.
Он ограничивает другие потоки одновременным доступом к ресурсу. Если синхронизированное ключевое слово применяется к статическому методу, как мы покажем его с классом, имеющим метод SyncStaticMethod, в приведенном ниже примере, весь класс будет заблокирован во время исполнения и управления одним потоком за раз.
Когда ключевое слово synchronized применяется к методу экземпляра, как это было сделано с SyncMethod в приведенном ниже примере, экземпляр блокируется при обращении к нему и под управлением и контролем одного потока за раз.
Когда ключевое слово synchronized применяется к объекту, тогда этот объект заблокирован, хотя связанный с ним кодовый блок получает выполнение одним потоком в момент времени.
Синхронный пример:
public class Class1{
public synchronized static String SyncStaticMethod(){
}
public synchronized String SyncMethod(){
}
{
public class Class2{
Object Obj;
public String Method2(){
<statements>
synchronized (Obj){
<statements affecting Obj>
}
}
}
Ответ 2
Простой ответ таков:
-
synchronized
всегда можно использовать, чтобы дать вам потокобезопасное/правильное решение,
-
volatile
, вероятно, будет быстрее, но может быть использован только для обеспечения потокобезопасности/правильности в ограниченных ситуациях.
Если есть сомнения, используйте synchronized
. Правильность важнее производительности.
Характеризуя ситуации, при которых volatile
можно безопасно использовать, необходимо определить, может ли каждая операция обновления быть выполнена как одно атомное обновление для одной изменчивой переменной. Если операция включает в себя доступ к другому (не финальному) состоянию или обновление более одной общей переменной, это невозможно сделать безопасно с просто изменчивым. Вы также должны помнить, что:
- обновления для энергонезависимых
long
или double
могут быть не атомными, а
- Операторы Java, такие как
++
и +=
, не являются атомарными.
Терминология: операция "атомарна", если операция либо происходит полностью, либо вообще не происходит. Термин "неделимый" является синонимом.
Когда мы говорим об атомарности, мы обычно подразумеваем атомарность с точки зрения внешнего наблюдателя; например другой поток к тому, который выполняет операцию. Например, ++
не является атомарным с точки зрения другого потока, потому что этот поток может наблюдать состояние поля, увеличивающегося в середине операции. Действительно, если поле является long
или double
, возможно даже наблюдать состояние, которое не является ни начальным состоянием, ни конечным состоянием!
Ответ 3
Ключевое слово synchronized
synchronized
указывает, что переменная будет использоваться несколькими потоками. Он использовался для обеспечения согласованности путем "блокировки" доступа к переменной, чтобы один поток не мог изменить его, пока другой использует его.
Классический пример: обновление глобальной переменной, которая указывает текущее время
Функция incrementSeconds()
должна иметь возможность завершать работу непрерывно, поскольку при запуске она создает временные несоответствия в значении глобальной переменной time
. Без синхронизации другая функция может видеть time
"12:60:00" или, в комментарии, отмеченном >>>
, она будет видеть "11:00:00", когда время действительно "12:00": 00 ", потому что часы еще не увеличились.
void incrementSeconds() {
if (++time.seconds > 59) { // time might be 1:00:60
time.seconds = 0; // time is invalid here: minutes are wrong
if (++time.minutes > 59) { // time might be 1:60:00
time.minutes = 0; // >>> time is invalid here: hours are wrong
if (++time.hours > 23) { // time might be 24:00:00
time.hours = 0;
}
}
}
Ключевое слово volatile
volatile
просто говорит компилятору не делать предположений о постоянстве переменной, потому что она может измениться, когда компилятор обычно этого не ожидает. Например, программное обеспечение в цифровом термостате может иметь переменную, которая указывает температуру и значение которой обновляется непосредственно аппаратным обеспечением. Это может измениться в местах, где обычная переменная не изменилась бы.
Если degreesCelsius
не объявлен как volatile
, компилятор может оптимизировать это:
void controlHeater() {
while ((degreesCelsius * 9.0/5.0 + 32) < COMFY_TEMP_IN_FAHRENHEIT) {
setHeater(ON);
sleep(10);
}
}
в это:
void controlHeater() {
float tempInFahrenheit = degreesCelsius * 9.0/5.0 + 32;
while (tempInFahrenheit < COMFY_TEMP_IN_FAHRENHEIT) {
setHeater(ON);
sleep(10);
}
}
Объявляя degreesCelsius
как volatile
, вы сообщаете компилятору, что он должен проверять свое значение при каждом прохождении цикла.
Резюме
Короче говоря, synchronized
позволяет вам контролировать доступ к переменной, поэтому вы можете гарантировать, что обновления являются атомарными (то есть набор изменений будет применен как единое целое; никакой другой поток не сможет получить доступ к переменной когда он наполовину обновится). Вы можете использовать его для обеспечения согласованности ваших данных. С другой стороны, volatile
это признание того, что содержимое переменной находится вне вашего контроля, поэтому код должен предполагать, что она может измениться в любое время.
Ответ 4
В вашем сообщении недостаточно информации, чтобы определить, что происходит, поэтому все советы, которые вы получаете, это общая информация о volatile
и synchronized
.
Итак, вот мой общий совет:
Во время цикла написания-компиляции программы выполняется две точки оптимизации:
- во время компиляции, когда компилятор может попытаться изменить порядок инструкций или оптимизировать кэширование данных.
- во время выполнения, когда у процессора есть свои собственные оптимизации, такие как кеширование и выполнение вне порядка.
Все это означает, что инструкции, скорее всего, не будут выполняться в том порядке, в котором вы их написали, независимо от того, должен ли поддерживаться этот порядок, чтобы обеспечить правильность программы в многопоточной среде. Классический пример, который вы часто найдете в литературе, таков:
class ThreadTask implements Runnable {
private boolean stop = false;
private boolean work;
public void run() {
while(!stop) {
work = !work; // simulate some work
}
}
public void stopWork() {
stop = true; // signal thread to stop
}
public static void main(String[] args) {
ThreadTask task = new ThreadTask();
Thread t = new Thread(task);
t.start();
Thread.sleep(1000);
task.stopWork();
t.join();
}
}
В зависимости от оптимизации компилятора и архитектуры ЦП вышеприведенный код никогда не может завершиться в многопроцессорной системе. Это связано с тем, что значение stop
будет кэшироваться в регистре процессора, выполняющего поток t
, так что поток больше не будет считывать значение из основной памяти, даже если бы основной поток обновил его.
Чтобы справиться с такой ситуацией, были введены ограждения памяти. Это специальные инструкции, которые не позволяют выполнять регулярные инструкции перед тем, как забор будет переупорядочен с инструкциями после забора. Одним из таких механизмов является ключевое слово volatile
. Переменные, отмеченные volatile
, не оптимизируются компилятором/процессором и всегда будут записываться/считываться напрямую в/из основной памяти. Короче говоря, volatile
обеспечивает видимость значения переменной в ядрах процессора.
Видимость важна, но ее не следует путать с атомарностью. Два потока, увеличивающие одну и ту же общую переменную, могут создавать противоречивые результаты, даже если объявлена переменная volatile
. Это связано с тем, что в некоторых системах приращение фактически преобразуется в последовательность инструкций ассемблера, которые могут быть прерваны в любой точке. Для таких случаев необходимо использовать критические разделы, такие как ключевое слово synchronized
. Это означает, что только один поток может получить доступ к коду, заключенному в блок synchronized
. Другим распространенным использованием критических разделов являются атомарные обновления для общей коллекции, когда обычно выполняется итерация по коллекции, в то время как другой поток добавляет/удаляет элементы, будет вызывать исключение.
Наконец, два интересных момента:
-
synchronized
, а некоторые другие конструкции, такие как Thread.join
, неявно вводят заграждения памяти. Следовательно, приращение переменной внутри блока synchronized
не требует, чтобы переменная также была volatile
, считая, что единственное место, где оно читается/записывается.
- Для простых обновлений, таких как обмен значениями, приращение, декремент, вы можете использовать неблокирующие атомные методы, такие как те, которые находятся в
AtomicInteger
, AtomicLong
и т.д. Они намного быстрее, чем synchronized
, потому что они не вызвать переключатель контекста в случае, если блокировка уже занята другим потоком. Они также вводят забор памяти при использовании.
Ответ 5
Примечание. В первом примере поле serverSocket
на самом деле никогда не инициализируется в коде, который вы показываете.
Что касается синхронизации, это зависит от того, является ли класс serverSocket
потокобезопасным. (Я предполагаю, что это так, но я никогда не использовал его.) Если это так, вам не нужно синхронизировать его.
Во втором примере переменные int
могут быть атомарно обновлены, поэтому volatile
может быть достаточно.
Ответ 6
volatile
решает проблему "видимости" в ядрах CPU. Поэтому значение из локальных регистров очищается и синхронизируется с ОЗУ. Однако, если нам нужна согласованная ценность и атомный op, нам нужен механизм для защиты критических данных. Это может быть достигнуто с помощью блока synchronized
или явного блокировки.
Ответ 7
Поле volatile будет доступно только по одному потоку за раз (Java будет устанавливать блокировки вокруг него).
Синхронизированный блок или метод будет доступен только по одному потоку за весь объект.
Итак, если у вас есть код:
volatile int a;
volatile int b;
synchronized int getA(){
return a;
}
synchronized int getB(){
return b;
}
тогда два потока могут обращаться к a и b одновременно, но не getA() и getB(). Только один поток может получить доступ к синхронизированному методу по всему объекту.
Ответ 8
Синхронизированный получает и освобождает блокировки на мониторах, которые могут заставить только один поток одновременно выполнять блок кода, если оба потока используют один и тот же монитор (фактически тот же объект блокировки). Это довольно хорошо известный аспект для синхронизации. Но синхронизируется и синхронизирует память. Фактически синхронизация синхронизирует всю память потока с "основной" памятью.
Volatile только синхронизирует значение одной переменной между памятью потоков и "основной" памятью, синхронизируется, синхронизирует значение всех переменных между памятью потоков и "основной" памятью и блокирует и освобождает монитор загрузки.
Очевидно, что синхронизация может иметь больше накладных расходов, чем летучие.