注:本博客是慕课网的一个免费课程的学习笔记, 有兴趣的同学可以去看看: 细说Java多线程之内存可见性
什么是内存可见性?
可见性: 一个线程对共享变量值的修改,能够及时地被其他线程看到
什么是共享变量?
共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量
什么是JMM?
Java内存模型JMM(Java Memory Model):描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存个从内存中读取出变量这样的底层细节.
JMM对共享变量有以下的规定:(结合下面的图来看)
*所有的变量都存储在主内存中
*每个线程都有自己的独立的工作内存,里面保存该线程使用到的变量副本(主内存中改变量的一份拷贝),每个线程只能操作自己的独立内存,无法操作柱内存
*线程对共享变量的所有操作都必须在自己的工作内存,不能直接从主内存中读写.
*不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存来完成.
我们根据JMM对共享变量的规定,要实现共享变量的可见性,即线程1对共享变量的修改要想被线程2及时看到,必须要经过如下2个步骤:
1.把工作内存1中更新过的共享变量刷新到主内存中
2.将主内存中最新的共享变量的值更新到工作内存2中
Java语言层面支持的可见性的实现方式 : synchronize 和 volatile , final(常量我们这里不探讨) , 这里我们讨论 synchronize 和 volatile
JMM关于synchronized的两条可见性规定:
*线程解锁前,必须把共享变量的最新值刷新到主内存中
*线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值
线程执行synchronize互斥(同步)代码的过程:
1.获得互斥锁
2.清空工作内存
3.从主内存中拷贝变量的最新副本到工作内存
4.执行代码
5.将更改后的共享变量的值刷新到主内存
6.释放互斥锁
导致共享变量在线程间不可见的原因,以及synchronized对应的解决方案:
1.线程的交叉执行-->synchronize相当于加了一把锁,锁内部的代码只能由一个线程来执行,保证了锁内部代码的原子性
2.重排序结合线程交叉执行 -->因为线程不会交叉执行,重排序只是在一个线程内重排序,结合"as-if-serial"原理,不会对执行结果产生影响. "as-if-serial"原理 : 无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致.
3.共享变量更新后的值没有在工作内存与主内存间及时更新-->根据synchronize的两条可见性规范,保证共享变量的可见性.
原理: 通过加入内存屏障和禁止重排序优化来实入一条store屏障指令,将工作内存刷新到主内存中.
对volatile变量执行读操作时,会在读操作前加入一条load屏障指令,读取主内存中的最新值.
这样任何时刻,不同的线程总能看到volatile变量的最新值.
需要注意的是,volatile不能保证一段代码的原子性. 比如下面的代码:
public class VolatileDemo {
private volatile int number = 0;
public int getNumber(){
return this.number;
}
public void increase(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.number++;
}
public static void main(String[] args) {
final VolatileDemo volDemo = new VolatileDemo();
for(int i = 0 ; i < 500 ; i++){
//开500个线程来执行increase()方法
new Thread(new Runnable() {
public void run() {
volDemo.increase();
}
}).start();
}
//如果还有子线程在运行,主线程就让出CPU资源,
//直到所有的子线程都运行完了,主线程再继续往下执行
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println("number : " + volDemo.getNumber());
}
}
运行多次会发现打印出的结果可能是: 500 , 495 , 498 , 499 ......
为什么会出现这种情况呢? 问题出在 number++上面:
number ++; 分为三步:线程读取number的值,执行+1操作,线程写入最新的number值.而volatile无法保证number++的原子性.
如果开500个线程来执行number++,最后输出结果可能小于500.因为假如number=5,线程a读取了number的值,这时CPU资源被线程b抢走了,b读取number值,执行+1操作,写入最新的number值=6.然后线程a拿到了CPU资源,但a的工作线程中的number=5,然后++,得6,写入主内存.也就是有两次number++,实际上只加了一.
针对volatile无法保证原子性,有三种解决方案:
*使用synchronize关键字 (常用)
synchronize(this){
number ++;
}
*使用ReentrantLock
private Lock lock = new ReentrantLock();
lock.lock();
try{
number ++;
}finally{
//用try...final...保证即使出现异常,锁也会释放.
lock.unlock();
}
*使用Atomicinterger(没用过....)
要在多线程中安全使用volatile变量,需同时满足:
*对变量的写入操作不依赖其当前值: 不满足:number++,count=count*5 满足:boolean变量,记录温度变化的变量等
*该变量没有包含在具有其他变量的不变式中: 不满足: 不变式 low < up
Java中实现多线程共享变量的可见性方法有synchronize 和 volatile :
synchronize:可以用在方法或者代码块上,能保证可见性,也能保证原子性.
volatitle:用在变量上,只保证可见性,不保证原子性,不加锁,比synchronize轻量级,不会造成线程阻塞.volatitle读相当于加锁,volatitle写相当于解锁.