zoukankan      html  css  js  c++  java
  • 设计模式——一步步实现《单例模式》

    在一个程序中,如果想要一个类的实例,我们知道可以使用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替我们保证了在初始化完成前,是不能使用这个属性的。

    作者:BobC

    文章原创。如你发现错误,欢迎指正,在这里先谢过了。博主的所有的文章、笔记都会在优化并整理后发布在个人公众号上,如果我的笔记对你有一定的用处的话,欢迎关注一下,我会提供更多优质的笔记的。
  • 相关阅读:
    Linux下分析某个进程CPU占用率高的原因
    Linux下查看某一进程所占用内存的方法
    jbd2导致系统IO使用率高问题
    Linux iotop命令详解
    1.Redis详解(一)------ redis的简介与安装
    Redis详解(十三)------ Redis布隆过滤器
    12.Redis详解(十二)------ 缓存穿透、缓存击穿、缓存雪崩
    面试问题总结
    算法与数据结构基础<二>----排序基础之插入排序法
    CarSim、Adams、Cruise和Simulink四款仿真软件的对比
  • 原文地址:https://www.cnblogs.com/Eastry/p/12760797.html
Copyright © 2011-2022 走看看