解读JVM如何保证三大特性及volatile和synchronized区别

并发编程有三个至关重要的特性
分别是原子性、有序性和可见性,理解这三个特性对于开发正确的高并发程序会有很大的帮助。
原子性
所谓原子性是指在一次的操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。
说起原子性一般都会用银行转账来进行举例说明,比如从Alex的账号往Tina的账号转入1000元,这个动作将包含两个最基本的操作:从Alex的账号上扣除1000元;给Tina的账号增加1000元。这两个操作必须符合原子性的要求,要么都成功要么都失败,总之不能出现Alex的账号扣除了1000元,但是Tina的账号并未增加1000元或者Alex账号未扣除1000元, Tina的账号反倒增加了1000元的情况。同样在我们编写代码的过程中,比如一个简单的赋值语句:object o - new object();1用类型。占用四个字节(32位),假设这样的赋值语句不能够保证原子性的话,那么会导致赋值出现错误的数据。
可见性
可见性是指,当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。
有序性
所谓有序性是指程序代码在执行过程中的先后顺序,由于Java在编译器以及运行期的,优化,导致了代码的执行顺序未必就是开发者编写代码时的顺序。
JVM如何保证三大特性
JVM采用内存模型的机制来屏蔽各个平台和操作系统之间内存访问的差异,以实现让Java程序在各种平台下达到一致的内存访问效果,比如C语言中的整型变量,在某些平台下占用了两个字节的内存,在某些平台下则占用了四个字节的内存, Java则在任何平台下,Int类型就是四个字节,这就是所谓的一致内存访问效果。
Java的内存模型规定了所有的变量都是存在于主内存(RAM)当中的,而每个线程都有自己的工作内存或者本地内存(这一点很像CPU的Cache),线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接对主内存进行操作,并且每一个线程都不能访问其他线程的工作内存或者本地内存。比如在某个线程中对变量i的赋值操作i=1,该线程必须在本地内存中对i进行修改之后才能将其写人主内存中。
一、JAVA如何保证原子性
多个原子性的操作在一起就不再是原子性操作了。
简单的读取与赋值操作是原子性的,将一个变量赋给另外一个变量的操作不是原子性的。
Java内存模型(JMM)只保证了基本读取和赋值的原子性操作,其他的均不保证,如果想要使得某些代码片段具备原子性,需要使用关键字synchronized,或者JUC中的lock。如果想要使得int等类型自增操作具备原子性,可以使用JUC包下的原子封装类型java.util.concurrent.atomic.*
二、Java如何保证可见性
使用关键字volatile,当一个变量被volatile关键字修饰时,对于共享资源的读操作会直接在主内存中进行(当然也会缓存到工作内存中,当其他线程对该共享资源进行了修改,则会导致当前线程在工作内存中的共享资源失效,所以必须从主内存中再次获取),对于共享资源的写操作当然是先要修改工作内存,但是修改结束后会立刻将其刷新到主内存中
通过synchronized关键字能够保证可见性, synchronized关键字能够保证同一时刻只有一个线程获得锁,然后执行同步方法,并且还会确保在锁释放之前,会将对变量的修改刷新到主内存当中。
通过JUC提供的显式锁Lock也能够保证可见性, Lock的lock方法能够保证在同一时刻只有一个线程获得锁然后执行同步方法,并且会确保在锁释放(Lock的unlock方法)之前会将对变量的修改刷新到主内存当中。
三、Java如何保证有序性
在Java的内存模型中,允许编译器和处理器对指令进行重排序,在单线程的情况下,重排序并不会引起什么问题,但是在多线程的情况下,重排序会影响到程序的正确运行,Java提供了三种保证有序性的方式,具体如下:
使用volatile关键字来保证有序性。
使用synchronized关键字来保证有序性。
使用显式锁Lock来保证有序性。
后两者采用了同步的机制,同步代码在执行的时候与在单线程情况下一样自然能够保证顺序性(最终结果的顺序性),对于volatile关键字后面还会进行更加详细的介绍。比外, Java的内存模型具备一些天生的有序性规则,不需要任何同步手段就能够保证有序性,这个规则被称为Happens-before原则。如果两个操作的执行次序无法从happensbefore原则推导出来,那么它们就无法保证有序性,也就是说虚拟机或者处理器可以随意对它们进行重排序处理。
下面我们来具体看看都有哪些happens-before原则
程序次序规则:在一个线程内,代码按照编写时的次序执行,编写在后面的操作发生于编写在前面的操作之后。这句话的意思看起来是程序按照编写的顺序来执行,但是虚拟机还是可能会对程序代码的指令进行重排序,只要确保在一个线程内最终的结果和代码顺序执行的结果一致即可。
锁定规则:一个unlock操作要先行发生于对同一个锁的lock操作。这句话的意思是,无论是在单线程还是在多线程的环境下,如果同一个锁是锁定状态那么必须先对其执行释放操作之后才能继续进行lock操作。
volatile变量规则:对一个变量的写操作要早于对这个变量之后的读操作。根据字面的意思来理解是,如果一个变量使用volatile关键字修饰,一个线程对它进行读操作,一个线程对它进行写操作,那么写入操作肯定要先行发生于读操作。 关于这个规则下面继续详细介绍:
传递规则:如果操作A先于操作B,而操作B又先于操作C,则可以出操作A肯定要先于操作C,这一点说明了happens-before原则具备传递性。
线程启动规则: Thread对象的start()方法先行发生于对该线程的任何动作,这也是我们在第一部分中讲过的,只有start之后线程才能真正运行,否则Thread也只是一,个对象而已。
线程中断规则:对线程执行interrupt()方法肯定要优先于捕获到中断信号,这句话的意思是指如果线程收到了中断信号,那么在此之前势必要有interrupt()。
线程的终结规则:线程中所有的操作都要先行发生于线程的终止检测,通俗地讲,线程的任务执行、逻辑单元执行肯定要发生于线程死亡之前。
对象的终结规则:一个对象初始化的完成先行发生于finalize()方法之前,这个更没什么好说的了,先有生后有死。
volatile只能保证可见性、有序性不能保证原子性。
volatile和synchronized区别
(1)使用上的区别
volatile关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量、常量等。O
synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者i句s。
volatile修饰的变量可以为null, synchronized关键字同步语句块的monitor对象不能-为null
(2)对原子性的保证
volatile无法保证原子性。
由于synchronized是一种排他的机制,因此被synchronized关键字修饰的同步代码是无法被中途打断的,因此其能够保证代码的原子性。
(3)对可见性的保证
两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不同。
synchronized借助于JVM指令monitor enter和monitor exit对通过排他的方式使得同步代码串行化,在monitor exit时所有共享资源都将会被刷新到主内存中。
相比较于synchronized关键字volatile使用机器指令(偏硬件) "lock "的方式迫使其他线程工作内存中的数据失效,不得到主内存中进行再次加载。
(4)对有序性的保证
volatile关键字禁止JVM编译器以及处理器对其进行重排序,所以它能够保证有序性。
虽然synchronized关键字所修饰的同步方法也可以保证顺序性,但是这种顺序性是以程序的串行化执行换来的,在synchronized关键字所修饰的代码块中代码指令也会发生指令重排序的情况。
(5)其他
volatile不会使线程陷入阻塞。
synchronized关键字会使线程进入阻塞状态。

本章首先非常详细地介绍了并发编程的三个重要特性:原子性、可见性和有序性,并且结合Java的内存模型JVM分别解释了Java是如何保证这三个重要特性的,在本章的最后·深入解析了本部分的"volatile关键字”的使用场景。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值