单例模式的N种场景N种写法

Posted fuyouj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式的N种场景N种写法相关的知识,希望对你有一定的参考价值。

里面包括了老生常谈的饿汉式,懒汉式以及枚举类 静态代码块 序列化场景下,多线程场景下的问题。

话不多说,直接开干。
饿汉式就是立即加载的意思,立即加载在中文中有着急,急迫的意思。所以就叫饿汉式吧。
1.饿汉式的最简洁版本

package 单例模式的几种写法.饿汉式;

/**
 * @Author:FuYouJie
 * @Date Create in 2020/1/23 13:32
 */
public class Singleton1 {
    /**饿汉式:直接创建实例 不管你是否需要
     * 1.构造器私有化 外部不能直接new实例化
     * 2.自行创建 并且用静态变量保存
     * 3.声明为public 对外公开这个实例
     * 4.final 修饰
     * 构造器 私有化
     */
    public static final Singleton1 instance=new Singleton1();
    private Singleton1(){}

}

测试代码:

Singleton1 singleton1 = Singleton1.instance;
        Singleton1 s=Singleton1.instance;
        //true
        System.out.println(singleton1==s);

这里先不贴图,结果是一样的哈。==在这里比较的是对象地址。
2.枚举类的简单写法

public enum  SingletonByEnum {
    INSTANCE;
    public void doA(){
        System.out.println("AA");
    }
}

测试代码:

//枚举
        SingletonByEnum instance1 = SingletonByEnum.INSTANCE;
        SingletonByEnum instance2 = SingletonByEnum.INSTANCE;
        //true
        System.out.println(instance1.hashCode()==instance2.hashCode());

3.静态代码块的写法:

public class SingletonStatic {
    public static final SingletonStatic INSTANCE;
    private String name;
    static {
        Properties properties=new Properties();
        try {
            properties.load(SingletonStatic.class.getClassLoader().getResourceAsStream("single.properties"));
        } catch (IOException e) {
            throw  new RuntimeException();
        }
        INSTANCE = new SingletonStatic(properties.getProperty("name"));
    }
    //构造器 私有化
    private SingletonStatic(String name){
        this.name=name;
    }

    public String getName() {
        return name;
    }
    
}

这里的静态代码块实现了可以从配置文件给属性复制的功能,避免了在代码里面把属性写死的情况。
技术图片
技术图片
小结:饿汉式就是空间换时间,类加载的方式是按需加载,且只加载一次。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用。换句话说,在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。

可是常说懒人改变世界,那么就一定有懒汉式。改变世界没有我不知道,至少改变了我的字数。
懒汉式也说延迟加载,就是在调用get方法的时候才创建实例。
1.懒汉式简单版本:

//Unsafe
public class Singleton1 {
    private static Singleton1 INSTANCE;
    public static Singleton1 getInstance(){
        if (INSTANCE==null){
            try {
            //模拟准备时间
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE=new Singleton1();
        }
        return INSTANCE;
    }
    private Singleton1(){}

}

==单线程下==,这个代码是没有问题的。
测试代码:

//单线程下
        Singleton1 singleton1=Singleton1.getInstance();
        Singleton1 singleton2=Singleton1.getInstance();
        System.out.println(singleton1==singleton2);

运行结果:
技术图片
可以看出来对象是一样的。
如果是多线程呢?

首先我们创建一个可以接收返回值的Callable

Callable<Singleton1> callable=new Callable<Singleton1>() {
            @Override
            public Singleton1 call() throws Exception {
                return Singleton1.getInstance();
            }
        };

然后创建一个线程池(因为装了阿里妈妈插件,直接创建线程屏幕一片黄)。

ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,
                2,1,
                TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
                new ThreadPoolExecutor.CallerRunsPolicy());

然后提交再用Future接收

Future<Singleton1> singleton1Future = threadPoolExecutor.submit(callable);
Future<Singleton1> singleton2Future = threadPoolExecutor.submit(callable);

最后取结果

