8000+字,就说一个字Volatile

news2024/10/3 6:37:57

简介

volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

Java volatile关键字用于将Java变量标记为“存储在主存储器中”。更确切地说,这意味着,每次读取一个volatile变量都将从计算机的主内存中读取,而不是从CPU缓存中读取,并且每次写入volatile变量都将写入主内存,而不仅仅是CPU缓存。

实际上,自Java 5以来,volatile关键字保证的不仅仅是向主存储器写入和读取volatile变量。我将在以下部分解释。

特性

可以把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步

当我们声明共享变量为volatile后,对这个变量的读/写将会很特别。理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。

COPYclass VolatileFeaturesExample {
    //使用volatile声明64位的long型变量
    volatile long vl = 0L;

    public void set(long l) {
        vl = l;   //单个volatile变量的写
    }

    public void getAndIncrement () {
        vl++;    //复合(多个)volatile变量的读/写
    }

    public long get() {
        return vl;   //单个volatile变量的读
    }
}

假设有多个线程分别调用上面程序的三个方法,这个程序在语义上和下面程序等价:

COPYclass VolatileFeaturesExample {
    long vl = 0L;               // 64位的long型普通变量

    //对单个的普通 变量的写用同一个锁同步
    public synchronized void set(long l) {             
       vl = l;
    }

    public void getAndIncrement () { //普通方法调用
        long temp = get();           //调用已同步的读方法
        temp += 1L;                  //普通写操作
        set(temp);                   //调用已同步的写方法
    }
    public synchronized long get() { 
        //对单个的普通变量的读用同一个锁同步
        return vl;
    }
}

如上面示例程序所示,对一个volatile变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个锁来同步,它们之间的执行效果相同。

锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

锁的语义决定了临界区代码的执行具有原子性。这意味着即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读写就将具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。

简而言之,volatile变量自身具有下列特性:

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

  • 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量,变量之间的相互赋值不是原子性操作。
  • 所有引用reference的赋值操作
  • java.concurrent.Atomic.* 包中所有类的一切操作

可见性

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

在线程使用非volatile变量的多线程应用程序中,出于性能原因,每个线程可以在处理它们时将变量从主存储器拷贝到CPU高速缓存中。如果您的计算机包含多个CPU,则每个线程可以在不同的CPU上运行。这意味着,每个线程都可以将变量复制到不同CPU的CPU缓存中。这在这里说明:

img

对于volatile变量,无法保证Java虚拟机(JVM)何时将数据从主内存读取到CPU缓存中,或将数据从CPU缓存写入主内存。这可能会导致一些问题,我将在以下部分中解释。

想象一下两个或多个线程可以访问共享对象的情况,该共享对象包含一个声明如下的计数器变量:

COPYpublic class SharedObject {
    public int counter = 0;
}

再想象一下,只有线程1对counter变量进行增加操作,但线程1和线程2都可能读取变量counter

如果counter变量未声明volatile,则无法保证何时将counter变量的值从CPU缓存写回主存储器。这意味着,CPU高速缓存中的counter变量值可能与主存储器中的变量值不同。这种情况如下所示:

img

线程没有看到变量的最新值的问题,是因为它还没有被另一个线程写回主内存,这被称为“可见性”问题,其他线程看不到一个线程的某些更新。

volatile可见性保证

Java volatile关键字旨在解决变量可见性问题。通过使用volatile声明counter变量,对变量counter的所有写操作都将立即写回主存储器。此外,counter变量的所有读取都将直接从主存储器中读取。

下面是counter变量声明为volatile的样子:

COPYpublic class SharedObject {
    public volatile int counter = 0;
}

声明变量为volatile,对其他线程写入该变量 保证了可见性

在上面给出的场景中,一个线程(T1)修改计数器,另一个线程(T2)读取计数器(但从不修改它),声明该counter变量为volatile足以保证写入counter变量对T2的可见性。

但是,如果T1和T2都在增加counter变量,那么声明counter变量为volatile就不够了。稍后会详细介绍。

完全volatile可见性保证

实际上,Java volatile的可见性保证超出了volatile变量本身。可见性保证如下:

  • 如果线程A写入volatile变量并且线程B随后读取这个volatile变量,则在写入volatile变量之前对线程A可见的所有变量在线程B读取volatile变量后也将对线程B可见。
  • 如果线程A读取volatile变量,则读取volatile变量时对线程A可见的所有变量也将从主存储器重新读取。

