欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

设计模式系列之单例模式(五种写法)

程序员文章站 2024-01-24 11:09:28
...

前言

设计模式是我们程序员应该要掌握的,可能没有用过,但是至少听过。毕竟没吃过猪肉,哪还能没见过猪跑,那么什么是设计模式呢?

设计模式 是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

反正很重要就对了,而且在找工作的时候也是经常问到的,通常会有面试官让你手写个设计模式,最常见的就是单例模式了,接下来我们就来看一下单例模式到底是什么?

单例模式保证一个类仅有一个实例,并提供一个访问它的全局访问点。

饿汉式

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全。
 * 缺点:不管用到与否,类加载的时候就完成实例化
 */
public class singleton1 {
    private static final singleton1 INSTANCE=new singleton1();//定义一个静态的实例INSTANCE为singleton1
    private singleton1(){};
    public static singleton1 getInstance(){
        return INSTANCE;
    }
}

饿汉式可以与接下来的懒汉式相对比,在这个类加载的时候就完成了实例化,通过将构造方法私有化,也就是别人无法直接将这个对象给new出来,毕竟private只属于这个类,如果想使用这个对象,那么就可以通过singleton1 s=singleton1.getInstance()来实现。这一种是线程安全的。

懒汉式

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 * 懒汉式:lazy loading
 * 用的时候才进行初始化,但是存在线程不安全的问题
 */
public class singleton2 {
    private static singleton2 INSTANCE;
    private singleton2(){};
    public static singleton2 getInstance(){
        if (INSTANCE==null){
        //线程一进行,判断了为空,那么就要去实例化,而第二个线程进行的时候
        //也是判断为空,那么也会去实例化,这样就生成了不止一个对象
            INSTANCE=new singleton2();
        }
        return INSTANCE;
    }
}

懒汉式可以与饿汉式进行对比理解,饿汉式是在加载的时候就完成了实例化,而懒汉式则是类加载完之后并不实例化,当需要的时候,再去进行实例化。
但是这种写法存在线程不安全的问题。如代码中的位置,当线程一进入,判断为空,那么就要去实例化,而此时第二个线程进入了,也是判断为空,那么也会去实例化,如果有多个线程,那么生成的就不止一个对象了。为了验证我们的猜想,我们写一个例子来测试一下:

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 * 懒汉式:lazy loading
 * 用的时候才进行初始化,但是存在线程不安全的问题
 */
