概述
JVM通过共享内存的方式来进行线程间通信,但是会存在多线程的本地内存和主内存的不一致性问题,也就是我们通常所说的线程安全问题。
为了解决线程安全问题,JVM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则也称为Java内存模型(即JMM),JMM是围绕着程序执行的原子性、有序性、可见性展开的,下面我们看看这三个特性。
原子性概念
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。有点要注意的是,对于32位系统的来说,long类型数据和double类型数据(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作),它们的读写并非原子性的,也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元,这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。但也不必太担心,因为读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。
可见性概念
可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。
有序性概念
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。
JMM提供的解决方案
在理解了原子性,可见性以及有序性问题后,看看JMM是如何保证的,在Java内存模型中都提供一套解决方案供Java工程师在开发过程使用,如原子性问题,除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性。而工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化,关于volatile稍后会进一步分析。除了靠sychronized和volatile关键字来保证原子性、可见性以及有序性外,JMM内部还定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。
volatile的定义与实现原理
公共的变量,多个线程都可以访问,每个线程访问时都可以拿到它的最新值,所以它是异变的。
可以把volatile修饰的变量比如成小孩子完的叠叠圈,多个小孩子可以同时看到叠叠圈的变化,每次去放圈或者取圈都是在最新变化的基础上且是排他的。
Java语言规范第3版中对volatile的定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。
Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile。Java内存模型确保所有线程看到这个变量的值是一致的。
理解volatile效果
理解volatile特性的最好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。下面通过具体的示例来说明,示例代码如下。
class VolatileFeaturesExample {
// 使用volatile声明的64位long型变量
volatile long v1 = 0;
// 单个volatile变量的写
public void set(long l) {
v1 = 1;
}
// 复合(多个)volatile变量的读/写
public void getAIncrement() {
v1++;
}
// 单个volatile变量的读
public long get() {
return v1;
}
}
假设有多个线程分别调用上面程序的3个方法,这个程序在语义上和下面程序等价。
class VolatileFeaturesExample {
// 64位的long型普通变量
long v1 = 0L;
// 对单个的普通变量的写用同一个锁同步
public synchronized void set(long l) {
v1 = l;
}
public void getAndIncrement() {
// 调用已同步的读方法
long temp = get();
// 普通写操作
temp += 1L;
// 调用已同步的写方法
set(temp);
}
// 对单个的普通变量的读用同一个锁同步
public synchronized long get() {
return v1;
}
}
简而言之,volatile变量自身具有下列特性。
- 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
- 原子性:对任意的单个volatile变量的读/写具有原子性,但类似于i++ 这种复合操作不具有原子性。
volatile写-读的内存语义
- volatile写的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存中。 - volatile读的内存语义如下。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
为了实现volatile内存语义,JMM会分别限制编译器和处理器重排序。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。
synchronized的实现原理与应用
Java并发编程-共享模型之管程(Monitor/Synchronized)(四)
原子操作的实现原理
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。
java中如何实现原子操作
在Java中可以通过锁和循环CAS(compare and swap:比较和交换,变量当前值和旧值相同设置,不同不设置)的方式来实现原子操作。
CAS实现原子操作的三大问题
CAS虽然很高效的解决了原子问题,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。
锁的内存语义
final域
final关键字主要影响的是指令重排序。
写final域的重排序规则可以确保,在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障(普通的字段,很可能字段的值还没有构造完成,就会被其他的读线程可见,因此会读出不正确的数据)。
本章小结
本章我们一起研究了volatile、synchronized和原子操作的实现原理。Java中的大部分容器和框架都是依赖于本章介绍的volatile和原子操作的实现原理,了解这些原理对我们进行并发编程会更有帮助。
参考
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/100139.html