让我用代码示例说明:

COPYpublic class MyClass {
    private int years;
    private int months
    private volatile int days;

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

udpate()方法写入三个变量,其中只有days是volatile变量。

完全volatile可见性保证意味着,当将一个值写入days时,对线程可见的其他所有变量也会写入主存储器。这意味着,当一个值被写入daysyearsmonths的值也被写入主存储器(注意days的写入在最后)。

当读取yearsmonthsdays的值你可以这样做:

COPYpublic class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays() {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

注意totalDays()方法通过读取days的值到total变量中开始。当读取days的值时,后续monthsyears值的读取也会从主存储器中读取。因此使用上述读取序列可以保证看到最新的daysmonthsyears值。

有序性

即程序执行的顺序按照代码的先后顺序执行。

java内存模型中的有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。 ​ 在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。最著名的例子就是单例模式里面的DCL(双重检查锁)。另外,可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

volatile变量的特性

保证可见性,不保证原子性

img

  • 当写一个volatile变量时,JMM会把该线程本地内存中的变量强制刷新到主内存中去;
  • 这个写会操作会导致其他线程中的缓存无效。

禁止指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:

  • 重排序操作不会对存在数据依赖关系的操作进行重排序。

    比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运

    行时这两个操作不会被重排序。

  • 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变

    比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发

    生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。

    重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。

指令重排序

出于性能原因允许JVM和CPU重新排序程序中的指令,只要指令的语义含义保持不变即可。例如,查看下面的指令:

COPYint a = 1;
int b = 2;

a++;
b++;

这些指令可以按以下顺序重新排序,而不会丢失程序的语义含义:

COPYint a = 1;
a++;

int b = 2;
b++;

然而,当其中一个变量是volatile变量时,指令重排序会出现一个挑战。让我们看看MyClass这个前面Java volatile教程中的例子中出现的类:

COPYpublic class MyClass {
    private int years;
    private int months
    private volatile int days;

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days   = days;
    }
}

一旦update()方法写入一个值days,新写入的值,以yearsmonths也被写入主存储器。但是,如果JVM重新排序指令,如下所示:

COPYpublic void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}

days变量被修改时monthsyears的值仍然写入主内存中,但是这一次它发生在新的值被写入monthsyears之前,也就是这两个变量的旧值会写入主存中,后面两句的写入操作只是写到缓存中。因此,新值不能正确地对其他线程可见。重新排序的指令的语义含义已经改变。

happens before

上面讲的是volatile变量自身的特性,对程序员来说,volatile对线程的内存可见性的影响比volatile自身的特性更为重要,也更需要我们去关注。

从JSR-133开始,volatile变量的写-读可以实现线程之间的通信。

从内存语义的角度来说,volatile与锁有相同的效果:volatile写和锁的释放有相同的内存语义;volatile读与锁的获取有相同的内存语义。

请看下面使用volatile变量的示例代码:

COPYclass VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;               //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a;           //4
            ……
        }
    }
}

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens before规则,这个过程建立的happens before 关系可以分为两类:

  1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
  2. 根据volatile规则,2 happens before 3。
  3. 根据happens before 的传递性规则,1 happens before 4。

上述happens before 关系的图形化表现形式如下:

img

上图中,每一个箭头链接的两个节点,代表了一个happens before 关系。黑色箭头表示程序顺序规则;橙色箭头表示volatile规则;蓝色箭头表示组合这些规则后提供的happens before保证。

这里A线程写一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

Happens-Before 保证

为了解决指令重排序挑战,除了可见性保证之外,Java volatile关键字还提供“happens-before”保证。happens-before保证保证:

volatile 之前读写

如果读取/写入最初发生在写入volatile变量之前,读取/写入其他变量不能重新排序在写入volatile变量之后。 ​ 写入volatile变量之前的读/写操作被保证 “happen before” 写入volatile变量。请注意,发生在写入volatile变量之后的读/写操作依然可以重排序到写入volatile变量前,只是不能相反。允许从后到前,但不允许从前到后。

volatile 之后读写

如果读/写操作最初发生在读取volatile变量之后,则读取/写入其他变量不能重排序到发生在读取volatile变量之前。请注意,发生在读取volatile变量之前的读/写操作依然可以重排序到读取volatile变量后,只是不能相反。允许从前到后,但不允许从后到前。

