二十三种设计模式第四篇--单例模式
  cEe6YWJIAuf2 2023年11月05日 37 0


今天,我们来学习单例模式,说到单例模式,我相信没有哪个初学者不认识他,他在我们项目中也用的贼多。并且单例模式,是java中最简单的一种设计模式,单例模式,也是创建型模式的一种。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意事项

1、单例类只能有一个实例。 2、单例类必须自己创建自己的唯一实例。 3、单例类必须给所有其他对象提供这一实例。

单例模式意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

应用实例:

  • Spring 的 ApplicationContext
  • Spring依赖注入Bean实例默认是单例的。DefaultSingletonBeanRegistry中的getSingleton()方法
  • Mybatis 中的 ErrorContext 与 ThreadLocal
  • java.awt.DeskTop 类允许一个Java应用程序启动本地的另一个应用程序去处理URI或文件请求,使用了单例模式中的懒汉式, 而且是容器单例模式
  • JDK Runtime 饿汉单例
    java.util.logging.LogManager作为全局日志管理器负责维护日志配置和日志继承结构, 使用了单例模式中的饿汉式。

单例模式的优缺点:

优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理系统的首页页面缓存)。
2、避免对资源的多重占用(比如经常对文件进行读写文件操作)。

缺点:
没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

单例模式的常见使用场景:

1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

单例模式

第一个单例(最简单)

public class Demo_1 {
	//手撕一个单例模式
	private static Demo_1 instance = new Demo_1();
	private Demo_1(){}//构造一个私有的单构造函数
	public static Demo_1 getInstance() {
		return instance;
	}
	public void showMesage() {
		System.out.println("这个就是一个基本的单例了");
	}
}

让我们来创建一个单例对象:

public class SingleObject {
 
   //创建 SingleObject 的一个对象
   private static SingleObject instance = new SingleObject();
 
   //让构造函数为 private,这样该类就不会被实例化
   private SingleObject(){}
 
   //获取唯一可用的对象
   public static SingleObject getInstance(){
      return instance;
   }
 
   public void showMessage(){
      System.out.println("你好,我的世界!");
   }
}

让我们来创建单例对象的测试类

public class SingletonPatternDemo {

   public static void main(String[] args) {
 
      //不合法的构造函数
      //编译时错误:构造函数 SingleObject() 是不可见的
      //SingleObject object = new SingleObject();  这里是错误的实例
 
      //获取唯一可用的对象  正确的方式
      SingleObject object = SingleObject.getInstance();
 
      //显示消息
      object.showMessage();

      SingleObject obj2=SingleObject.getInstance();
      System.out.println(   object+"\t"+obj2);
   }
}

好了,上述代码,我们就已经创建一个最基本的单例对象和其测试类了,接下来我将围绕单例模式的类型进行开展了。

单例模式类型

  1. 饿汉式单例
  2. 懒汉式单例
  3. 注册式单例

饿汉式单例

它是在类加载的时候就立即初始化,并且创建单例对象

优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好

缺点:类加载的时候就初始化,不管你用还是不用,我都占着空间,浪费了内存,有可能占着茅坑不拉屎,绝对线程安全,在线程还没出现以前就是实例化了,不可能存在访问安全问题

public class HungrySingleton1 {
    //先静态、后动态
    //先属性、后方法
    //先上后下
    private static final HungrySingleton1 hungrySingleton = new HungrySingleton1();
    private HungrySingleton1(){}
    public static HungrySingleton1 getInstance(){
        return  hungrySingleton;
    }
}
//饿汉式静态块单例
public class HungryStaticSingleton2 {
    //以final保证可见
    private static final HungryStaticSingleton2 hungrySingleton;
    static {
        hungrySingleton = new HungryStaticSingleton2();
    }
    private HungryStaticSingleton2(){}
    public static HungryStaticSingleton2 getInstance(){
        return  hungrySingleton;
    }
}
public class Test1 {
    public static void main(String[] args) {
        HungrySingleton1 h1=  HungrySingleton1.getInstance();
        HungrySingleton1 h2=  HungrySingleton1.getInstance();
        System.out.println(   h1+"   "+h2);

        HungryStaticSingleton2 h3=  HungryStaticSingleton2.getInstance();
        HungryStaticSingleton2 h4=  HungryStaticSingleton2.getInstance();
        System.out.println(   h3+"   "+h4);
    }
}

懒汉式单例

这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题,完美地屏蔽了这两个缺点。

