目录
1.JMM
- JMM(Java Memory Model):Java内存模型
- JMM本身是 一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式
- JMM是大多数多线程开发的规范
JMM关于同步的规定:
- 1.线程解锁前,必须把共享变量的值刷新回自己的主内存
- 2.线程加锁前,必须读取主内存的最新值到自己的工作内存
- 3.加锁解锁是同一把锁
JMM有三大特性:
- 可见性
- 原子性
- 有序性
1.1 可见性
主内存:物理内存,即我们的硬件
线程的工作内存:
- 每个线程在创建时JVM都会为其分配一个自己的工作内存(有的地方叫做栈空间),如上图的本地内存A和本地内存B
- 工作内存是每个线程的私有数据区域
Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但是线程对变量的操作(读取赋值等)必须在工作内存中进行
- 首先要将变量从主内存拷贝到自己的工作内存空间,
- 然后对变量进行操作,
- 操作完成后再将变量写回主内存
不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如上图
JMM的可见性示例:
假设有一个主内存中有一个student对象,它有一个属性age=25
现在有三个线程t1,t2,t3要来修改age的值
并不是每个线程都去修改主内存中的age,而是它们每个线程都有自己的工作内存,每个线程都会把主内存的共享变量age的值拷贝一份回自己的工作内存
t1线程要将25修改为37,第一步:它先在自己线程的工作内存中修改为37,此时线程t2,t3并不知道t1已经修改为37了,因为它们在各自的内存空间
t1线程在自己的工作内存修改完之后,第二步要将自己修改后的值写回给主内存,此时t2和t3并不知道主内存中的值从原来的25修改为了37
我们必须有一种机制,在修改完自己的工作空间的值并写回给主内存之后,要及时通知其他线程,这样及时通知的这种机制就称为JMM的可见性
1.2 原子性
- 原子性:即不可分割,完整性,也即某个线程正在做某个具体的业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
- 原子性,也即在执行过程中不会被线程调度机制打断的操作,这种操作从开始就一直运行到结束,中间不存在任何上下文切换。
i++不具有原子性的原理解释:
假设三个线程1,2,3共享同一个变量i
假设某个时间段线程1,2都执行了i++操作,某一时段,它们先后都将共享变量i拷贝到各自线程的工作内存中
假设由于调度,线程1将和线程2首先它们会在自己的工作内存中将值修改为1
线程1要写回主内存了,但是此时由于调度被挂起了,线程2先将修改后的值1写回主内存
z
正常情况下,1应该先拿到2修改后的值,再加1,再写回到主内存,i的值变为2
线程2刚写回,还没来得及通知其他线程,线程1又被调度,将又将1写回到主内存
这就是i++的线程不安全,不具有原子性的示例情况
如下,实际在底层,i++被分解为3个指令,所以i++不具有原子性
1.3 有序性
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,一般分以下3种
- 单线程里面确保程序最终执行结果和代码顺序执行的结果一致,即单线程环境不用担心指令重排
- 处理器在重排序时必须要考虑指令之间的数据依赖性
- 多线程的环境下线程的交替执行,由于编译器优化重排的存在,两个线程种使用的变量能否保证一致性是无法确定的,结果无法预测
重排序案例1:
由于指令重排序,上述代码可能的执行顺序为:
- 语句1234
- 语句1324
- 语句2134
问题:能否把语句4挪到语句3或者语句2的前面?
- 不能,因为语句4对语句2和语句3中的数据存在依赖性
重排序案例2:
2.volatile
2.1 volatile是什么
- volatile是Java虚拟机提供的一种轻量级的同步机制
2.2 三大特性
- 保证可见性
- 不保证原子性
- 禁止指令重排
(1)保证可见性
示例代码
import java.util.concurrent.TimeUnit;
/**
* 验证volatile的可见性
*
* 1.假如int number=0,number变量之前根本没有添加volatile关键字
*
* 此时程序不具有可见性
*/
class MyData{
int number = 0;
public void addTo60(){
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData mydata =new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
//暂停一会线程,保证其他线程也已经读取了共享变量number的值
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//AAA线程修改共享变量number的值
mydata.addTo60();
System.out.println(Thread.currentThread().getName()+"更新后共享变量number的值为:"+mydata.number);
},"AAA").start();
//第二个线程就是我们的main线程
while(mydata.number == 0){
}
System.out.println("main线程被通知了共享变量number已经被修改,即具有可见性");
System.out.println("此时number的值为:"+mydata.number);
}
}
import java.util.concurrent.TimeUnit;
/**
* 验证volatile的可见性
*
* 为number添加volatile关键字后
*
* volatile具有可见性
*/
class MyData{
volatile int number = 0;
public void addTo60(){
this.number = 60;
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData mydata =new MyData();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
//暂停一会线程,保证其他线程也已经读取了共享变量number的值
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//AAA线程修改共享变量number的值
mydata.addTo60();
System.out.println(Thread.currentThread().getName()+"更新后共享变量number的值为:"+mydata.number);
},"AAA").start();
//第二个线程就是我们的main线程
while(mydata.number == 0){
}
System.out.println("main线程被通知了共享变量number已经被修改,即具有可见性");
System.out.println("此时number的值为:"+mydata.number);
}
}
(2)不保证原子性
示列代码:
class MyData{
volatile int number = 0;
public void addPlusPlus(){
this.number++;
}
}
/**
* 验证volatile的原子性
*
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() >2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t 最终共享变量number的值:"+myData.number);
}
}
如果各个线程能正常执行的话,结果应该每次都是20000,出现上述结果的原因就是volatile不保证原子性,出现了丢失写值的情况
那么如何解决上述原子性问题?
- 1.加synchronize,这种做法有点杀鸡用牛刀了
- 2.使用原子包装类AtomicInteger
解决代码:
import java.util.concurrent.atomic.AtomicInteger;
/**
* 解决原子性问题
*
*/
class MyData{
volatile int number = 0;
public void addPlusPlus(){
number++;
}
AtomicInteger atomicInteger = new AtomicInteger(); //默认构造为0
public void addAtomic(){
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
myData.addPlusPlus();
myData.addAtomic();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() >2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t 最终int类型共享变量number的值:"+myData.number);
System.out.println(Thread.currentThread().getName()+"\t 最终AtomicInteger共享变量atomicInteger的值:"
+myData.atomicInteger);
}
}
那么为什么使用AtomicInteger就能解决呢?底层原理?
因为底层使用了CAS,关于CAS的原理见下一篇博客:待补充链接
(3)禁止指令重排序
volatile底层通过内存屏障实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一下内存屏障
内存屏障(Memory Barrier),又称为内存栅栏,是一个CPU指令
作用:
- 1.保证特定操作的顺序
- 2.保证某些变量的内存可见性(利用此特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
2.3 总结
对于工作内存和主内存同步延迟现象导致的可见性问题:
- 可以使用synchronized或者volatile关键字来解决,它们都可以使一个线程修改后的变量立即对其他线程可见
对于指令重排导致的可见性问题和有序性问题:
- 可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化
JMM规范要求三个特性,而volatile满足其中两个,所以它是轻量级的同步机制