java单例模式浅析

单例模式
确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。


单例模式有三个要点:
  •     某个类只能有一个实例;
  •     必须自行创建这个实例;
  •     必须自行向整个系统提供这个实例。
普通的单例模式,代码如下
public class Singleton {
      private static Singleton  instance;

      private Singleton() {
          //do something
         System. out.println( "thread id:"+Thread. currentThread().getId());
          try {
            Thread. sleep( 1000);
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

      public static Singleton getInstance() {
          if ( instance ==  null) {
              instance new Singleton();
        }
          return  instance;
    }
}
满足了一个单例模式的定义
但是,当多个线程的情况并且
做了一些工作,简单的说耗时1秒才搞定的情况,上面的单例 类Singleton 就不满足了。
代码继续,小小改造:
public class Singleton {
      private static Singleton  instance;

      private Singleton() {
          //do something
          try {
            Thread. sleep( 1000);
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

      public static Singleton getInstance(String string) {
          if ( instance ==  null) {
            System. out.println(string);
              instance new Singleton();
        }
          return  instance;
    }
}
然后客户端的代码:
public class Test {
      public static void main(String[] args) {
        Thread thread =  new Thread( new Runnable() {
              @Override
              public void run() {
                Singleton singleton1 = Singleton. getInstance();
            }
        });
        thread.start();
        Thread thread1 =  new Thread( new Runnable() {
              @Override
              public void run() {
                Singleton singleton2 = Singleton. getInstance();
            }
        });
        thread1.start();
    }
}
最后输出:
thread id:10
thread id:11
这里解决办法有两种:
先说第一种(饿汉式):继续改造单例类
public class Singleton {
      private static Singleton  instance= new Singleton();

      private Singleton() {
          //do something
         System. out.println( "thread id:"+Thread. currentThread().getId());
          try {
            Thread. sleep( 1000);
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

      public static Singleton getInstance() {

          return  instance;
    }
}
客户端代码不动,输出
hread id:10
当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。 如果使用饿汉式单例来设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。
第二种实现方式(懒汉式):
代码继续调整
public class Singleton {
      private static Singleton  instance;

      private Singleton() {
          //do something
         System. out.println( "thread id:" + Thread. currentThread().getId());
          try {
            Thread. sleep( 1000);
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

      synchronized public static Singleton getInstance() {
          if ( instance ==  null) {
              instance new Singleton();
        }
          return  instance;
    }
}
客户端依旧不变,输出
thread id:11
证明也实现了, 当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。 如果使用饿汉式单例来设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。
该单例类在getInstance()方法前面增加了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。那么问题来了,上述代码虽然解决了线程安全问题, 但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。如何既解决线程安全问题又不影响系统性能呢? 我们继续对懒汉式单例进行改进。事实上,我们无须对整个getInstance()方法进行锁定
再次对单例类进行改变,
public class Singleton {
      private volatile static Singleton  instance;

      private Singleton() {
          //do something
         System. out.println( "thread id:" + Thread. currentThread().getId());
          try {
            Thread. sleep( 1000);
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

      public static Singleton getInstance() {
          if ( instance ==  null) {
              synchronized(Singleton. class){
                  if ( instance ==  null) {
                      instance new Singleton();
                }
            }
        }
          return  instance;
    }
}
客户端依旧不变,输出
thread id:10
由于做了两次判断外加锁,所以都称之为 双重锁,
需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理, 且该代码只能在JDK 1.5及以上版本中才能正确执行。由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现 单例模式也不是一种完美的实现方式
饿汉式VS懒汉式
      饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象, 由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
      懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间, 这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响
由于上述两种方式都各有问题, 那就出现另外一种更好的方式呢?
答案是Yes
 
能够把饿汉式和懒汉式的有点合二为一,   Initialization Demand Holder (IoDH)
其实也就是静态内部类方式
单例类代码继续修改:
public class Singleton {
      private static class HolderClass {
          private final static Singleton  instance new Singleton();
    }

      private Singleton() {
          //do something
         System. out.println( "thread id:" + Thread. currentThread().getId());
          try {
            Thread. sleep( 1000);
        }  catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

      public static Singleton getInstance() {
          return HolderClass. instance;
    }
}
这次为了区分多线程的情况和两次getInstance,所以把客户端的代码调整一下;
public class Test {
      public static void main(String[] args) {
        Singleton s1=Singleton. getInstance();
        Singleton s2=Singleton. getInstance();
          if(s1.equals(s2)){
            System. out.println( "对象s1和s2为同一对象");
        }

        Thread thread =  new Thread( new Runnable() {
              @Override
              public void run() {
                Singleton singleton1 = Singleton. getInstance();
            }
        });
        thread.start();
        Thread thread1 =  new Thread( new Runnable() {
              @Override
              public void run() {
                Singleton singleton2 = Singleton. getInstance();
            }
        });
        thread1.start();
    }
}
运行,输出
thread id:1
对象s1和s2为同一对象
即创建的单例对象s1和s2为同一对象。由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量, 由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式
另外就是 枚举类来实现单例模式。这里引用一个朋友的:

最佳实践单例之枚举

  1. public enum Singleton{
  2. INSTANCE;
  3. }

这种方式的好处是:

  1. 利用的枚举的特性实现单例
  2. 由JVM保证线程安全
  3. 序列化和反射攻击已经被枚举解决

调用方式为Singleton.INSTANCE, 出自《Effective Java》第二版第三条: 用私有构造器或枚举类型强化Singleton属性。关于单例最佳实践的讨论可以看Stackoverflow:what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java


单列模式的总结
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。
1.主要 优点
       单例模式的主要优点如下:
       (1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
       (2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。
       (3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。
2.主要 缺点
       单例模式的主要缺点如下:
       (1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
       (2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
       (3) 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
3. 适用场景
       在以下情况下可以考虑使用单例模式:
       (1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。
       (2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。



欢迎关注公众号:Java后端技术全栈