Java内存模型和Volatile关键字

前言

学习并发关键在于学会解决并发过程中线程通信以及线程同步中出现的问题,线程通信有两类机制,一是共享内存,另一个是消息传递。JAVA使用的是第一种,通过在共享内存中进行读写来进行消息传递,在共享内存中,线程通信是隐性的,对编程人员是透明的,因此容易出现可见性问题;线程同步则是显性的,需要编程人员来指定线程之间的互斥以及同步。高效并发系列皆是围绕着介绍虚拟机如何实现线程、多线程之间由于共享和竞争数据而导致的一系列问题及解决方案。我在这部分主要介绍下java定义的内存模型以及内存模型是如何解决并发中的可见性、原子性、有序性问题。

内存模型

内存模型

1、java内存模型的目标是屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各个平台下都能达到一致的内存访问效果。
2、内存模型主要目标是通过定义程序中各个变量的访问规则来实现并发安全。这里的访问规则指的是虚拟机将变量存储到内存以及从内存取出变量这种细节。这里的变量指的是线程共享、存在线程竞争问题的变量(包括实例字段、静态字段和构成数组对象的元素等)
3、内存模型规定了所有变量存储在主内存中,而每个线程只能在自己的工作内存(类比于cache)中对变量进行操作,所以线程需要将变量从主内存拷贝一个副本到工作内存,当对副本变量进行修改时需要将变量更新回主内存,线程间变量值的传递需要通过主内存来完成,线程、主内存、工作内存之间存在交互关系,与此同时会出现缓存一致性问题。交互图如下:
在这里插入图片描述
4、主内存、工作内存与Java内存区域对应的关系–主内存对应于Java堆中的对象实例数据部分,工作内存对应于虚拟机栈中的部分区域。物理角度来说,主内存对应于物理硬件的内存,虚拟机更可能让工作内存优先存储在cache中。

5、内存模型通过8个原子操作完成变量在内存间的交互操作,分别为lock、unlock、read、load、use、assign、store、write(操作功能不在这详述)read与load以及write和store之间需要顺序执行,但无需连续执行。8种原子操作在执行时需要满足一定的规则(不在这详述)
在这里插入图片描述

原子性、可见性与有序性

Java内存模型在并发过程中通过处理好原子性、可见性、有序性三者来保证并发安全。volatile可保证可见性以及有序性。而锁(synchronized以及Lock)是三者皆可保证。关键字final可保证可见性。volatile是最轻量级的同步机制,在大多数情况下,总开销比锁略低,性能相差无几,选择volatile还是锁的唯一依据仅仅是volatile是否符合使用场景需求。(后面会叙述使用场景需求)

原子性

原子性指的是操作要么都执行,不可中断,要么都不执行。例如一个++i操作,这看似是一个原子性操作,但其实它包含了三个独立的操作,读取i值,将值加1,再将值写入i中,所以它不是一个原子性操作,非原子性操作在单线程中可能不会出错,但在多线程中就会出现偏差。

原子性操作包括内存模型提供的八种原子操作,对基本数据类型的访问读写也是具备原子性的,synchronized以及Lock可实现大范围的原子操作,由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

可见性

可见性指的是当一个线程修改了共享变量的值,其它线程能够立马得到这个修改。Java内存模型通过在工作内存中修改后将修改的新值同步回主内存,在变量读取前从主内存中将值刷新到工作内存中来完实现可见性。这在单线程中不会出错,但在多线程中会出现错误。因为值更新回主内存往往不会立刻得到执行,而且值在工作内存和主内存传递过程中耽搁了时间,可能出现读值和写值延迟。

public class thread_test  extends  Thread{
     private String name;
     private static int count=10;