public class singleton2 {
    private static singleton2 INSTANCE;
    private singleton2(){};
    public static singleton2 getInstance(){
        if (INSTANCE==null){//在多线程情况下,如果第一个线程判断INSTANCE为空,那么
            //就会去执行INSTANCE=new singleton2(),但是如果在实例化的过程中,另一个
            //线程也进来了,判断出INSTANCE为空,那么他也会去实例化singleton2,这样
            //的话,会实例化两个singleton2,生成两个singleton2,
            try {
                Thread.sleep(1);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            INSTANCE=new singleton2();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i=0;i<20;i++){
            new Thread(()->{
                System.out.println(singleton2.getInstance().hashCode());
            }).start();
        }
    }
}

运行结果:

124791833
636619253
1175053369
805244208
2109215169
1834323973
1582315003
1781696970
805244208
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970
1781696970

我们知道,在一个方法生成的对象如果hashCode不相同,那么就说明并不是一个对象,因此,这种方法是存在线程安全问题滴。

加锁的懒汉式

在并发编程中,有个关键字叫做synchronized,那么我们能否通过对懒汉式加锁来实现线程安全呢?答案是可以的:

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 * 加synchronized锁实现线程安全,但是加锁会降低性能
 */
public class singleton3 {
    private static singleton3 INSTANCE;
    private singleton3(){};
    public static synchronized singleton3 getInstance(){
        if (INSTANCE==null){
            INSTANCE=new singleton3();
        }
        return INSTANCE;
    }
}

通过在方法上加锁,保证在运行方法的时候,不会被其它线程使用。当然了,使用synchronized锁也就会导致性能会有所下降。

双检锁

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 * 双检锁
 */
public class singleton5 {
    private static volatile singleton5 INSTANCE;
    private singleton5(){};
    public static singleton5 getInstance(){
        if (INSTANCE==null){//如果不判断的话,也可以,只不过每次都要进行加锁,消耗性能
            synchronized (singleton5.class){
                if (INSTANCE==null){
                    try {
                        Thread.sleep(10);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    INSTANCE=new singleton5();
                }
            }
        }
        return INSTANCE;
    }

双检锁这种方式通过两次判断是否为空来创建对象以及synchronized锁来保证线程安全。那么第一个未加锁的判断是否可以省去呢?
这个方法的含义就是说:如果INSTANCE为空,那么加上锁,再去判断是否为空,如果成立,才去实例化对象。第一个判断从线程安全的角度来看,是可以省略去的,但是会造成每次判断都要去获取到synchronized锁,因此会稍微降低下性能。
看到这里,你是否会有个疑惑:为什么要判断两次?还要加锁,直接判断为空就创建对象呗,还整那么麻烦干嘛?OK,我们来看下下面的例子:

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 */
public class singleton4 {
    private static singleton4 INSTANCE;
    private singleton4(){};
    public static singleton4 getInstance(){
        //不是线程安全,第一个线程进来判断为null,还没有拿到锁的时候,又进来
        //其它线程拿到了锁,实例化对象之后释放了锁,而之前的线程因为判断过为Null了,
        //所以会拿到所继续实例化对象,造成线程不安全
        if (INSTANCE==null){
            synchronized (singleton4.class){
                INSTANCE=new singleton4();
            }
        }
        return INSTANCE;
    }
}

我们假设有多个线程同时进来,线程一进来的时候,一看还没有实例化,正要执行实例化的时候,线程二、三、四…都进来了,正好也发现没有实例化,都去实例化了,当线程一将对象实例化之后,虽然已经有了对象,但是对于线程二、三、四来说,他们是经过了判断,认为是没有的,那么必然要去实例化,所以就会生成多个对象,为了验证我们的猜想,写个例子跑一下:

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 */
public class singleton4 {
    private static singleton4 INSTANCE;
    private singleton4(){};
    public static singleton4 getInstance(){
        //不是线程安全,第一个线程进来判断为null,还没有拿到锁的时候,又进来
        //其它线程拿到了锁,实例化对象之后释放了锁,而之前的线程因为判断过为Null了,
        //所以会拿到所继续实例化对象,造成线程不安全
        if (INSTANCE==null){
            synchronized (singleton4.class){
                try {
                    Thread.sleep(10);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                INSTANCE=new singleton4();
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i=0;i<20;i++){
            new Thread(()->{
                System.out.println(singleton4.getInstance().hashCode());
            }).start();
        }
    }
}

执行结果:

1781696970
1998854885
1756277772
749748823
1274452530
1119442503
1627956625
1117110646
1343599390
1926683415
122651855
757961580
588558273
805244208
1175053369
636619253
2109215169
1834323973
1582315003
124791833

emmm,这个结果就离谱,这么多不一样的,这也证明了这种方式是不安全的。

静态内部类

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 * 静态内部类
 */
public class singleton6 {
    private singleton6(){};
    private static class singleton6Holder{
        private final static singleton6 INSTANCE=new singleton6();
    }
    public static singleton6 getInstance(){
        return singleton6Holder.INSTANCE;
    }
}

在前面的写法中,懒汉式需要考虑线程安全,饿汉式利用类加载的特性帮助我们省去了对线程安全的考虑,那么,将这两者结合就是这中静态内部类的方式。既能保证线程安全,又能延迟加载。静态内部类的特性是使用的时候才去加载,而加载的时候又是线程安全的。

枚举

package com.dong.patten.singleton;

/**
 * @author 雪浪风尘
 * @Remember Keep thinking
 * 不仅可以解决线程安全问题,还可以防止序列化
 * 不会被反序列化的原因:枚举类没有构造方法,因此即使拿到这个对象的class文件,也无法构造它的对象,它返回的只是那么INSTANCE,
 */
public enum  singleton7 {

    INSTANCE;

    public void n(){
        //业务代码
    }
}

这种据说是最完美的写法,不仅简洁,还能够防止序列化,因为枚举类没有构造方法,因此即使拿到这个对象的class文件,也无法构造它的对象,它返回的只是那么INSTANCE,

git地址

git地址:
https://github.com/PonnyDong/pattern
后续在github上会持续更新设计模式的相关代码。