第二期里从线程安全的角度聊了聊系统设计
本期继续结合具体的技术点来聊聊线程安全
惯例,先看栗子
++i 或者 i++ 是否原子操作?
这里先不进行分析,大家可以先想想
我们先来看看Java的内存模型
Java能够实现跨平台,得益于Java虚拟机规范所定义的Java内存模型
这个模型屏蔽对硬件和操作系统的内存访问差异,使得Java程序在各个平台里运行都能够达到一致的内存访问效果
《深入Java虚拟机》
我们先具体看一下Java内存模型
通过上图可以很好的理解Java内存模型是怎样的
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的细节
《深入Java虚拟机》
共享变量存储在主内存里,如何从主内存读取变量,从本地内存写回到主内存
就是JMM所负责的事情了,其定义好这个变量的访问规则
本地内存也叫作工作内存
每个线程都有自己的工作内存,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存的变量
JMM定义了8种操作来完成主内存与工作内存之间的交互
这些操作在虚拟机实现时必须保证是原子的、不可再分的
lock、unlock、read、load、use、assign、store、write
比如:从主内存读取变量:
1.要先read,从主内存传输变量值到工作内存
2.执行load,把read操作的变量值放入工作内存变量副本中
从工作内存写入主内存:
1.要先store,把工作内存的变量值传输到主内存
2.执行write,把store操作的变量值放入主内存的变量中
read、load必须同时出现,同样,store、write必须同时出现
看完Java内存模型,再来了解下Java虚拟机字节码执行引擎
Java执行引擎的实现需要保证JMM及其所指定的规则
我们的JVM是基于栈的执行引擎,基于栈的指令集解释器
运行过程中,会保存方法调用执行的数据,这个存储的数据结构叫栈帧
栈帧保存了方法的局部变量表、操作数栈、动态链接和方法返回地址
方法执行与结束也就会对应着栈帧在虚拟机栈里的入栈和出栈
每个线程会有自己的线程栈,也可以理解为线程自己的工作空间
执行引擎运行过程,对于当前线程,只有位于栈顶的栈帧才是有效的,称为当前栈帧,相关联的方法是当前方法
执行过程,通过局部变量表来存储方法入参和方法内部局部变量的数据
一些计算和参数传递则通过操作数栈来进行
下面我们回到开头提到的例子
简单地写段代码,然后从JMM和字节码解释执行的角度来具体分析下
代码里定义了一个全局共享变量和一个局部变量
JDK1.8下编译得到的字节码
使用命令 javap -c class文件 得到如下的字节码
我们来具体看看main方法里的逻辑
对于全局变量a,我们可以看到首先是先获取变量的值,执行字节码 getstatic
然后放入栈中,并跟栈中的数值1 进行加操作然后把结果再入栈
最后把结果写回变量里
回到我们的内存模型,分为主内存和工作内存
再这里可以看到,要对变量a进行自增操作需要先获取a的值
放到线程栈里,然后再把执行自增操作的结果写回到变量a中
Java代码里对应的是 a++ 操作
后面的++a操作也是一样的字节码逻辑
而后面对变量b 的自增操作,我们可以看到有明显的不同
把数值0入栈,并放到局部变量表里的变量1中
然后直接进行自增操作
并没有像变量a 那样有变量的读写操作
回到变量a的自增操作字节码逻辑里
比如线程A 执行字节码 getstatic ,获取到变量a的值后,进行处理
此时,可能线程B 执行 putstatic ,对变量a进行写操作
则线程A 里的变量a 的值是脏的了
而变量b的自增操作则不会受到这样的影响,因为自始至终都在线程栈中执行的
PS:更加严谨的方法其实应该是对汇编代码进行分析,这里字节码已经足够说明问题了
如果我们对变量a 添加volatile属性,能不能保证线程安全呢?
我们看下字节码:
从字节码角度还是一样的,并没有变化
那么用volatile 给我们带来了什么?
有时间可以再好好学习研究下