单例模式属于创建型模式的一种,应用于保证一个类仅有一个实例的场景下,并且提供了一个访问它的全局访问点,如spring中的全局访问点BeanFactory,spring下所有的bean都是单例。
单例模式的特点:从系统启动到终止,整个过程只会产生一个实例。
饿汉式:类加载的时候就实例化,并且创建单例对象。
类加载的方式是按需加载,且只加载一次。
因此,在单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。也就是说,在线程访问单例对象之前就已经创建好了。由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。
public class Hungry {
//饿汉式,一上来就把对象加载了,有问题,比如耗费内存
private String[] data = new String[2048];
private String[] data2 = new String[2048];
private String[] data3 = new String[2048];
//构造器私有,一旦构造器私有了,别人就无法new这个对象
//保证内存中只有一个对象
private Hungry() {
}
//一开始就new一个对象,保证它是唯一的了
private final static Hungry hungry = new Hungry();
//静态保证可见性
public static Hungry getInstance(){
return hungry;
}
}
懒汉式:默认不会实例化,什么时候用什么时候new。
public class LazyMan {
//构造器私有,一旦构造器私有了,别人就无法new这个对象
//保证内存中只有一个对象
private LazyMan(){
}
//定义一个对象,并没有直接拿来使用
private static LazyMan lazyMan;
public static LazyMan getLazyMan(){
//如果为空,则创建
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
}
问:上面这段代码有什么问题呢?
答:单线程下是ok的,如果在并发多线程上,就有问题了。
public class LazyMan {
//构造器私有,一旦构造器私有了,别人就无法new这个对象
//保证内存中只有一个对象
private LazyMan(){
//测试线程创建情况
System.out.println(Thread.currentThread().getName()+":启动成功");
}
//定义一个对象,并没有直接拿来使用
private static LazyMan lazyMan;
public static LazyMan getInstance(){
//如果为空,则创建
if(lazyMan == null){
lazyMan = new LazyMan();
}
return lazyMan;
}
//在多线程并发下,例如在8个线程下
public static void main(String[] args){
for (int i = 0; i < 8;i++){
//启动线程
new Thread(() -> {
//启动之后,调用创建对象的方法
LazyMan.getInstance();
}).start();
}
}
}
测试结果:
从上面的结果来看的话,这个饿汉式单例模式,在多线程是有问题的,它不能保证只创建一次对象
创建对象的时候,进行双重检测,并给对象加锁
public class LazyMan {
//构造器私有,一旦构造器私有了,别人就无法new这个对象
//保证内存中只有一个对象
private LazyMan(){
//测试线程创建情况
System.out.println(Thread.currentThread().getName()+":启动成功");
}
//定义一个对象,并没有直接拿来使用
private static LazyMan lazyMan;
//懒汉式单例 DCL(双重检测锁模式)
public static LazyMan getInstance(){
//如果为空,则给对象加锁
if(lazyMan == null) {
//锁住当前对象
synchronized (LazyMan.class) {
//加了锁之后,再次判断,如果为空,则创建对象
if(lazyMan == null){
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
//在多线程并发下,例如在8个线程下
public static void main(String[] args){
for (int i = 0; i < 8;i++){
//启动线程
new Thread(() -> {
//启动之后,调用创建对象的方法
LazyMan.getInstance();
}).start();
}
}
}
测试结果:
问:上面这个双重检测操作有什么问题?
答:双重检测锁是原子性操作,每次创建对象都会经历分配内存、执行构造方法(也就是初始化对象)、把对象指向内存空间
问:应该怎么解决指令重排?
public class LazyMan {
//构造器私有,一旦构造器私有了,别人就无法new这个对象
//保证内存中只有一个对象
private LazyMan(){
//测试线程创建情况
System.out.println(Thread.currentThread().getName()+":启动成功");
}
//定义一个对象,并没有直接拿来使用
//volatile,多线程并发工作区变量的可见性,避免指令重排
private volatile static LazyMan lazyMan;
//懒汉式单例 DCL(双重检测锁模式)
public static LazyMan getInstance(){
//如果为空,则给对象加锁
if(lazyMan == null) {
//锁住当前对象
synchronized (LazyMan.class) {
//加了锁之后,再次判断,如果为空,则创建对象
if(lazyMan == null){
lazyMan = new LazyMan();//不是一个原子性操作
/**
* 不是原子性操作,每次创建对象,就会有下面三个步骤
* 这三个步骤,与可能会发生指令重排过程,比如希望执行123,但是可能会出现132
* 1.分配内存空间
* 2.执行构造方法,也就是初始化对象
* 3.把这个对象指向这个内存空间
* 避免指令重排,在定义对象的时候加上volatile
*/
}
}
}
return lazyMan;
}
//在多线程并发下,例如在8个线程下
public static void main(String[] args){
for (int i = 0; i < 8;i++){
//启动线程
new Thread(() -> {
//启动之后,调用创建对象的方法
LazyMan.getInstance();
}).start();
}
}
}
问:这种模式可以破解吗?
答:可以,利用反射模式,让构造器私有失效
根据单例模式源码分析,利用enum枚举可以阻止单例模式被破坏
//enum其实本身也是一个class类
public enum EnumSingle {
//定义一个枚举变量,instance
instance;
//返回定义的枚举变量
public EnumSingle getInstance(){
return instance;
}
}
class TestEnum{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//获取自定义的枚举变量
EnumSingle instance1 = EnumSingle.instance;
//用反射动态获取构造方法,注意,这里的枚举构造方法是有参构造方法,如果是无参的话,会出现下面这种报错
//java.lang.NoSuchMethodException: de.wen.dewen.single.EnumSingle.()
Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
//让构造方法私有给失效
declaredConstructor.setAccessible(true);
//试图利用反射破坏单例模式
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
运行结果: