单例模式 订阅
单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例) 展开全文
单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例(根据需要,也有可能一个线程中属于单例,如:仅线程上下文内使用同一个实例)
信息
外文名
Singleton pattern
类    别
设计模式
中文名
单例模式
解    释
常用的软件设计模式
单例模式定义
数学与逻辑学中,singleton定义为“有且仅有一个元素的集合”。单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”Java单例模式例子
收起全文
精华内容
下载资源
问答
  • JAVA设计模式之单例模式

    万次阅读 多人点赞 2014-04-16 06:51:34
    本文继续介绍23种设计模式系列之单例模式。 概念:  java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。  单例模式有以下特点:  1、单例类...

    本文继续介绍23种设计模式系列之单例模式。

    概念:
      java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例。
      单例模式有以下特点:
      1、单例类只能有一个实例。
      2、单例类必须自己创建自己的唯一实例。
      3、单例类必须给所有其他对象提供这一实例。
      单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。


    一、懒汉式单例

     

    //懒汉式单例类.在第一次调用的时候实例化自己 
    public class Singleton {
        private Singleton() {}
        private static Singleton single=null;
        //静态工厂方法 
        public static Singleton getInstance() {
             if (single == null) {  
                 single = new Singleton();
             }  
            return single;
        }
    }

     

    Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

    (事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)

    但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,如果你第一次接触单例模式,对线程安全不是很了解,可以先跳过下面这三小条,去看饿汉式单例,等看完后面再回头考虑线程安全的问题:

     

    1、在getInstance方法上加同步

     

    public static synchronized Singleton getInstance() {
             if (single == null) {  
                 single = new Singleton();
             }  
            return single;
    }

     

     

     

    2、双重检查锁定

     

    public static Singleton getInstance() {
            if (singleton == null) {  
                synchronized (Singleton.class) {  
                   if (singleton == null) {  
                      singleton = new Singleton(); 
                   }  
                }  
            }  
            return singleton; 
        }

     

    3、静态内部类

     

    public class Singleton {  
        private static class LazyHolder {  
           private static final Singleton INSTANCE = new Singleton();  
        }  
        private Singleton (){}  
        public static final Singleton getInstance() {  
           return LazyHolder.INSTANCE;  
        }  
    }  

    这种比上面1、2都好一些,既实现了线程安全,又避免了同步带来的性能影响。

     

     

     

     

     

     

    二、饿汉式单例

     

    //饿汉式单例类.在类初始化时,已经自行实例化 
    public class Singleton1 {
        private Singleton1() {}
        private static final Singleton1 single = new Singleton1();
        //静态工厂方法 
        public static Singleton1 getInstance() {
            return single;
        }
    }

    饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

     

     

     

     

    三、登记式单例(可忽略)

    //类似Spring里面的方法,将类名注册,下次从里面直接获取。
    public class Singleton3 {
        private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
        static{
            Singleton3 single = new Singleton3();
            map.put(single.getClass().getName(), single);
        }
        //保护的默认构造子
        protected Singleton3(){}
        //静态工厂方法,返还此类惟一的实例
        public static Singleton3 getInstance(String name) {
            if(name == null) {
                name = Singleton3.class.getName();
                System.out.println("name == null"+"--->name="+name);
            }
            if(map.get(name) == null) {
                try {
                    map.put(name, (Singleton3) Class.forName(name).newInstance());
                } catch (InstantiationException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
            }
            return map.get(name);
        }
        //一个示意性的商业方法
        public String about() {    
            return "Hello, I am RegSingleton.";    
        }    
        public static void main(String[] args) {
            Singleton3 single3 = Singleton3.getInstance(null);
            System.out.println(single3.about());
        }
    }

     登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。 

    这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。

     

    饿汉式和懒汉式区别

    从名字上来说,饿汉和懒汉,

    饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,

    而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

    另外从以下两点再区分以下这两种方式:

     

    1、线程安全:

    饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

    懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。


     

    2、资源加载和性能:

    饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,

    而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

    至于1、2、3这三种实现又有些区别,

    第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,

    第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗

    第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

     

    什么是线程安全?

    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

    或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。

     

    应用

    以下是一个单例类使用的例子,以懒汉式为例,这里为了保证线程安全,使用了双重检查锁定的方式:

     

    public class TestSingleton {
    	String name = null;
    
            private TestSingleton() {
    	}
    
    	private static volatile TestSingleton instance = null;
    
    	public static TestSingleton getInstance() {
               if (instance == null) {  
                 synchronized (TestSingleton.class) {  
                    if (instance == null) {  
                       instance = new TestSingleton(); 
                    }  
                 }  
               } 
               return instance;
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public void printInfo() {
    		System.out.println("the name is " + name);
    	}
    
    }

    可以看到里面加了volatile关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢,原因已经在下面评论中提到,

    还有疑问可参考http://www.iteye.com/topic/652440
    和http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

     

     

     

    public class TMain {
    	public static void main(String[] args){
    		TestStream ts1 = TestSingleton.getInstance();
    		ts1.setName("jason");
    		TestStream ts2 = TestSingleton.getInstance();
    		ts2.setName("0539");
    		
    		ts1.printInfo();
    		ts2.printInfo();
    		
    		if(ts1 == ts2){
    			System.out.println("创建的是同一个实例");
    		}else{
    			System.out.println("创建的不是同一个实例");
    		}
    	}
    }
    

     运行结果:

    结论:由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。

    对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,还有懒汉式为了实现线程安全的3种方式的细微差别。

    更多设计模式:23种设计模式系列

    作者:jason0539

    博客:http://blog.csdn.net/jason0539(转载请说明出处)

    展开全文
  • 深入理解设计模式-双锁单例模式 文章目录一、什么是单例模式二、应用场景三、优缺点四、代码样例总结 一、什么是单例模式 单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。 许多...

    深入理解设计模式-双锁单例模式


    一、什么是单例模式

    单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
    许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

    在这里插入图片描述


    二、应用场景

    举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制 同样,对于一些应用程序的日志应用,或者web开发中读取配置文件都适合使用单例模式,如HttpApplication 就是单例的典型应用。 从上述的例子中我们可以总结出适合使用单例模式的场景和优缺点: 适用场景: 1.需要生成唯一序列的环境 2.需要频繁实例化然后销毁的对象。 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 4.方便资源相互通信的环境

    三、优缺点

    优点:
    1.在内存中只有一个对象,节省内存空间;
    2.避免频繁的创建销毁对象,可以提高性能;
    3.避免对共享资源的多重占用,简化访问;
    4.为整个系统提供一个全局访问点。
    缺点:
    1.不适用于变化频繁的对象;
    2.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共 享连接池对象的程序过多而出现连接池溢出;
    4.如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;


    四、代码实现

    饿汉单例模式

    public class Singleton1 {
        private static Singleton1 instance = new Singleton1();
    
        private Singleton1(){}
    
        public static Singleton1 getInstance(){
            return instance;
        }
    }
    

    类加载的方式是按需加载,且加载一次。。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例,这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

    懒汉单例模式

    public class Singleton2 {
        private static Singleton2 instance;
    
        private Singleton2(){}
    
        public static Singleton2 getInstance(){
            if (instance == null) {
                instance = new Singleton2();
            }
            return instance;
        }
    }
    

    我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
    这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

    双锁单例模式

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

    双锁模式,进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。


    总结

    单例模式的实现方法还有很多。上述是比较经典的实现方式,也是我们应该掌握的几种实现方式。
    从这四种实现中,我们可以总结出,要想实现效率高的线程安全的单例,我们必须注意以下两点:
    1.尽量减少同步块的作用域;
    2.尽量使用细粒度的锁。

    展开全文
  • 单例模式

    千次阅读 多人点赞 2020-12-16 10:20:13
    设计模式之单例模式 单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该...

    设计模式之单例模式

    单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

    优点:系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。

    缺点:可读性差,当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。单例模式中在使用反射时,对象也可能不唯一,所以我们要注意不要认定单例模式唯一是其好处,从而生成刻板印象。

    适用场合

    • 需要频繁的进行创建和销毁的对象
    • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
    • 工具类对象或者频繁访问数据库或文件的对象。

    基本实现思路

    单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)。

    单例的实现主要是通过以下步骤:

    (1)将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。

    (2)在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型。

    (3)定义一个静态方法返回这个唯一对象。

    注意事项

    单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

    饿汉式(静态常量)[可用]

    public class Singleton{
    
        private final static Singleton singleton = new Singleton();
    
        private Singleton(){}
    
        public static Singleton getInstance(){
            return singleton;
        }
    }
    

    优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

    缺点:在类装载的时候就完成实例化,没有达到懒加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费


    饿汉式(静态代码块)[可用]

    public class Singleton {
    
        private static Singleton singleton;
    
        static {
            instance = new Singleton();
        }
    
        private Singleton() {}
    
        public Singleton getInstance() {
            return singleton;
        }
    }
    

    和上面的方式类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。
    优缺点和上面是一样的。


    懒汉式(线程不安全)[不可用]

    public class Singleton {
    
        private static Singleton singleton;
    
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    

    这种写法起到了懒加载的效果,但只能在单线程下使用。如果在多线程下,一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。


    懒汉式(线程安全,同步方法)[不推荐用]

    public class Singleton {
    
        private static Singleton singleton;
    
        private Singleton() {}
    
        public static synchronized Singleton getInstance() {
            if (singleton == null) {
                singleton = new Singleton();
            }
            return singleton;
        }
    }
    

    解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对 getInstance() 方法进行了线程同步。

    缺点:同步效率低,每个线程在想获得类的实例时候,执行 getInstance() 方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。


    懒汉式(线程安全,同步代码块)[不可用]

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

    由于第四种实现方式同步效率低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了 if (singleton == null) 判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。


    懒汉式双检锁[推荐用]

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

    这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要 Singleton 类被装载就会实例化,没有懒加载的作用,而静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。

    类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

    优点:避免了线程不安全,延迟加载,效率高。


    懒汉式三重检查[推荐用]

    public class LazyMan {
        //设置标志位,防止反射通过无参构造新建对象
        private static boolean flag=false;
    
        private LazyMan(){
            synchronized (LazyMan.class){
                if(flag==false){
                    flag=true;
                } else {
                    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;
        }
    
    
        public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
            //如果获取了该标志位的属性,还是可以通过反射篡改该private属性
            Field flag = LazyMan.class.getDeclaredField("flag");
            flag.setAccessible(true);   //篡改属性
            Constructor<LazyMan> declaredConstructor= LazyMan.class.getDeclaredConstructor();   //获取无参构造
            declaredConstructor.setAccessible(true);
            LazyMan l1 = declaredConstructor.newInstance(); // 通过newInstance方法新建对象
            flag.set(l1,false);  
            System.out.println(l1.hashCode());  //后台输出该对象的哈希值
            LazyMan l2 = LazyMan.getInstance();  //通过传统的方式创建,不通过反射创建
            System.out.println(l2.hashCode());  //后台输出正常创建对象的哈希值,输出结果为两者不一致
        }
    }
    
    

    这种方式可以避免通过反获取该对象的无参构造函数,但是魔高一尺,道高一丈,只需要获取设置标志位的字段名,通过getDeclaredField方法就可以获得该字段,通过setAccessible方法就可以篡改该字段属性,一样来拿无参数构造就行,无视标志位,直接创建新对象,此时发生单例模式对象并不单一,所以我们不要有传统刻板的印象,有的时候多了解一些,对自己眼界很有帮助。


    静态内部类单例

    //静态内部类
    public class Holder {
    
        private Holder(){
    	  }
    
        private static Holder getInstance(){
            return InnerClasses.holder;
         }
    
        public static class InnerClasses{
            private static final Holder holder=new Holder();
        }
    
    
    }
    

    静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

    静态内部类在外部类加载的时候不会被加载,只有外部类中用到内部类时候才会被加载。由于静态内部类没有使用任何锁机制,所以性能优于双重检查实现方式。


    枚举单例

    public enum SingletonEnum {
     
         INSTANCE;
     
         private Object data = new Object();
     
         public Object getData(){
             return data;
         }
     
         public static SingletonEnum getInstance(){
             return INSTANCE;
         }
     }
    

    jvm判断了枚举无法反射获取对象,无法序列化,防止了反射破坏单例和序列化破坏单例,那么我们就来测试一下

    //测试枚举能否被反射获取对象
    //enum是什么?本身也是一个Class类
    public enum  EnumSingle {
        INSTANCE;
        public EnumSingle getInstance(){
            return INSTANCE;
        }
    }
    
    
    
    class Test{
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
            //这里并不是无参构造,是一个有参数构造,且是两个,可以通过jad反编译查看
               /* private EnumSingle(String s, int i)  // 查看到的带参数构造,不是无参构造,注意!
        			{
            		super(s, i);
        			}
    				*/
            //通过带参数构造创建对象
            Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
            declaredConstructor.setAccessible(true);  // 篡改属性
            EnumSingle  instance2= declaredConstructor.newInstance(); // 通过反射的方式拿到对象
            EnumSingle instance=EnumSingle.INSTANCE;   //通过传统的方式拿到对象
            System.out.println(instance.hashCode()); 
            System.out.println(instance2.hashCode());  //这里被捕获到了异常
            //Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    
        }
    }
    

    为什么可以通过带参构造呢创建呢而不是默认的无参呢?是否可以通过无参构造对象呢?枚举单例是否可以被反射实例化呢?

    为什么要用枚举单例,私有化构造器并不保险?接下来我们深入研究一下。

    ​ 《effective java》中只简单的提了几句话:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。可以发现是EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,然后看下Enum源码就明白,这两个参数是name和ordial两个属性:

    //通过带参数构造创建对象
    Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);

    public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
            private final String name;
            public final String name() {
                return name;
            }
            private final int ordinal;
            public final int ordinal() {
                return ordinal;
            }
            protected Enum(String name, int ordinal) {
                this.name = name;
                this.ordinal = ordinal;
            }
            //余下省略
    

    枚举Enum是个抽象类,其实一旦一个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然是可以获取到父类Enum的构造器,那你也许会说刚才我的反射是因为自身的类没有无参构造方法才导致的异常,并不能说单例枚举避免了反射攻击。好的,那我们就使用父类Enum的构造器,结果是继续报异常。之前是因为没有无参构造器,这次拿到了父类的构造器了,说是不能够反射,我们看下Constructor类的newInstance方法源码:

    @CallerSensitive
        public T newInstance(Object ... initargs)
            throws InstantiationException, IllegalAccessException,
                   IllegalArgumentException, InvocationTargetException
        {
            if (!override) {
                if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                    Class<?> caller = Reflection.getCallerClass();
                    checkAccess(caller, clazz, null, modifiers);
                }
            }
            if ((clazz.getModifiers() & Modifier.ENUM) != 0)
                throw new IllegalArgumentException("Cannot reflectively create enum objects");
            ConstructorAccessor ca = constructorAccessor;   // read volatile
            if (ca == null) {
                ca = acquireConstructorAccessor();
            }
            @SuppressWarnings("unchecked")
            T inst = (T) ca.newInstance(initargs);
            return inst;
        }
    

    请看黄颜色标注的源码,说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

    通过实践也的得出了如下异常

    Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)

    展开全文
  • 单例模式单例模式单例模式单例模式单例模式单例模式单例模式单例模式
  • 设计模式之美(2)-创建型-单例模式

    万次阅读 2021-08-22 17:40:45
    一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。 如何实现一个单例? 要实现一个单例。我们需要关注的如下几个点: 构造函数需要是private访问权限...

    单例设计模式(Singleton Design pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

    如何实现一个单例?
    要实现一个单例。我们需要关注的如下几个点:
    构造函数需要是private访问权限的,这样才能避免

    1. 如何实现一个单例?
      要实现一个单例。我们需要关注的如下几个点:
      1.外部通过new创建实例;
      2.考虑对象创建时的线程安全问题;
      3.考虑是否支持延迟加载;
      4.考虑 getInstance() 性能是否高(是否加锁)

    2. 饿汉式

    饿汉式的实现方式比较简单。在类加载的时候,instance静态实例就已经创建并初始化好了,所以instance实例的创建过程是线程安全的。不过这样的实现方式不支持延迟加载。

    public class IdGenerator {
        private AtomicLong id = new AtomicLong();
        private static final IdGenerator idGenerator = new IdGenerator();
        private IdGenerator(){}
        public static IdGenerator getInstance(){
            return idGenerator;
        }
        public long getId(){
            return id.incrementAndGet();
        }
    }
    

    有人认为这种实现方式不好,因为不支持延迟加载,如果占用资源多或初始化耗时长,提前初始化会造成资源浪费。
    但是如果初始化耗时长,那如果等到使用它的时候,才去执行这个耗时长的初始化过程,会影响到系统性能。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样能避免在程序运行的时候,再去初始化导致的性能问题。
    如果实例占用资源多,提早暴露问题,也能第一时间排查修复问题。不会在程序运行一段时间后,突然初始化导致系统崩溃,影响系统的可用性。
    2. 懒汉式
    有饿汉式,对应的,就有懒汉式。懒汉式相对于饿汉式的优势是支持延迟加载。

    public class IdGeneratorV2 {
        private AtomicLong atomicLong = new AtomicLong(0);
        private static IdGeneratorV2 idGeneratorV2;
        private IdGeneratorV2(){}
        public static synchronized IdGeneratorV2 getInstance(){
            if(idGeneratorV2 == null){
                idGeneratorV2 = new IdGeneratorV2();
            }
            return idGeneratorV2;
        }
        public long getId(){
            return atomicLong.incrementAndGet();
        }
    }
    

    懒汉式的缺点也是很明显,我们给getinstance()这个方法加了锁,导致这个函数并发度很低。如果这个单例类偶尔被用到,那这种实现方式还是可以接受。但是平凡的使用,那频繁的加锁、释放锁及并发度低等问题,会导致性能瓶颈。

    1. 双重检测
      饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。双重检测的实现方式会同时支持延迟加载,又支持高并发。
    public class IdGeneratorV3 {
        private AtomicLong atomicLong = new AtomicLong(0);
        private IdGeneratorV3(){}
        private static IdGeneratorV3 idGeneratorV3;
        public static IdGeneratorV3 getInstance(){
            if(idGeneratorV3 == null){
                synchronized (idGeneratorV3.getClass()){
                    if(idGeneratorV3 == null) {
                        idGeneratorV3 = new IdGeneratorV3();
                    }
                }
            }
            return idGeneratorV3;
        }
        public long getId(){
            return atomicLong.incrementAndGet();
        }
    }
    

    网上有人说,这种实现方式有些问题。因为指令重排序,可能会导致IdGeneratorV3对象被new出来,并赋值给instance之后,还没来得及初始化,就被另一个线程使用。
    要解决这个问题,需要给idGeneratorV3成员变量加上volatile关键字,禁止指令重排序,实际上,只有低版本的Java才会有这个问题。我们现在用的高版本的Java已经在JDK内部实现中解决了这个问题。
    4. 静态内部类
    比双重检查更简单的实现方法,利用Java的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。

    public class IdGeneratorV4 {
        private AtomicLong atomicLong = new AtomicLong(0);
        private IdGeneratorV4(){}
        private static class SingletonHolder{
            private static final IdGeneratorV4 ID_GENERATOR_V_4 = new IdGeneratorV4();
        }
        public static IdGeneratorV4 getInstance(){
            return SingletonHolder.ID_GENERATOR_V_4;
        }
        public long getID(){
            return atomicLong.incrementAndGet();
        }
    }
    

    SingletonHolde是静态内部类,当外部类IdGeneratorV4被加载的时候,并不会创建SingletonHolde实例对象。只有当调用getInstance()方法时,SingletonHolde才会被加载,这个时候才会去创建IdGeneratorV4类实例,IdGeneratorV4的唯一性、创建过程的线程安全,都有JVM来保证。所以这种实现方式既保证了线程安全,又能做到延迟加载。
    5. 枚举
    枚举的实现方式通过Java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

    public enum IdGeneratorV5 {
        INSTANCE;
        private AtomicLong atomicLong = new AtomicLong(0);
        public long getID(){
            return atomicLong.incrementAndGet();
        }
    }
    

    单例存在哪些问题?

    1. 单例对OOP特性的支持不友好
      OOP的四大特性是封装、抽象、继承、多态。单例模式对于其中的抽象、继承、多态都支持的不好。
      单例的使用方式违背了基于接口而非实现的设计原则,违背了广义上理解的OOP的抽象特性,如果针对业务采用不同的ID生成策略。为了应对需求的变化,就需要修改所有用到IdGenerator类的地方,代码改动比较大。
      理论上单例类也可以被继承,实现多态,不过实现起来会非常的奇怪,导致可读性变差。所以一旦选择把类设计成单例类,也就意味着放弃了继承和多态这两个特性。
    2. 单例会隐藏类之间的依赖关系
      通过构造函数、参数传递等方式声明类之间的依赖关系,但是单例类不需要显示创建、不需要依赖参数传递,在函数中直接调用就可以。如果代码复杂,那这种调用关系就会非常隐蔽。
    3. 单例对代码的扩展性不友好
      单例类只有一个对象实例,如果未来某一天,需要在代码中创建两个实例或者多个实例,那就要改动比较多的代码。
      例如数据库连接池,在系统初期,系统中只有一个数据库连接池,所以把数据库连接池类设计成了单例。随着时间推移,有一些SQL非常耗时,希望将慢SQL与其他SQL隔离开来执行。为了实现这个目的,可以在系统中创建两个数据库连接池,慢SQL独享一个数据库连接池,其他SQL独享另外的,避免慢SQL影响其他SQL。
    4. 单例对代码的可测试性不友好
      单例模式的使用会影响到代码的可测试性。如果单例类依赖比较重的外部资源,比如DB,在写单元测试的时候,希望能通过mock的方式将它替换掉。而单例类这种硬编码的方式,导致无法实现mock替换。
    5. 单例不支持有参数的构造函数
      单例不支持有参数的构造函数,比如我们创建一个连接池的单例对象,我们没法通过参数来指定连接池的大小。
      代替解决方案
      为了保证全局唯一,除了使用单例,还可以使用静态方法来实现。不过静态方法这种实现思路,并不能解决我们之前遇到的问题,如果要完全解决这些问题,可以通过工厂模式,IOC容器来保证。

    如何实现一个多例模式?
    “单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建3个对象。

    public class BackendServer {
        public BackendServer(Integer serverNo, String serverAddress) {
            this.serverNo = serverNo;
            this.serverAddress = serverAddress;
        }
    
        private Integer serverNo = 3;
        private String serverAddress;
        private static final int SERVER_COUNT=3;
        private static final Map<Integer, BackendServer> BACKEND_SERVER_MAP = Maps.newHashMap();
        static {
            BACKEND_SERVER_MAP.put(1, new BackendServer(1, "127.0.0.1"));
            BACKEND_SERVER_MAP.put(2, new BackendServer(2, "127.0.0.2"));
            BACKEND_SERVER_MAP.put(3, new BackendServer(3, "127.0.0.3"));
        }
        public BackendServer getBackend(Integer serverNo){
            return BACKEND_SERVER_MAP.get(serverNo);
        }
        public BackendServer getRandomBackend(Integer serverNo){
            Random random = new Random();
            int i = random.nextInt(SERVER_COUNT)+1;
            return BACKEND_SERVER_MAP.get(i);
        }
    }
    

    多例模式跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。

    展开全文

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 321,167
精华内容 128,466
关键字:

单例模式