try {
            //false
            System.out.println(singleton1Future.get().hashCode()==singleton2Future.get().hashCode());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        threadPoolExecutor.shutdown();

运行结果:
技术图片
那么这是为啥呢?
在线程1进去的时候,此时对象是不存在的,遇到sleep(1000),线程1开始罚站1秒。
技术图片
在线程1罚站的时候,线程2也进来了。他们一起在刚才的位置罚站,此时一秒钟还没过去,对象依然不存在。
然后线程1罚站结束,进入后面代码,new一个对象。不久后,线程2到了后面,此时并没有非空判断,所以线程2页创建了一个对象。
解决办法,加锁。这里就使用synchronized
懒汉式方法加锁版本:

public class SingletonWithSynMethod {
    private static SingletonWithSynMethod INSTANCE;
    public synchronized static SingletonWithSynMethod getInstance(){
        if (INSTANCE==null){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE=new SingletonWithSynMethod();
        }
        return INSTANCE;
    }
    private SingletonWithSynMethod(){}
}

测试一下:

 class TestSingletonWithSynMethod{
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,
                2,1,
                TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
                new ThreadPoolExecutor.CallerRunsPolicy());
        Callable<SingletonWithSynMethod>singleton1=new Callable<SingletonWithSynMethod>() {
            @Override
            public SingletonWithSynMethod call() throws Exception {
                return SingletonWithSynMethod.getInstance();
            }
        };
    Future<SingletonWithSynMethod> future1= threadPoolExecutor.submit(singleton1);
    Future<SingletonWithSynMethod> future2= threadPoolExecutor.submit(singleton1);
        try {
            //true
            System.out.println(future1.get()==future2.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        threadPoolExecutor.shutdown();

    }
}

技术图片
但是本着锁块不锁方法的意思,我们可以改变一下synchronized的位置。

public class SingletonWithSynBlock {
    private static SingletonWithSynBlock INSTANCE;
    public  static SingletonWithSynBlock getInstance(){
        if (INSTANCE==null){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (SingletonWithSynBlock.class){
                INSTANCE=new SingletonWithSynBlock();
            }
        }
        return INSTANCE;
    }
    private SingletonWithSynBlock(){}
}

你以为这就完事了?
我们运行一下。
运行代码:

class TestSingletonWithSynBlock{
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,
                2,1,
                TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
                new ThreadPoolExecutor.CallerRunsPolicy());
        Callable<SingletonWithSynBlock> singleton1=new Callable<SingletonWithSynBlock>() {
            @Override
            public SingletonWithSynBlock call() throws Exception {
                return SingletonWithSynBlock.getInstance();
            }
        };
        Future<SingletonWithSynBlock> future1= threadPoolExecutor.submit(singleton1);
        Future<SingletonWithSynBlock> future2=  threadPoolExecutor.submit(singleton1);
        try {
            //false
            System.out.println(future1.get()==future2.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        threadPoolExecutor.shutdown();
    }
}

技术图片
因此我们引入DCL双检查机制
代码:

public class SingletonWithDCL {
    private volatile static SingletonWithDCL INSTANCE;
    public static SingletonWithDCL getInstance(){
        try {
            if (INSTANCE != null){
            }else {
                Thread.sleep(3000);
                synchronized (SingletonWithDCL.class){
                    if (INSTANCE == null){
                        INSTANCE=new SingletonWithDCL();
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return INSTANCE;
    }
    private SingletonWithDCL(){}
}

测试代码:

class TestSingletonWithDCL{
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,
                2,1,
                TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
                new ThreadPoolExecutor.CallerRunsPolicy());
        Callable<SingletonWithDCL> singleton1=new Callable<SingletonWithDCL>() {
            @Override
            public SingletonWithDCL call() throws Exception {
                return SingletonWithDCL.getInstance();
            }
        };
        Future<SingletonWithDCL> future1= threadPoolExecutor.submit(singleton1);
        Future<SingletonWithDCL> future2=   threadPoolExecutor.submit(singleton1);
        try {
            //true
            System.out.println(future1.get()==future2.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

运行:
技术图片
技术图片
懒汉模式也是可以用内部类的

public class SingletonWithInner {
    /**
     * 内部类不会随着外部类的加载初始化
     */
    private static class MyClassHandler{
        private static SingletonWithInner INSTANCE=new SingletonWithInner();
    }
    public static SingletonWithInner getInstance(){
        return MyClassHandler.INSTANCE;
    }
    private SingletonWithInner(){}

}

但是没有这样的便宜,内部类在序列化的时候会有问题。
重写内部类

public class SingletonAndSer implements Serializable {
    private static final long serialVersionUID=888L;
    //内部类
    private static class MyClassHandler{
        private static final SingletonAndSer INSTANCE=new SingletonAndSer();
    }
    private SingletonAndSer(){}
    public static SingletonAndSer getInstance(){
        return MyClassHandler.INSTANCE;
    }
//  protected Object readResolve() throws ObjectStreamException{
//      System.out.println("调用了本方法!");
//      return MyClassHandler.INSTANCE;
//  }
}

测试类:

class SaveAndRead{
    public static void main(String[] args) {
            SingletonAndSer singletonAndSer=SingletonAndSer.getInstance();
            try {
            FileOutputStream fosRef=new FileOutputStream(new File("test.txt"));
            ObjectOutputStream oosRef=new ObjectOutputStream(fosRef);
            oosRef.writeObject(singletonAndSer);
            oosRef.close();
            fosRef.close();
            System.out.println(singletonAndSer.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }


        try {
            FileInputStream fileInputStream=new FileInputStream(new File("test.txt"));
            ObjectInputStream objectInputStream=new ObjectInputStream(fileInputStream);
            SingletonAndSer singletonAndSer1= (SingletonAndSer) objectInputStream.readObject();
            objectInputStream.close();
            fileInputStream.close();


            System.out.println(singletonAndSer1.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

运行:
技术图片
纳尼??
如果在反序列化的时候没有指定readResolve 那么还是多例的。
解决办法就是把注释去掉。
添加readResolve方法。

protected Object readResolve() throws ObjectStreamException{
        System.out.println("调用了本方法!");
        return MyClassHandler.INSTANCE;
    }

再运行:
技术图片
好了,到这里就差不多结束啦。但是最开始的枚举类直接暴露,违反了“职责单一原则”,现在来完善完善。
以Connection为例。

/**
 * @Author:FuYouJie
 * @Date Create in 2020/1/23 16:40
 */
public class Connection {
    String url;
    String name;

    public Connection(String url, String name) {
        this.url = url;
        this.name = name;
    }
}
public class MyObject {
    public enum MyEnumSinggleton{
        //工厂
        connectionFactory;
        private Connection connection;
        private MyEnumSinggleton(){
                System.out.println("创建MyObject");
                String url="jdbc:mysql://127.0.0.1:3306/bookdb?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false";
                String username="root";
                connection=new Connection(url,username);
        }
        public Connection getConnection(){
            return connection;
        }
    }
    public static Connection getConnection(){
        return MyEnumSinggleton.connectionFactory.getConnection();
    }

}

测试代码:

class TestMyObject{
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(5,
                5,1,
                TimeUnit.MINUTES,new LinkedBlockingQueue<>(5),
                new ThreadPoolExecutor.CallerRunsPolicy());
        Callable<Connection> singleton1=new Callable<Connection>() {
            @Override
            public Connection call() throws Exception {
                return MyObject.getConnection();
            }
        };
        for(int i=0;i<5;i++){
            Future<Connection> future= threadPoolExecutor.submit(singleton1);
            System.out.println(future.get());
        }
        threadPoolExecutor.shutdown();
    }
}

运行:
技术图片
好嘞,好累。本人微信qazwsxFuYouJie
扔砖可以加微信。
GitHub代码地址:JaveSE知识点

以上是关于单例模式的N种场景N种写法的主要内容,如果未能解决你的问题,请参考以下文章

单例模式的七种写法

C# 单例模式的五种写法

C#单例模式的三种写法

单例模式的六种花式写法

Android设计模式之单例模式的七种写法

设计模式之单例模式的几种写法——java