详解Java volatile 内存屏障底层原理语义

目录
  • 一、volatile关键字介绍及底层原理
    • 1.volatile的特性(内存语义)
    • 2.volatile底层原理
  • 二、volatile——可见性
  • 三、volatile——无法保证原子性
  • 四、volatile——禁止指令重排
    • 1.指令重排
    • 2.as-if-serial语义
  • 五、volatile与内存屏障(Memory Barrier)
    • 1.内存屏障(Memory Barrier)
    • 2.volatile的内存语义实现
  • 六、JMM对volatile的特殊规则定义

一、volatile关键字介绍及底层原理

1.volatile的特性(内存语义)

当一个变量被定义成volatile之后,它将具备两项特性:第一项是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。比如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再对主内存进行读取操作,新变量值才会对线程B可见。

使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在同一个线程的方法执行过程中无法感知到这点,这就是Java内存模型中描述的所谓“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。

2.volatile底层原理

volatile关键字修饰的变量可以保证可见性与有序性,无法保证原子性。那么volatile关键字的底层原理是什么呢?我们可以通过查看Java代码的汇编指令去看一下volatile的底层原理:查询Java代码的汇编指令需要设置JVM允许参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp;如果你的jdk版本小于等于8还要在jdk里面添加Hsdis插件,将该插件目录里面的两个文件(hsdis-amd64.dll,hsdis-i386.dll)复制到 %JAVA_HOME%\jre\bin\server 下,然后运行你的Java程序,就可以看到控制台里面一堆的汇编指令代码输出了。

public class Singleton {
    private volatile static Singleton myinstance;

    public static Singleton getInstance() {
        if (myinstance == null) {
            synchronized (Singleton.class) {
                if (myinstance == null) {
                    myinstance = new Singleton();//对象创建过程,本质可以分文三步
                }
            }
        }
        return myinstance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

上面所示是一段标准的双锁检测(Double Check Lock,DCL)单例代码,可以观察加入volatile和未加入volatile关键字时所生成的汇编代码的差别。不加volatile关键字时在控制台输出指令搜索myinstance可以看到如下两行

0x00000000038064dd: mov %r10d,0x68(%rsi)
0x00000000038064e1: shr $0x9,%rsi
0x00000000038064e5: movabs $0xf1d8000,%rax
0x00000000038064ef: movb $0x0,(%rsi,%rax,1) ;*putstatic myinstance
; - com.it.edu.jmm.Singleton::getInstance@24 (line 22)

加了volatile关键字后,变成下面这样了:

0x0000000003cd6edd: mov %r10d,0x68(%rsi)
0x0000000003cd6ee1: shr $0x9,%rsi
0x0000000003cd6ee5: movabs $0xf698000,%rax
0x0000000003cd6eef: movb $0x0,(%rsi,%rax,1)
0x0000000003cd6ef3: lock addl $0x0,(%rsp) ;*putstatic myinstance
; - com.it.edu.jmm.Singleton::getInstance@24 (line 22)

通过对比发现,关键变化在于有volatile修饰的变量,赋值后(前面movb $0x0,(%rsi,%rax,1)这句便是赋值操作)多执行了一个“lock addl $0x0,(%rsp)”操作,这个操作的作用相当于一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置,只有一个处理器访问内存时,并不需要内存屏障;但如果有两个或更多处理器访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。

这里的关键在于lock前缀,它的作用是将本处理器的缓存写入了内存,该写入动作也会引起别的处理器或者别的内核无效化(Invalidate,MESI协议的I状态)其缓存,这种操作相当于对缓存中的变量做了一次前面介绍Java内存模式中所说的“store和write”操作。所以通过这样一个操作,可让前面volatile变量的修改对其他处理器立即可见。lock指令的更底层实现:如果支持缓存行会加缓存锁(MESI);如果不支持缓存锁,会加总线锁。

二、volatile——可见性

volatile修饰变量之后,可以保证可见性,下面通过一个程序示例演示一下:

public class VolatileVisibilitySample {
    private volatile boolean initFlag = false;
    static Object object = new Object();

    public void refresh(){
        this.initFlag = true;
        System.out.println("线程:"+Thread.currentThread().getName()+":修改共享变量initFlag");
    }

    public void load(){
        int i = 0;
        while (!initFlag){
//            synchronized (object){
//                i++;
//            }
        }
        System.out.println("线程:"+Thread.currentThread().getName()+"当前线程嗅探到initFlag的状态的改变"+i);
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.refresh();
        },"threadA");

        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");

        threadB.start();
        Thread.sleep(2000);
        threadA.start();
    }
}

