Какое ключевое слово volatile полезно для

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

Теория и практика Java: управление волатильностью

Учитывая детали, в которых эта статья объясняет ключевое слово, о котором идет речь, вы когда-либо использовали его или вы когда-либо видели случай, когда вы могли бы использовать это ключевое слово правильно?

Ответ 1

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

Чтобы ответить на ваш вопрос: Да, я использую переменную volatile, чтобы контролировать, продолжает ли какой-либо код цикл. Цикл проверяет значение volatile и продолжает, если он true. Условие можно установить в false, вызвав метод "stop". Цикл видит false и завершается, когда он проверяет значение после того, как метод stop завершает выполнение.

Книга " Java Concurrency на практике, которую я настоятельно рекомендую, дает хорошее объяснение volatile. Эта книга написана тем же человеком, который написал статью IBM, на которую ссылается в вопросе (фактически, он цитирует свою книгу в нижней части этой статьи). Мое использование volatile - это то, что его статья называет" флаг состояния шаблона 1".

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

Ответ 2

"... изменчивый модификатор гарантирует, что любой поток, который читает поле, увидит последнее записанное значение." - Джош Блох

Если вы думаете об использовании volatile, прочитайте в пакете java.util.concurrent, который посвящен атомному поведению.

Статья Википедии на Синглтонном шаблоне показывает изменчивость в использовании.

Ответ 3

Важный момент о volatile :

  1. Синхронизация в Java возможна с использованием ключевых слов Java synchronized и volatile и блокировок.
  2. В Java у нас не может быть synchronized переменной. Использование synchronized ключевого слова с переменной недопустимо и приведет к ошибке компиляции. Вместо использования synchronized переменной в Java вы можете использовать переменную java volatile, которая будет указывать потокам JVM считывать значение переменной volatile из основной памяти и не кэшировать ее локально.
  3. Если переменная не используется несколькими потоками, нет необходимости использовать ключевое слово volatile.

источник

Пример использования volatile :

public class Singleton {
    private static volatile Singleton _instance; // volatile variable
    public static Singleton getInstance() {
        if (_instance == null) {
            synchronized (Singleton.class) {
                if (_instance == null)
                    _instance = new Singleton();
            }
        }
        return _instance;
    }
}

Мы создаем экземпляр лениво во время первого запроса.

Если мы не сделаем переменную _instance volatile _instance создающий экземпляр Singleton, не сможет связаться с другим потоком. Таким образом, если поток A создает экземпляр Singleton и сразу после его создания процессор поврежден и т.д., Все остальные потоки не смогут увидеть значение _instance как ненулевое, и они будут считать, что ему по-прежнему присваивается значение null.

Почему это происходит? Поскольку потоки считывателя не блокируются, и пока поток _instance не выйдет из синхронизированного блока, память не будет синхронизирована и значение _instance не будет обновлено в основной памяти. С ключевым словом Volatile в Java это обрабатывается самой Java, и такие обновления будут видны всем потокам читателей.

Вывод: ключевое слово volatile также используется для передачи содержимого памяти между потоками.

Пример использования без volatile:

public class Singleton{    
    private static Singleton _instance;   //without volatile variable
    public static Singleton getInstance(){   
          if(_instance == null){  
              synchronized(Singleton.class){  
               if(_instance == null) _instance = new Singleton(); 
      } 
     }   
    return _instance;  
    }

Код выше не является потокобезопасным. Хотя он проверяет значение экземпляра еще раз в синхронизированном блоке (по соображениям производительности), JIT-компилятор может перегруппировать байт-код таким образом, чтобы ссылка на экземпляр была установлена до того, как конструктор завершит свое выполнение. Это означает, что метод getInstance() возвращает объект, который, возможно, не был полностью инициализирован. Чтобы сделать код потокобезопасным, ключевое слово volatile может использоваться начиная с Java 5 для переменной экземпляра. Переменные, помеченные как volatile, становятся видимыми для других потоков только после того, как конструктор объекта полностью завершит свое выполнение.
Источник

enter image description here

volatile использование в Java:

Отказоустойчивые итераторы обычно реализуются с использованием volatile счетчика в объекте списка.