上述 “happens-before”规则保证确保volatile关键字的可见性保证在强制执行。

COPYpublic class VolatileTest {
    private volatile int vi = 1;
    private int i = 2;
    private int i2 = 3;

    @Test
    public void test() {
        System.out.println(i);      //1  读取普通变量
        i=3;                        //2  写入普通变量

        //1 2 不能重排序到3之后,操作4可以重排序到3前面
        vi = 2;                     //3  写入volatile变量
        i2 = 5;                     //4  写入普通变量
    }

    @Test
    public void test2() {
        System.out.println(i);      //1  读取普通变量

        //3不能重排序到在2前,但1可以重排序到2后
        System.out.println(vi);     //2  读取volatile变量
        System.out.println(i2);     //3  读取普通变量
    }
}

volatile注意事项

volatile 线程不安全

即使volatile关键字保证volatile变量的所有读取直接从主存储器读取,并且所有对volatile变量的写入都直接写入主存储器,仍然存在声明volatile变量线程不安全。

在前面解释的情况中,只有线程1写入共享counter变量,声明counter变量为volatile足以确保线程2始终看到最新的写入值。

实际上,如果写入volatile变量的新值不依赖于其先前的值,则甚至可以多个线程写入共享变量,并且仍然可以在主存储器中存储正确的值。换句话说,就是将值写入共享volatile变量的线程开始并不需要读取其旧值来计算其下一个值。

一旦线程需要首先读取volatile变量的旧值,并且基于该值为共享volatile变量生成新值,volatile变量就不再足以保证正确的可见性。读取volatile 变量和写入新值之间的短时间间隔会产生竞争条件 ,其中多个线程可能读取volatile变量的同一个旧值,然后为其生成新值,并将该值写回主内存 - 覆盖彼此的值。

多个线程递增同一个计数器的情况正是 volatile变量并不安全的情况。以下部分更详细地解释了这种情况。

想象一下,如果线程1将值为0的共享变量counter读入其CPU高速缓存,将其增加到1并且不将更改的值写回主存储器。然后,线程2也从主存储器读取相同的counter变量进入自己的CPU高速缓存,其中变量的值仍为0。然后,线程2也将计数器递增到1,也不将其写回主存储器。这种情况如下图所示:

img

线程1和线程2现在失去了同步。共享变量counter的实际值应为2,但每个线程的CPU缓存中的变量值为1,而在主内存中,该值仍为0。这是一个混乱!即使线程最终将共享变量counter的值写回主存储器,该值也将是错误的。

保证线程安全

正如我前面提到的,如果两个线程都在读取和写入共享变量,那么使用 volatile关键字是不安全的。 在这种情况下,您需要使用synchronized来保证变量的读取和写入是原子性的。读取或写入一个volatile变量不会阻塞其他线程读取或写入这个变量。为此,您必须在临界区周围使用synchronized关键字。

作为synchronized块的替代方法,您还可以使用java.util.concurrent包中众多的原子数据类型。例如,AtomicLong或者 AtomicReference或其他的。

如果只有一个线程读取和写入volatile变量的值,而其他线程只读取这个变量,那么此线程将保证其他线程能看到volatile变量的最新值。如果不将变量声明为volatile,则无法保证。

volatile关键字也可以保证在64位变量上正常使用。

volatile的性能考虑

读取和写入volatile变量会导致变量从主存中读取或写入主存,读取和写入主内存比访问CPU缓存开销更大。访问volatile变量也会阻止指令重排序,这是一种正常的性能提升技术。因此,当您确实需要强制实施变量可见性时,才使用volatile变量。

原理

volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

内存语义

volatile写的内存语义

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

以上面示例程序VolatileExample为例,假设线程A首先执行writer()方法,随后线程B执行reader()方法,初始时两个线程的本地内存中的flag和a都是初始状态。下图是线程A执行volatile写后,共享变量的状态示意图:

img

如上图所示,线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值被刷新到主内存中。此时,本地内存A和主内存中的共享变量的值是一致的。

volatile读的内存语义

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

下面是线程 B 读同一个 volatile 变量后,共享变量的状态示意图:

img

如上图所示,在读flag变量后,本地内存B已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值也变成一致的了。