可以看到共享变量被volatile修饰之前,线程B中调用的方法中 “当前线程嗅探到initFlag的状态的改变” 这句输出是打印不出来的,也就意味着线程A中将initFlag改为true,但是线程B并没有获取到最新值,程序一直在循环空跑。此时JMM操作如下图:虽然线程A中将initFlag改为了true并且最终会同步回主内存,但是线程B中循环读取的initFlag一直都是从工作内存读取的,所以会一直进行死循环无法退出。

添加了volatile修饰之后,“当前线程嗅探到initFlag的状态的改变” 这句话就会被打印出来,因为添加volatile关键字后,就会有lock指令,使用缓存一致性协议,线程B中会一直嗅探initFlag是否被改变,线程A修改initFlag后会立即同步回主内存,这时候会通知线程B将缓存行状态改为I(无效状态),需要重新从主内存读取。如下图所示:

我们将上面的代码的load()方法进行修改——去掉volatile关键字,添加synchronized同步块,即修改为下面这样的情况,会达到跟添加volatile关键字相同的效果,这是因为添加了锁同步块,CPU会分配时间片,线程进行锁竞争导致线程上下文切换,重新读取主存的变量。

public void load(){
        int i = 0;
        while (!initFlag){
            synchronized (object){
                i++;
            }
        }
        System.out.println("线程:"+Thread.currentThread().getName()+"当前线程嗅探到initFlag的状态的改变"+i);
    }

三、volatile——无法保证原子性

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized、java.util.concurrent中的锁或原子类)来保证原子性:

  1. 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  2. 变量不需要与其他的状态变量共同参与不变约束

下面通过一个示例演示一下:10个线程,每个线程加1000次(counter++不是一个原子性的操作,可以通过javap命令查看底层指令,可以看到有加载变量数据、将变量放到操作数栈顶、执行加法运算等操作)。运行几次发现,有时运行结果是小于10000的。下面分析一下:

  • 1.首先counter不加volatile修饰时:因为10个线程同时对变量进行自加1运算,每个运算一次后去写会主内存,会覆盖其他线程的运算结果,所以运行结果可能会小于10000。
  • 2.counter添加volatile修饰时:添加volatile修饰之后,变量被修改后会立即同步回主存,一直嗅探其他线程是否对变量进行过修改,修改后重新从主存读取变量。但是正因为添加了volatile关键字时MESI缓存一致性协议生效了,当一个变量执行加1操作后,需要同步回主存,这是会锁缓存行,通知其他线程变量已经被修改过了,将本地缓存行改为I无效状态,这样被改为无效状态的线程本地加1操作的结果被丢弃了,没有写回主内存,也就是白加了一次,所以运行结果也可能会小于10000。

想要实现原子性操作,可以通过synchronized,ReentrantLock加锁,或者使用AtomicInteger进行原子性运算。

public class VolatileAtomicSample {
    private static volatile int counter = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    counter++;
                }
            });
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println(counter);
    }
}

四、volatile——禁止指令重排

1.指令重排

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与
它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。

下图为从源码到最终执行的指令序列示意图

指令重排主要有两个阶段:

1.编译器编译阶段:编译器加载class文件编译为机器码时进行指令重排

2.CPU执行阶段: CPU执行汇编指令时,可能会对指令进行重排序

