JVM
Java虚拟机 (Java Virtual Machine)
JMM
Java内存模型 (Java Memory Model),简称JMM 本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM对于同步的规定:
- 线程解锁前,必须把共享变量的值刷新成
主内存
- 线程加锁前,必须读取主内存的最新值到
自己的工作内存
- 加锁和解锁是同一把锁
主内存是共享内存区域,是所有线程都可以访问的,也就是我们常说的内存。但是线程对变量的操作(读取赋值等)必须在工作内存中进行,首先就是将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存。每个线程都有自己的工作空间,线程间的工作内存不能互相访问。至于为什么要拷贝,主要因为CPU的效率远远高于内存。拷贝到自己的工作内存,实际上就是拷贝到CPU的缓存中去。
JMM特性:
- 可见性
- 原子性
- 有序性
volatile
英[ˈvɒlətaɪl],美[ˈvɑːlətl]
是Java虚拟机提供的轻量级的同步机制,有三大特性:
- 保证可见性
不保证原子性
(synchronized保证原子性)- 禁止指令重排序
可见性与非原子性
可见性就是一个线程修改了主内存的时候,其他线程马上获得通知。
如何解决非原子性问题?比如i++,推荐使用AtomicInteger这些类,其次可以使用synchronized之类的解决方法
指令重排
计算机在执行程序的时候,为了提高性能,编译器和处理器会对指令做重排: 【1.源代码 –> 2.编译器优化的重排 –> 3.指令并行的重排 –> 4.内存系统的重排 –> 最终执行的指令】 其中2,3,4都有可能发生指令重排
- 单线程环境里面也会有指令重排,但是会确保执行结果和代码顺序执行的结果一致,所以指令重排在单线程下完全没有问题
- 处理器在进行指令重排序的时候必须要考虑指令之间的
数据依赖性
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
指令重排序可能引入的问题,如下:
1 | public class ReSortDemo { |
想象一下,多线程场景,fun01
方法内部语句发生指令重排(因为a和flag没有数据依赖性),变成先执行语句2再执行语句1
。当先执行完语句2还没有执行语句1的时候,另外线程执行fun02方法,此时由于flag==true
所以会执行里面的语句3,并且打印5,因为语句1a=1
还没有执行,所以结果是5。预期的结果应该是6,发生线程不安全问题。解决方法就是给变量加上volatile修饰符。
volatile应用于懒汉单例,就是因为指令重排问题
CAS
Compare And Swap 介绍
可以通过Java中AtomicInteger类中的getAndIncrement
方法来理解
1 | public final int getAndAddInt(Object o, long offset, int delta) { |
比如我想i++,原先i=5,先做运算然后赋值,赋值时比较下当前i是不是还是5,如果i还是5,就直接赋值了,如果不是,说明其他线程做了修改,就需要将这个新的值重新运算,再判断赋值,如果原先的值又变了,再运算判断赋值…
CAS优点
不需要加锁,也就减少线程上下文切换带来的消耗,线程切换好像设计内核态和用户态,这个以后再补充。// TODO
CAS有哪些缺点以及问题
1. 一直循环问题
会锁升级,计算机已经做了优化,不需要在代码中体现
2. ABA问题
Java中的AtomicStampedReference
类有解决这个问题,原理是加了一个stamp,相当于一个版本号,如果有修改,版本号就会有变化,这样当从A->B->A就会知道修改过。如果不在意这种修改,其实是可以不用使用stamp的,如果在意这种修改就可以使用这种方法。