之前一直在接触使用synchronized和volatile这些方法来实现内存的可见性,其中synchronized可以保证内存的可见性和原子性,而volatile只能保证可见性,不能保证原子性,所以volatile声明的变量不能执行如a+=a、a=a*a这一类的操作,因为这样不能不能保证其原子性。从这里出发我分析了一下Java多线程中的内存可见性与原子性。
一、java内存模型(JMM)
在内存分配的时候都有一个线程内存和主内存。之前研究过java的人应该都知道指令重排序的问题。而在Java内存模型下,重排序问题是会导致这样的内存的可见性问题的。在Java内存模型下,每个线程都有它自己的工作内存(主要是CPU的cache或寄存器),它对变量的操作都在自己的工作内存中进行,而线程之间的通信则是通过主存和线程的工作内存之间的同步来实现的。我查了一些概念:
1、指令重排序:代码书写的顺序和实际执行的顺序不同,目的是提高程序的性能。
2、编译器优化重排序:编译器重新排序
3、指令级并行优化从排序:CPU并行执行时优化
4、内存系统从排序:读写缓存的优化
5、as-if-serial:无论如何重排序,程序执行的结果都应该与代码顺序执行的结果一致。
6、java编译器保证as-if-serial,但是在多线程程序中并不能保证顺序执行。
二、原子性与可见性概念:
1.可见性
在多核处理器中,如果多个线程对一个变量(假设)进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。(这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主存通信的次数);同样在单核处理器中这样由于“备份”造成的问题同样存在!
这样的优化带来的问题之一是变量可见性——如果线程t1与线程t2分别被安排在了不同的处理器上面,那么t1与t2对于变量A的修改时相互不可见,如果t1给A赋值,然后t2又赋新值,那么t2的操作就将t1的操作覆盖掉了,这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。
2.原子性:
众所周知,原子是构成物质的基本单位(当然电子等暂且不论),所以原子的意思代表着——“不可分”;
由不可分性可知,原子性是拒绝多线程操作的(只有分解为多步操作,多个线程才能对其操作:就像一个盒子里有多个兵乓球,多个人能够从盒子里拿乒乓球;如果盒子只有一个兵乓球,一个人拿的话,其他人就拿不到了;这就是原子性,乒乓球就具有原子性,人就相当于原子)
简而言之——不被线程调度器中断的操作,如:
赋值或者return。比如"a = 1;"和 "return a;"这样的操作都具有原子性
原子性不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作!
三、synchronized实现可见性和原子性
JMM对Synchronized规定:
线程加锁时,将清空线程内存中共享变量的值,从而使用共享变量时从主内存中重新读取新值。
线程解锁前,必须把共享变量的最新值刷新到主内存中。
线程执行互斥代码过程:
1、 获得互斥锁
2、 清空线程内存
3、 从主内存中拷贝最新副本到线程内存
4、 执行代码
5、 将更改后的变量刷新到主内存
6、 释放互斥锁
四、Volatile实现可见性
加入内存屏障和禁止重排序优化来实现,会在volatile写操作后加入store屏障指令,读操作前加入load屏障指令。
Volatile不能保证变量操作的原子性。在如a+=1;等自身作为操作的不能使用,两个Volatile声明的变量a、b不能进行操作
五、Lock实现可见性和原子性:
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}