2.as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

通过一个程序代码,演示一下指令重排的效果:只有x=0并且y=0的情况下才会跳出循环

public class VolatileReOrderSample {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;
    static Object object = new Object();

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

        for (;;){
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    x = b;
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();

            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}

通过分析,会有三种可能的输出:[0,1],[1,0],[1,1]。

  • 输出可能1——[0,1]:线程1先执行完,线程2再执行,则会出现x=0,y=1
  • 输出可能1——[1,0]:线程2先执行完,线程1再执行,则会出现x=1,y=0
  • 输出可能1——[1,1]:线程1、线程2交替执行,a=1,b=1,然后执行x=1,y=1,则会出现x=1,y=1

当运行之后会发现上面分析的三种情况确实出现了,但是程序最终跳出了循环,也就是出现了x=0并且y=0的情况,这说明出现了指令重排的情况,即线程1中a=1 x=b的指令出现了顺序调整或线程2中b=1 y=a的指令出现了顺序调整。

当我们给变量a和b添加volatile关键字修饰后(private volatile static int a = 0, b =0;),再次运行发现程序一直在循环输出,没有出现x=y=0的情况从而退出循环。

volatile可以禁止指令重排的原因是因为添加了lock指令,会添加内存屏障。

五、volatile与内存屏障(Memory Barrier)

1.内存屏障(Memory Barrier)

内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障(lock指令)实现其在内存中的语义,即可见性和禁止重排优化。

上面的程序示例:synchronized+volatile实现的DCL模式的单例模式,就是利用了volatile禁止指令重排的特性。因为myinstance = new Singleton();这句代码本质上是有三步:1.为对象分配内存空间;2.实例化对象数据;3.将引用指向对象实例的内存空间。如果第一个线程执行创建对象时出现了指令重排,比如3排到了2之前,那么线程2在最外层代码判断myinstance!=null为true返回对象引用,但是实际上这时候对象尚未初始化完成,这样是有问题的,需要通过添加volatile关键字去禁止指令重排。

2.volatile的内存语义实现

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

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

  • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
  • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
  • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

上述内存屏障插入策略非常保守,但它可以保证在任意处理器平台,任意的程序中都能得到正确的volatile内存语义。

下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图,如图所示。

上图中StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。

而volatile写后面的StoreLoad屏障,作用是避免volatile写与后面可能有的volatile读/写操作重排序

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

上图中LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

上述volatile写和volatile读的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

六、JMM对volatile的特殊规则定义

最后我们再Java内存模型中对volatile变量定义的特殊规则的定义。假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store和write操作时需要满足如下规则:

只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联的,必须连续且一起出现。

这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改。

只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联的,必须连续且一起出现。

这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改。

假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;与此类似,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q。

这条规则要求volatile修饰的变量不会被指令重排序优化,从而保证代码的执行顺序与程序的顺序相同。

下一篇预告——并发编程三大特性:原子性,可见性,有序性,happen-before原则

到此这篇关于详解Java volatile 内存屏障底层原理语义的文章就介绍到这了,更多相关Java volatile 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java中volatile防止指令重排

    目录 什么是指令重排? 为什么指令重排能够提高性能 volatile是怎么禁止指令重排的? volatile可以防止指令重排,在多线程环境下有时候我们需要使用volatile来防止指令重排,来保证代码运行后数据的准确性 什么是指令重排? 计算机在执行程序时,为了提高性能,编译器和处理器一般会进行指令重排,一般分为以下三种: 指令重排有以下三个特点: 1.单线程环境下指令重排后可以保证与顺序执行指令的结果一致(就是不进行指令重排的情况) //原来的执行顺序 a=1; b=0; //进行指令重排后执

  • 详解Java并发编程基础之volatile

