一起手撕设计模式---单例模式

1.序

今天为什么谈设计模式呢,因为设计模式对于我们找工作来说非常重要,记得我在面试华为的时候基本上把我知道的都讲了一遍,大概15-16种,因此给面试官留下了很好的印象,在面试其它的大公司的时候同样是这样,因为当时我花了很多时间把设计模式整理了一遍,所以因此受益,今天开始把每一种分享出来,今天先从单例模式开始吧,希望大家梦想成真!!!

2.单例模式初认识

单例模式是一种对象创建型模式,使用单例模式可以保证一个类只生成唯一的实例对象。也就是说在整个程序空间中,该类只存在一个实例对象。

其实就是保证一个类只有一个实例,同时提供能对该实例加以访问的全局访问方法。

在应用系统开发中,我们常常有以下需求:

1在多线程之间,比如servlet 环境。共享同一个资源或者操作同一个对象。

2在整个程序空间使用全局变量,共享资源。

3大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。

因此singleton模式可以保证为一个类只生成唯一的实例对象,所以这些情况,singleton模式就派上用场了。

3.单例模式的使用

3.1“懒汉式”与“饿汉式”的区别

所谓“懒汉式”与“饿汉式”的区别,是在与建立单例对象的时间的不同。

“懒汉式”

“懒汉式”是在你真正用到的时候才去建这个单例对象:

  1. 私有化构造器

  2. 创建一个私有的实例static 先不实例化 为 null

  3. 通过公共方法调用 static 在方法里面进行判断,if = null实例化 !=null 直接return

比如:有个单例对象

public class Singleton{
   
    private Singleton(){
   }
    private static Singleton singleton = null//不建立对象
    public static synchronized Singleton getInstance(){
   
             if(singleton == null) {
           //先判断是否为空 
                 singleton = new Singleton ();  //懒汉式做法
             }
             return singleton ;
     }
}

“饿汉式”

“饿汉式”是在不管你用的用不上,一开始就建立这个单例对象:比如:有个单例对象

:一个类只能创建一个对象

  1. 私有化构造器

  2. 在类的内部创建一个类的实例,且为static

  3. 私有化对象,通过公共方法调用

  4. 此公共方法只能通过类来调用,因为设置的是static,同时类的实例也是static

public class Singleton{
   

    public Singleton(){
   }

    private static Singleton singleton = new Singleton()//建立对象

    public static Singleton getInstance(){
   

        return singleton ;//直接返回单例对象 

   }

}

3.2 GetInstance与new区别

new的使用:如Object _object = new Object(),new就是通过生产一个新的实例对象,或者在栈上声明一个对象,每部分的调用 *都是用的一个新的对象。

GetInstance的使用:在主函数开始时调用,返回一个实例化对象,此对象是static的,在内存中保留着它的引用,即内存中有一块区域专门用来存放静态方法和变量,可以直接使用,调用多次返回同一个对象。

3.3 实现Singleton 模式 7种方式

3.3.1饿汉,常用

首先是写明私有的构造方法防止被new,然后直接就实例化,最后调用,不存在线程安全问题。

/** * 单例模式,饿汉式,线程安全 */

public static class Singleton1{
   

    private final static  Singleton instance = new Singleton();

    private Singleton1(){
   
    }

    public static Singleton getInstance(){
   

        return instance;

    }

3.3.2懒汉,不安全

可能会出现线程安全问题,因为new一个对象在JVM底层做了如下工作:

1 给 instance 分配内存

2 调用 Singleton 的构造函数来初始化成员变量

3 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

显然是不能保证原子性的。

单线程的时候工作正常,但在多线程的情况下就有问题了。设想如果两个线程同时运行到判断instance是否为null的 if语句,并且instance的确没有创建时,那么两个线程都会创建一个实例,此时类型Singletonl就不再满足单例模式的要求了。为了保证在多线程环境下我们还只能得到类型的一个实例,需要加上一个同步锁。把 Singletonl稍作修改。

3.3.3加锁的懒汉,性能低

我们还是假设有两个线程同时想创建一个实例。由于在一个时刻只有一个线程能得到同步锁,当第一个线程加上锁时,第二个线程只能等待。当第一个线程发现实例还没有创建时,它创建出一个实例。接着第一个线程释放同步锁,此时第二个线程可以加上同步锁,并运行接下来的代码。这时候由于实例己经被第一个线程创建出来了,第二个线程就不会重复创

建实例了,这样就保证了我们在多线程环境中也只能得到一个实例。但是类型Singleton2 还 是 很 完 美 。我们每次通过属性Instance得到Singleton2 的实例,都会试图加上一个同步锁,而加锁是一个非常耗时的操作,在没有必要的时候我们应该尽量避免。

/** * 单例模式,懒汉式,线程安全,多线程环境下效率不高 */

    public static class Singleton3 {
   
        private static Singleton3 instance = null;
        private Singleton3() {
   
        }
        public static synchronized Singleton3 getInstance() {
   

            if (instance == null) {
   
                instance = new Singleton3();
            }
            return instance;
        }
}

3.3.4 静态块,可以

当第一次引用getInstance()方法的时候,访问静态内部类中的静态成员变量,此时该内部类需要调用static代码块(因为首次访问该类)。而后再次访问getInstance()方***直接返回instace引用。这种做法相对于传统做法更加巧妙。

/** * 单例模式,懒汉式,变种,线程安全 */
    public static class Singleton4 {
   