  • Когда список обновляется, счетчик увеличивается.
  • При создании Iterator текущее значение счетчика внедряется в объект Iterator.
  • Когда выполняется операция Iterator, метод сравнивает два значения счетчика и выдает ConcurrentModificationException если они различаются.

Реализация отказоустойчивых итераторов обычно легкоspring. Они обычно полагаются на свойства конкретных структур данных реализации списка. Там нет общей картины.

Ответ 4

volatile очень полезен для остановки потоков.

Не нужно писать собственные потоки, у Java 1.6 есть много хороших пулов потоков. Но если вы уверены, что вам нужен поток, вам нужно знать, как его остановить.

Я использую для потоков:

public class Foo extends Thread {
  private volatile boolean close = false;
  public void run() {
    while(!close) {
      // do work
    }
  }
  public void close() {
    close = true;
    // interrupt here if needed
  }
}

Обратите внимание, что нет необходимости в синхронизации

Ответ 5

Когда volatile достаточно?

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

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

Особенности использования volatile:

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

Ответ 6

Одним из распространенных примеров использования volatile является использование переменной volatile boolean в качестве флага для завершения потока. Если вы запустили поток, и хотите, чтобы вы могли безопасно прерывать его из другого потока, вы можете периодически проверять поток. Чтобы остановить его, установите флаг в значение true. Сделав флаг volatile, вы можете убедиться, что поток, который его проверяет, увидит, что он был установлен при следующем его проверке без необходимости использовать блок synchronized.

Ответ 7

Переменная, объявленная с ключевым словом volatile, имеет два основных качества, которые делают ее особенной.

  1. Если у нас есть переменная volatile, она не может быть кэширована в кэш-память компьютера (микропроцессора) каким-либо потоком. Доступ всегда происходил из основной памяти.

  2. Если существует операция записи, выполняющая переменную, и внезапно запрашивается операция чтения, гарантируется, что операция записи будет завершена до операции чтения.

Два вышеуказанных качества выводят, что

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

А с другой стороны,

  • Если мы продолжим исследование # 2, о котором я упоминал, мы увидим, что ключевое слово volatile - это идеальный способ поддерживать общую переменную, которая имеет 'n' число потоков читателей и только один Автор темы для доступа к нему. Как только мы добавим ключевое слово volatile, все готово. Никаких других накладных расходов на безопасность потоков.

Conversly,

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

Ответ 8

Никто не упомянул об обработке операций чтения и записи для длинного и двойного переменных типов. Считывание и запись - это атомарные операции для ссылочных переменных и для большинства примитивных переменных, за исключением длинных и двойных типов переменных, которые должны использовать ключевое слово volatile для атомных операций. @link

Ответ 9

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

Ответ 10

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

  1. Дважды проверил механизм блокировки. Часто используется в дизайне Singleton шаблон. В этом случае одноэлементный объект должен быть объявлен как volatile.
  2. Ложные пробуждения. Поток может иногда просыпаться от ожидающего вызова, даже если не было отправлено уведомление. Такое поведение называется ложным пробуждением. Этому можно противостоять, используя условную переменную (логический флаг). Поместите вызов wait() в цикл while, пока флаг имеет значение true. Поэтому, если поток выходит из режима ожидания по каким-либо причинам, отличным от Notify/NotifyAll, тогда он обнаруживает, что флаг все еще имеет значение true, и, следовательно, вызовы ожидают снова. Перед вызовом уведомления установите этот флаг в значение true. В этом случае логический флаг объявляется как volatile.

Ответ 11

Вам нужно будет использовать ключевое слово "volatile" или "synchronized" и любые другие инструменты и методы управления concurrency, которые могут быть у вас в распоряжении, если вы разрабатываете многопоточное приложение. Пример такого приложения - настольные приложения.

Если вы разрабатываете приложение, которое будет развернуто на сервере приложений (Tomcat, JBoss AS, Glassfish и т.д.), вам не придется управлять concurrency самим собой, поскольку он уже адресован сервером приложений. На самом деле, если я правильно помню, стандарт Java EE запрещает какой-либо элемент управления concurrency в сервлетах и ​​EJB, так как он является частью слоя "инфраструктуры", который вы должны освободить от его обработки. В этом приложении вы управляете только concurrency, если вы реализуете одноэлементные объекты. Это даже уже рассмотрено, если вы вяжете свои компоненты с помощью frameworkd, например Spring.

Таким образом, в большинстве случаев разработки Java, где приложение является веб-приложением и с использованием инфраструктуры IoC, например Spring или EJB, вам не нужно будет использовать "volatile".

Ответ 12

volatile гарантирует, что все потоки, даже сами по себе, увеличиваются. Например: счетчик видит одно и то же лицо переменной одновременно. Он не используется вместо синхронизированного или атомного или другого материала, он полностью синхронизирует чтение. Пожалуйста, не сравнивайте его с другими ключевыми словами Java. Как видно из приведенного ниже примера, операции volatile variable также являются атомарными, они терпят неудачу или становятся успешными сразу.

package io.netty.example.telnet;

import java.util.ArrayList;
import java.util.List;

public class Main {

