Когда использовать энергозависимые и синхронизированные

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

Пример 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 блокировка или явная блокировка.

Другие вопросы по тегам