        private static Singleton4 instance = null;

        static {
   
            instance = new Singleton4();
        }
        private Singleton4() {
   
        }
        public static Singleton4 getInstance() {
   

            return instance;

        }

}

3.3.5静态内部类 推荐

定义一个私有的内部类,在第一次用这个嵌套类时,会创建一个实例。而类型为SingletonHolder的类,只有在Singleton.getInstance()中调用,由于私有的属性,他人无法使用SingleHolder,不调用Singleton.getInstance()就不会创建实例。

优点:达到了lazy loading的效果,即按需创建实例。

/** * 单例模式,使用静态内部类,线程安全【推荐】 */
    public static class Singleton5 {
   

        private final static class SingletonHolder {
   

            private static final Singleton5 INSTANCE = new Singleton5();

        }
        private Singleton5() {
   
        }
        public static Singleton5 getInstance() {
   
            return SingletonHolder.INSTANCE;
        }
}

虚拟机会保证一个类的类构造器()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器(),其他线程都需要阻塞等待,直到活动线程执行()方法完毕。

特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

3.3.6枚举,推荐

/** * 静态内部类,使用枚举方式,线程安全【推荐】 */

public enum Singleton06{
   
     INSTANCE;
}

在《Effective Java》最后推荐了这样一个写法,简直有点颠覆,不仅超级简单,而且保证了线程安全。这里引用一下,此方法无偿提供了序列化机制,绝对防止多次实例化,及时面对复杂的序列化或者反射攻击。单元素枚举类型已经成为实现Singleton的最佳方法。

很多人会对枚举法实现的单例模式很不理解。这里需要深入理解的是两个点:

1 枚举类实现其实省略了private类型的构造函数

2枚举类的域(field)其实是相应的enum类型的一个实例对象

对于第一点实际上enum内部是如下代码:

public enum Singleton {
   

    INSTANCE;

    // 这里隐藏了一个空的私有构造方法

    private Singleton () {
   }

}

可以通过Singleton .INSTANCE来访问。

 

比较清楚的写法是:(底层解析下来)

 

public class SingletonExample5 {
   

    private SingletonExample5(){
   }

    public static SingletonExample5 getInstance(){
   
        return Singleton.INSTANCE.getInstance();
    }
    private enum Singleton{
   
        INSTANCE;
        private SingletonExample5 singleton;
        //JVM保证这个方法绝对只调用一次
        Singleton(){
   
            singleton = new SingletonExample5();
        }
        public SingletonExample5 getInstance(){
   
            return singleton;
        }
    }
}

为什么能保证线程安全

当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的。所以,创建一个enum类型是线程安全的。

还有一个重要的就是所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例的了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

3.3.7双重校验锁 推荐

思想:先判断一下是不是null,然后加锁,再判断一下是否为null。如果还是null,则可以放心地new。

还是不大明白为什么要判断两次null?

第二次校验:如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

/** * 静态内部类,使用双重校验锁,线程安全【推荐】 */
    public static class Singleton7 {
   
        private volatile static Singleton7 instance = null;
        private Singleton7() {
   
        }
        public static Singleton7 getInstance() {
   
            if (instance == null) {
   
                synchronized (Singleton7.class) {
   
                    if (instance == null) {
   
                        instance = new Singleton7();
                    }
                }
            }
            return instance;
        }
}

执行过程

双重校验锁方式的执行过程如下:

1.线程A进入 getInstance() 方法。

2.由于 singleton为 null,线程A在 //1 处进入 synchronized 块。

3.线程A被线程B预占。

4.线程B进入 getInstance() 方法。

5.由于 singleton仍旧为 null,线程B试图获取 //1 处的锁。然而,由于线程A已经持有该锁,线程B在 //1 处阻塞。

6.线程B被线程A预占。

7.线程A执行,由于在 //2 处实例仍旧为 null,线程A还创建一个 Singleton 对象并将其引用赋值给 instance。

8.线程A退出 synchronized 块并从 getInstance() 方法返回实例。

9.线程A被线程B预占。

10.线程B获取 //1 处的锁并检查 instance 是否为 null。

  1. 由于 singleton是非 null 的,并没有创建第二个 Singleton 对象,由线程A所创建的对象被返回。

3.3.8主函数

public static void main(String[] args) {
   

        System.out.println(Singleton.getInstance() == Singleton.getInstance());

        System.out.println(Singleton2.getInstance() == Singleton2.getInstance());

        System.out.println(Singleton3.getInstance() == Singleton3.getInstance());

        System.out.println(Singleton4.getInstance() == Singleton4.getInstance());

        System.out.println(Singleton5.getInstance() == Singleton5.getInstance());

        System.out.println(Singleton6.INSTANCE == Singleton6.INSTANCE);

        System.out.println(Singleton7.getInstance() == Singleton7.getInstance());

}

4.总结

对于单例模式的描述就到这里,希望大家多实践,真正的了解每一种单例模式,并且能手写出其中的几种,因为这个是面试中必考的题目。并且在项目中也是经常使用的,所以大家加油啊!!!

5.个人推广

博客地址

https://blog.csdn.net/weixin_41563161

全部评论

相关推荐

点赞 评论 收藏
转发
点赞 收藏 评论
分享
牛客网
牛客企业服务