Простейший и понятный пример изменчивого ключевого слова в Java

Я читаю об изменчивом ключевом слове в Java и полностью понимаю его теоретическую часть.

Но я ищу хороший пример, который показывает, что произошло бы, если бы переменная не была изменчивой и была бы.

Приведенный ниже фрагмент кода не работает должным образом (взято из здесь):

class Test extends Thread {

    boolean keepRunning = true;

    public void run() {
        while (keepRunning) {
        }

        System.out.println("Thread terminated.");
    }

    public static void main(String[] args) throws InterruptedException {
        Test t = new Test();
        t.start();
        Thread.sleep(1000);
        t.keepRunning = false;
        System.out.println("keepRunning set to false.");
    }
}

В идеале, если keepRunning не является энергозависимым, поток должен работать бесконечно долго. Но он останавливается через несколько секунд.

У меня есть два основных вопроса:

  • Кто-нибудь может объяснить изменчивость с примером? Не с теорией из JLS.
  • Является ли энергозависимый заменитель синхронизации? Достигает ли он атомарности?

Ответ 1

Летучие → Гарантирует видимость и НЕ атомарность

Синхронизация (блокировка) → гарантирует видимость и атомарность (если выполняется правильно)

Volatile не заменяет синхронизацию

Использовать volatile только тогда, когда вы обновляете ссылку и не выполняете некоторые другие операции над ней.

Пример:

volatile int i = 0;

public void incrementI(){
   i++;
}

не будет потокобезопасным без использования синхронизации или AtomicInteger, поскольку приращение является составной операцией.

Почему программа не запускается бесконечно?

Хорошо, что зависит от различных обстоятельств. В большинстве случаев JVM достаточно умен, чтобы очистить содержимое.

Правильное использование volatile обсуждает различные возможные варианты использования volatile. Правильно использовать volatile - это сложно, я бы сказал: "Если у вас есть сомнения, оставьте это", вместо этого используйте синхронизированный блок.

также:

синхронизированный блок можно использовать вместо volatile, но инверсия не является истиной.

Ответ 2

Для вашего конкретного примера: если не объявлено volatile, серверная JVM может вывести переменную keepRunning из цикла, потому что она не изменена в цикле (превращая ее в бесконечный цикл), но клиентская JVM не будет. Вот почему вы видите разные результаты.

Ниже приведено общее объяснение изменчивых переменных:

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

Эффекты видимости изменчивых переменных выходят за пределы значения самой изменчивой переменной. Когда поток A записывает в переменную volatile и впоследствии поток B считывает эту же переменную, значения всех переменных, которые были видимы для A до записи в переменную volatile, становятся видимыми для B после чтения переменной volatile.

Наиболее часто используемые переменные изменяются в качестве флага завершения, прерывания или состояния:

  volatile boolean flag;
  while (!flag)  {
     // do something untill flag is true
  }

Изменчивые переменные могут использоваться для других видов информации о состоянии, но при попытке сделать это требуется больше внимания. Например, семантика volatile недостаточно сильна, чтобы сделать операцию приращения (count++) атомарной, если только вы не можете гарантировать, что переменная записана только из одного потока.

Блокировка может гарантировать как видимость, так и атомарность; изменчивые переменные могут гарантировать только видимость.

Вы можете использовать переменные переменные только при соблюдении всех следующих критериев:

  • Запись в переменную не зависит от ее текущего значения, или вы можете убедитесь, что только один поток обновляет значение;
  • Переменная не участвует в инвариантах с другими переменными состояния; и
  • Блокировка не требуется по любой другой причине во время обращения к переменной.

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

Это отрывок из "Параллелизма Java на практике", лучшей книги по этой теме.

Ответ 3

Я немного изменил ваш пример. Теперь используйте пример с keepRunning как volatile и non volatile член:

class TestVolatile extends Thread{
    //volatile
    boolean keepRunning = true;

    public void run() {
        long count=0;
        while (keepRunning) {
            count++;
        }

        System.out.println("Thread terminated." + count);
    }

    public static void main(String[] args) throws InterruptedException {
        TestVolatile t = new TestVolatile();
        t.start();
        Thread.sleep(1000);
        System.out.println("after sleeping in main");
        t.keepRunning = false;
        t.join();
        System.out.println("keepRunning set to " + t.keepRunning);
    }
}

