类初始化锁
怎么理解?
为什么需要了解?
常见的单例模式分析
懒汉式
为什么线程不安全
验证
饿汉式
为什么线程安全
双重检查锁定方式
演变由来
为什么线程不安全
如何解决线程不安全
静态类方式
为什么线程安全
结论
Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应,从C到LC的映射,由JVM的具体实现去自由实现。JVM在初始化期间会获取这个初始化锁,并且每个线程至少获取一次锁来确保这个类被初始化了。
这个过程比较冗长,这里不做过多描述,总之就是JVM通过初始化锁同步了多个线程同时初始化一个对象的操作,保证类不会被多次初始化。
线程A 、线程B 同时去访问类的属性或者方法,两个线程都会去获取类初始化锁
1. 假设线程A先获取到锁,此时线程B阻塞等待。
2. 线程A获取到锁 -----> 对类初始化(初始化静态属性),设置state = initialized -----> 释放锁
3. 线程B获取到锁,读取到state = initialized,得知类已经初始化了,释放锁
根据happen-before原则,线程A的释放锁happen-before线程B获取锁,这样线程A对类的初始化,线程B是可见的
结论就是,当类已经被初始化了,其他线程能够可见类的静态属性的值,但是如果一个线程在初始化之后,比如调用类的静态方法(静态方法没有做同步控制)改变类的属性的值,对其他线程不一定可见。
单例模式,实际上都是多个线程通过静态方法访问一个类的静态变量
常见的场景是:
首次调用一个类的静态方法的过程,首先进行的是获取类锁、初始化、释放类锁,在调用类的静态方法。
如果一个类不是被首次访问,当前线程也会去获取类锁,读取到state = initialized 、释放锁,在调用类的静态方法。
静态方法对静态变量的修改线程之间不具有可见性,不是立即可见的。
public class SingleTon {
private SingleTon() {}
private static SingleTon instance;
public static SingleTon getInstance() {
if(instance==null) {
instance = new SingleTon();
}
return instance;
}
}
多个线程访问,类只会被初始化一次,假设存在线程A和线程B调用getInstance方法,调用方法前类已经初始化,此时instance为null,对于两个线程而言,instance都是null。
线程A 执行到 instance ==null时候,向下执行,创建对象
对象的创建分为3步骤
- 给对象分配内存空间
- 对象初始化
- 将引用指向对象的内存空间
对于线程B而言,不会等待线程A对象创建完成,也会创建对象,这样就可能存在创建多个对象的可能性。
为了模拟对象创建耗时的过程,在构造函数里面sleep 一段时间。
package cn.bing.singleton;
import java.util.Date;
/**
* 懒汉式
* @author Administrator
*
*/
public class SingleTon {
private SingleTon() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static SingleTon instance;
public static SingleTon getInstance() {
System.out.println(Thread.currentThread().getName()+" enter : "+System.currentTimeMillis());
if(instance==null) {
System.out.println(Thread.currentThread().getName()+" contruct : "+System.currentTimeMillis());
instance = new SingleTon();
}
return instance;
}
public static void main(String[] args) {
Runnable run = new Runnable() {
@Override
public void run() {
System.out.println(SingleTon.getInstance());
}
};
System.out.println("current time: "+System.currentTimeMillis());
Thread t1 = new Thread(run, "线程A");
t1.start();
Thread t2 = new Thread(run,"线程B");
t2.start();
}
}
运行结果:
current time: 1541646424662
线程B enter : 1541646424663
线程B contruct : 1541646424663
线程A enter : 1541646424663
线程A contruct : 1541646424663
cn.bing.singleton.SingleTon@24e59eb1
cn.bing.singleton.SingleTon@52826699
懒汉式是因为类初始化的时候,没有对实例初始化,出现线程安全问题,那么类初始化的时候就创建对象(上面说过初始化静态属性的过程对于其他线程而言是可见的),就可以保证对象的一致性了,这就是饿汉式.
/**
* 饿汉式
* @author Administrator
*
*/
public class SingleTonHungry {
private static SingleTonHungry instance = new SingleTonHungry();
private SingleTonHungry() {
}
public static SingleTonHungry getInstance() {
return SingleTonHungry.instance;
}
}
懒汉式不安全的原因,静态方法被多个线程同时访问,只要只能一个线程去构建对象,其他线程只能阻塞,等到另一个线程释放锁了,也就是这个对象创建好了,再获取这个对象,便线程安全 了,于是在静态方法上加上类锁synchronize
public class SingleTonLazySynchronize {
private static SingleTonLazySynchronize instance;
private SingleTonLazySynchronize() {}
public static synchronized SingleTonLazySynchronize getInstance() {
if(instance == null) {
instance = new SingleTonLazySynchronize();
}
return instance;
}
}
优点:线程安全
缺点: 一个线程等待另一个线程执行完毕,在多线程环境下,效率很低
那么,是否只要控制对象的创建在同步代码块中的话,是不是就行了呢?
package cn.bing.singleton;
/**
* 双重检查锁定延迟加载
* @author Administrator
*
*/
public class SingleTonDoubleLock {
private static volatile SingleTonDoubleLock instance;
private SingleTonDoubleLock() {}
public static SingleTonDoubleLock getInstance() {
if(instance==null) {//1
synchronized (SingleTonDoubleLock.class) {//2
if(instance==null)
instance = new SingleTonDoubleLock();
}
}
return instance;
}
}
假设线程A和线程B调用getInstance方法,线程A获取到类锁,进入2,创建对象
对象的创建分为3步骤
- 给对象分配内存空间
- 对象初始化
- 将引用指向对象的内存空间
jvm可能对上面的指定进行重排序,可能是1,3,2的顺序
此时,线程B执行到1,看到instance的地址不为空(由于重排序,可能对象还没有初始化),直接就返回了地址,但是此时对象还没有被初始化。
第一种方式,jvm禁止重排序
禁止对象创建过程中2,3的重排序,只要将instance申明为volatile类型.
package cn.bing.singleton;
/**
* 双重检查锁定延迟加载
* @author Administrator
*
*/
public class SingleTonDoubleLock {
private static volatile SingleTonDoubleLock instance;
private SingleTonDoubleLock() {}
public static SingleTonDoubleLock getInstance() {
if(instance==null) {//1
synchronized (SingleTonDoubleLock.class) {//2
instance = new SingleTonDoubleLock();
}
}
return instance;
}
}
第二种方式,只要另一个线程看不到重排序(静态类解决方案)
package cn.bing.singleton;
public class SingleTonStaticClass {
private SingleTonStaticClass() {}
static class InstanceHolder{
private static SingleTonStaticClass instance = new SingleTonStaticClass();
}
public static SingleTonStaticClass getInstance() {
return InstanceHolder.instance;
}
}
假设存在两个线程A,B ,此时SingleTonStaticClass没有初始化
1. 线程A先获取到外部类的初始化锁,线程B只能等待。
2. 线程A执行类的初始化完毕,将state设置为initialized,释放外部类的锁,调用getInstance方法,获取内部类的锁
3. 线程B获取到外部类的锁,尝试调用getInstance方法,因为没有获取到内部类的锁,只能等待
4. 线程A完成内部类的初始化,释放内部类的锁,线程B拿到内部类的锁,因为内部类已经初始化了,不会继续初始化,直接释放锁。
这个过程中,线程B是看不到线程A对内部类的对象的重排序的。
根据happen-before原则, 线程A对内部类的静态属性初始化后的的值对线程B是可见的。
线程A,B在这个过程中是获取两次初始化锁,后续线程C调用getInstance只会获取一次外部类锁,读取到外部类state=initialized,释放锁。
-- 来自方腾飞《JAVA并发编程的艺术》
个人感觉和线程A、线程B一样还是要获取两次类锁
考虑到延迟加载,线程安全的单例模式,选择基于volatile的双重检查方式或者基于静态类的方式创建。
参考:方腾飞《JAVA并发编程的艺术》