一. Java内存模型
1. 内存模型
如果线程A与线程B之间要通信的话
- 线程A把本地内存A中更新过的共享变量刷新到主内存中去。
- 线程B到主内存中去读取线程A之前已更新过的共享变量。
2. happens-before
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。如程序存在数据依赖关系,编译器与处理器不会对串行程序进行指令重排序。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
-
start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的
ThreadB.start()操作happens-before于线程B中的任意操作。 -
join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作
happens-before于线程A从ThreadB.join()操作成功返回。
二. Volatile关键字
特性:
-
可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。 - 原子性:对任意单个volatile变量的读/写具有原子性,无论32位还是64位,但类似于volatile++这种复合操作不具有原子性。
-
禁止重排序:会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
三. Synchronized关键字
1. 锁对象:
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步方法块,锁是Synchonized括号里配置的对象。
2. 锁原理:
- monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处
- 线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。
3. 锁种类:
无锁:修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如CAS原理就是无锁的实现。
偏向锁:
定义:指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
过程:当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。
升级条件:偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。轻量级锁
定义:指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
过程:
1.代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。
2. 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。
3. 这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。
4. 轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。
5. 若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
升级条件:当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。重量级锁: 等待锁的线程都会进入阻塞状态。
四. 双重检查锁定与延迟初始化
1.假设A线程执行代码1的同时,B线程执行代码2。此时,线程A可能会看到instance引用的对象还没有完成初始化,导致同时创建对象。
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) // 1:A线程执行
instance = new Instance(); // 2:B线程执行
return instance;
}
}
2.对getInstance()方法做了同步处理,synchronized将导致性能开销。
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (instance == null)
instance = new Instance();
return instance;
}
}
3.双重检查锁定:多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。
public class DoubleCheckedLocking { // 1
private static Instance instance; // 2
public static Instance getInstance() { // 3
if (instance == null) { // 4:第一次检查
synchronized (DoubleCheckedLocking.class) { // 5:加锁
if (instance == null) // 6:第二次检查
instance = new Instance(); // 7:问题的根源出在这里
} // 8
} // 9
return instance; // 10
} // 11
}
问题:
第7行instance=new Singleton()可以分解为如下3行代码:
memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址
A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象。
优化思路:
不允许2和3重排序。
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance(); // instance为volatile,禁止如上代码2和3之间的重排序
}
}
return instance;
}
}
允许2和3重排序,但不允许其他线程“看到”这个重排序。
//允许3.8.2节中的3行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance ; // 这里将导致InstanceHolder类被初始化
}
}