设计模式——一步步实现《单例模式》

在一个程序中,如果想要一个类的实例,我们知道可以使用new来实例化一个。如果在程序中调用了两次new xxx(),那么这两个对象都是不一样的。即使他们的每个属性的值都一样,但是他们在内存中储存的地址是不同的。
在工作中经常会遇到这样的需求:某个类在整个程序中,只需要一个实例化对象。这时候就需要用到单例模式了。

模式有什么特点?

  1. 单例模式只允许有一个实例。

    比如:一个Student类,只有一个学生——小明。小明在这个学校中就是单例的。

  2. 单例模式只能自己创建自己的实例。

    我们知道每次new的时候,都会产生一个新的对象。而且就算对象的属性一样,他们在内存中储存的地址并不相同。所以为了实现单例,就不能让别的类来new对象,而需要类自己去。

  3. 单例类必须提供给其他对象获取这一实例的方法。

    因为不允许其他类通过new来创建实例,就必须提供一个方法来获取这个单例。

单例模式的的实现思路

  1. 首先不能让别的类来new单例类,所以我们可以给单例类的无参构造函数加让private关键字。这样,就只有单例类内部能够调用构造函数了。也就满足了上面的第二点。
public class SimpleSingleton {
    //将唯一的一个无参构造函数设置成私有,防止其他地方通过new来获取单例对象从而破坏单例。
    private SimpleSingleton(){}
}
  1. 既然只能在类内部实现,但是别的类还需要获取这个实例要怎么办呢?可以提供一个static修饰的方法暴露给外部,让别的类通过这个方法来获取实例。如:getInstance()
public class SimpleSingleton {
    //在类加载的时候实力一个单例对象
    private static SimpleSingleton simpleSingleton=new SimpleSingleton();
    //将唯一的一个无参构造函数设置成私有,防止其他地方通过new来获取单例对象从而破坏单例。
    private SimpleSingleton(){}
    //获取单例的实例
    public static SimpleSingleton getInstance(){
        return simpleSingleton;
    }
}

使用一个静态属性simpleSingleton指向单例的实现。上面的这种实现单例的方式也叫饿汉单例模式

  1. 测试一下
public class Test {
    public static void main(String[] args) {
        SimpleSingleton s1=SimpleSingleton.getInstance();
        SimpleSingleton s2=SimpleSingleton.getInstance();
        System.out.printf("两个实例是否是同一个实例:"+(s1==s2));
        //两个实例是否是同一个实例:true
    }
}

饿汉单例模式有什么缺点?

饿汉模的缺点就是一旦我访问了这个单例类的任何静态方法,就会生成实例。就算这个单例从头到尾都没使用过,它也会始终存在内存中。这样的单例如果多了,就会造成内存资源的浪费。我们想要的是仅仅当我们需要使用这个单例的时候,才会生成实例。这就需要使用单例模式中的懒汉实现方法了。

懒汉单例模式(线程不安全)

懒汉单例模式也就是单例模式的lazy-loading(懒加载)效果。也就是当第一次获取这个单例的时候才会去创建它的实例,之后再获取就不会在创建。

//单例模式-懒汉模式
public class LazySingleton {
    //私有化构造函数
    private LazySingleton(){}

    //内部实例对象的引用先指向空
    private static LazySingleton lazySingleton=null;

    //获取实例对象
    public LazySingleton getInstance(){
        //判断是实例对象的引用是否为空。
        //如果是null说明是第一次引用,所以要实例化一个对象。
        if(lazySingleton == null){
            //创建一个
            lazySingleton=new LazySingleton();
        }
        //返回唯一实例
        return lazySingleton;
    }
}

但是这样写是线程不安全的。假如有“线程A”和“线程B”同时需要使用这个实例。可能会发生这种情况:当线程A第一次调用getInstance()方法获取单例,这时候判断出lazySingleton为空,进入了if语句。这时候线程A释放了资源,线程B开始执行了,它也同样第一次调用getInstance()方法获取单例,这时候判断出lazySingleton为空,进入了if语句。这时候“线程A”和“线程B”都会执行new LaySingleton()操作,这样便会有两个不同的实例。

我们来写两个线程来测一下。

public class Test {
    public static void main(String[] args) {
        Thread t1=new Thread(){
            @Override
            public void run() {
                System.out.println(LazySingleton.getInstance());
            }
        };

        Thread t2=new Thread(){
            @Override
            public void run() {
                System.out.println(LazySingleton.getInstance());
            }
        };
        t1.start();
        //输出:com.dbwos.singleton.LazySingleton@2f5aff2b
        t2.start();
        //输出:com.dbwos.singleton.LazySingleton@59e72e28
    }
}

上面的测试例子我运行了5遍就出现了问题,两次输出的结果是不同的实例。

懒汉单例模式(线程安全)

解决上面的线程安全的问题第一个想到的就是使用synchronized关键字来确保线程安全。那还不简单,伸手就来。
把上面的方法改成下面的代码。

    //获取实例对象
    public LazySingleton synchronized getInstance(){
        //判断是实例对象的引用是否为空。
        //如果是null说明是第一次引用,所以要实例化一个对象。
        if(lazySingleton == null){
            //创建一个
            lazySingleton=new LazySingleton();
        }
        //返回唯一实例
        return lazySingleton;
    }