    public static volatile  int a = 0;
    public static void main(String args[]) throws InterruptedException{

        List<Thread> list = new  ArrayList<Thread>();
        for(int i = 0 ; i<11 ;i++){
            list.add(new Pojo());
        }

        for (Thread thread : list) {
            thread.start();
        }

        Thread.sleep(20000);
        System.out.println(a);
    }
}
class Pojo extends Thread{
    int a = 10001;
    public void run() {
        while(a-->0){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Main.a++;
            System.out.println("a = "+Main.a);
        }
    }
}

Даже вы ставите изменчивые или не результаты всегда будут отличаться. Но если вы используете AtomicInteger, как ниже, результаты будут всегда одинаковыми. Это также синхронизируется.

    package io.netty.example.telnet;

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;

    public class Main {

        public static volatile  AtomicInteger a = new AtomicInteger(0);
        public static void main(String args[]) throws InterruptedException{

            List<Thread> list = new  ArrayList<Thread>();
            for(int i = 0 ; i<11 ;i++){
                list.add(new Pojo());
            }

            for (Thread thread : list) {
                thread.start();
            }

            Thread.sleep(20000);
            System.out.println(a.get());

        }
    }
    class Pojo extends Thread{
        int a = 10001;
        public void run() {
            while(a-->0){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Main.a.incrementAndGet();
                System.out.println("a = "+Main.a);
            }
        }
    }

Ответ 13

Да, я использую его довольно много - он может быть очень полезен для многопоточного кода. Статья, на которую вы указали, является хорошей. Хотя есть две важные вещи, которые нужно иметь в виду:

  • Вы должны использовать только volatile, если вы полностью понять, что он делает и как он отличается от синхронизации. Во многих ситуациях возникает нестабильность, на поверхности, чтобы быть проще альтернатива синхронизированы, когда часто лучше понимание изменчивости ясно, что синхронизация является единственной который будет работать.
  • volatile фактически не работает в много старых JVM, хотя синхронизированный... Я помню, что видел документ, который ссылался на различные уровни поддержки в разных JVM, но, к сожалению, я не могу его найти сейчас. Определенно изучите его, если вы используете Java pre 1.5 или если у вас нет контроля над JVM, в котором будет работать ваша программа.

Ответ 14

Абсолютно, да. (И не только в Java, но также и в С#.) Есть моменты, когда вам нужно получить или установить значение, которое гарантируется как атомная операция на вашей данной платформе, например int или boolean, но не требует накладные расходы на блокировку резьбы. Ключевое слово volatile позволяет вам убедиться, что когда вы читаете значение, которое вы получаете текущее значение, а не кешированное значение, которое было просто устарело, записав в другой поток.

Ответ 15

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

Только переменная-член может быть изменчивой или временной.

Ответ 16

Существует два варианта использования ключевого слова volatile.

  • Запрещает JVM считывать значения из регистра (считать кешем) и заставляет его значение считываться из памяти.
  • Снижает риск ошибок в последовательности.

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

Флаг занятости используется для предотвращения продолжения потока, когда устройство занято, а флаг не защищен блокировкой:

while (busy) {
    /* do something else */
}

Тестовый поток будет продолжаться, когда другой поток отключит флаг занятости:

busy = 0;

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

Снижает риск ошибок согласованности памяти.

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

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

Атомное действие - это действие, которое эффективно происходит сразу. Атомное действие не может остановиться посередине: оно либо происходит полностью, либо вообще не происходит. Никакие побочные эффекты атомного действия не видны до тех пор, пока действие не будет завершено.

Ниже приведены действия, которые вы можете указать атомарно:

  • Чтения и записи являются атомарными для ссылочных переменных и для большинства примитивные переменные (все типы, кроме длинного и двойного).
  • Чтения и записи являются атомарными для всех объявленных переменных volatile (включая длинные и двойные переменные).

Ура!

Ответ 17

Летучий делает следующее.

1> Чтение и запись изменчивых переменных различными потоками всегда осуществляется из памяти, а не из собственного кэша потока или регистра процессора. Таким образом, каждый поток всегда имеет дело с последним значением. 2> Когда 2 разных потока работают с одним и тем же экземпляром или статическими переменными в куче, можно увидеть другие действия не по порядку. Смотрите блог Джереми Мэнсона по этому вопросу. Но волатильность помогает здесь.

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

thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3
thread 0 prints 0
thread 1 prints 1
thread 2 prints 2
thread 3 prints 3

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

public class Solution {
    static volatile int counter = 0;
    static int print = 0;
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Thread[] ths = new Thread[4];
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new Thread(new MyRunnable(i, ths.length));
            ths[i].start();
        }
    }
    static class MyRunnable implements Runnable {
        final int thID;
        final int total;
        public MyRunnable(int id, int total) {
            thID = id;
            this.total = total;
        }
        @Override
        public void run() {
            // TODO Auto-generated method stub
            while (true) {
                if (thID == counter) {
                    System.out.println("thread " + thID + " prints " + print);
                    print++;
                    if (print == total)
                        print = 0;
                    counter++;
                    if (counter == total)
                        counter = 0;
                } else {
                    try {
                        Thread.sleep(30);
                    } catch (InterruptedException e) {
                        // log it
                    }
                }
            }
        }
    }
}

