>Java >java지도 시간 >휘발성 변수를 이해하는 방법 - Java 동시 프로그래밍 및 기술 내부자

휘발성 변수를 이해하는 방법 - Java 동시 프로그래밍 및 기술 내부자

php是最好的语言
php是最好的语言원래의
2018-07-26 15:29:521511검색

Java 언어는 변수 업데이트 작업을 다른 스레드에 알릴 수 있도록 약간 약한 동기화 메커니즘, 즉 휘발성 변수를 제공합니다. 휘발성 성능: 휘발성의 읽기 성능 소비는 일반 변수와 거의 동일하지만 쓰기 작업은 프로세서가 순서대로 실행되지 않도록 네이티브 코드에 많은 메모리 장벽 명령을 삽입해야 하기 때문에 약간 느립니다.

1. 휘발성 변수

Java 언어는 약간 약한 동기화 메커니즘, 즉 휘발성 변수을 제공합니다. 업데이트 작업은 다른 스레드에 통보됩니다. 변수가 휘발성으로 선언되면 컴파일러와 런타임은 변수가 공유된다는 것을 인식하므로 변수에 대한 작업은 다른 메모리 작업과 함께 재정렬되지 않습니다. 휘발성 변수는 레지스터에 캐시되지 않거나 다른 프로세서에 표시되지 않으므로 휘발성 유형의 변수를 읽으면 항상 가장 최근에 작성된 값이 반환됩니다.

휘발성 변수에 액세스할 때 잠금 작업이 수행되지 않으므로 실행 스레드가 차단되지 않습니다. 따라서 휘발성 변수는 동기화된 키워드보다 더 가벼운 동기화 메커니즘입니다.

휘발성 변수를 이해하는 방법 - Java 동시 프로그래밍 및 기술 내부자

비휘발성 변수를 읽거나 쓸 때 각 스레드는 먼저 변수를 메모리에서 CPU 캐시로 복사합니다. 컴퓨터에 여러 개의 CPU가 있는 경우 각 스레드는 서로 다른 CPU에서 처리될 수 있습니다. 이는 각 스레드가 서로 다른 CPU 캐시에 복사될 수 있음을 의미합니다.

그리고 변수가 휘발성으로 선언되면 JVM은 변수를 읽을 때마다 CPU 캐시 단계를 건너뛰고 메모리에서 읽히도록 합니다.

변수가 휘발성으로 정의되면 두 가지 특성을 갖게 됩니다:

 1. 언급한 대로 이 변수가 모든 스레드에 대해 "가시성"이 표시되는지 확인하세요. 이 기사의 시작 부분에서 스레드가 이 변수의 값을 수정할 때 휘발성은 새 값이 즉시 메인 메모리에 동기화되고 각 사용 직전에 메인 메모리에서 플러시되도록 보장합니다. 그러나 일반 변수는 이를 수행할 수 없습니다. 일반 변수의 값은 주 메모리를 통해 스레드 간에 전송되어야 합니다(자세한 내용은 Java 메모리 모델 참조).

 2. 명령어 재정렬 최적화를 비활성화합니다. 휘발성 수정이 있는 변수의 경우 할당 후 추가 "load addl $0x0, (%esp)" 작업이 수행됩니다. 이 작업은 메모리 장벽과 동일합니다(명령어가 재정렬되면 후속 명령을 메모리 이전 위치로 재정렬할 수 없습니다). Barrier) ), 하나의 CPU만 메모리에 액세스하는 경우 메모리 배리어가 필요하지 않습니다. 프로그램에서 지정하지 않음).

휘발성 성능:

휘발성의 읽기 성능 소모는 일반 변수와 거의 비슷하지만 메모리 배리어 명령을 많이 삽입해야 하기 때문에 쓰기 작업이 약간 느립니다. 프로세서가 순서대로 실행되지 않도록 네이티브 코드에 포함합니다.

2. 메모리 가시성

JMM(Java Memory Model)으로 인해 모든 변수는 메인 메모리에 저장되며, 각 스레드는 자체 작업 메모리(캐시)를 갖습니다.

스레드가 작동 중일 때 메인 메모리에 있는 데이터를 작업 메모리로 복사해야 합니다. 이렇게 하면 데이터에 대한 모든 작업이 작업 메모리를 기반으로 이루어지며(효율성이 향상됨) 메인 메모리의 데이터와 다른 스레드의 작업 메모리에 있는 데이터를 직접 조작할 수 없으며 업데이트된 데이터가 메인 메모리에 새로 고쳐집니다. 메모리.

여기서 언급하는 메인 메모리는 간단히 힙 메모리라고 생각하면 되고, 워킹 메모리는 스택 메모리라고 생각하면 됩니다.

따라서 동시에 실행하는 경우 스레드 B가 읽은 데이터가 스레드 A가 업데이트되기 전의 데이터인 경우가 발생할 수 있습니다.

분명히 문제가 발생할 수 있으므로 휘발성의 역할이 나타납니다.

변수가 휘발성에 의해 수정되면 모든 스레드에서 쓰기 이에 대한 작업은 즉시 주 메모리로 플러시되고 변수를 캐시한 스레드의 데이터가 지워지도록 하며 최신 데이터를 주 메모리에서 다시 읽어야 합니다.

휘발성 수정 후에도 스레드는 주 메모리에서 직접 데이터를 가져오지 않으며 여전히 변수를 작업 메모리에 복사해야 합니다.

메모리 가시성 적용

메인 메모리를 기반으로 두 스레드 간 통신이 필요한 경우 통신 변수를 휘발성으로 수정해야 합니다. #🎜🎜 #

public class Test {

private static /*volatile*/ boolean stop = false;

public static void main(String[] args) throws Exception {
    Thread t = new Thread(
            () -> {
                int i = 0;
                while (!stop) {
                    i++;
                 System.out.println("hello");
                }
            });
    t.start();

    Thread.sleep(1000);
    TimeUnit.SECONDS.sleep(1);
    System.out.println("Stop Thread");
    stop = true;
}
}
#🎜🎜 #위의 예가 휘발성으로 설정되지 않으면 스레드가 절대 종료되지 않을 수 있습니다

그러나 여기서는 오해가 있습니다. #

휘발성 수정 변수에 대한 동시 작업은 스레드로부터 안전합니다.

여기에서 휘발성이 스레드 안전성을 보장하지 않는다는 점을 강조해야 합니다!

다음 프로그램:

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 < 100; i++) {
        count++;
        //count.incrementAndGet() ;

    }
}

public static void main(String[] args) throws InterruptedException {
    VolatileInc volatileInc = new VolatileInc();
    IntStream.range(0,100).forEach(i->{
        Thread t= new Thread(volatileInc, "t" + i);
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    System.out.println(count);
}
}
세 개의 스레드(t1, t2, main)가 동시에 int를 축적하면 최종 값은 100000보다 작습니다.

这是因为虽然 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 之后可以防止这样的重排优化,保证业务的正确性。

指令重排的的应用

一个经典的使用场景就是双重懒加载的单例模式了:

class Singleton{
private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {
    if(instance==null) {
        synchronized (Singleton.class) {
            if(instance==null)
                instance = new Singleton();
        }
    }
    return instance;
}

这里的 volatile 关键字主要是为了防止指令重排。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:

    1.给 instance 分配内存

    2.调用 Singleton 的构造函数来初始化成员变量

    3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

       但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

相关文章:

具体介绍java高并发中volatile的实现原理

Java中如何正确使用Volatile变量?

相关视频:

Java多线程与并发库高级应用视频教程

위 내용은 휘발성 변수를 이해하는 방법 - Java 동시 프로그래밍 및 기술 내부자의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.