    目录 一.volatile的定义和实现原理 1.Java并发模型采用的方式 2.volatile的定义 3.volatile的底层实现原理 二.volatile的内存语义 1.volatile的特性 2.volatile写-读建立的happens-before关系 3.volatile的写/读内存语义 三.volatile内存语义的实现 1.volatile重排序规则 2.内存屏障 3.内存屏障示例 四.volatile与死循环问题 五.volatile对于复合操作非原子性问题 一.volati

  • Java并发编程之关键字volatile的深入解析

    目录 前言 一.可见性 二.有序性 总结 前言 volatile是研究Java并发编程绕不过去的一个关键字,先说结论: volatile的作用: 1.保证被修饰变量的可见性 2.保证程序一定程度上的有序性 3.不能保证原子性 下面,我们将从理论以及实际的案例来逐个解析上面的三个结论 一.可见性 什么是可见性? 举个例子,小明和小红去看电影,刚开始两个人都还没买电影票,小红就先去买了两张电影票,没有告诉小明.小明以为小红没买,所以也去买了两张电影票,因为他们只有两个人,所以他们只能用两张票,这就是

  • java volatile案例讲解

    本篇来自java并发编程实战关于volatile的总结. 要说volatile,先得明白内存可见性.那我们就从内存可见性说起. 一.内存可见性 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.在单线程环境中,如果向某个变量先写入值,然后在没有其他写入操作的情况下读取这个变量,那么总能得到相同的值.这看起来很自然.然而,当读操作和写操作在不同的线程中执行时,情况却并非如此,这听起来或许有些难以接受.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能

  • 深度理解Java中volatile的内存语义

    volatile可见性实验 举个栗子 我这里开了两个线程,后面的线程去修改volatile变量,前面的线程不断获取volatile变量, 结果是会一致卡在死循环,控制台没有任何输出 假如将flag让volatile来进行修饰 结果是:三秒后,就不会不断打印出信息出来 注意,Thread.sleep是会刷新线程内存的,所以不要使用Thread.sleep来分别让一个线程获取两次volatile变量 volatile的特性 volatile其实相当于对变量的单词读或写操作加了锁.做了同步 由于是加了

  • 详解Java volatile 内存屏障底层原理语义

    目录 一.volatile关键字介绍及底层原理 1.volatile的特性(内存语义) 2.volatile底层原理 二.volatile--可见性 三.volatile--无法保证原子性 四.volatile--禁止指令重排 1.指令重排 2.as-if-serial语义 五.volatile与内存屏障(Memory Barrier) 1.内存屏障(Memory Barrier) 2.volatile的内存语义实现 六.JMM对volatile的特殊规则定义 一.volatile关键字介绍及底

  • 详解Java的内存模型

    JVM的内存模型 Java "一次运行,到处编译" 的真面目 说JVM内存模型之前,先聊一个老生常谈的问题,为什么Java可以 "一次编译,到处运行",这个话题最直接的答案就是,因为Java有JVM啊,解释这个答案之前,我想先回顾一下一个语言被编译的过程: 一般编程语言的编译过程大抵就是,编译--连接--执行,这里的编译就是,把我们写的源代码,根据语义语法进行翻译,形成目标代码,即汇编码.再由汇编程序翻译成机器语言(可以理解为直接运行于硬件上的01语言):然后进行连

  • 详解Java单例模式的实现与原理剖析

    目录 一.什么是单例模式 二.哪些地方用到了单例模式 三.单例模式的优缺点 优点 缺点 四.手写单例模式 饿汉式 枚举饿汉式 DCL懒汉式 双检锁懒汉式 内部类懒汉式 小结 一.什么是单例模式 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式. 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建.这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对

  • 详解Java 中泛型的实现原理