如果我们把volatile写和volatile读这两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

小结

下面对volatile写和volatile读的内存语义做个总结

  • 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
  • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
  • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。

volatile内存语义的实现

前文我们提到过重排序分为编译器重排序和处理器重排序。为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。下面是JMM针对编译器制定的volatile重排序规则表:

是否能重排序第二个操作第二个操作第二个操作
第一个操作普通读/写volatile读volatile写
普通读/写NO
volatile读NONONO
volatile写NONO

举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作。

从上表我们可以看出

  • 当第二个操作为volatile写操作时,不管第一个操作是什么(普通读写或者volatile读写),都不能进行重排序。这个规则确保volatile写之前的所有操作都不会被重排序到volatile写之后;
  • 当第一个操作为volatile读操作时,不管第二个操作是什么,都不能进行重排序。这个规则确保volatile读之后的所有操作都不会被重排序到volatile读之前;
  • 当第一个操作是volatile写操作时,第二个操作是volatile读操作,不能进行重排序。

  为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。下面是基于保守策略的 JMM 内存屏障插入策略:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障(禁止前面的写与volatile写重排序)。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障(禁止volatile写与后面可能有的读和写重排序)。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障(禁止volatile读与后面的读操作重排序)。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障(禁止volatile读与后面的写操作重排序)。

  其中重点说下StoreLaod屏障,它是确保可见性的关键,因为它会将屏障之前的写缓冲区中的数据全部刷新到主内存中。上述内存屏障插入策略非常保守,但它可以保证在任意处理平台,任意的程序中都能得到正确的volatile语义。下面是保守策略(为什么说保守呢,因为有些在实际的场景是可省略的)下,volatile 写操作 插入内存屏障后生成的指令序列示意图:

img

其中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作对任意处理器可见(把它刷新到主内存)。

另外volatile写后面有StoreLoad屏障,此屏障的作用是避免volatile写与后面可能有的读或写操作进行重排序。因为编译器常常无法准确判断在一个volatile写的后面是否需要插入一个StoreLoad屏障(比如,一个volatile写之后方法立即return)为了保证能正确实现volatile的内存语义,JMM采取了保守策略:在每个volatile写的后面插入一个StoreLoad屏障。因为volatile写-读内存语义的常见模式是:一个写线程写volatile变量,多个度线程读同一个volatile变量。当读线程的数量大大超过写线程时,选择在volatile写之后插入StoreLoad屏障将带来可观的执行效率的提升。从这里也可看出JMM在实现上的一个特点:首先确保正确性,然后再去追求效率(其实我们工作中编码也是一样)。

下面是在保守策略下,volatile读插入内存屏障后生产的指令序列示意图:

img

 上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况忽略不必要的屏障。在JMM基础中就有提到过各个处理器对各个屏障的支持度,其中x86处理器仅会对写-读操作做重排序。

下面我们通过具体的示例代码来说明

COPYclass VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           //第一个volatile读
        int j = v2;           // 第二个volatile读
        a = i + j;            //普通写
        v1 = i + 1;          // 第一个volatile写
        v2 = j * 2;          //第二个 volatile写
    }

    …                    //其他方法
}

针对 readAndWrite() 方法,编译器在生成字节码时可以做如下的优化:

img

注意,最后的StoreLoad屏障不能省略。因为第二个volatile写之后,方法立即return。此时编译器可能无法准确断定后面是否会有volatile读或写,为了安全起见,编译器常常会在这里插入一个StoreLoad屏障。

上面的优化是针对任意处理器平台,由于不同的处理器有不同“松紧度”的处理器内存模型,内存屏障的插入还可以根据具体的处理器内存模型继续优化。以x86处理器为例,上图中除最后的StoreLoad屏障外,其它的屏障都会被省略。

前面保守策略下的volatile读和写,在 x86处理器平台可以优化成:

img

前文提到过,x86 处理器仅会对写 - 读操作做重排序。,x86处理器仅会对写-读操作做重排序。X86不会对读-读,读-写和写-写操作做重排序,因此在x86处理器中会省略掉这三种操作类型对应的内存屏障。在x86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在x86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。

为什么要增强volatile的内存语义

