当前位置: 首页 > 后端技术 > Java

一篇彻底看懂Singleton-Pattern(单例模式)

时间:2023-04-02 01:13:34 Java

文章已收录到我的仓库:Java学习笔记和免费书籍分享设计动机顾名思义,Singleton-Pattern保证只有类的一个实例,那为什么要设计单例呢?示例模式?对于某些类,只有一个实例非常重要。例如,一台电脑应该只有一个文件系统,厂商不应该为一台电脑配置两个文件系统;一个应用程序应该有一个专用的日志对象,不能一会儿写这里,一会儿写那里;程序中往往只有一个线程池,由一个线程池管理线程,而不是使用多个线程池,这样会导致线程杂乱,难以维护;在抽象工厂中,应该只有一个具体的工厂类……等等,我们需要保证一个类只有一个实例,然后才能使用单例模式。设计我们必须防止用户实例化多个对象。解决方法是让类保存它的唯一实例,对外隐藏构造函数,暴露一个特定的静态方法返回唯一实例,这样用户就不能自己实例化多个对象,而只能实例化唯一实例可以通过类暴露的静态方法获取。代码示例假定以下要求:在应用程序的全局状态中需要相同的日志对象。1.惰性模式惰性模式不直接初始化实例,而是等到实例被使用后再初始化,避免了不必要的资源浪费。//日志文件类classLogFile{//唯一实例privatestaticLogFilelogFile=null;//构造方法隐藏privateLogFile(){};//公开的方法publicstaticLogFilegetInstance(){//惰性模式if(logFile==null){logFile=newLogFile();}返回日志文件;}}publicclassTest{publicstaticvoidmain(String[]args){vars1=LogFile.getInstance();vars2=LogFile.getInstance();System.out.println(s1==s2);//输出true,生成同一个对象}}这里的代码在单线程中运行良好,logFile属于临界区资源,所以这种写法是线程不安全的,一开始实例为null,线程A在执行完if判断语句后,在执行logFile=newLogFile()之前被分派给线程B,此时线程B看到的实例也是空的,因为A还没有初始化,所以线程B初始化实例。当回到线程A时,线程A会继续执行构造logFile的语句。此时logFile已经初始化了两次,A和B拿到的不是同一个实例。一个简单的解决方案是锁定它们:}}returnlogFile;}但是这样加锁效率太低了,还是用饿汉式比较好,因为即使logFile不为空的时候,多个线程也要排队去获取实例,但实际上没有必要要排队,当logFile不为空时,应该有多少个Thread可以同时获取logFile实例,因为他们只是读取实例,不会改变实例,共享读是线程安全的。更好的解决方案是使用双重检查锁定:);}}}returnlogFile;}通过在外层加一个判断,我们就解决了上面提到的问题,现在代码效率已经够高了——加锁只涉及到初始阶段。请注意,上面的代码仍然是线程不安全的。如果我们要线程安全,就必须在logFile实例的声明中加上volatile关键字,即:privatevolatilestaticLogFilelogFile;要理解这一点,就必须理解ne??w一个对象的大致过程是:申请内存空间,并对空间中的字段使用默认初始化(此时对象为null)。调用类的构造函数进行初始化(此时对象为null)。返回地址(执行完成后对象不为空)。如果不加volatile关键字,Java虚拟机可能会在保证序列化的前提下对指令进行重新排序,即虚拟机可能先执行第3步再执行第2步(很少),当对象初始化时,虚拟机machine只考虑单线程的情况,此时的指令重排不会影响单线程的运行,所以为了加快速度,指令重排是可以的。从多线程的角度来看,如果发生指令重排,线程A在执行new对象时先执行第一步,然后执行第三步。这个时候对象已经不是null了,只是对象还没有被构造出来。虽然此时线程A还持有锁,但是对线程B没有影响——线程B闯入发现对象不为null,直接拿走一个还没有构建完成的对象实例——不会通过第一层全部判断并申请锁。添加volatile关键字可确保可见性并禁止指令重排。但是从解决问题的角度,我们还是有更好的解决方案——静态内部类://日志文件类classLogFile{//实例交给静态内部类保管privatestaticclassLazyHolder{privatestatic日志文件logFile=new日志文件();}//构造方法隐藏privateLogFile(){};//暴露的方法publicstaticLogFilegetInstance(){returnLazyHolder.logFile;}}publicclassTest{publicstaticvoidmain(String[]args){vars1=LogFile.getInstance();vars2=LogFile.getInstance();System.out.println(s1==s2);//输出true,生成同一个对象}}静态内部类效果最好。静态内部类只有在其成员变量或方法被引用时才会被加载,也就是说只有在我们第一次访问该类时才会初始化实例。我们将实例委托给静态内部类帮助初始化,虚拟机加载静态内部类是线程安全的。我们通过加锁机制避免委托给虚拟机,效率很高。惰性风格可以避免产生无用的垃圾对象——只有在使用时才进行初始化,但是我们也不得不为此多写一点代码来保证它的安全性。如果一个类不是很常用,使用惰性风格,可以在一定程度上节省资源。2、HungrystyleHungry模式在加载时初始化单个实例,这样当用户获取到实例时就已经初始化好了。//日志文件类classLogFile{//唯一实例privatestaticLogFilelogFile=newLogFile();//构造方法隐藏privateLogFile(){};//暴露的方法publicstaticLogFilegetInstance(){returnlogFile;}}publicclassTest{publicstaticvoidmain(String[]args){vars1=LogFile.getInstance();vars2=LogFile.getInstance();System.out.println(s1==s2);//输出true,生成了同一个对象}}饿了么风格是线程安全的,因为logFile已经初始化了,所以饿了么比懒惰的方式效率更高,但同时,如果实例全局无用的话,饥饿的中国模式会产生垃圾和消耗资源。优缺点总结主要优点:提供对独特实例的受控访问。系统内存中只有一个对象,节省了系统资源。单例模式可以允许可变数量的实例。主要缺点:可扩展性比较差。单例类的职责太多,一定程度上违背了“单一职责原则”。单例的滥用会带来一些负面的问题,比如为了节省资源,将数据库连接池对象设计成单例类,可能会因为过多的程序共享连接池对象(每个人都使用一个池,池可能承受不了),如果实例化的对象长期不用,系统会认为是垃圾对象而被回收,会导致对象状态丢失。