面试的时候是否被问过volitale关键字?多线程并发编程时是否直接怼synchronized?volitale到底有什么用?volitale和synchronized又有什么区别?可见性,指令重排,原子性又是怎么回事?volitale原理是什么?如果你有这样的疑问,那么先恭喜你看到了这篇宝藏文章!,在本合集的《Java线程安全问题和解决方案》一文中,指出Java多线程在操作共享数据时会有线程安全问题,解决线程安全问题通常手段是加锁,通过 synchronized 关键字或者通过Lock接口实现。使用锁之后线程在执行程序时会去获取锁,在执行效率上会降低,所以在一些简单场景下,我们可以使用volatile关键字来代替,注意不是所有的场景都可以使用,文中会根据理论和代码逐一介绍volatile的相关特性,在文章末尾总结了使用场景。,如果想了解 volatile 需要从Java内存模型【JMM】以及并发编程中的可见性、有序性、原子性入手,《Java虚拟机规范》中定义Java内存模型来屏蔽各个硬件和操作系统的内存访问差异。Java的内存模型(Java Memory Mode, JMM)指定了Java虚拟机如何与计算机的主存(RAM)进行工作。如下图所示:,,Java内存模型规定了一个线程对共享变量的写入何时对其他线程可见,定义了线程和内存之间的抽象关系。具体如下:,比如下方代码:,运行结果:,当子线程修改flag值为false后,主线程的while循环并未停止,说明主线程并没有发现flag值被另外的线程修改,,分析:,想要解决这个问题有两种方案,我们对flag的判断进行加锁处理,运行结果:,,分析:为什么加了锁就能获取到最新的值了呢?,因为线程进入 synchronized 代码块之后,它的执行过程如下:,加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值。当写线程修改一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存。当读线程获取一个volatile变量时,JMM会把该线程对应的本地工作内存置为无效,线程将到主内存中重新读取共享变量,解决方案:我们对变量flag使用 volatile 修饰,就可以保障线程在使用该变量时会从主内存获取最新值,运行结果:,发现当修改了flag值之后,main线程也跳出了while循环,,分析:,volatile修饰的变量可以在多线程情况下,修改数据可以实现线程之间的可见性,指令重排:在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。,一个好的内存模型实际上会放松对处理器和编译器规则的束缚,软件和硬件都会为了:在不改变程序执行结果的前提下,尽可能提高执行效率,JMM对底层尽量减少束缚,使其能够发挥自身优势。因此:在程序运行时,为了提高性能,编译器和处理器常常会对指令进行重排。重排序一般分3种类型:,,我们知道Java执行会将代码转换为指令集,变量a和变量b并没有直接的依赖关系,左边为重排之前代码,需要加载和保存a变量两次,右侧为重排之后的代码,只需要加载和保存a变量一次,提高了执行效率,,根据以下来需求证明存在指令重排:,运行结果:这段程序的i和j的值有四种情况,,分析:,出现这四中情况的原因是程序并没有同步加锁,导致线程切换执行,同时因为指令重排,同一个线程内部的程序在执行时调换了代码的顺序,按照之前的认识,线程内代码执行的顺序是不变的,也就是线程1的a = 1肯定在 i = b之前执行,第二个线程的b = 1在j = a之前执行,但是实际上线程1和线程2内部的两行代码的执行顺序和源代码中写的不一致,因为虚拟机在执行代码时发现i = b这行代码与上边的a = 1这行之间没有必然联系,它认为重排不会影响执行结果,线程1对线程2的代码是无知的,线程之间的代码是独立的,所以就出现了i = 0,j = 0 的情况。,如果没有指令重排,即保障线程中两行代码的执行顺序和编写顺序一样,那么就不会出现i = 0,j = 0的情况,所谓原子性是指在一次操作或多次操作中,要么所有的操作全部得到执行并且不受任何因素干扰而中断,要么所有的操作都不执行,而volatile不保障原子性,案例:开启100个线程每个线程对count值累加10000次,那么最后的正确结果应该是 1000000。,运行结果:发现多次运行结果并没有累加到1000000,当然也可能加到正确的结果,这里我的运气比较差,,结果分析:,以上的问题出现在count++上,这个操作其实是分为了三个步骤:,count++不是一个原子操作,也就是在某一个时刻对某一个指令操作时,可能被其他线程打断,,volatile原子性测试:,如下图,通过对变量count添加volatile发现并没有解决多线程count的累加问题,多次运行仍然累加不到1000000。,,那是因为volatile不保障原子性,也就是count++还是被分割为三个操作,iload,iadd和istore。只保障线程使用值时获取到的是别的线程修改后的最新值,并不能保障一个操作的原子性,如下图:,,volatile关键字可以保证可见性和禁止指令重排,但是不能保证对数据操作的原子性,所以在多线程并发编程的情况下如果对共享数据进行计算,使用volatile仍然是不安全的,我们可以通过加锁或者使用原子类保障线程安全,通过synchronized将操作count共享变量的代码同步起来,加锁方式1:,这里我将线程中的for循环直接同步了,锁的范围有点大,但是可以减少获取锁的次数,如果在线程的10000循环内加锁的话,线程内部每次循环都需要重新获取锁,反而影响性能,加锁方式2:,一般都是建议减小锁粒度,即只锁住操作共享数据的代码,也就是只锁住count++就行了,但是线程内有循环,这样每次循环都需要再获取一次锁,虽然synchronized是可重入锁,虽然不用判断是否被占用,可以直接获取到锁,但是还是仍然会执行 monitorenter 和 monitorexit指令,多少还是影响性能,不建议此种写法,执行结果:,加锁之后,就可以保障线程安全,可以获取到正确的结果,当然我们也可以使用Lock加锁在本合集的《Java线程安全问题和解决方案》一文中有介绍,在这就不多赘述!,,Java5开始提供了java.util.concurrent.atomic简称【Atomic包】,这个包中的原子操作类提供了一种用法简单,性能高效,线程安全的操作变量的方式,,原子型Integer,可以实现整型原子修改操作,,通过原子类改造:,运行结果:,多次运行发现都是正确的结果,实现了线程安全,,原子类中的值通过volatile修饰,保障数据可见性,,incrementAndGet方法源码:,上边我们说为了提高运算速度,JVM会编译优化,也就是进行指令重排,并发编程下指令重排会带来安全隐患:如指令重排导致的多个线程之间的不可见性,如果让程序员再去了解这些底层的实现规则,那就太难太卷了,严重影响并发编程效率,从Java5开始,提出了happens-before【发生之前】的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这两个操作既可以是在一个线程之内,也可以是在不同线程之间。 所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器还会优化我们的语句,所以等于是给了编译器优化的约束。不能让它瞎优化!,简单来说: happens-before 应该翻译成: 前一个操作的结果可以被后续的操作获取。就是前面一个操作变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。这就是volatile修饰变量可见性的原因,单例模式大家应该熟悉不过了吧,就是保障程序中的某实例只存在一个,单例模式一般有8种写法,大概分为懒汉式、饿汉式、静态内部类和枚举,其中懒汉式的常见四种写法中有部分写法存在多线程安全问题,这里借此演示一下多线程懒汉式的安全问题,并使用volatile解决安全问题。,在真正需要单例对象的时候才创建对象,在Java程序中,有时候可能需要延迟一些高开销的对象创建操作,以提升性能,这时就可以采用饿汉式实现单例对象的创建。,这种方式是先判断是否为null,之后创建对象,再返回对象,这种方式是线程不安全的,单例类:,测试类:,运行结果:,发现结果中有8条线程的哈希值一样,说明多线程下并非仅创建了一个对象,线程不安全,,同步方法:可以使用 synchronized 修饰获取单例对象的方法,线程调用方法时就会去获取锁,同步代码块:缩小锁范围,将方法中共享数据【单例对象】锁住,运行结果:,加锁之后,就可以解决线程安全问题,,问题:,代码中发现将if判断也锁起来了,理想的情况是只有对象是null的时候才去竞争锁,不是null的话就直接返回就行,显然上边的加锁方式太简单粗暴,影响性能,,有的小机灵鬼就是想到那我先判断是否为空,再加锁不就行啦,其实下边的这个代码也是线程不安全的,因为线程可能判断为null之后在加锁之前发生线程切换,运行结果:,,在面试时可以直接甩出,面试官必然满意,通过双重检查机制,并且使用volatile修饰单例对象,最好最安全的方式,强烈建议使用,思考:为什么要加上 volatile 修饰实例呢?因为创建对象并不是一个原子操作,而是分为了以下三步:,指令重排:,其中第二步和第三步有可能发生指令重排,即先将地址返回,再初始化实例,此时引用就不是null了,但是对象内部的属性,初始值等还没有完成赋值,对象内部的数据可能还是null,此时如果发生线程切换,线程2进来判断 INSTANCE 引用其实已经不为null,此时就会直接返回对象,但是该对象是一个残疾,在使用对象内部数据时就可能发生NEP即空指针异常,,可见性:,由此可见:使用volatile修饰绝不是花活而是科学的必要,因为volatile并不能保障原子性,所以如果多线程对共享数据进行计算的场景下还是需要加锁,使用volatile并不能保障线程安全,volatile适用于:,比如:我们上边的可见性问题的案例,所以volatile应用场景并不是非常广泛,主要是为了解决同步加锁太重的问题,在某些场景下可以使用volatile解决部分线程安全问题,文章出自:石添的编程哲学,如有转载本文请联系【石添的编程哲学】今日头条号。
文章版权声明
1 原创文章作者:cmcc,如若转载,请注明出处: https://www.52hwl.com/22540.html
2 温馨提示:软件侵权请联系469472785#qq.com(三天内删除相关链接)资源失效请留言反馈
3 下载提示:如遇蓝奏云无法访问,请修改lanzous(把s修改成x)
4 免责声明:本站为个人博客,所有软件信息均来自网络 修改版软件,加群广告提示为修改者自留,非本站信息,注意鉴别