在 JSR-133 之前的旧 Java 内存模型中,虽然不允许 volatile 变量之间重排序,但旧的 Java 内存模型允许 volatile 变量与普通变量之间重排序。在旧的内存模型中,VolatileExample 示例程序可能被重排序成下列时序来执行:

img

在旧的内存模型中,当 1 和 2 之间没有数据依赖关系时,1 和 2 之间就可能被重排序(3 和 4 类似)。其结果就是:读线程 B 执行 4 时,不一定能看到写线程 A 在执行 1 时对共享变量的修改。

因此在旧的内存模型中 ,volatile的写-读没有锁的释放-获所具有的内存语义。为了提供一种比锁更轻量级的线程之间通信的机制,JSR-133专家组决定增强volatile的内存语义:严格限制编译器和处理器对volatile变量与普通变量的重排序,确保volatile的写-读和锁的释放-获取一样,具有相同的内存语义。从编译器重排序规则和处理器内存屏障插入策略来看,只要volatile变量与普通变量之间的重排序可能会破坏volatile的内存语意,这种重排序就会被编译器重排序规则和处理器内存屏障插入策略禁止。

由于volatile仅仅保证对单个volatile变量的读/写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。在功能上,锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势。如果读者想在程序中用volatile代替监视器锁,请一定谨慎,具体细节请参阅参考Java理论与实践:正确使用Volatile变量。

本文由传智教育博学谷狂野架构师教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/369534.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023最新版网络安全保姆级指南,手把手带你从零基础进阶渗透攻防工程师

前言 一份网络攻防渗透测试的学习路线,不藏私了! 1、学习编程语言(phpmysqljshtml) 原因: phpmysql可以帮助你快速的理解B/S架构是怎样运行的,只有理解了他的运行原理才能够真正的找到问题/漏洞所在。所以对于国内那些上来就说…

Java实现在线沟通功能

文章目录1、介绍 和 特点2、整合SpringBoot2.1、导入依赖2.2、websocket 配置类2.3、消息处理类2.4、启动服务2.5、前端代码:张三2.6、前端代码:李四3、效果4、小结1、介绍 和 特点 t-io是基于JVM的网络编程框架,和netty属同类,所…

MES系统需求误区,一文告诉你需求分析有哪些

在企业的实际应用中,对MES系统需求的分析常常会出现六个错误。 要求广泛,目标不明确由于对MES系统的概念和企业的实际运作不了解,导致企业在提出MES系统的要求时,常常会笼统而不明确,有时会混淆目标和需要。比如&#…

LabVIEW主VI前面板中显示或使用多个子VI

LabVIEW主VI前面板中显示或使用多个子VI想在程序中连接一个或多个子VI的前面板,但是当调用它们时,每个子VI在计算机屏幕上显示为一个新窗口。那么怎么能让每个子VI作为主VI前面板的一部分进行显示,而不是在屏幕上显示多个窗口?正在…

python读取tif图像+经纬度

python读取tif的包很多,但大都只能读出图像像素值,不能读取到经纬度信息。原因:TIFF 简单理解就是一种图像格式,类似于 jpg、png 等。GeoTIFF 就是在普通 TIFF 文件上增加了地理位置、投影信息、坐标信息等,常用于遥感…

BBS系统的设计与实现

技术:Java、JSP等摘要:BBS全称为Bulletin Board System,中文就是“电子公告板”。 BBS是一种电子信息服务系统。它向用户提供了一块公共电子白板,每个用户都可以在上面发布信息或提出问题,早期的BBS由教育机构或研究机…

电脑硬盘如何重新分区 ?教你两招磁盘分区方法

摘要:对于刚刚购买的电脑来说,有些厂商在装机的时候没有根据用户需求,就给硬盘随意分区了,有的分区划分的不是很合理,在使用过程中会遇到一些麻烦,那么电脑硬盘如何重新分区 ?本文将给大家详细介…

OpenShift 简介

OpenShift 是红帽 Red Hat 公司基于开源的云平台,是平台即服务(PaaS),是一种容器应用平台。允许开发人员构建、测试和部署云应用。该系统是在 K8S 核心之上添加工具,从而实现更快的应用开发、部署及扩展。 在 OpenShi…

leetcode 1675. Minimize Deviation in Array(最小化数组偏差)

数组里面有n个正整数,里面的数字可以无限次进行如下操作: 1.偶数可以除以2 2.奇数可以乘以2 数组中任意两元素差的最大值称为偏差。 把数组中的元素进行上面2种操作,使偏差最小。 思路: 数组中现有2种数字,一种是奇数…

