java多线程之内存可见性(浅谈synchronize 和volatile)

注:本博客是慕课网的一个免费课程的学习笔记, 有兴趣的同学可以去看看: 细说Java多线程之内存可见性

什么是内存可见性?

     可见性: 一个线程对共享变量值的修改,能够及时地被其他线程看到

什么是共享变量?

     共享变量:如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是这几个线程的共享变量

什么是JMM?

     Java内存模型JMM(Java Memory Model):描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存个从内存中读取出变量这样的底层细节.

JMM对共享变量有以下的规定:(结合下面的图来看)

      *所有的变量都存储在主内存中
      *每个线程都有自己的独立的工作内存,里面保存该线程使用到的变量副本(主内存中改变量的一份拷贝),每个线程只能操作自己的独立内存,无法操作柱内存
      *线程对共享变量的所有操作都必须在自己的工作内存,不能直接从主内存中读写.
      *不同线程之间无法直接访问其他线程工作内存中的变量,线程间的变量值的传递需要通过主内存来完成.

  JMM中的主内存和工作内存

       我们根据JMM对共享变量的规定,要实现共享变量的可见性,即线程1对共享变量的修改要想被线程2及时看到,必须要经过如下2个步骤:

       1.把工作内存1中更新过的共享变量刷新到主内存中
       2.将主内存中最新的共享变量的值更新到工作内存2中

扫描二维码关注公众号,回复: 2937541 查看本文章

      Java语言层面支持的可见性的实现方式 : synchronize 和 volatile , final(常量我们这里不探讨) , 这里我们讨论 synchronize 和 volatile

synchronize: 可以保证变量可见性和一段代码的原子性

     JMM关于synchronized的两条可见性规定:
            *线程解锁前,必须把共享变量的最新值刷新到主内存中
            *线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

    线程执行synchronize互斥(同步)代码的过程:
            1.获得互斥锁
            2.清空工作内存
            3.从主内存中拷贝变量的最新副本到工作内存
            4.执行代码
            5.将更改后的共享变量的值刷新到主内存
            6.释放互斥锁

    导致共享变量在线程间不可见的原因,以及synchronized对应的解决方案:

            1.线程的交叉执行-->synchronize相当于加了一把锁,锁内部的代码只能由一个线程来执行,保证了锁内部代码的原子性
            2.重排序结合线程交叉执行 -->因为线程不会交叉执行,重排序只是在一个线程内重排序,结合"as-if-serial"原理,不会对执行结果产生影响.  "as-if-serial"原理 : 无论如何重排序,程序执行的结果应该与代码顺序执行的结果一致.
            3.共享变量更新后的值没有在工作内存与主内存间及时更新-->根据synchronize的两条可见性规范,保证共享变量的可见性.

volatile:可以保证可见性,不能保证原子性

       原理: 通过加入内存屏障和禁止重排序优化来实入一条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写相当于解锁.
         

猜你喜欢

转载自blog.csdn.net/weixin_42354330/article/details/81944625