单例模式:
1,静态的变量和静态的方法
2,私有化构造方法
volatile 关键字用于保持线程之间的可见性和防止指令重排序问题,主内存和工作内存交换时,保持原子操作的连续性,从而让变量始终看到的是自己的最新值。即线程操作的相互可见性,理解volatile作用之前我们先了解一下修改的过程
线程在jvm运行时内存分配过程:

线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存
变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,
在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。
竞态条件是指的是程序需要判断状态来做出操作,然后多线程中可能在完成操作之前就对状态作出了修改,这时候结果不是我们想要的,需要通过synchronized来同步。
比如单例模式中第一次新建时判断完状态,还未完成,另一个线程来按照空进行判断,产生两次创建。
但又出现串行化的单例,同步有比较耗时,所以采用双重检查锁,即在同步之前作一个非空判断
class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if ( instance == null ) { //当instance不为null时,仍可能指向一个“被部分初始化的对象”
synchronized (Singleton.class) {
if ( instance == null ) {
instance = new Singleton();
}
}
}
return instance;
}
}
“看起来”非常完美:既减少了阻塞,又避免了竞态条件。不错,但实际上仍然存在一个问题——当instance不为null时,仍可能指向一个被部分初始化的对象。
问题出在这行简单的赋值语句:
instance = new Singleton();
它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令:
memory = allocate(); //1:分配对象的内存空间
initInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行重排序,经过重排序后如下:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址(此时对象还未初始化)
ctorInstance(memory); //2:初始化对象
可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化——即,引用instance指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
解决这个该问题,只需要将instance声明为volatile变量:
private static volatile Singleton instance;
对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,Java内存模型采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
volatile和synchronized区别
1、volatile不会进行加锁操作:
volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制。
2、volatile变量作用类似于同步变量读写操作:
从内存可见性的角度看,写入volatile变量相当于退出同步代码块,而读取volatile变量相当于进入同步代码块。
3、volatile不如synchronized安全:
在代码中如果过度依赖volatile变量来控制状态的可见性,通常会比使用锁的代码更脆弱,也更难以理解。仅当volatile变量能简化代码的实现以及对同步策略的验证时,才应该使用它。一般来说,用同步机制会更安全些。
4、volatile无法同时保证内存可见性和原则性:
加锁机制(即同步机制)既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性,原因是声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:“count++”、“count = count+1”。
最后回归正题:
三种安全的单例模式:
恶汉模式
/**
* 单例模式,饿汉模式,不管为不为空,先直接new一个出来
*/
public class SingletonImp {
// 饿汉模式
private static SingletonImp singletonImp = new SingletonImp();
// 私有化(private)该类的构造函数
private SingletonImp() {
}
public static SingletonImp getInstance() {
return singletonImp;
}
public static void main(String[] args) {
System.out.println(SingletonImp.getInstance());
}
}
双重检查锁:
/**
* SingletonImp3中每次调用getInstance都会加同步锁,而加锁是一个很耗时的过程
* 实际上加锁只需要在第一次创建对象时
*/
public class SingletonImp4 {
private static SingletonImp4 singletonImp4;
private SingletonImp4() {}
public static SingletonImp4 getInstance() {
// 第一次创建时才加锁
if (singletonImp4 == null) {
synchronized (SingletonImp4.class) {
if (singletonImp4 == null) {
singletonImp4 = new SingletonImp4();
}
}
}
return singletonImp4;
}
}
public class SingletonImpl7 {
//用volatile保证线程可见性和指令重排序
private static volatile SingletonImpl7 singletonImp;
private SingletonImpl7(){
}
public static SingletonImpl7 getInstance(){
if (null == singletonImp){
synchronized(SingletonImpl7.class){
if (null == singletonImp){
singletonImp = new SingletonImpl7();
}
}
}
return singletonImp;
}
}
内部类模式:
/**
* SingletonImp5使用静态代码块,如果该类中还有其他静态方法,调用后就会执行静态代码块使得对象过早创建
* 使用一个静态类来创建Singleton,其他静态方法只要没有调用Nested.singletonImp6就不会创建Singleton
* 实现了需要时才创建实例对象,避免过早创建
*/
public class SingletonImp6 {
private SingletonImp6() {}
// 专门用于创建Singleton的静态类,加载类时,同时执行静态块,给静态变量赋值
private static class Nested{
private static SingletonImp6 singletonImp6;
static {
singletonImp6 = new SingletonImp6();
}
}
//直接返回静态内部类的静态变量,实现需要时在加载
public static SingletonImp6 getInstance() {
return Nested.singletonImp6;
}
网友评论