zoukankan      html  css  js  c++  java
  • JAVA设计模式——单例模式

      单例模式的定义
      Ensure a class has only one instance, and provide a global point of access to it.( 确保某一个类只有一个实例, 而且自行实例化并向整个系统提供这个实例。)

      单例模式的优缺点

      优点:

      ● 由于单例模式在内存中只有一个实例, 减少了内存开支, 特别是一个对象需要频繁地创建、 销毁时, 而且创建或销毁时性能又无法优化, 单例模式的优势就非常明显。
      ● 由于单例模式只生成一个实例, 所以减少了系统的性能开销, 当一个对象的产生需要比较多的资源时, 如读取配置、 产生其他依赖对象时, 则可以通过在应用启动时直接产生一个单例对象, 然后用永久驻留内存的方式来解决( 在Java EE中采用单例模式时需要注意JVM垃圾回收机制) 。
      ● 单例模式可以避免对资源的多重占用, 例如一个写文件动作, 由于只有一个实例存在内存中, 避免对同一个资源文件的同时写操作。
      ● 单例模式可以在系统设置全局的访问点, 优化和共享资源访问, 例如可以设计一个单例类, 负责所有数据表的映射处理。
      

      缺点:

      ● 单例模式一般没有接口, 扩展很困难, 若要扩展, 除了修改代码基本上没有第二种途径可以实现。 单例模式为什么不能增加接口呢? 因为接口对单例模式是没有任何意义的, 它要求“自行实例化”, 并且提供单一实例、 接口或抽象类是不可能被实例化的。 当然, 在特殊情况下, 单例模式可以实现接口、 被继承等, 需要在系统开发中根据环境判断。
      ● 单例模式对测试是不利的。 在并行开发环境中, 如果单例模式没有完成, 是不能进行测试的, 没有接口也不能使用mock的方式虚拟一个对象。
      ● 单例模式与单一职责原则有冲突。 一个类应该只实现一个逻辑, 而不关心它是否是单例的, 是不是要单例取决于环境, 单例模式把“要单例”和业务逻辑融合在一个类中。

      两种实现方式

      单例模式有五种写法:懒汉、饿汉、双重检验锁、静态内部类、枚举。这里我们主要讨论饿汉模式和懒汉模式,并且实现他们的线程安全。

       单例模式有以下特点:
      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、双重检查锁定

     1 public static Singleton getInstance() {  
     2         if (singleton == null) {    
     3             synchronized (Singleton.class) {    
     4                if (singleton == null) {    
     5                   singleton = new Singleton();   
     6                }    
     7             }    
     8         }    
     9         return singleton;   
    10     }  

    3、静态内部类

    1 public class Singleton {    
    2     private static class LazyHolder {    
    3        private static final Singleton INSTANCE = new Singleton();    
    4     }    
    5     private Singleton (){}    
    6     public static final Singleton getInstance() {    
    7        return LazyHolder.INSTANCE;    
    8     }    
    9 }    

    这种比上面1、2都好一些,既实现了线程安全,又避免了同步带来的性能影响。
    补充:既然3中定义了INSTANCE静态成员,但是又把他放入了静态内部类中,都跟类的实例是没有关系的(实际是有区别的),去掉内部类就是下面所说的饿汉模式。

    二、饿汉式单例

    1 //饿汉式单例类.在类初始化时,已经自行实例化   
    2 public class Singleton1 {  
    3     private Singleton1() {}  
    4     private static final Singleton1 single = new Singleton1();  
    5     //静态工厂方法   
    6     public static Singleton1 getInstance() {  
    7         return single;  
    8     }  
    9 }  
     
    饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

     

    饿汉式和懒汉式区别

    饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。另外从以下两点再区分以下这两种方式:

    1、线程安全:

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

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

    2、资源加载和性能:

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

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

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

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

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

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

    实验1:

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

     1 public class TestSingleton {  
     2     String name = null;  
     3   
     4         private TestSingleton() {  
     5     }  
     6   
     7     private static volatile TestSingleton instance = null;  
     8   
     9     public static TestSingleton getInstance() {  
    10            if (instance == null) {    
    11              synchronized (TestSingleton.class) {    
    12                 if (singleton == null) {    
    13                    singleton = new TestSingleton();   
    14                 }    
    15              }    
    16            }   
    17            return instance;  
    18     }  
    19   
    20     public String getName() {  
    21         return name;  
    22     }  
    23   
    24     public void setName(String name) {  
    25         this.name = name;  
    26     }  
    27   
    28     public void printInfo() {  
    29         System.out.println("the name is " + name);  
    30     }  
    31   
    32 }  

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

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

     1 public class TMain {  
     2     public static void main(String[] args){  
     3         TestStream ts1 = TestSingleton.getInstance();  
     4         ts1.setName("jason");  
     5         TestStream ts2 = TestSingleton.getInstance();  
     6         ts2.setName("0539");  
     7           
     8         ts1.printInfo();  
     9         ts2.printInfo();  
    10           
    11         if(ts1 == ts2){  
    12             System.out.println("创建的是同一个实例");  
    13         }else{  
    14             System.out.println("创建的不是同一个实例");  
    15         }  
    16     }  
    17 }  

     运行结果:

    实验2:

    下面是饿汉模式:

     1 public class Singleton {
     2 
     3     private static final Singleton INSTANCE = new Singleton();        
     4 //    private static class LazyHolder {  
     5 //        
     6 //        private static final Singleton INSTANCE = new Singleton();    
     7 //    }  
     8     private Singleton (){
     9          System.out.println("init");
    10     }    
    11     public static final Singleton getInstance() {    
    12         System.out.println("1");
    13         return INSTANCE;    
    14     }
    15 }
    1     public static void main(String[] args) {
    2         
    3         Singleton ss = Singleton.getInstance();
    4         Singleton s2 = Singleton.getInstance();
    5         System.out.println(ss==s2);
    6     }

    输出:

    init
    1
    1
    true

    再改动一下将下面内部类注释去掉,

    private static final Singleton INSTANCE = new Singleton();屏蔽掉,这样就成了懒汉模式

    输出:

    1
    init
    1
    true

      有实验二可见,在饿汉模式下,类一旦加载,就把单例初始化完成,同时说明了类内部执行顺序符合调用原则:父类静态代码块-->子类静态代码块-->父类普通代码块-->父类构造方法-->子类代码块-->子类构造方法。

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

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

      单例模式的扩展

      如果只需要一个对象, 使用单例模式就可以了, 但是如果要求一个类只能产生两三个对象呢? 下面的例子中,最多能产生两个对象,每次使用时随机选择一个对象使用:

     1 public class Emperor {
     2 
     3     //定义最多能产生的实例数量
     4     private static int maxNumOfEmperor = 2;
     5     
     6     //每个皇帝都有名字, 使用一个ArrayList来容纳, 每个对象的私有属性
     7     private static ArrayList<String> nameList=new ArrayList<String>();
     8     
     9     //定义一个列表, 容纳所有的皇帝实例
    10     private static ArrayList<Emperor> emperorList=new ArrayList<Emperor>();
    11     
    12     //当前皇帝序列号
    13     private static int countNumOfEmperor =0;
    14     
    15     //产生所有的对象
    16     static{
    17         for(int i=0;i<maxNumOfEmperor;i++){
    18             emperorList.add(new Emperor("皇"+(i+1)+"帝"));
    19         }
    20     }
    21     
    22     private Emperor(){
    23         //世俗和道德约束你, 目的就是不产生第二个皇帝
    24     }
    25     
    26     //传入皇帝名称, 建立一个皇帝对象
    27     private Emperor(String name){
    28         nameList.add(name);
    29     }
    30     
    31     //随机获得一个皇帝对象
    32     public static Emperor getInstance(){
    33         Random random = new Random();
    34         //随机拉出一个皇帝, 只要是个精神领袖就成
    35         countNumOfEmperor = random.nextInt(maxNumOfEmperor);
    36         return emperorList.get(countNumOfEmperor);
    37     }
    38     
    39     //皇帝发话了
    40     public static void say(){
    41         System.out.println(nameList.get(countNumOfEmperor));
    42     }
    43 }

    测试结果:

     1 public class Minister {
     2     public static void main(String[] args) {
     3         //定义5个大臣
     4         int ministerNum =5;
     5         for(int i=0;i<ministerNum;i++){
     6             Emperor emperor = Emperor.getInstance();
     7             System.out.print("第"+(i+1)+"个大臣参拜的是: ");
     8             emperor.say();
     9         }
    10     }
    11 }

     引用博客:http://blog.csdn.net/jason0539http://www.cnblogs.com/ShanHeDiao/p/5265622.html

  • 相关阅读:
    工作常用mysql命令以及函数
    mybati 字段映射
    关于idea切换账号,上传的代码依旧是之前账号提交/操作git
    java 开发过程中常用
    简单了解微服务
    zookeeper 学习(二) java操作zookeeper
    zookeeper 学习(一) 初识zookeeper
    漫画:我们为何结婚,又为何不忠?
    适用 selenium 自动化的十大测试场景
    女朋友买房了,我我我....
  • 原文地址:https://www.cnblogs.com/centor/p/5728590.html
Copyright © 2011-2022 走看看