单例模式

什么是单例模式

哈喽,大家好,我是花臂,今天给大家讲一下单例模式。

单例模式就是一个类在内存中有且仅有一个实例。

单例模式的应用场景

电脑上的回收站、任务管理器

具体代码实现

饿汉式实现单例模式

public class SingletonV1 {
 private static SingletonV1 singletonV1 = new SingletonV1();

 //不让实例化
 private SingletonV1() {
 }

 //返回对象的实例
 public static SingletonV1 getInstance() {
  return singletonV1;
 }
}

这种方式是线程安全的,在类加载的时候就会被创建对象,并且只会创建一此,因为加了static关键字,但是,如果项目过多使用饿汉式的话,就会造成项目启动过慢,而且对象会很占内存的。

懒汉式(线程不安全)实现单例模式

public class SingletonV2 {
 private static SingletonV2 singletonV2;

 //不让实例化
 private SingletonV2() {
 }

/**
  * @return
  */

 public  static SingletonV2 getInstance() {

  if (singletonV2 == null) {
   try {
    Thread.sleep(2000);//测试线程不安全
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   singletonV2 = new SingletonV2();
  }
  return singletonV2;
 }
}

这种方式不会在类加载的时候去创建对象,而是在我们需要对象的时候才会去创建对象,很好的解决了饿汉式的缺点,但是在高并发的情况下,会出现线程安全问题,为什么出现线程安全问题呢?测试一下就知道了。

public class test {
 public static void main(String[] args) {
  for (int i = 0; i <100 ; i++) {
   new Thread(new Runnable() {
    public void run() {
     SingletonV2 instance = SingletonV2.getInstance();
     System.out.println(Thread.currentThread().getName()+"-"+instance);
    }
   }).start();

  }
 }
}

可以看出每次都可以创建出新的对象,线程就不安全了,可以通过下面的方式解决。

懒汉式(线程安全)实现单例模式

public class SingletonV2 {
 private static SingletonV2 singletonV2;

 //不让实例化
 private SingletonV2() {
 }

 public synchronized static SingletonV2 getInstance() {

  if (singletonV2 == null) {
   try {
    Thread.sleep(2000);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   singletonV2 = new SingletonV2();
  }
  return singletonV2;
 }
}

加一个同步锁就很好的解决了这个问题,但是这种效率非常低,因为我们只需要在创建对象的加上锁,拿对象的时候不需要加锁,但是我们这里都加上锁,所以效率非常低。

双重校验锁实现单例模式

public class SingletonV3 {
  /**
   * volatile 禁止重排序和 提高可见性
   */
 private volatile  static SingletonV3 singletonV3;

 private SingletonV3() {

 }

 public static SingletonV3 getInstance() {

  if (singletonV3 == null) {
   synchronized (SingletonV3.class) {
    /***
    * 只要在多个线程可能new对象的地方加上锁才加锁,保证线程安全
    * 第二个if判断:可能会有人说没必要,这个是非常有必要的,(1)判断一下是不是null,以防万一其他地方创建了对象了,在决定
    * 是不是要创建对象 (2)假如有多个线程14行等待获取锁,那么获取到锁之后又会创建对象,这样线程就不安全了
    * 缺点:在多线程情况下,可能会阻塞,所以第一次创建对象可能会很慢
   */
    if (singletonV3 == null) {
     singletonV3 = new SingletonV3();
    }
   }
  }
  return singletonV3;
 }
}

这种方式很好的解决上面方式效率慢的问题,但是这种方式因为加了锁,所以在第一次new对象的时候还是会很慢,所以通过下面这种方式解决。

静态内部类实现单例模式

public class SingletonV4 {

 private SingletonV4(){

 }

