Ключевое слово volatile, которое вы должны знать

интервью Java задняя часть JVM

предисловие

Будь то в интервью или фактическом развитииvolatileЭто навык, которым следует овладеть.

Давайте сначала посмотрим, почему появляется это ключевое слово.

видимость памяти

потому чтоJavaмодель памяти (JMM) предусматривает, что все переменные хранятся в основной памяти, а каждый поток имеет свою рабочую память (кеш).

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

Упомянутую здесь основную память можно просто рассматривать каккуча памяти, а рабочую память можно рассматривать какстек памяти.

Как показано ниже:

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

Очевидно, что это обязательно пойдет не так, поэтомуvolatileПоявляется эффект:

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

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

Приложения видимости памяти

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

public class Volatile implements Runnable{

    private static volatile boolean flag = true ;

    @Override
    public void run() {
        while (flag){
            System.out.println(Thread.currentThread().getName() + "正在运行。。。");
        }
        System.out.println(Thread.currentThread().getName() +"执行完毕");
    }

    public static void main(String[] args) throws InterruptedException {
        Volatile aVolatile = new Volatile();
        new Thread(aVolatile,"thread A").start();


        System.out.println("main 线程正在运行") ;

        TimeUnit.MILLISECONDS.sleep(100) ;

        aVolatile.stopThread();

    }

    private void stopThread(){
        flag = false ;
    }
}

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

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

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

Здесь важно подчеркнуть,volatileа такжене можетГарантированная безопасность потока!

Следующая процедура:

public class VolatileInc implements Runnable{

    private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性

    //private static AtomicInteger count = new AtomicInteger() ;

    @Override
    public void run() {
        for (int i=0;i<10000 ;i++){
            count ++ ;
            //count.incrementAndGet() ;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileInc volatileInc = new VolatileInc() ;
        Thread t1 = new Thread(volatileInc,"t1") ;
        Thread t2 = new Thread(volatileInc,"t2") ;
        t1.start();
        //t1.join();

        t2.start();
        //t2.join();
        for (int i=0;i<10000 ;i++){
            count ++ ;
            //count.incrementAndGet();
        }


        System.out.println("最终Count="+count);
    }
}

Когда наши три потока (t1, t2, main) одновременноintПри накоплении вы обнаружите, что конечное значение будет меньше 30000.

Это потому, что хотяvolatileВидимость памяти гарантируется, и значение, полученное каждым потоком, является последним значением, ноcount ++Эта операция не является атомарной, и операции, связанные с получением, увеличением и присвоением значений, не могут выполняться одновременно.

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

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

  • также можно использоватьAtomicв сумкеAtomicIntegerзаменитьint, который используетCASалгоритм, обеспечивающий атомарность.

перестановка инструкций

Видимость памяти простоvolatileодна из семантик , это также предотвращаетJVMВыполните оптимизацию перестановки команд.

Возьмем псевдокод:

int a=10 ;//1
int b=20 ;//2
int c= a+b ;//3

Особенно простой фрагмент кода, в идеале порядок выполнения будет таким:1>2>3. Но возможно, что порядок выполнения после оптимизации JVM станет2>1>3.

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

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

private static Map<String,String> value ;
private static volatile boolean flag = fasle ;

//以下方法发生在线程 A 中 初始化 Map
public void initMap(){
	//耗时操作
	value = getMapValue() ;//1
	flag = true ;//2
}


//发生在线程 B中 等到 Map 初始化成功进行其他操作
public void doSomeThing(){
	while(!flag){
		sleep() ;
	}
	//dosomething
	doSomeThing(value);
}

Проблема видна здесь, когдаflagне былvolatileПри изменении,JVMпереставляет 1 и 2, в результате чегоvalueОн может использоваться потоком B до его инициализации.

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

Применение перестановки инструкций

Классический сценарий использования — двойная отложенная одноэлементная загрузка:

public class Singleton {

    private static volatile Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    //防止指令重排
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

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

Если не,singleton = new Singleton();, этот код фактически разделен на три шага:

  • Выделить место в памяти. (1)
  • Инициализировать объект. (2)
  • будетsingletonОбъект указывает на выделенный адрес памяти. (3)

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

Суммировать

volatileсуществуетJavaОн часто используется в параллелизме, напримерAtomicв упаковкеvalue,так же какAbstractQueuedLongSynchronizerсерединаstateопределяются какvolatileдля обеспечения видимости памяти.

Полное понимание этого может очень помочь, когда мы пишем параллельные программы.

Дополнительный

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

адрес:GitHub.com/crossover J я…