多线程十一 单例模式


本篇博文,将整理关于单例模式(就是让一个类从始至终,只能产生一个对象,而且spring管理的类也全部是单例模式的)与多线程摩擦出的火花

1 . 懒汉模式(存在线程安全性问题)


public class demo01 {

//要实现单例,肯定不能new对象,因此我们私有化构造函数
private demo01(){}

//定义一个属于本类的单例对象,每次返回的都是这个对象
public static demo01  instance = null;

//因为我们没有自己创造出来的对象了,故提供一个静态工厂方法,返回对象的实例
public static demo01 getInstance(){
    if (instance==null){  //在多线程并发访问的情况下,是存在线程安全问题的
        instance = new demo01();
        return instance;
    }
    return instance;
}
}
  • 懒汉模式---在使用的时候初始化对象

2 . 饿汉模式(简单粗暴,实现线程安全)----静态域

public class demo02 {

//要实现单例,肯定不能new对象,因此我们私有化构造函数
private demo02(){}

//定义一个属于本类的单例对象,每次返回的都是这个对象
private static demo02  instance = new  demo02();  // 静态域

//因为我们没有自己创造出来的对象了,故提供一个静态工厂方法,返回对象的实例
public static demo02 getInstance(){
    return instance;
}
}
  • 饿汉模式---在类加载的时候初始化对象,

缺点:

1 . 如果在构造函数中有过多的其他耗时操作的话,对象的创建会很慢
2 . 而且对象创建出来了还不一定会马上使用,造成资源的浪费

使用饿汉模式相应的注意点 :

  1. 对象创建出来以后肯定会被使用
  2. 构造函数没有太多其他处理

3 . 饿汉模式(简单粗暴,实现线程安全)----静态块

public class demo05 {
//要实现单例,肯定不能new对象,因此我们私有化构造函数
private demo05(){}

//注意点, 下面两段代码是有先后顺序的,假如说颠倒顺序,那么已经初始化的实例也会被重置为null
public static demo05  instance = null;

static {
    instance = new demo05();
}


//因为我们没有自己创造出来的对象了,故提供一个静态工厂方法,返回对象的实例
public static demo05 getInstance(){
    return instance;
}

}

  • 饿汉模式---在类加载的时候初始化对象,

缺点:

1 . 如果在构造函数中有过多的其他耗时操作的话,对象的创建会很慢
2 . 而且对象创建出来了还不一定会马上使用,造成资源的浪费

使用饿汉模式相应的注意点 :

  1. 对象创建出来以后肯定会被使用
  2. 构造函数没有太多其他处理

4. 懒汉模式----使用synchronized实现线程安全


public class demo03 {
//要实现单例,肯定不能new对象,因此我们私有化构造函数
private demo03(){}

//定义一个属于本类的单例对象,每次返回的都是这个对象
public static demo03  instance = null;


public static synchronized demo03 getInstance(){
    if (instance==null){
        instance = new demo03();
        return instance;
    }
    return instance;
}

}
  • 加上synchronized 在多线程并发访问的情况下,不再有线程安全问题,但是并不推荐,因为同一时间只有有一个线程进入此静态方法,因此效率低

5. 懒汉模式---- 双重同步锁单例模式+volatile 实现线程安全

public class demo04 {

//要实现单例,肯定不能new对象,因此我们私有化构造函数
private demo04(){}

//定义一个属于本类的单例对象,每次返回的都是这个对象
public static volatile demo04 instance = null;


// 双重同步锁单例模式
public static  demo04 getInstance(){
    if (instance==null){  // 检测 1
        synchronized (demo04.class){ //锁
            if (instance==null){  //检查 2
                instance = new demo04();
            }
        }
        return instance;
    }
    return instance;
}

}

为什么要加上volatile关键字?

这就要从CPU的指令说起, 当我们执行 

new demo04();

分下面三步走

  1. memory = allocate(); //分配内存空间
  2. ctorInstance() //初始化对象
  3. instance = memory // 将对象的引用指向刚分配的内存空间

在单线程的情况下是不会发生任何线程安全问题的,但是! 多线程就会受到 指令重排序的影响, JVM和CPU优化--指令重排序可能出现下面的顺序

  1. memory = allocate(); //分配内存空间
  2. instance = memory // 将对象的引用指向刚分配的内存空间,
  3. ctorInstance() //初始化对象

这时候双重同步锁单例模式就会出现问题,比如AB两条线程 A运行到指令3却没有真正创建对象 , 然后B去判断instance此时不为空,拿到了instance,一旦调用就会出现问题


6. 使用枚举实现单例模式 --线程安全


public class demo06 {

private demo06(){}

public static demo06 getInstance(){
        return Singleton.INSTANCE.getDemo06Instance();
}

//私有的枚举类
private enum Singleton{
    INSTANCE;

    private demo06 demo06Instance;

    //JVM保证此构造方法绝对只会调用一次
    Singleton(){
        demo06Instance= new demo06();  //调用外部类私有的构造方法
    }

    public demo06 getDemo06Instance(){
        return demo06Instance;
    }
}
}

推荐使用这种方法

  • 相对懒汉模式,绝对性的保证的安全问题
  • 相对饿汉模式,当实例在使用的时候才开始初始化