 public static SingletonV4 getInstance(){
  return SingletonV4Uuils.singletonV4;
 }

private  static class SingletonV4Uuils{
  private static SingletonV4 singletonV4=new SingletonV4();

 }

}

这种方式解决了双重校验锁的缺点,取消了同步,static关键字修饰只会调用一次,静态内部类的方式,项目启动的不会创建项目,而是在需要的时候创建对象,继承了饿汉式和懒汉式的优点,解决了双重校验锁第一次记载慢的问题。

容器实现单例模式

public class SingletonManager {
 private static Map<String, Object> objMap = new HashMap<String, Object>();
 public static void registerService(String key, Object instance) {
  if (!objMap.containsKey(key)) {
   objMap.put(key, instance);
  }
 }
 public static Object getService(String key) {
  {
   return objMap.get(key);
  }
 }

}

这种方式将多种单例类统一管理,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

防止破坏单例模式

使用反射技术破解单例

public class SingletonV6  {
 private  static SingletonV6 singletonV6 = new SingletonV6();

 //不让实例化
 private SingletonV6() {

 }

 //返回对象的实例
 public static SingletonV6 getInstance() {
  return singletonV6;
 }

}
public class Test {
 public static void main(String[] args) throws Exception {
  SingletonV6 instance = SingletonV6.getInstance();

  //拿到无参构造方法
  Constructor<SingletonV6> declaredConstructor1 = SingletonV6.class.getDeclaredConstructor();
  //设置可以使用私有构造器的权限
  declaredConstructor1.setAccessible(true);
  SingletonV6 singletonV6 = declaredConstructor1.newInstance();
  System.out.println(instance == singletonV6);
 }

防止反射技术破解单例

在私有构造函数做一下判断

public class SingletonV6  {
 private volatile static SingletonV6 singletonV6 = new SingletonV6();

 private SingletonV6() {
if (singletonV6!=null){
 try {
  throw new Exception("对象不能通过外部构造器初始化");
 } catch (Exception e) {
  e.printStackTrace();
 }

}
 }

 //返回对象的实例
 public static SingletonV6 getInstance() {
  return singletonV6;
 }

}

使用序列化技术破解单例

要实现Serializable接口

public class SingletonV6 implements Serializable   {
 private  static SingletonV6 singletonV6 = new SingletonV6();

 //不让实例化
 private SingletonV6() {

 }

 //返回对象的实例
 public static SingletonV6 getInstance() {
  return singletonV6;
 }

}
public class Test {
 public static void main(String[] args) throws Exception {
  SingletonV6 instance = SingletonV6.getInstance();
  FileOutputStream fos = new FileOutputStream("D:\\code\\instance.obj");
  ObjectOutputStream oos = new ObjectOutputStream(fos);
  oos.writeObject(instance);
  oos.flush();
  oos.close();

  FileInputStream fis = new FileInputStream("D:\\code\\instance.obj");
  ObjectInputStream ois = new ObjectInputStream(fis);
  SingletonV6 singleton2 = (SingletonV6) ois.readObject();
  System.out.println(singleton2==instance);
 }

防止被序列化破解单例

定义readResolve()方法,在反序列的时候会返回对象的实例,保证单例

public class SingletonV6  implements Serializable {
 private volatile static SingletonV6 singletonV6 = new SingletonV6();

 //不让实例化
 private SingletonV6() {

 }

 //返回对象的实例
 public static SingletonV6 getInstance() {
  return singletonV6;
 }
 //反序列化直接返回该对象保证为单例
 public Object readResolve() {
  return singletonV6;
 }
}

枚举实现单例模式

public enum SingletonV5 {

 INSTANCE;//对象

 public void add() {
  System.out.println("执行add方法>>>>>>");
 }

}

枚举实现单例模式代码非常简介,同时也是最牛逼的一种方式,它能完全防止反射和序列化破坏单例模式。
枚举底层是将枚举类转换为普通类然后继承Enum枚举类,枚举中定义的对象在静态代码块种快速初始化的,枚举转换的类是没有无参构造的,默认有一个有参构造(String s,int i)
如果强行使用Java反射破解单例的话会报一个错误:

可以通过查看底层代码发现做了一个判断:无法通过反射创建枚举对象

ok,本次教程比较详细的讲了一下单例模式创建的7种方式和优缺点以及破解单例和防止破坏单例的方法,如果有疑问,欢迎下方评论区留言!

本文参考蚂蚁课堂

评论区



© [2020] · Powered by Typecho · Theme by Morecho
鄂ICP备20005123号