JMM内存模型
参考文章:www.jianshu.com/p/d52fea0d6… 在java多线程程序运行的过程中,很有可能出现共享变量的并发访问安全问题,出现多线程的安全问题一般都是因为主内存和工作内存数据不一致和指令重排序引起的.
1.哪些变量可以作为共享变量
- java程序中所有的实例对象,静态变量,都可以放在堆内存中,被所有的线程共享,而成员变量,局部变量只会出现在每个线程的线程栈中.这些变量无法被其他线程共享
2.JMM抽象结构模型
我们知道CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每个CPU都会有缓存。因此,共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去。
3.指令重排序
为了加快程序的执行速度,JMM对底层的约束尽量的减少.为了提高程序的执行性能,编译器和处理器经常会对程序的指令进行重新排序.
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;
- 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。
经典的例子.单例模式DCL的指令重排序blog.csdn.net/zx48822821/…
4.as-if-serial语意
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的
happens-before规则
1.happens-before定义
JMM利用happens-before的概念来定义两个操作之间的执行顺序,由于这两个操作可能在一个线程内,也可能在两个线程中,因此,JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见).
2.happens-before规则的具体定义
1.程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
2.监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
3.volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
4.start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
5.join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
6.程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
7.对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
总结:JMM通过happens-before规则禁止会影响程序执行结果的重排序
1.JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在,比如上面的A happens-before B)。
2.JMM对编译器和处理器的束缚已经尽可能少。从上面的分析可以看出,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。
volatilte
volatitle参考文章www.jianshu.com/p/0f8b1066c… www.cnblogs.com/wangwudi/p/…
1.volatitle的作用
volatite是JVM提供的轻量级的同步机制
主要特点包括以下三点:
- 1 保证可见性
- 2 不保证原子性
- 3 禁止指令重排
2.内存屏障
内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个
1、保证特定操作的执行顺序,通过在编译器指令间插入内存屏障,告诉编译器,位于内存屏障之后的指令,不能够发生在内存屏障之指令之前.
2、保证某些变量的内存可见性.保证变量被修改之后能够即使的刷新回内存,这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
3.happens-befores与volatitle的关系
volatile变量可以通过缓存一致性协议保证每个线程都能获得最新值,即满足数据的“可见性”。
在六条happens-before规则中有一条是:**volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
volatitle关键字是对happends-before规则具体的实现.
synchronized
参考文章 互斥锁的特点:
- 互斥性:即在同一时刻只允许一个线程持有某个对象锁,通过这种特性来实现多线程协调机制,这样在同一时刻只有一个线程对所需要的同步的代码块(复合操作)进行访问。互斥性也成为了操作的原子性。
- 可见性:必须确保在锁释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程可见(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起数据不一致。
1.只有一个线程对对象访问,没有其他线程的竞争,所以对该对象不用加锁,这就是无锁状态. 2.当某一个线程,初次执行到synchronized代码块的时候,首先检查锁对象是否处于无锁状态,若为无锁状态,则修改对象头里的锁标志位,并且将当前线程的id保存在mark word中,锁状态变为偏向锁, 3.如果这是有第二个线程访问代码快,会先检查锁对锁对象中的mark word线程id是否为自己,如果是自己,则直接进行同步代码快.如果不是则通过cas操作修改mark中的值,如果修改成功,说明获得偏向锁,如果修改不成功,说明存在锁竞争,将锁升级为轻量级锁 4.JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,如果失败则,通过cas进行重试.如果cas重试某个达到最大自旋次数的线程,锁竞争情况严重,会将轻量级锁升级为重量级锁.