代码改变世界

设计模式-单例模式

2019-04-26 18:36  QQ~sunshine  阅读(249)  评论(0编辑  收藏  举报

单例,顾名思义全局只能有一个实例对象。

基本原理就是

  1. 使构造方法私有化(不能随便的通过构造方法创建实例来保证单例);

  2. 有一个可以获取实例的静态方法(因为自己不可以创建实例,所以该方法必须是静态的,否则无法调用);

  3. 有一个静态私有的局部变量指向自己(私有是因为不可外部调用,静态是因为2中的静态方法需要引用)。

单例模式又有饿汉式和懒汉式之分

  按名字理解,饿汉式比较急,不管用没用到直接先创建一个实例,好处是快,在需要的时候直接用,坏处是占用资源,因为如果没有用到,它也创建好在那放着:

 1 //单例模式-饿汉式-线程安全
 2 public class Singleton{
 3     //静态私有的全局变量
 4     private static Singleton singleton = new Singleton();
 5     //构造方法私有化,不允许外部创建
 6     private Singleton(){}
 7     //获取实例的静态方法,直接调用即可
 8     public static Singleton getInstance(){
 9         return singleton;
10     }
11 }

  懒汉式比较懒,等到用的时候才会创建出一个实例,好处是不会浪费资源,坏处是初次调用有一个创建的时间:

 1 //单例模式-懒汉式-非线程安全
 2 public class Singleton{
 3     //静态私有的全局变量
 4     private static Singleton singleton;
 5     //构造方法私有化,不允许外部创建
 6     private Singleton(){}
 7     //获取实例的静态方法,直接调用即可
 8     public static Singleton getInstance(){
 9         //判断实例是否为空,如果为空也就是第一次调用则创建一个实例对象
10         if(singleton == null){
11             singleton = new Singleton();
12         }
13         return singleton;
14     }
15 }

  饿汉式是线程安全的,因为它是在类加载时直接初始化了一个实例,多个线程同时调用也只是这一个实例;而懒汉式则是非线程安全的,因为它是调用的时候才会创建实例,这样就有一个问题,当多个线程假如同时到了判断为空的逻辑时,就会每个线程创建一个实例,这样就违背了单例的规则。

  为了保证线程安全我们可以使用synchronized关键字进行方法同步或代码块,建议只同步判断为空的代码块,因为如果同步方法同步的作用域会比较大,锁的粒度比较粗,效率会低。下面同步方法的代码已经注释掉,保留了同步代码块的形式,这样即可保证线程安全。

 1 //单例模式-懒汉式-线程安全
 2 public class Singleton{
 3     //静态私有的全局变量
 4     private static Singleton singleton;
 5     //构造方法私有化,不允许外部创建
 6     private Singleton(){}
 7     //获取实例的静态方法,直接调用即可
 8     /*public static synchronized Singleton getInstance(){
 9         //判断实例是否为空,如果为空也就是第一次调用则创建一个实例对象
10         if(singleton == null){
11             singleton = new Singleton();
12         }
13         return singleton;
14     }*/
15 
16     //获取实例的静态方法,直接调用即可
17     public static Singleton getInstance(){
18         synchronized (Singleton.class) {
19             //判断实例是否为空,如果为空也就是第一次调用则创建一个实例对象
20             if (singleton == null) {
21                 singleton = new Singleton();
22             }
23         }
24         return singleton;
25     }
26 }

   但这样因为有同步代码块效率其实还是低,我们可以使用内部类的方式实现:

 1 //单例模式-懒汉式-线程安全
 2 public class Singleton{
 3     //私有内部类,需要时进行加载
 4     private static class Inner {
 5         private static Singleton singleton = new Singleton();
 6     }
 7     //构造方法私有化,不允许外部创建
 8     private Singleton(){}
 9     //获取实例的静态方法,直接调用即可
10     public static Singleton getInstance(){
11         return Inner.singleton;
12     }
13 }

   这样既保证了效率(因为是在使用时才进行的实例初始化),也保证了线程安全问题(同饿汉式相似,是在内部类加载时直接进行的初始化)。

  还有一种,使用双重检查的方式创建单例,也可以提高运行效率:

 1 //单例模式-懒汉式-双重检查-线程安全
 2 public class Singleton{
 3     //静态私有的局部变量,使用volatile关键字防止重排序,因为new Singleton()的操作不是原子的,可能会创建一个不完整的实例
 4     private static volatile Singleton singleton;
 5     //构造方法私有化,不允许外部创建
 6     private Singleton(){}
 7     //获取实例的静态方法,直接调用即可
 8     public static Singleton getInstance(){
 9         //Double-Check
10         if(singleton == null){
11             synchronized (Singleton.class){
12                 //只需要在第一次创建实例时进行同步
13                 if(singleton == null){
14                     singleton = new Singleton();
15                 }
16             }
17         }
18         return singleton;
19     }
20 }

  如上所示,为了保证单例的条件下提高效率,我们对唯一实例进行了二次检查,这样只有第一次创建实例时会进入同步块,但是需要注意,这种方式必须使用volatile修饰实例。

  因为创建实例的过程不是原子的(1.分配内存空间;2.初始化对象;3.使实例指向刚分配的内存地址),这个过程可能发生无序写入(改变指令顺序),假如有两个线程,第一个首次调用无疑会创建实例,在创建实例的时候指令顺序变成了1 3 2,在执行完第三步而没有初始化对象的时候,另一个线程到了第一个判断为空的逻辑,这时候实例不为空会直接返回,但是也没有进行初始化,这样就会导致程序出现问题。而volatile关键字正好可以解决这个问题。

  总结以上实现单例模式一共六种:

  1. 传统饿汉式,线程安全;

  2. 传统懒汉式,非线程安全;

  3. 使用synchronized关键字修饰方法的懒汉式,线程安全;

  4. 使用synchronized关键字修饰代码块的懒汉式,线程安全;

  5. 使用内部类延迟加载的懒汉式,线程安全;

  6. 使用双重检查的懒汉式,线程安全;

  到这里单例模式基本已经介绍完,但是需要注意的是:单例模式如果使用反射的方式依然是可以创建多个实例的,因为反射可以拿到私有的构造方法,就可以自己创造实例,所以我们使用单例模式要禁止使用反射进行对象的创建。