//懒汉式单例
//在外部需要使用的时候才进行实例化
public class LazySimpleSingleton1 {
    private LazySimpleSingleton1(){}
    //静态块,公共内存区域
    private static LazySimpleSingleton1 lazy = null;

//先不叫锁操作
//    public static LazySimpleSingleton1 getInstance(){
//        if(lazy == null){
//            lazy = new LazySimpleSingleton1();
//        }
//        return lazy;
//    }

//加锁操作
    public synchronized static LazySimpleSingleton1 getInstance(){
        if(lazy == null){
            lazy = new LazySimpleSingleton1();
        }
        return lazy;
    }

    /*
    这段代码其实是分为三步执行:
     */
}
public class LazyDoubleCheckSingleton2 {
    //注意volatile:内存可见性.
    private volatile static LazyDoubleCheckSingleton2 lazy = null;

    private LazyDoubleCheckSingleton2(){}

    public static LazyDoubleCheckSingleton2 getInstance(){
        //为什么要双重检查  lazy==null
        if(lazy == null){
            //在些阻塞并不是基于整 个LazySimpleSingleton2的阻塞,而是在getInstance内部的阻塞,。
            synchronized (LazyDoubleCheckSingleton2.class){  //性能上稍逊
                if(lazy == null){
                    lazy = new LazyDoubleCheckSingleton2();
                    //1.分配内存给这个对象
                    //2.初始化对象
                    //3.设置lazy指向刚分配的内存地址
                    //4.初次访问对象
                }
            }
        }
        return lazy;
    }
}
public class LazyInnerClassSingleton3 {
    //默认使用LazyInnerClassGeneral的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private LazyInnerClassSingleton3(){
    }

    //static 是为了使单例的空间共享保证这个方法不会被重写,重载
    public static final LazyInnerClassSingleton3 getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder{
        // 当构造函数结束时,final类型的值是被保证其他线程访问该对象时,它们的值是可见的
        private static final LazyInnerClassSingleton3 LAZY = new LazyInnerClassSingleton3();
    }
}

测试:

public class Test_LazySimpleSingleton1 {
    public static void main(String[] args) {
        Thread t1=new Thread(   new ExectorThread() );
        Thread t2=new Thread( new ExectorThread() );
        t1.start();
        t2.start();
    }
}

//通过断点来观察 懒汉单例的问题
class ExectorThread implements Runnable{

    @Override
    public void run() {
        //在断点上右击, 选择  suspend  ->   thread 模式.
        LazyInnerClassSingleton3 singleton=LazyInnerClassSingleton3.getInstance();
        System.out.println(   Thread.currentThread().getName()+":"+singleton );
    }
}

注册式单例

ContainerSingleton: 容器式单例 spring的方案
EnumSingleton: 枚举常量式单例

Spring中经常使用这种方式。
让我们先定义一个枚举类:

//常量中去使用,常量不就是用来大家都能够共用吗?
//通常在通用API中使用
public enum EnumSingleton1 {
    INSTANCE;
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton1 getInstance(){
        return INSTANCE;
    }


}

//利用   jad工具反向编译   EnumSingleton1的 字节码.
/*
public final class EnumSingleton1 extends Enum
{
    ......
    public static final EnumSingleton1 INSTANCE;
    private Object data;
    private static final EnumSingleton1 $VALUES[];

    static     //通过  static  创建了实例,实际上是一个饿汉式单例
    {
        INSTANCE = new EnumSingleton1("INSTANCE", 0);
        $VALUES = (new EnumSingleton1[] {
            INSTANCE
        });
    }
}

那么序列化能否破坏枚举式单例模式呢?
我们来分析一下  ObjectInputStream的 readObject() 方法:
    case TC_ENUM:
                    if (type == String.class) {
                        throw new ClassCastException("Cannot cast an enum to java.lang.String");
                    }
                    return checkResolve(readEnum(unshared));

readEnum() 方法:
     Enum<?> en = Enum.valueOf((Class)cl, name);
     通过类名和类对象找到唯一一个枚举对象.因此反射是无法破坏枚举式单例的. 
   cl指的是   EnumSingleton1 反射实例
     name指的是:   INSTANCE
 */
