首先定义几个下面会用到术语
原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
可见性
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
有序性
java有序性可以总结为:在本地线程观察操作都是有序的,在一个线程观察另外一个线程,所有的操作都是无序的。前半句指的是线程内部指令串行执行,后半句指,在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile关键字来保证一定的“有序性”(具体原理下面会讲)。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性
操作系统硬件的效率与一致性
了解过操作系统我们知道为了解决I/O速度慢跟不上CPU所以在处理器和主内存之间增加cache,高速缓存,主内存是所有处理器共享的,cache也是,那么就要定义一些规范来保证cache一致性
JAVA内存模型
虚拟机规范中试图定义一种java内存模型来屏蔽硬件和操作系统的内存访问差异,它主要定义程序中各个变量的访问规则,这个变量指的是实例对象,静态字段和数组对象元素,不包括局部变量与方法参数,因为这部分是线程私有的,其他变量不会访问,不存在竞争。
Java主内存和工作内存
所以这里的变量指的是主内存的的变量,对应JVM的堆,和操作系统的主内存
工作内存是每个线程私有的,类比JVM栈和操作系统的高速缓存
那么线程的工作内存保存了被线程用到的变量的副本(拷贝指的是复制在主内存变量的引用,对象在该线程中被访问到的字段,而不是拷贝整个对象),线程对变量的操作必须在工作内存中实现,而不能直接读写主内存中的变量,线程之间也不能直接访问工作内存中的变量,要通过主内存
内存之间交互操作
那么和操作系统一样,必须定义一些约束保证java工作内存中副本和主内存之间的一致性,即一个变量如何从主内存拷贝到工作内存以及再如何把变量同步回主内存的实现细节,Java内存模型定义了下面几种操作
lock(锁定):作用于主存变量,把一个变量标识为线程独占状态
read(读取):作用于主存变量,把变量传输到工作内存
load(载入):作用于工作内存变量,把从主存中拿到的值放入工作内存的变量副本中
use(使用):作用于工作内存变量,把工作内存的一个值传递给执行引擎
assign(赋值):作用于工作内存变量,执行引擎收到的值复制给变量
store(存储):作用于工作内存变量,把工作内存变量传送到主存
write(写入):作用于主存变量,把传送过来的值放到主存中的变量
unlock(解锁):作用于主存变量,释放主存变量,其他线程可用
java内存模型规定,要想实现内存交互就必须顺序执行read load以及store write,但是不一定是连续执行
也可以是read a;read b;load a;load b;
内存模型还规定了这些操作满足的规则,满足一定的规则保证读取基本数据的原子性,再了解了volatile修饰变量的特殊规则我们就可以判断线程并发访问变量的操作是否安全
long 和double的非原子性协议
由于long和double都是64位的,在32位的系统里,上述的原子操作都只能操作这两种类型数据的“一半”,无法保证原子性,所以JVM内存模型模型允许long 和double的非原子操作,但是java JVM一半还是选择把这些操作实现为原子性的操作,比如用Volatile,volatile本身不保证获取和设置操作的原子性,仅仅保持修改的可见性。但是java内存模型保证声明为volatile的long和double变量的get和set操作是原子的。
请分析以下哪些操作是原子性操作:
1 x = 10; //语句1
2 y = x; //语句2
3 x++; //语句3
4 x = x + 1; //语句4
咋一看,有些朋友可能会说上面的4个语句中的操作都是原子性操作。其实只有语句1是原子性操作,其他三个语句都不是原子性操作。
语句1是直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。
语句2实际上包含2个操作,它先要去读取x的值,再将x的值写入工作内存,虽然读取x的值以及 将x的值写入工作内存 这2个操作都是原子性操作,但是合起来就不是原子性操作了。
同样的,x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。
所以上面4个语句只有语句1的操作具备原子性
Volatile修饰变量
volatile修饰的变量是对所有线程透明的,就是一旦volative变量值修改,各个线程内立刻知道了,并改变volative变量值,区别于普通变量,一个线程修改普通变量的值,要经过上面的操作返回给主内存,其他线程在通过主内存来访问。我们说所有线程立刻知道了volatile的值,但是并不能保证在各个线程工作内存中 volatile值是一致的,但是由于经过每次刷新,执行引擎看不到不一致的情况,所以说一致的。但是在并发的情况下,volatile也是不安全的,因为volatile不能保证原子性。
我们称关键字 volatile最轻量级的同步机制,为什么是轻量级的呢?区别于sychronized重量级同步锁
比如:使用volatile修饰int型变量i,多个线程同时进行i++操作,这样可以实现线程安全吗?
答案是不安全的,比如我们20个线程并发进行i++操作,每次线程加1000次,最后我们得到的值一般会比2000小(如果是线程安全的那么i最后会是2000)那么为什么,因为i++操作并不是原子的,我们可以从字节码的角度分析,jvm执行上述代码的字节码时,i++这个指令是被解释成四条字节码来操作的,当第一条字节码执行把i变量的值拿到操作栈顶,这个时候volatile保证i的值是正确的,接着执行其他字节码的时候,别的线程可能把i的值已经执行过+1的操作了(这个时候不能体现volatile的透明性,认为执行完其他字节码回来i的值因为透明性也变了,这是不行的,这不是一个层面的概念,透明性针对一条指令的,而不是一行字节码,一行代码的字节码执行线程是不可感觉到的。第二点不是说一条指令的字节码对应一行字节码就是原子性的,编译执行还有其他很多的机器指令),那它再拿去它栈顶的值就是一个比正确值小的数了,当该线程执行完字节码由于volatile透明性,更新到其他线程,i变小。所以在多线程下也是不安全的。
那么在i++的函数外加上sychronized就是安全的,它能保证原子性
关键字 volatile应用场景
(1)volatile boolean shutdownRequested;
public void shutdown(){
shutdownRequested=true;
}
public dowork(){
while(!shutdownRequested)
//*********
}
当执行shutdown方法,能保证所有执行dowork的方法停下来
(2)使用volatile关键字修饰变量就是禁止指令重排序,自带内存屏障
CPU在执行指令的时候会对执行顺序做出优化,指令不一定会按照顺序执行,当然CPU能保证和顺序执行的结果一致
一个例子DCL单例,这是前面单例模式的第一种实现方法,当时并没与意识到有问题
public class MyJvm {
private volatile static MyJvm jvm;//创建私有静态变量
private MyJvm(){//构造器私有化
}
public static MyJvm getInstance3() throws InterruptedException{//双重检查 提高效率
if(jvm==null){//如果已经有对象 线程都不在等待 没有对象在进去
synchronized(Jvm.class){//同步快 不能this对象 静态方法中没有this对象 放入类的字节码信息 效率低
if(jvm==null){
jvm=new MyJvm();
}
}
}
return jvm;
}
}
这里我们应该对jvm变量加volatile关键字修饰,不然会因为指令重拍发生问题,就是synchronized同步中的代码执行new MyJvm()到底发生了什么?
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
jvm= memory; //3:设置instance指向刚分配的内存地址
上面的伪代码中2、3步可能重排 那么线程A已经把 new出来的对象给了jvm 但是里面的属性还没有初始化 ,然后这个时候B 线程来了 判断jvm已经不为null(已经分配了内存地址),但是里面的属性还没初始化
但是jvm加了volatile 修饰之后,JVM就不会对2和3步重排序
总结选用什么机制来同步线程取决于不同的情景