单例模式详细介绍

单例模式详细介绍

单例模式可以说只要是一个合格的开发都会写,但是如果要深究,小小的单例模式可以牵扯到很多东
西,比如 多线程是否安全,是否懒加载,性能等等。

1、饿汉式

public class Hungry {
  //私有构造方法,使得别人无法new这个对象,使内存中只有一个对象
  private Hungry() {
 }
    //饿汉式就是很饿,一上来就new这个对象
  private final static Hungry hungry = new Hungry();
  public static Hungry getInstance() {
    return hungry;
 }
}

私有构造方法,使得别人无法new这个对象,使内存中只有一个对象
private Hungry() {
}

饿汉式是最简单的单例模式的写法,保证了线程的安全,在很长的时间里,我都是饿汉模式来完成单例的,因为够简单,后来才知道饿汉式会有一点小问题,看下面的代码:

public class Hungry {
  private byte[] data1 = new byte[1024];
  private byte[] data2 = new byte[1024];
  private byte[] data3 = new byte[1024];
  private byte[] data4 = new byte[1024];
 
  private Hungry() {
 }
  private final static Hungry hungry = new Hungry();
  public static Hungry getInstance() {
    return hungry;
 }
}

在Hungry类中,我定义了四个byte数组,当代码一运行,这四个数组就被初始化,并且放入内存了,如果长时间没有用到getInstance方法,不需要Hungry类的对象,这不是一种浪费吗?我希望的是 只有用到了 getInstance方法,才会去初始化单例类,才会加载单例类中的数据。所以就有了 第二种单例模式:懒汉式。

2、懒汉式

正常的 懒汉式单例:

public class LazyMan {
  private LazyMan() {
    System.out.println(Thread.currentThread().getName()+"Start");
 }
  private static LazyMan lazyMan;
  public static LazyMan getInstance() {
    if (lazyMan == null) {
      lazyMan = new LazyMan();
   }
    return lazyMan;
 }
  // 单线程下是ok的,但是测试并发环境,发现单例失效,多个线程创建了多个对象
  public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
      new Thread(()->{
        LazyMan.getInstance();
        }).start();
   }
 }
}

多加一层检测可以避免问题,也就是DCL懒汉式!

public class LazyMan {
  private LazyMan() {
 }
  private static LazyMan lazyMan;
  public static LazyMan getInstance() {
    if (lazyMan == null) {
      synchronized (LazyMan.class) {
        if (lazyMan == null) {
          lazyMan = new LazyMan();
       }
     }
   }
    return lazyMan;
 }
}

为什么要双层判定if (lazyMan == null)呢?

第一层不加if (lazyMan == null)的话,每次都会先上锁,浪费资源,如果加上了第一层if (lazyMan == null),那如果第一次创建了lazyMan对象后,不为null,下次就不加锁了。

DCL懒汉式的单例,保证了线程的安全性,又符合了懒加载,只有在用到的时候,才会去初始化,调用
效率也比较高,但是这种写法在极端情况还是可能会有一定的问题。因为

lazyMan = new LazyMan(); 

不是原子性操作,至少会经过三个步骤:

  1. 分配对象内存空间
  2. 执行构造方法初始化对象
  3. 设置instance指向刚分配的内存地址,此时instance !=null;

由于指令重排,导致A线程执行 lazyMan = new LazyMan();的时候,可能先执行了第三步(还没执行第
二步),此时线程B又进来了,发现lazyMan已经不为空了,直接返回了lazyMan,并且后面使用了返回
的lazyMan,由于线程A还没有执行第二步,导致此时lazyMan还不完整,可能会有一些意想不到的错
误,所以就有了下面一种单例模式。

这种单例模式只是在上面DCL单例模式增加一个volatile关键字来避免指令重排:

public class LazyMan {
  private LazyMan() {
 }
  private volatile static LazyMan lazyMan;
  public static LazyMan getInstance() {
    if (lazyMan == null) {
      synchronized (LazyMan.class) {
        if (lazyMan == null) {
          lazyMan = new LazyMan();
       }
     }
   }
    return lazyMan;
    }
}

3、静态内部类

还有这种方式是第一种饿汉式的改进版本,同样也是在类中定义static变量的对象,并且直接初始化,不过是移到了静态内部类中,十分巧妙。既保证了线程的安全性,同时又满足了懒加载。

public class Holder {
  private Holder() {
 }
  public static Holder getInstance() {
    return InnerClass.holder;
 }
  private static class InnerClass {
    private static final Holder holder = new Holder();
 }
}

4.万恶的反射

万恶的反射登场了,反射是一个比较霸道的东西,无视private修饰的构造方法,可以直接在外面newInstance,破坏我们辛辛苦苦写的单例模式。

public static void main(String[] args) {
  try {
    LazyMan lazyMan1 = LazyMan.getInstance();
    Constructor<LazyMan> declaredConstructor =
LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazyMan lazyMan2 = declaredConstructor.newInstance();
    System.out.println(lazyMan1.hashCode());
    System.out.println(lazyMan2.hashCode());
    System.out.println(lazyMan1 == lazyMan2);
 } catch (Exception e) {
    e.printStackTrace();
 }
}

我们分别打印出lazyMan1,lazyMan2的hashcode,lazyMan1是否相等lazyMan2,结果显而易见,不相等;

那么,怎么解决这种问题呢?

public class LazyMan {
  private LazyMan() {
    synchronized (LazyMan.class) {
      if (lazyMan != null) {
        throw new RuntimeException("不要试图用反射破坏单例模式");
     }
   }
 }
  private volatile static LazyMan lazyMan;
  public static LazyMan getInstance() {
    if (lazyMan == null) {
      synchronized (LazyMan.class) {
        if (lazyMan == null) {
          lazyMan = new LazyMan();
       }
     }
   }
    return lazyMan;
 }
}

在私有的构造函数中做一个判断,如果lazyMan不为空,说明lazyMan已经被创建过了,如果正常调用
getInstance方法,是不会出现这种事情的,所以直接抛出异常!

5.枚举

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类
(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性
以及便捷性。

public enum EnumSingleton {
  INSTANCE;
  public EnumSingleton getInstance(){
    return INSTANCE;
 }
}
class Demo04{
  public static void main(String[] args) {
    EnumSingleton singleton1=EnumSingleton.INSTANCE;
    EnumSingleton singleton2=EnumSingleton.INSTANCE;
    System.out.println("正常情况下,实例化两个实例是否相同:"+
(singleton1==singleton2));
      }
}

枚举是目前最推荐的单例模式的写法,因为足够简单,不需要开发自己保证线程的安全,同时又可以有效的防止反射破坏我们的单例模式。

package 单例模式;
import java.lang.reflect.Constructor;
public enum EnumSingleton {
  INSTANCE;
  public EnumSingleton getInstance(){
    return INSTANCE;
 }
}
class Demo04{
  public static void main(String[] args) throws Exception {
    EnumSingleton singleton1=EnumSingleton.INSTANCE;
    EnumSingleton singleton2=EnumSingleton.INSTANCE;
    System.out.println("正常情况下,实例化两个实例是否相同:"+
(singleton1==singleton2));
    //Constructor<EnumSingleton> constructor =
EnumSingleton.class.getDeclaredConstructor(); //自身的类没有无参构造方法
    Constructor<EnumSingleton> constructor =
EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
    constructor.setAccessible(true);
    EnumSingleton enumSingleton = constructor.newInstance();
 }
}

试图破坏,真的破坏不了!
假如有人问你单例模式,再也不用害怕了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值