ホームページ  >  記事  >  Java  >  揮発性変数を理解する - Java 同時プログラミングとテクノロジー内部関係者

揮発性変数を理解する - Java 同時プログラミングとテクノロジー内部関係者

php是最好的语言
php是最好的语言オリジナル
2018-07-26 15:29:521462ブラウズ

Java 言語は、他のスレッドに変数の更新操作が確実に通知されるように、少し弱い同期メカニズム、つまり揮発性変数を提供します。 Volatile のパフォーマンス: volatile の読み取りパフォーマンスの消費量は通常の変数の場合とほぼ同じですが、プロセッサが順序どおりに実行されないようにネイティブ コードに多くのメモリ バリア命令を挿入する必要があるため、書き込み操作はわずかに遅くなります。

1. 揮発性変数

Java 言語は、他のスレッドに変数の更新操作が確実に通知されるようにするために、揮発性変数 という少し弱い同期メカニズムを提供します。変数が volatile として宣言されると、コンパイラーとランタイムはその変数が共有されていることを認識するため、変数に対する操作は他のメモリー操作と並べ替えられません。揮発性変数はレジスタにキャッシュされず、他のプロセッサからは見えないため、揮発性タイプの変数を読み取ると、常に最後に書き込まれた値が返されます。

volatile 変数にアクセスするときにロック操作は実行されないため、実行スレッドはブロックされません。したがって、volatile 変数は sychronized キーワードよりも軽量な同期メカニズムです。

揮発性変数を理解する - Java 同時プログラミングとテクノロジー内部関係者

不揮発性変数の読み取りまたは書き込みを行う場合、各スレッドはまずメモリから CPU キャッシュに変数をコピーします。コンピュータに複数の CPU がある場合、各スレッドは異なる CPU で処理される可能性があります。これは、各スレッドが異なる CPU キャッシュにコピーされる可能性があることを意味します。 V v 変数は揮発性です。JVM は、各読み取り変数がメモリーから読み取られることを保証し、CPU キャッシュのステップはスキップされます。

変数が volatile として定義されている場合、次の 2 つの特性があります:

1. この記事の冒頭で述べたように、スレッドがこれを変更するときに、この変数の可視性を確保します。変数の値が volatile であるため、新しい値が直ちにメイン メモリに同期され、使用される直前にメイン メモリからリフレッシュされます。ただし、通常の変数ではこれができません。通常の変数の値は、メイン メモリを介してスレッド間で転送される必要があります (詳細については、「Java メモリ モデル」を参照)。

2. 命令の並べ替えの最適化を無効にします。揮発性の変更が含まれる変数の場合、割り当て後に追加の「load addl $0x0, (%esp)」操作が実行されます。この操作はメモリ バリアに相当します (命令が並べ替えられると、後続の命令をメモリの前の位置に並べ替えることはできません)。バリア) )、1 つの CPU だけがメモリにアクセスする場合、メモリ バリアは必要ありません。(命令リオーダリングとは: CPU が、複数の命令を対応する各回路ユニットに個別に送信して順番に処理できるようにする方法を使用することを意味します。プログラムによって指定されていません)。

volatile パフォーマンス:

volatile の読み取りパフォーマンスの消費は通常の変数の消費パフォーマンスとほぼ同じですが、プロセッサが実行されないようにネイティブ コードに多くのメモリ バリア命令を挿入する必要があるため、書き込み操作はわずかに遅くなります。故障中。

2. メモリの可視性

Java メモリ モデル (JMM) により、すべての変数はメイン メモリに保存され、各スレッドは独自の作業メモリ (キャッシュ) を持ちます。

スレッドが動作しているとき、メインメモリのデータを作業メモリにコピーする必要があります。このようにして、データに対するすべての操作は作業メモリに基づいて行われ (効率が向上します)、メイン メモリ内のデータや他のスレッドの作業メモリを直接操作することはできなくなり、更新されたデータはメイン メモリに更新されます。メモリ。

ここで言うメインメモリは単にヒープメモリと考えることができ、作業メモリはスタックメモリと考えることができます。

そのため、並行して実行している場合、スレッドBが読み取ったデータがスレッドAが更新する前のデータである可能性があります。

明らかにこれは間違いなく問題を引き起こすため、volatile の役割が登場します:

変数が volatile によって変更されると、それに書き込むスレッドは直ちにメイン メモリにフラッシュされ、キャッシュが強制的にデータこの変数にアクセスしたスレッドでは変数がクリアされ、最新のデータをメイン メモリから再読み取る必要があります。

揮発性変更後、スレッドはメインメモリからデータを直接取得しませんが、変数を作業メモリにコピーする必要があります。

メモリ可視性の適用

メインメモリに基づいて 2 つのスレッド間で通信する必要がある場合、通信変数を volatile で変更する必要があります:

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;
}
}

上記の例が 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 < 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);
}
}

3 つのスレッド (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 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。