//Spring中的做法,就是用这种注册式单例
public class ContainerSingleton2 {
    private ContainerSingleton2(){}
    private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
    public static Object getInstance(String className){
        synchronized (ioc) {
            if (!ioc.containsKey(className)) {
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className, obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            } else {
                return ioc.get(className);
            }
        }
    }
}
public class Test1_EnumSingleton {
    public static void main(String[] args) {
        EnumSingleton1 s1=null;
        EnumSingleton1 s2=EnumSingleton1.getInstance();
        try(FileOutputStream fos=new FileOutputStream(   "EnumSingleton1.obj"  );
            ObjectOutputStream oos=new ObjectOutputStream(   fos ) ){
            oos.writeObject(   s2 );
            oos.flush();
        }catch (Exception ex){
            ex.printStackTrace();
        }
        System.out.println("序列化完成");
        try(
                FileInputStream fis=new FileInputStream(   "EnumSingleton1.obj");
                ObjectInputStream ois=new ObjectInputStream( fis );
        ){
            s1=(EnumSingleton1)ois.readObject();
        }catch(Exception ex){
            ex.printStackTrace();
        }
        System.out.println(   s1+"\t"+s2);
        System.out.println(   s1==s2);
    }
}

说实在的,二十三种设计模式,要想从了解的熟练使用,那就需要去了解没种设计模式的具体案例,借助具体相关的案例,去完成设计模式的拓展使用,这个就需要长期对设计模式的知识面的拓展。编程这条路,学无止境,不断学习,然后发现自己眼见如此之低,不断的发现自己学的东西如此浅显,不断地发现,原来编程不止是CURD,更是一种思维的体现,不同的人有不同的思维,不同的思维针对不同问题有不同的解决方式,当哪天,你可以从CV工程师变成业务工程师,然后从业务工程师变成架构师,你会发现新的世界。

好了,废话说多了,有点过了,既然单例模式已经有了,那么我们来聊聊如何打破单例模式。

打破单例模式:

利用反射打破单例模式

懒汉模式单例

//这种形式兼顾饿汉式的内存浪费,也兼顾synchronized性能问题
//完美地屏蔽了这两个缺点
public class LazyInnerClassSingleton {
    //默认使用LazyInnerClassGeneral的时候,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private LazyInnerClassSingleton(){
        if( LazyHolder.LAZY!=null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }


    //static 是为了使单例的空间共享保证这个方法不会被重写,重载
    public static final LazyInnerClassSingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder{
        //因为加了final , 保证了
        // 当构造函数结束时,final类型的值是被保证其他线程访问该对象时,它们的值是可见的
        private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
    }
}

懒汉模式的反射打破方式

public class Test2_innerclassSingleton {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //破坏单例
        Class cls= LazyInnerClassSingleton.class;
        //获取构造方法
        Constructor con=cls.getDeclaredConstructor(null);
        //修改访问权限
        con.setAccessible(true);
        //初始化
        Object o1=con.newInstance();
        Object o2=con.newInstance();

        System.out.println(  o1+"   "+o2);
    }
}

反序列化打破单例模式

一个对象创建好了后,有时需要将对象序列化后存到磁盘,下次再反序列化出来,转为内存对象, 此时,反序列化后的对象会重新分配内存,但如果反序列化后的对象为单例对象,就违反了单例初衷。

//反序列化时导致单例破坏
public class SeriableSingleton implements Serializable {

    //序列化就是说把内存中的状态通过转换成字节码的形式
    //从而转换一个IO流,写入到其他地方(可以是磁盘、网络IO)
    //将内存中状态给永久保存下来了

    //反序列化: 将已经持久化的字节码内容,转换为IO流
    //通过IO流的读取,进而将读取的内容转换为Java对象, 在转换过程中会重新创建对象new

    //SeriableSingleton,会先初始化内部类
    //如果没使用的话,内部类是不加载的
    private SeriableSingleton(){
        if( SeriableSingleton.LazyHolder.LAZY!=null){
            throw new RuntimeException("不允许创建多个实例");
        }
    }


    //static 是为了使单例的空间共享保证这个方法不会被重写,重载
    public static final SeriableSingleton getInstance(){
        //在返回结果以前,一定会先加载内部类
        return SeriableSingleton.LazyHolder.LAZY;
    }

    //默认不加载
    private static class LazyHolder{
        //因为加了final , 保证了
        // 当构造函数结束时,final类型的值是被保证其他线程访问该对象时,它们的值是可见的
        private static final SeriableSingleton LAZY = new SeriableSingleton();
    }