Ответ 4

Что такое ключевое слово volatile?

volatile предотвращает caching of variables.

Рассмотрим код, сначала без изменчивого ключевого слова

class MyThread extends Thread {
    private boolean running = true;   //non-volatile keyword

    public void run() {
        while (running) {
            System.out.println("hello");
        }
    }

    public void shutdown() {
        running = false;
    }
}

public class Main {

    public static void main(String[] args) {
        MyThread obj = new MyThread();
        obj.start();

        Scanner input = new Scanner(System.in);
        input.nextLine(); 
        obj.shutdown();   
    }    
}

Идеально, эта программа должна print hello до тех пор, пока не будет нажата кнопка RETURN key. Но в some machines может случиться, что переменная работает равна cached, и вы не можете изменить ее значение из метода shutdown(), что приводит к infinite печати приветственного текста.

Таким образом, используя ключевое слово volatile, это guaranteed, что ваша переменная не будет кэшироваться, т.е. будет run fine на all machines.

private volatile boolean running = true;  //volatile keyword

Таким образом, использование ключевого слова volatile - это good и safer programming practice.

Ответ 5

Variable Volatile: изменчивое ключевое слово применимо к переменным. Ключевое слово volatile в Java гарантирует, что значение переменной volatile всегда будет считываться из основной памяти, а не из локального кэша потока.

Access_Modifier volatile DataType Variable_Name;

Volatile Field: Указание для VM, что несколько потоков могут пытаться получить доступ/обновить значение поля одновременно. К особому виду переменных экземпляра, который должен использоваться всеми потоками с измененным значением. Подобно переменной Static (Class), только одна копия энергозависимого значения кэшируется в основной памяти, так что перед выполнением любых операций ALU каждый поток должен прочитать обновленное значение из основной памяти, после операции ALU он должен записать в директиву основной памяти. (Запись в энергозависимую переменную v синхронизируется со всеми последующими чтениями v любым потоком) Это означает, что изменения в энергозависимой переменной всегда видны другим потокам.

enter image description here

Здесь к nonvoltaile variable, если поток t1 изменяет значение в кэше t1, поток t2 не может получить доступ к измененному значению до тех пор, пока не будет записано t1, а t2 не прочитает из основной памяти последнее измененное значение, что может привести к Data-Inconsistancy.

volatile cannot be cached - assembler

    +--------------+--------+-------------------------------------+
    |  Flag Name   |  Value | Interpretation                      |
    +--------------+--------+-------------------------------------+
    | ACC_VOLATILE | 0x0040 | Declared volatile; cannot be cached.|
    +--------------+--------+-------------------------------------+
    |ACC_TRANSIENT | 0x0080 | Declared transient; not written or  |
    |              |        | read by a persistent object manager.|
    +--------------+--------+-------------------------------------+

Shared Variables: Память, которая может быть разделена между потоками, называется общей памятью или кучей памяти. Все поля экземпляров, статические поля и элементы массива хранятся в динамической памяти.

Синхронизация: синхронизировано применимо к методам, блокам. позволяет выполнять только 1-нить одновременно на объекте. Если t1 берет на себя управление, то остальные потоки должны ждать, пока он не освободит управление.

Пример:

public class VolatileTest implements Runnable {

    private static final int MegaBytes = 10241024;

    private static final Object counterLock = new Object();
    private static int counter = 0;
    private static volatile int counter1 = 0;