    泛型是 Java 开发中常用的技术,了解泛型的几种形式和实现泛型的基本原理,有助于写出更优质的代码.本文总结了 Java 泛型的三种形式以及泛型实现原理. 泛型 泛型的本质是对类型进行参数化,在代码逻辑不关注具体的数据类型时使用.例如:实现一个通用的排序算法,此时关注的是算法本身,而非排序的对象的类型. 泛型方法 如下定义了一个泛型方法, 声明了一个类型变量,它可以应用于参数,返回值,和方法内的代码逻辑. class GenericMethod{ public <T> T[] sort(T[]

  • 详解Java TCC分布式事务实现原理

    概述 之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下.很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用. 所以这篇文章,就用大白话+手工绘图,并结合一个电商系统的案例实践,来给大家讲清楚到底什么是 TCC 分布式事务. 业务场景介绍 咱们先来看看业务场景,假设你现在有一个电商系统,里面有一个支付订单的场景. 那对一个订单支付之后,我们需要做下面的步骤: 更改订单的状态为"已支付" 扣减商品库存 给会员增加积分 创建销售出库单通知仓

  • 详解C++中多态的底层原理

    目录 前言 1.虚函数表 (1)虚函数表指针 (2)虚函数表 2.虚函数表的继承–重写(覆盖)的原理 3.观察虚表的方法 (1)内存观察 (2)打印虚表 (3)虚表的位置 4.多态的底层过程 5.几个原理性问题 6.多继承中的虚表 前言 要了解C++多态的底层原理需要我们对C指针有着深入的了解,这个在打印虚表的时候就可以见功底,理解了多态的本质我们才能记忆的更牢,使用起来更加得心应手. 1.虚函数表 (1)虚函数表指针 首先我们在基类Base中定义一个虚函数,然后观察Base类型对象b的大小:

  • 详解Java ReentrantReadWriteLock读写锁的原理与实现

    目录 概述 原理概述 加锁原理 图解过程 源码解析 解锁原理 图解过程 源码解析 概述 ReentrantReadWriteLock读写锁是使用AQS的集大成者,用了独占模式和共享模式.本文和大家一起理解下ReentrantReadWriteLock读写锁的实现原理.在这之前建议大家阅读下下面3篇关联文章: 深入浅出理解Java并发AQS的独占锁模式 深入浅出理解Java并发AQS的共享锁模式 通俗易懂读写锁ReentrantReadWriteLock的使用 原理概述 上图是ReentrantR

  • 详解Java中跳跃表的原理和实现

    目录 一.跳跃表的引入 二.算法分析 1.时间复杂度 2.空间复杂度 三.跳跃表介绍 四.跳跃表的实现 1.数据结构定义 2.查找 3.插入 4.删除 五.实战 1.代码 2.测试结果 一.跳跃表的引入 对有序顺序表可以采用二分查找,查找的时间复杂度为O (logn),插入.删除的时间复杂度为 O(n ).但是对有序链表不可以采用二分查找,查找.插入和删除的时间复杂度均为O (n). 有序链表如下图所示,若查找 8,则必须从第 1 个节点开始,依次比较 8 次才能查找成功. 如何利用链表的有序性

  • 详解Java线程池和Executor原理的分析

    详解Java线程池和Executor原理的分析 线程池作用与基本知识 在开始之前,我们先来讨论下"线程池"这个概念."线程池",顾名思义就是一个线程缓存.它是一个或者多个线程的集合,用户可以把需要执行的任务简单地扔给线程池,而不用过多的纠结与执行的细节.那么线程池有哪些作用?或者说与直接用Thread相比,有什么优势?我简单总结了以下几点: 减小线程创建和销毁带来的消耗 对于Java Thread的实现,我在前面的一篇blog中进行了分析.Java Thread与内

  • 详解Java回环屏障CyclicBarrier

    上一篇说的CountDownLatch是一个计数器,类似线程的join方法,但是有一个缺陷,就是当计数器的值到达0之后,再调用CountDownLatch的await和countDown方法就会立刻返回,就没有作用了,那么反正是一个计数器,为什么不能重复使用呢?于是就出现了这篇说的CyclicBarrier,它的状态可以被重用: 一.简单例子 用法其实和CountDownLatch差不多,也就是一个计数器,当计数器的值变为0之后,就会把阻塞的线程唤醒: package com.example.d

随机推荐