    //解决方案: 增加一个   readResolve()方法即可.
    private  Object readResolve(){
        return  SeriableSingleton.LazyHolder.LAZY;
    }

}
public class Test1_serialize_error {
    public static void main(String[] args) {
        SeriableSingleton s1=null;
        SeriableSingleton s2=SeriableSingleton.getInstance();
        try(FileOutputStream fos=new FileOutputStream(   "seriableSingleton.obj"  );
            ObjectOutputStream oos=new ObjectOutputStream(   fos ) ){
            oos.writeObject(   s2 );
            oos.flush();
        }catch (Exception ex){
            ex.printStackTrace();
        }
        System.out.println("序列化完成");
        try(
                FileInputStream fis=new FileInputStream(   "seriableSingleton.obj");
                ObjectInputStream ois=new ObjectInputStream( fis );
                ){
            s1=(SeriableSingleton)ois.readObject();
        }catch(Exception ex){
            ex.printStackTrace();
        }
        System.out.println(   s1+"\t"+s2);
    }
}

那么我们如何避免反序列化破坏单例模式呢?

解决方案加入  readResolve()   的源码分析:
1. ObjectInputStream类  -> readObject ()  在这里调用了重写的  readObject0()方法
2. 在readObject0()中
       case TC_OBJECT:
               if (type == String.class) {
                         throw new ClassCastException("Cannot cast an object to java.lang.String");
               }
               return checkResolve(readOrdinaryObject(unshared));
3. readOrdinaryObject()方法中
        Object obj;
            try {
                //只要有无参构造方法,就调用.
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
         //上面这一段代码已经完成了对象的创建

          if (obj != null &&
                     handles.lookupException(passHandle) == null &&
                     desc.hasReadResolveMethod())
                 {
                     Object rep = desc.invokeReadResolve(obj);
           但在这里,它判断 了一下  是否有   readResolve() 方法,如果有,则调用.
4. 那么问题来了, 这个   desc.hasReadResolveMethod() 方法用来判断    readResolve()是否存在又是在哪里创建的呢?
   它是在  ObjectStreamClass的构造方法中在初始化阶段就创建好了:
       readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class);
       
最后,虽然通过   增加   readResolve()方法返回实例以解决单例模式被破坏的问题,但实际上底层此对象是被实例化过两次.

对于上述注册式单例模式的补充,使用枚举来以防序列化打破单例模式

public class Test2_EnumConstructor {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class cl= EnumSingleton1.class;
        Constructor c= cl.getDeclaredConstructor( );
        c.newInstance();

        //以上代码异常: NoSuchMethodException: com.yc.designpattern.DMA3_单例模式.singleton.register5.EnumSingleton1.<init>()
        //Enum类中没有这个无参构造方法,只一个protected修饰的带两个参数的构造方法
        Enum e;
        /*
            protected Enum(String name, int ordinal) {
                this.name = name;
                this.ordinal = ordinal;
            }
         */
    }
}
public class Test3_EnumConstructor2 {
    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        Class clz=EnumSingleton1.class;
        Constructor c=clz.getDeclaredConstructor(   String.class,  int.class);
        c.setAccessible(true);
        EnumSingleton1 es1= (EnumSingleton1) c.newInstance("tom",666);

        /*
        IllegalArgumentException: Cannot reflectively create enum objects  不能用反射的方式来创建对象
        为什么呢?  查看一下   反射中   Constructor中的 newInstance()方法.

         if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
         它在newInstance()中做了强制性判断 , 如果修饰符为  Enum,则抛出异常.

         这种枚举型单例是effective java中推荐的实现写法。
         小结:   jdk橘生淮南的语法特殊性及反射为枚举式单例保驾。
         */
    }
}

使用ThreadLocal创建单例

/**
 * ThreadLocal不能保证其创建的对象是全局唯一的,但能保证单个线程中是唯一的。
 *    线程id      Map
 *     A         Map
 *     B         Map
 *     C         Map
 */
public class ThreadLocalSingleton {

    private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance =
            new ThreadLocal<ThreadLocalSingleton>(){
                @Override
                protected ThreadLocalSingleton initialValue() {
                    return new ThreadLocalSingleton();
                }
            };

    private ThreadLocalSingleton(){}

    public static ThreadLocalSingleton getInstance(){
        return threadLocalInstance.get();
    }
}
public class Test1 {
    public static void main(String[] args) {
        System.out.println(    ThreadLocalSingleton.getInstance() );
        System.out.println(    ThreadLocalSingleton.getInstance() );
        System.out.println(    ThreadLocalSingleton.getInstance() );
        System.out.println(    ThreadLocalSingleton.getInstance() );
    }
}


【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
cEe6YWJIAuf2