Когда использовать энергозависимые и синхронизированные
Я знаю, что есть много вопросов по этому поводу, но я все еще не совсем понимаю. Я знаю, что делают оба эти ключевых слова, но я не могу определить, какие использовать в определенных сценариях. Вот несколько примеров, которые я пытаюсь определить, какой из них лучше всего использовать.
Пример 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;
}
}
Любая помощь с благодарностью.
6 ответов
Изменчивое ключевое слово:
Если вы работаете с многопоточным программированием, ключевое слово volatile будет более полезным. Когда несколько потоков используют одну и ту же переменную, каждый поток будет иметь свою собственную копию локального кэша для этой переменной.
Таким образом, когда оно обновляет значение, оно фактически обновляется в локальном кэше, а не в основной переменной памяти. Другой поток, использующий ту же переменную, ничего не знает о значениях, измененных другим потоком.
Чтобы избежать этой проблемы, если вы объявите переменную как 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();
}
}
Синхронизированное ключевое слово:
Синхронизированное ключевое слово может быть применено к блоку оператора или к методу. Ключевое слово synchronized обеспечивает защиту для важных разделов, которые должны выполняться только одним потоком по одному за раз. Ключевое слово synchronized предотвращает выполнение критического кода более чем одним потоком одновременно.
Это ограничивает другие потоки для одновременного доступа к ресурсу. Если ключевое слово synchronized применяется к статическому методу, как мы покажем с классом, имеющим метод 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>
}
}
}
Простой ответ заключается в следующем:
synchronized
всегда может быть использован, чтобы дать вам потокобезопасное / правильное решение,volatile
вероятно, будет быстрее, но может быть использован только для обеспечения многопоточности / исправления в ограниченных ситуациях.
Если сомневаетесь, используйте synchronized
, Правильность важнее, чем производительность.
Характеризуя ситуации, в которых volatile
может безопасно использоваться, включает определение того, может ли каждая операция обновления выполняться как одно атомарное обновление для одной изменчивой переменной. Если операция включает в себя доступ к другому (не финальному) состоянию или обновление более чем одной совместно используемой переменной, ее нельзя безопасно выполнить только с помощью volatile. Вы также должны помнить, что:
- обновления для энергонезависимой
long
илиdouble
не может быть атомным, и - Операторы Java, такие как
++
а также+=
не атомарны.
Терминология: операция является "атомарной", если операция либо происходит полностью, либо вообще не происходит. Термин "неделимый" является синонимом.
Когда мы говорим об атомности, мы обычно имеем в виду атомарность с точки зрения внешнего наблюдателя; например, поток, отличный от того, который выполняет операцию. Например, ++
не является атомарным с точки зрения другого потока, потому что этот поток может быть в состоянии наблюдать состояние поля, увеличиваемого в середине операции. Действительно, если поле long
или double
может даже наблюдаться состояние, которое не является ни начальным, ни конечным состоянием!
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
Это признание того, что содержимое переменной находится вне вашего контроля, поэтому код должен предполагать, что она может измениться в любое время.
В вашем сообщении недостаточно информации, чтобы определить, что происходит, поэтому все советы, которые вы получаете, - это общая информация о 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
потому что они не вызывают переключение контекста в случае, если блокировка уже занята другим потоком. Они также вводят ограждения памяти при использовании.
Примечание. В первом примере поле serverSocket
на самом деле никогда не инициализируется в коде, который вы показываете.
Что касается синхронизации, это зависит от того, ServerSocket
класс потокобезопасен. (Я предполагаю, что это так, но я никогда не использовал его.) Если это так, вам не нужно синхронизироваться вокруг него.
Во втором примере int
переменные могут быть обновлены атомарно так volatile
может хватить.
volatile
решает проблему "видимости" между ядрами процессора. Поэтому значение из локальных регистров сбрасывается и синхронизируется с ОЗУ. Однако, если нам нужны единообразные значения и атомарная операция, нам нужен механизм для защиты критических данных. Это может быть достигнуто либо synchronized
блокировка или явная блокировка.