JAVA内存模型
这里做的笔记是结合JVM中的java内存模型
和java并发编程艺术中讲的java内存模型
再结合一些面试题
JVM内存区域和JAVA内存模型有明显的区别
要分清他们之间的关系
JMM是一种规则,主要是研究并发多线程内存的可见性
是一种高速缓存进行读写访问的过程抽象
目的就是保证程序运行时内存应该是==内存一致==的
主内存和工作内存
Java内存模型为每个线程都创建了一个工作内存
但是所有的变量都应该存储在主内存中,这里的主内存不是硬件上的内存。
而是JVM中的一部分
Volatile 的内存语义
语义1 对Volatile变量修改对其他线程是立即可见的
但使用Volatile变量的计算==并不是==线程安全的
例如如果多线程对volatile变量自增而不加锁,就会同步出错
因为
num++其实是由4个字节码操作组成的。位于栈顶的num很可能被其他线程加大。
问题解决:
1.可以使用AtomicInteger 自增操作则是 incrementAndGet()方法
2.方法synchronized
使用场景:
1.运算结果不依赖当前的值,或者单线程改变 比如set get 方法 不依赖原值
2.变量不需要其他线程参与变量的约束
语义2 Volatile 禁止指令重排序优化
volatitle boolean config = false ;
finishconfig(config);
config = true;
while(!config){
sleep();
}
dosomethings();
如果 config 不是Volatile变量
config可能比 fhinishconfig还快
这就可能导致还没配置就执行其他的了。
总结一下volatile 变量就三个特性
本身的读和写是原子的,其他的不是比如自增==原子性==
对Volatile变量操作是所有线程可见的,强制刷新缓存 ==可见性==
Volatile 变量不可以被重排序添加内存屏障 ==有序性==
锁的内存语义
锁的释放和获取本质上就是消息通知
一个线程释放锁,是告诉下一个获取线程的锁消息。
一个线程获取锁,其实是接受了某个线程发出的锁消息。
sychronized 本质上就是加锁 和 lock 方法类似
sychronized 里的操作是原子的,因为只有一个线程执行==原子性==
sychronized 操作完是对所有线程可见的 对一个变量unlock会同步变量到主内存==可见性==
sychronized 里的代码不能被重排序因为只有一个线程执行 ==有序性==
一些锁的实现本质上就是 修改Volatile变量 进行CAS操作来达到内存一致性的
final域的内存语义
final不同于锁和Volatile
final域访问类似普通变量
但 final 域必须遵守两个重排序规则
- 构造函数里对final域的写入,与随后这个对象的引用赋值给一个引用变量,这个操作不能重排序
- 第一次读一个final 域 的引用,和随后读这个final域,这个操作不能重排序
// 一个对象
static Ex object;
// 构造函数
public Ex(){
// i 是 final域
i=0;
// j 是普通域
j=2;
}
// A线程写
public void write(){
object = new Ex();
}
// B 线程读
public void read(){
Ex obj = object ;
int a = obj.i;
int b = obj.j;
}
看上面这个例子
B 线程对 obj.i 即final 域 一定读出来是 构造函数初始化过后的值。
而对 obj.j 即普通域不一定读出来初始化的值,而可能是默认初值。
==final 域保证对象的值或者引用一定是正确初始化过后的值==
==final域保证读对象的final域之前一定先读对象的引用==
happens-before
先行规则是判断数据是否存在竞争,线程是否安全的重要原则。
- 程序顺序 在单线程内按顺序流执行
- 管程锁定(监视器锁) 对一个锁的解锁在这个锁加锁之前
- volatile原则 对Volatile变量的写在这个变量的读之前
- 线程开始原则 线程的启动方法start()先于所有线程内的操作之前执行
- 线程终止原则 线程所有的操作比 线程终止先执行 如Thread.join结束方法
- 线程中断原则 对线程interrupted()操作先于 中断线程中的检测中断事件
- 对象终结原则 一个对象的构造方法先于他的finalize方法
- 传递性 , A 比 B 先,B比C 先 ,那么A比C 先
双重检查锁定 DCL
double check lock简称 DCL是非常常见的延迟加载技术
即 对于某些开销大的对象,在使用的时候才加载。按需加载。也叫懒加载
下面是线程不安全的初始化对象
public class LazyInital {
class Instatnce{
}
private static Instatnce instatnce;
public Instatnce getInstatnce(){
if(instatnce==null) // Thread A
instatnce = new Instatnce(); // Thread B
return instatnce;
}
}
简单的改就在getInstance 方法上加sychronized标识即可。
但这样性能不好
下面是DCL的错误版本
public Instatnce getInstatnce(){
if(instatnce==null){
synchronized (LazyInital.class){
if(instatnce==null)
instatnce = new Instatnce();
}
}
return instatnce;
}
这样就叫双重检查锁定 看上去很舒服但是有大问题
java对象的初始化 分为3个部分
- memory = allocate() // 分配内存
- initclass(memory) // 初始化对象
- instance = memory // 指向对象
2 和3 是可能被重排序的。
最后的一种情况可能是
线程A 还没有初始化instance 线程B就已经拿到对象的引用了,此时返回的是一个没有初始化的对象
解决方法
- 禁止 初始化对象 和指向对象 重排序
使用 Volatile的语义即可
加上Volatile 描述
- A线程对象的初始化可以重排序,但对线程B应该是不可见的。
使用类初始化的解决方法,依靠JVM初始化类的对象加锁的特性
public class InstanceFactory {
static class Instance{
}
// instanceHolder类的初始化加锁
private static class InstanceHolder{
private static Instance instance = new Instance();
}
public static Instance getInstance(){
return InstanceHolder.instance;
}
}
volatile和synchronized的区别
-
volatile的本质是告诉jvm当前变量在工作内存中的值不确定,要从主内存中读取
而synchronized是锁定当前变量,只能由一个线程读取,其他线程阻塞,直到锁释放。
Volatile 变量级别,synchronized 方法变量
Volatile只能实现变量修改的可见性,不能保证操作的原子性。比如Volatile的读和写是原子的,但Volatile操作不一定是原子的,自增。
Volatile不会阻塞,synchronized会阻塞
volatitle变量不会优化,synchronized会优化