    public thread_test(String name) {
        this.name = name;
    }
    @Override
    public void run(){
     for(int i=0;i<10;i++){
         if(count>0)
         {
             count--;
             System.out.println("线程"+name+"进行倒计时之后的count值"+count);
         }
         else{
             break;

         }
         try{
             sleep((int)Math.random()*10);
         }
         catch(InterruptedException e){
             e.printStackTrace();
         }
     }
   }
     public static void main(String[] args)
     {
         Thread t1=new thread_test("A");
         Thread t2=new thread_test("B");
         t1.start();
         t2.start();
     }
}
线程B进行倒计时之后的count值8
线程A进行倒计时之后的count值9
线程A进行倒计时之后的count值7
线程B进行倒计时之后的count值7
线程A进行倒计时之后的count值6
线程B进行倒计时之后的count值5
线程A进行倒计时之后的count值4
线程B进行倒计时之后的count值3
线程A进行倒计时之后的count值2
线程B进行倒计时之后的count值1
线程A进行倒计时之后的count值0

上述代码前四行就可知因为存在可见性问题,所以两个线程读取以及修改的值都出错,出现了线程安全问题。线程A经过一次减操作,将新值9从工作内存传回主内存,线程B从工作内存将值9从主内存读到工作内存进行减操作,将值8写回主内存,而此时线程A将主内存中的值8读回工作内存,然后线程A和B都进行减操作,但没有及时通知另一个线程它的更新,因此此时就出现可见性问题。

因此在多线程间,普通变量存在可见性问题,需要通过加锁机制来保证变量的可见性。或者通过volatile来实现,volatile变量的特殊规则保证了新值立即同步到主内存,以及每次使用时立即从主内存刷新。

有序性

有序性即程序执行的顺序按照代码的先后顺序执行。但Java中往往是线程内有序,而线程间无序,这是由于Java的指令重排序、主内存和工作内存同步延迟导致的。
(注:指令重排序指的是处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的,因为它不会对变量之间存在数据依赖的变量进行重排序)

Java提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile通过禁止指令重排序来实现这点,而synchronized通过规定同一时刻只有一个线程能对变量进行上锁,从而规定了线程只能串行的访问变量来实现有序性。

但Java中如果所有的有序性都要依靠volatile和synchronized来完成那在Java中操作就会变得繁琐、开销大,Java也不会有这么高的性能。在Java中,存在天然的先行发生关系,它无需任何同步器协助就已经存在,当满足"先行发生原则时",则无需其它操作,操作即满足有序性。

先行发生是JMM定义的两项操作之间的偏序关系,与时间无关,它指的是在发生操作B之前,操作A的修改变量、发送消息、调用方法等行为造成的结果能被操作B观察到。

先行发生原则有八种,只要满足以下八种,则无需其它同步手段就能保证有序性。
1、程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
2、锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作
4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
5、线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
8、对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

volatile

在Java多线程编程中,我们往往习惯用锁机制来保障线程安全问题,尽管虚拟机对锁进行了很大程度上的优化,但在某些情况下,volatile的同步性能的确比锁机制更优,而且voatile变量读操作的性能消耗与普通变量几乎没有什么差别,写操作稍微慢一点,因为需要插入内存屏障指令来保证有序性。volatile是最轻量级的同步机制,在大多数情况下,总开销比锁略低,性能相差无几,选择volatile还是锁的唯一依据仅仅是volatile是否符合使用场景需求。

因为volatile可以保证可见性和有序性,但无法保证原子性,所以在不满足原子性的场合下无法使用,因此在符合以下两条规则的运算场景中,可以使用volatile关键字,其它场景需要使用锁机制来保证原子性。
1、运算结果并不依赖变量的当前值或者能够确保只有单一线程修改变量的值。(适合一写多读的应用场景)
2、变量不需要与其他状态共同参与不变约束。

volatile可以确保变量对所有线程的可见性和有序性。volatile型变量实现可见性的机制是通过其特殊规则保证了修改后的新值立马更新同步到主内存,以及使用前都立即从主内存刷新。volatile实现有序性是通过禁止指令重排序优化来实现。volatile修饰的变量在复制后会进行一个相当于内存屏障的操作,指令重排序时不能将后面的指令重排序到屏障之前,volatile会将本线程中的工作内存中的变量强制性立即写入主内存,且写入操作引起别的cpu中的cache无效化,别的线程只能重新从主内存中读取该变量。变量值写入内存之后,意味着这个变量的操作已经完成,其它线程只能再其后对其进行操作。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值