Следующая ссылка на github содержит файл readme, который дает правильное объяснение. https://github.com/sankar4git/volatile_thread_ordering

Ответ 18

Из документации oracle страница возникает потребность в изменчивой переменной для устранения проблем согласованности памяти:

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

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

Как объяснено в ответе Peter Parker, в отсутствие модификатора volatile каждый стек потока может иметь свою собственную копию переменной. Сделав переменную как volatile, проблемы согласованности памяти были исправлены.

Посмотрите на страницу jenkov для лучшего понимания.

Посмотрите на связанный вопрос SE для получения более подробной информации о неустойчивых и используемых случаях для использования volatile:

Разница между изменчивой и синхронизированной в Java

Один практический пример использования:

У вас много потоков, которые должны печатать текущее время в определенном формате, например: java.text.SimpleDateFormat("HH-mm-ss"). Yon может иметь один класс, который преобразует текущее время в SimpleDateFormat и обновляет переменную каждую секунду. Все остальные потоки могут просто использовать эту переменную volatile для печати текущего времени в файлах журналов.

Ответ 19

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

Ответ 20

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

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

для справки, обратитесь к http://techno-terminal.blogspot.in/2015/11/what-are-volatile-variables.html

Ответ 21

Мне нравится объяснение Дженкова:

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

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

Это расширенная гарантия видимости, так называемая гарантия "происходит раньше".

Производительность Соображения изменчивы

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

Ответ 22

Это часть аппаратного управления памятью. На самом деле значение свойства может быть сохранено как минимум в RAM memory или CPU cache memory

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

У Java есть ключевое слово volatile, чтобы справиться с этой ситуацией. Он используется для пометки переменной Java для чтения и записи в оперативную память напрямую и атомарно.

Java 5 расширяет ответственность volatile, которая называется happens-before guarantee[About]. Запись в поле volatile happens-before при каждом последующем чтении того же поля.

Но как насчет ситуации, когда несколько потоков записывают разные значения в поле volatile одновременно (т.е. race condition)? Ключевое слово synchronized для спасения[About]

Когда летучих достаточно?

Когда только один поток пишет, а другие просто читают volatile, это гарантирует, что они увидят актуальное значение

Также учтите, что использование переменной volatile является более дорогой задачей, а также предотвращает переупорядочение команд (т.е. использование техники happens-before), которая является обычной техникой повышения производительности. Следовательно, вы должны использовать переменные volatile только тогда, когда вам это действительно нужно.

Прочитайте объяснение Дженкова

Ответ 23

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

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

Ответ 24

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

Ответ 25

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

// Code to prove importance of 'volatile' when state of one thread is being mutated from another thread.
// Try running this class with and without 'volatile' for 'state' property of Task class.
public class VolatileTest {
    public static void main(String[] a) throws Exception {
        Task task = new Task();
        new Thread(task).start();

        Thread.sleep(500);
        long stoppedOn = System.nanoTime();

        task.stop(); // -----> do this to stop the thread

        System.out.println("Stopping on: " + stoppedOn);
    }
}

class Task implements Runnable {
    // Try running with and without 'volatile' here
    private volatile boolean state = true;
    private int i = 0;

    public void stop() {
        state = false;
    } 

    @Override
    public void run() {
        while(state) {
            i++;
        }
        System.out.println(i + "> Stopped on: " + System.nanoTime());
    }
}

Когда volatile не используется: вы никогда не увидите сообщение "Stopped on: xxx" даже после "Stopping on: xxx", и программа продолжит работу.

Stopping on: 1895303906650500

Когда используется volatile : вы сразу увидите "Остановлено: ххх".

Stopping on: 1895285647980000
324565439> Stopped on: 1895285648087300

Демонстрация: https://repl.it/repls/SilverAgonizingObjectcode