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

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

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

Ниже фрагмент кода не работает должным образом ( от aioobe)

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.
  • Является ли энергозависимый заменитель синхронизации? Достигает ли он атомарности?

14 ответов

Volatile -> Гарантирует видимость, а не атомарность

Синхронизация (Блокировка) -> Гарантирует видимость и атомарность (если все сделано правильно)

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

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

Пример:

volatile int i = 0;

public void incrementI(){
   i++;
}

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

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

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

Правильное использование volatile обсуждает различные возможные варианты использования volatile. Правильно использовать 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++) atomic, если только вы не можете гарантировать, что переменная записана только из одного потока.

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

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

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

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

Это отрывок из Java Concurrency in Practice, лучшая книга, которую вы можете найти по этому вопросу.

Я немного изменил ваш пример. Теперь используйте пример с 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);
    }
}

Что такое изменчивое ключевое слово?

изменчивое ключевое слово предотвращает caching of variables,

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

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,

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

Access_Modifier volatile DataType Variable_Name;

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

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

volatile не может быть кэширована - ассемблер

    +--------------+--------+-------------------------------------+
    |  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();
        }
    }
}

Статический [ Class Field ] против летучих [ Instance Field ] - оба не кэшируются потоками

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

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

@увидеть

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

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

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

Будь то println(...) линии или нет, ваша программа вращается для меня под Java6 на MacBook Pro с Intel i7.

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

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

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

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

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

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

      public class VolatileDemo {
    static class Processor {
        //without volatile program keeps running on my platform
        private boolean flag = false;

        public void setFlag() {
            System.out.println("setting flag true");
            this.flag = true;
        }

        public void process() {
            while(!flag) {
                int x = 5;
                // using sleep or sout will end the program without volatile.
                // Probably these operations, cause thread to be rescheduled, read from memory. Thus read new flag value and end.
            }

            System.out.println("Ending");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Processor processor = new Processor();
        Thread t1 = new Thread(processor::process);

        t1.start();

        Thread.sleep(2000);
        processor.setFlag();

    }
}

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

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

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

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

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

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

Ключевое слово 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.

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

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

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 чтобы получить больше ясности в этом.

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

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

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

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

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