但是这样是不是太浪费了效率了。其实只需要吧if语句进行同步就行了,而像return这样的语句并不需要同步他们的。这时候就可以使用同步代码块来同步指定的几行代码了。
代码修改成下面这个样子:

//单例模式-懒汉模式
public class LazySingleton {
    //私有化构造函数
    private LazySingleton(){}

    //内部实例对象的引用先指向空
    private static LazySingleton lazySingleton=null;
    //获取实例对象
    public LazySingleton getInstance(){
        //同步代码块
        synchronized (Singleton.class){
            //判断是实例对象的引用是否为空。
            //如果是null说明是第一次引用,所以要实例化一个对象。
            if(lazySingleton == null){
                //创建一个
                lazySingleton=new LazySingleton();
            }
        }
        //返回唯一实例
        return lazySingleton;
    }
}

懒汉单例模式(线程安全、双重校验锁)

但是上面的这种实现还是有效率上的浪费的,因为每次判断单例的引用字段是否为空的时候,都是在同步代码块里面的。但是大多数情况下这个lazySingleton是不为空的,但是每次获取的时候都要加锁。所以我们可以在同步代码块外面再加一个if判断。
修改代码如下:

//单例模式-双重校验锁  
public class LazySingleton {
    //私有化构造函数
    private LazySingleton(){}

    //内部实例对象的引用先指向空
    private static LazySingleton lazySingleton=null;
    //获取实例对象
    public LazySingleton getInstance(){
        if(lazySingleton == null){
            //同步代码块
            synchronized (Singleton.class){
                //判断是实例对象的引用是否为空。
                //如果是null说明是第一次引用,所以要实例化一个对象。
                if(lazySingleton == null){
                    //创建一个
                    lazySingleton=new LazySingleton();
                }
            }
        }
        //返回唯一实例
        return lazySingleton;
    }
}

再深入考虑一下

上面我们已经处理的很完美了,满足多线程安全,也不怎么损耗效率,也可以保证是单例了。但是这样就一定不会有问题了嘛?当然会有问题,要不也不会这么问。但是问题在哪里呢?
首先看看JVM(java虚拟机)在创建一个对象的时候要执行以下几个关键步骤:

  1. 分配一块内存用于储存需要创建的对象。
  2. 初始化构造器,构造一个实例化对象。
  3. 将对象指向分配的内存。
    如果按照上面的的顺序执行,是没有问题的。但是JVM为了调优,可能会修改执行的顺序。比如:执行1、3、2。在执行完步骤3的时候,此时还没有实例化完对象。这时候如果另一个线程调用了getInstance(),那么会认为lazySingleton不是null,但实际上对象是没有被实例化的。这就相当于你买了个房子,开发商告诉了你地址。你兴奋极了,急急忙忙搬了过去,到了地方才发现房子还没有建,或者还没建成,全部不是很懵逼。。

那应该怎么解决呢?

再优化一下

要解决上面的问题,可以使用volatile关键字。使用这个关键字修饰的属性,无论哪个线程修改了其值,其他线程也会立马知道这个值被修改了。使用volatile关键字,JVM不会对指令的执行顺序进行优化,也就不会出现上面的问题了。这个是虚拟机级别保证的。
代码修改如下:

//单例模式-双重校验锁  
public class LazySingleton {
    //私有化构造函数
    private LazySingleton(){}

    //内部实例对象的引用先指向空
    //添加volatile关键字
    private static volatile LazySingleton lazySingleton=null;

    //获取实例对象
    public LazySingleton getInstance(){
        if(lazySingleton == null){
            //同步代码块
            synchronized (Singleton.class){
                //判断是实例对象的引用是否为空。
                //如果是null说明是第一次引用,所以要实例化一个对象。
                if(lazySingleton == null){
                    //创建一个
                    lazySingleton=new LazySingleton();
                }
            }
        }
        //返回唯一实例
        return lazySingleton;
    }
}

volatile关键字是JDK1.5之后才出现的,所以如果项目使用的是JDK1.5之前的远古版本,就不要使用volatile。

换一种写法?内部类实现单例

上面的实现可以说比较完美的实现了单例模式了。但是我们可以发现代码比较啰嗦,比较复杂。而且只支持JDK1.5以后的版本。
那么我们可以使用内部类的方式来实现单例模式。
代码如下:

public class InnerSingleton {

    //私有构造函数,防止其他类实例化
    private InnerSingleton(){}

    //提供对外的获取单例方法
    public static InnerSingleton getInstance(){
        //返回一个内部类的属性
        return Inner.innerSingleton;
    }

    //内部类
    private static class Inner{
        //只有这个内部类第一次被调用的时候,才会实例化InnerSingleton,而且只会执行一次。
        private static InnerSingleton innerSingleton=new InnerSingleton();
    }
    
}

我们来看看上面的例子为什么可以保证单例:

  1. Inner是InnerSingleton的内部类,所以它可以调用构造方法。
  2. getInstance()方法是通过获取内部类Inner中的属性innerSingleton获取单例的。
  3. Inner的属性innerSingleton只会在其被调用的时候初始化一次,这是JVM的功劳。

JVM保证了一个类的静态属性只会在第一次加载的时候初始化一次,也不用担心多线程的问题,因为JVM替我们保证了在初始化完成前,是不能使用这个属性的。

posted @ 2020-04-26 11:56  BobCheng  阅读(143)  评论(0编辑  收藏  举报