并发
三大特性
- 原子性
- 可见性
- 有序性
原子性
对外部来说原子性内的操作要么全部成功,要么全部失败
在多线程场景下,如果共享变量为i
i++操作是不能保障原子性的
原因就是翻译成汇编语言的时候i++有4步
- 读取
- 入栈
- +1
- 设置
一个线程A执行到“步骤3”后另一个线程B有可能也在执行3,4,此时在原来的线程A在步骤4设置其实就设置的不对
怎么保证
- synchronized
- Lock
- CAS
- 基础变量单个赋值操作使用volatile只能保证单个基本数据类型字段的赋值操作的原子性
在32位机器上对64位长度的数据进行操作是不能保证原子性的
所以需要加上volatile关键字
可见性
每个线程操作的都是线程的本地内存
如果本地没有读取到共享内存的数据就会出现可见性问题
比如:
存在一个共享变量state
一个线程A while(state){} 空转
另一个线程B 1s后修改state为false
线程A会一直空转下去
下边JMM模型详细分析
怎么保证
- synchronized
- Lock
- volatile关键字
- 内存屏障:java unsafe类提供的有
有序性
在不影响结果的情况下,会对代码指令进行重排
比如
int a = 3;
int b = 5;
int c = 7;
int d = a + b;
此时JVM就可能把int c = 7; 挪到最后执行
单线程下可能没有问题
但是多线程下可能存在问题
例如下边经典的例子:
例子中可能出现的结果如下三种
但是实际出现
这种结果就是发生了指令重排的结果
JVM无法保证多线程下的重排结果一定正确
怎么保证
- synchronized
- Lock
- volatile关键字
- 内存屏障
JMM内存模型
Java线程间的通讯由JMM(Java Memory Model)Java线程模型控制
JMM决定线程对共享变量的写入是否对另一个线程可见
JMM定义了线程、变量和内存之间的抽象关系
共享变量储存在主内存中。每个线程操作的都是从主内存读取的共享变量的副本,不能直接操作主线程
- 变量a read 到本地内存 load 作为本地内存中变量a副本
- use 读取工作内存的变量a的副本到线程中
- assign 赋值给本地内存中变量a的副本
- store 存储变量a的副本的值到主内存中 write 值写入变量a中
如果一个线程值进行1,2操作
那么就无法看到其他线程执行的3,4操作
为什么要有JMM
屏蔽不同操作系统内存使用的差异
主内存一般存储在内存中
本地内存一般是存储在CPU寄存器内高速缓存
但是这也是导致多线程环境下可见性和有序性问题的原因
解决方案
加锁
线程获取锁:本地内存失效
线程释放锁:所有本地内存共享变量直接刷新到主内存中
注意:加锁只能保证加锁线程的可见性和有序性
如果一个线程没有加锁,对它来说共享变量就算写回主内存还是不可见的
volatile
写一个volatile变量的时候,JMM会把变量直接刷新到主内存中
读一个volatile变量的时候,JMM直接从主内存读取
并且volatile禁止了某些场景的指令重排
假设有2个操作:
如果第二个是volatile写,不能重排
如果第一个是volatile读,不能重排
如果第一个是volatile写,第二个是volatile读,不能重排
volatile就是通过内存屏障来实现的