    private volatile int counter2 = 0;
    private int counter3 = 0;

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            concurrentMethodWrong();
        }

    }

    void addInstanceVolatile() {
        synchronized (counterLock) {
            counter2 = counter2 + 1;
            System.out.println( Thread.currentThread().getName() +"\t\t « InstanceVolatile :: "+ counter2);
        }
    }

    public void concurrentMethodWrong() {
        counter = counter + 1;
        System.out.println( Thread.currentThread().getName() +" « Static :: "+ counter);
        sleepThread( 1/4 );

        counter1 = counter1 + 1;
        System.out.println( Thread.currentThread().getName() +"\t « StaticVolatile :: "+ counter1);
        sleepThread( 1/4 );

        addInstanceVolatile();
        sleepThread( 1/4 );

        counter3 = counter3 + 1;
        sleepThread( 1/4 );
        System.out.println( Thread.currentThread().getName() +"\t\t\t\t\t « Instance :: "+ counter3);
    }
    public static void main(String[] args) throws InterruptedException {
        Runtime runtime = Runtime.getRuntime();

        int availableProcessors = runtime.availableProcessors();
        System.out.println("availableProcessors :: "+availableProcessors);
        System.out.println("MAX JVM will attempt to use : "+ runtime.maxMemory() / MegaBytes );
        System.out.println("JVM totalMemory also equals to initial heap size of JVM : "+ runtime.totalMemory() / MegaBytes );
        System.out.println("Returns the amount of free memory in the JVM : "+ untime.freeMemory() / MegaBytes );
        System.out.println(" ===== ----- ===== ");

        VolatileTest volatileTest = new VolatileTest();
        Thread t1 = new Thread( volatileTest );
        t1.start();

        Thread t2 = new Thread( volatileTest );
        t2.start();

        Thread t3 = new Thread( volatileTest );
        t3.start();

        Thread t4 = new Thread( volatileTest );
        t4.start();

        Thread.sleep( 10 );;

        Thread optimizeation = new Thread() {
            @Override public void run() {
                System.out.println("Thread Start.");

                Integer appendingVal = volatileTest.counter2 + volatileTest.counter2 + volatileTest.counter2;

                System.out.println("End of Thread." + appendingVal);
            }
        };
        optimizeation.start();
    }

    public void sleepThread( long sec ) {
        try {
            Thread.sleep( sec * 1000 );
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Static[Class Field] vs Volatile[Instance Field] - Both are not cached by threads

  • Статические поля являются общими для всех потоков и хранятся в области методов. Статический с летучим бесполезным. Статическое поле не может быть сериализовано.

  • Volatile в основном используется с переменной экземпляра, которая хранится в области кучи. Основное использование volatile - поддерживать обновленное значение для всех потоков. изменяемое поле экземпляра может быть сериализованным.

@see

Ответ 6

  В идеале, если keepRunning не является изменчивым, поток должен продолжать работать бесконечно. Но он останавливается через несколько секунд.

Если вы работаете в однопроцессорном режиме или ваша система очень загружена, ОС может выгружать потоки, что вызывает некоторые уровни аннулирования кэша. Как уже упоминали другие, отсутствие volatile не означает, что память не будет использоваться совместно, но JVM пытается не синхронизировать память, если это возможно по соображениям производительности, поэтому память может не обновляться.

Следует также отметить, что System.out.println(...) синхронизируется, потому что базовый PrintStream выполняет синхронизацию, чтобы остановить перекрывающийся вывод. Таким образом, вы получаете синхронизацию памяти "бесплатно" в основном потоке. Это все еще не объясняет, почему цикл чтения вообще видит обновления.

Независимо от того, включены ли линии println(...), ваша программа вращается для меня под Java6 на MacBook Pro с Intel i7.

Кто-нибудь может объяснить изменчивость с примером? Не с теорией из JLS.

Я думаю, что твой пример хорош. Не уверен, почему он не работает со всеми удаленными операторами System.out.println(...). Это работает для меня.

Является ли энергозависимый заменитель синхронизации? Достиг ли он атомарности?

С точки зрения синхронизации памяти, volatile создает те же барьеры памяти, что и блок synchronized, за исключением того, что барьер volatile является однонаправленным, а не двунаправленным. volatile читает выбрасывает барьер нагрузки, в то время как записи выбрасывает барьер магазина. Блок synchronized является двунаправленным барьером с добавлением блокировки мьютекса.

В терминах atomicity, однако, ответ "это зависит". Если вы читаете или записываете значение из поля, тогда volatile обеспечивает правильную атомарность. Однако увеличение поля volatile страдает от ограничения, заключающегося в том, что ++ фактически является 3 операциями: чтение, увеличение, запись. В этом случае или в более сложных случаях мьютекса может потребоваться полный блок synchronized. AtomicInteger решает проблему ++ с помощью сложной спин-циклы с проверкой и настройкой.

Ответ 7

Когда переменная имеет значение volatile, это гарантирует, что она не будет кэширована и что разные потоки увидят обновленное значение. Однако отсутствие маркировки volatile не гарантирует обратное. volatile был одной из тех вещей, которые были сломаны в JVM в течение долгого времени и до сих пор не всегда хорошо поняты.

Ответ 8

volatile не обязательно будет создавать гигантские изменения, в зависимости от JVM и компилятора. Тем не менее, для многих (крайних) случаев может быть разница между оптимизацией, в результате которой переменные изменения не могут быть замечены, а не правильно написаны.

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

volatile гарантирует, что такая оптимизация не произойдет, и все чтения и записи будут непосредственно в кучу или в другом месте, где будут видеть все потоки.

Ответ 9

Пожалуйста, найдите решение ниже,

Значение этой переменной никогда не будет кэшироваться нить-локально: все чтения и записи перейдут прямо к "основной памяти". Волатильность заставляет поток обновлять исходную переменную за каждый раз.

public class VolatileDemo {

    private static volatile int MY_INT = 0;

    public static void main(String[] args) {

        ChangeMaker changeMaker = new ChangeMaker();
        changeMaker.start();

        ChangeListener changeListener = new ChangeListener();
        changeListener.start();

    }

    static class ChangeMaker extends Thread {

        @Override
        public void run() {
            while (MY_INT < 5){
                System.out.println("Incrementing MY_INT "+ ++MY_INT);
                try{
                    Thread.sleep(1000);
                }catch(InterruptedException exception) {
                    exception.printStackTrace();
                }
            }
        }
    }

    static class ChangeListener extends Thread {

        int local_value = MY_INT;

        @Override
        public void run() {
            while ( MY_INT < 5){
                if( local_value!= MY_INT){
                    System.out.println("Got Change for MY_INT "+ MY_INT);
                    local_value = MY_INT;
                }
            }
        }
    }

}

Пожалуйста, обратитесь к этой ссылке http://java.dzone.com/articles/java-volatile-keyword-0, чтобы получить больше ясности в ней.

Ответ 10

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

Ответ 11

Ключевое слово volatile сообщает JVM, что оно может быть изменено другим потоком. Каждый поток имеет свой собственный стек и, следовательно, свою собственную копию переменных, к которой он может получить доступ. Когда поток создается, он копирует значение всех доступных переменных в своей собственной памяти.

public class VolatileTest {
    private static final Logger LOGGER = MyLoggerFactory.getSimplestLogger();

    private static volatile int MY_INT = 0;

    public static void main(String[] args) {
        new ChangeListener().start();
        new ChangeMaker().start();
    }

    static class ChangeListener extends Thread {
        @Override
        public void run() {
            int local_value = MY_INT;
            while ( local_value < 5){
                if( local_value!= MY_INT){
                    LOGGER.log(Level.INFO,"Got Change for MY_INT : {0}", MY_INT);
                     local_value= MY_INT;
                }
            }
        }
    }

    static class ChangeMaker extends Thread{
        @Override
        public void run() {

            int local_value = MY_INT;
            while (MY_INT <5){
                LOGGER.log(Level.INFO, "Incrementing MY_INT to {0}", local_value+1);
                MY_INT = ++local_value;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) { e.printStackTrace(); }
            }
        }
    }
}

опробуйте этот пример с использованием и без использования volatile.

Ответ 12

Объекты, объявленные как энергозависимые, обычно используются для передачи информации о состоянии между потоками. Чтобы обеспечить обновление кэша ЦП, то есть храниться в синхронизации, при наличии изменчивых полей, инструкции ЦП, барьера памяти, часто называемого membar или забор, испускается для обновления кэшей CPU с изменением значения изменчивых полей.

Модификатор volatile сообщает компилятору, что переменная, измененная volatile, может быть неожиданно изменена другими частями вашей программы.

Изменчивая переменная должна использоваться только в контексте потока. см. пример здесь

Ответ 13

Много хороших примеров, но я просто хочу добавить, что есть ряд сценариев, где требуется volatile, поэтому нет одного конкретного примера, чтобы управлять ими a.

  1. Вы можете использовать volatile, чтобы заставить все потоки получать последнее значение переменной из основной памяти.
  2. Вы можете использовать synchronization для защиты важных данных
  3. Вы можете использовать Lock API
  4. Вы можете использовать переменные Atomic

Ознакомьтесь с другими волатильными примерами Java.