新手如何入门黑客技术,黑客技术入门该学什么?

你是否曾经也对黑客技术感兴趣呢?感觉成为黑客是一件很酷的事,那么作为新手如何入门黑客技术,黑客技术入门该学什么呢? 其实不管你想在哪个新的领域里有所收获,你需要考虑以下几个问题。 首先你要想明白为什么学这个&…

程序员是世界上最理性、最睿智的群体,耶稣也反驳不了我,我说的!

有人说,程序员是吃青春饭的,35 岁就提前退休了。 猛一看,这句话是对的;仔细一看,这句话是不对的。 说它对,是因为现实中确实有很多程序员 35 岁就被毕业了;说它不对,是因为 35 岁以…

【数据库】redis集群环境详解

目录 集群环境 一,集群介绍 1、为什么需要redis集群 2、什么是redis集群 二,数据分片 三, 主从复制模型 四,一致性保证 五,集群搭建 1, 集群结构 2,创建配置文件 (1&#…

播放器问答弹题功能(视频播放弹出问题)教程与实际演示案例

阿酷tony / 原创 / 2023-2-24 长沙问答弹题功能是指酷播云产品在视频播放的指定时间点弹出问答题目,适合在教学、培训类视频中使用。使用问答功能,既可以增加学生与内容的互动,有利于教学质量的提升,又可以评估学生的学习效果和课…

【 K8s 源码之调度学习】Pod 间亲和性和反亲和性的源码分析

查看案例 字段含义podAffinityPod 间的亲和性定义podAntiAffinityPod 间的反亲和性定义requiredDuringSchedulingIgnoredDuringExecution硬性要求,必须满足条件,保证分散部署的效果最好使用用此方式preferredDuringSchedulingIgnoredDuringExecution软性…

duilib.dll丢失怎么办?dll文件丢失修复方法分享

duilib.dll丢失怎么办?其实在使用 Windows 系统的过程中,有时会出现提示“duilib.dll丢失”的错误。这个错误可能会影响电脑的正常运行,但是不用担心,今天小编来给大家详细的讲解一下duilib.dll丢失都有哪些解决方法。 一.什么是…

SAFe(Scaled Agile Framework)学习笔记

1.SAFe 概述 SAFe(Scaled Agile Framework)是一种面向大型企业的敏捷开发框架,旨在协调多个团队和部门的协同工作,以实现高效的软件开发和交付。下面是SAFe框架的简单介绍总结: SAFe框架包括以下四个层次&#xff1a…

金测评 手感更细腻的游戏手柄,双模加持兼容更出色,雷柏V600S上手

很多朋友周末都喜欢玩玩游戏放松一下,在家玩游戏的时候,PC是大家常用的平台,当然了,玩游戏的时候用键鼠的话,手感难免差点意思,还是要手柄才能获得更好的体验。我现在用的是雷柏V600S,这是一款支…

飞鹅打印机怎么样?飞鹅打印机好用吗?飞鹅打印机怎么知道订单是否漏单?

外卖打印机怎么选?飞鹅打印机好用吗?飞鹅智能云打印机产品专注于云打印的解决方案和技术服务提供。2019 年飞鹅已经成为国内先进的云打印服务提供商,主要是服务美团、饿了么客户,产品主要优势:自动接单、自动打印,无需…

美好音乐不只在现场,索尼播放器NW-WM1ZM2和NW-WM1AM2满足聆听热爱

当两点一线的单调生活成了多数人的生活常态,那些有过程有讲究的仪式感开始变得弥足珍贵起来,爱乐者们不远千里奔赴音乐节、Livehouse的现场,除了追求当下高燃兴奋的感受,同样是为了获得一份全心投入的听音仪式感。而当不便出行的日…

.net core 本地环境切换网络遇到的问题 500.19 502.5 invalid_request

问题一 运行环境 IIS 部署.NET CORE 项目 出现 HTTP 错误 500.19 - Internal Server Error附上.NET CORE2.1版本的下载链接下载 .NET Core 2.1 (Linux、macOS 和 Windows) (microsoft.com)下载完成以后重启IIS,有的版本还需要在IIS设置.NET CLR版本为无托管代码二 H…