美文网首页
Java设计模式之——单例模式

Java设计模式之——单例模式

作者: 南国的小狼 | 来源:发表于2018-03-12 02:17 被阅读402次

题引:

总结自己学习Java常用设计模式后的理解,本篇为单例模式。

未知的事物往往令人不知所措,为了透彻理解单例模式,我们必须知道:

什么是Java中的单例模式?

百度百科给出了Java中单例模式概念的内涵,Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”

由定义我们可以很清晰的抽象出:

实现Java单例模式类有哪些通用设计规则?

(1)私有化类构造器。
(2)定义静态私有的类对象。
(3)提供公共静态的获取该私有类对象的方法。

了解了单例模式的概念,以及单例模式的通用设计规则,对于如何实现一个Java单例,相信我们已不再感觉束手无策。但在了解具体实现之前,我们还需要了解:

Java单例模式解决了什么问题?

Java的单例模式主要解决了多线程并发访问共享资源的线程安全问题。

Java单例模式主要应用场景有哪些?

1.共享资源的访问与操作场景,如Windows系统的资源管理器,Windows系统的回收站,显卡的驱动程序,系统的配置文件,工厂本身(类模板),应用程序的日志对象等。
2.控制资源访问与操作的场景,如数据库的连接池,Java的线程池等。

了解了Java单例模式出现的缘由以及出现的场合,那么,Java的单例究竟是以怎样的方式出现在这些场合中呢?

Java中单例模式的常用实现方式有哪些?

本文主要介绍4中常用的Java单例实现方式:饿汉式、懒汉式、注册登记式、序列化与反序列化式。

饿汉式

饿汉式,顾名思义,就是指在JVM首次访问到该单例类时,就会把该单例类对象创建出来,并保存在内存中,不管后续是否会使用到这个单例。

/**
 * @author jiangsj
 * 饿汉式单例
 * 优点: (1)没有加任何的锁,执行效率较高
 *       (2)线程安全
 * 缺点: 类加载的时候就初始化,后续不一定会使用该实例,导致内存浪费
 */
public class HungrySingleton {
    // 1.私有化类构造器
    private HungrySingleton() {}

    // 2.定义静态私有的类对象
    private static HungrySingleton instance = new HungrySingleton();

    // 3.提供公共静态的获取该私有类对象的方法
    public static HungrySingleton getInstance() {
        return instance;
    }
}

懒汉式

懒汉式,相比于饿汉式,它是指JVM首次访问到该单例类时,并不会实例化该单例类对象,只有等到后续被外部调用时,才会实例化该单例类对象。

饿汉式的写法比较固定,懒汉式由于延时加载的特性,写法上有一些变化,一般来说,懒汉式存在4个变种。

懒汉式1
/**
 * @author jiangsj
 * 懒汉式1单例
 * 优点: 由于懒汉式延时加载特性,使用该实例时才实例化,节省了内存资源
 * 缺点: (1)该种实现方式存在线程不安全问题
 *       (2)反序列化,反射与克隆可破坏单例
 */
public class LazySingleton1 {
    // 1.私有化类构造器
    private LazySingleton1() {}

    // 2.定义静态私有的类对象
    private static LazySingleton1 instance = null;

    // 3.提供公共静态的获取该私有类对象的方法
    public static LazySingleton1 getInstance() {
        if (instance == null) {
            instance = new LazySingleton1();
        }

        return instance;
    }
}
懒汉式2
/**
 * @author jiangsj
 * 懒汉式2单例
 * 优点: (1)由于懒汉式延时加载特性,使用该实例时才实例化,节省了内存资源
 *       (2)线程安全
 * 缺点: (1)给获取实例的公共方法加上同步锁synchronized,性能受到影响
 *       (2)反序列化,反射与克隆可破坏单例
 */
public class LazySingleton2 {
    // 1.私有化类构造器
    private LazySingleton2() {}

    // 2.定义静态私有的类对象
    private static LazySingleton2 instance = null;

    // 3.提供公共静态的获取该私有类对象的方法,为了线程安全,给方法加上同步锁synchronized
    public static synchronized LazySingleton2 getInstance() {
        if (instance == null) {
            instance = new LazySingleton2();
        }

        return instance;
    }
}
懒汉式3——双重锁检查
/**
 * @author jiangsj
 * 懒汉式3——双重锁检查单例
 * 优点: (1)由于懒汉式延时加载特性,使用该实例时才实例化,节省了内存资源
 *       (2)线程安全
 * 缺点: (1)如果不加volatile关键词防止指令重排,双重锁检查单例可能会出现不完整实例
 *       (2)反序列化,反射与克隆可破坏单例
 */
public class LazySingleton3 {
    // 1.私有化类构造器
    private LazySingleton3() {}

    // 2.定义静态私有的类对象,为了防止出现不完整实例,加了防止指令重排的volatile关键字
    private static volatile LazySingleton3 instance = null;

    // 3.提供公共静态的获取该私有类对象的方法,为了线程安全,做了双重锁检查机制
    public static LazySingleton3 getInstance() {
        if (instance == null) {
            synchronized (LazySingleton3.class) {
                if (instance == null) {
                    instance = new LazySingleton3();
                }
            }
        }

        return instance;
    }
}
懒汉式4——静态内部类
/**
 * @author jiangsj
 * 懒汉式4——静态内部类单例
 * 优点: (1)由于懒汉式延时加载特性,使用该实例时才实例化,节省了内存资源
 *       (2)在外部类被调用的时候内部类才会被加载,内部类要在方法调用之前初始化,巧妙地避免了线程安全问题
 *       (3)兼顾了synchronized的性能问题
 * 缺点: 反序列化,反射与克隆可破坏单例
 */
public class LazySingleton4 {
    private boolean initialized = false;
    
    // 1.私有化类构造器
    private LazySingleton4() {
        synchronized (LazySingleton4.class) {
            if (initialized == false) {
                initialized = !initialized;
            } else {
                throw new RuntimeException("单例已被侵犯");
            }
        }
    }

    // 2.定义静态私有的类对象
    private static class LazyHolder {
        private static final LazySingleton4 INSTANCE = new LazySingleton4();
    }
    
    // 3.提供公共静态的获取该私有类对象的方法
    public static final LazySingleton4 getInstance() {
        return LazyHolder.INSTANCE;
    }
}

注册登记式

每使用一次,都往一个固定的容器中去注册并将使用过的对象进行缓存,下次去取对象的时候,就直接从缓存中取值,以保证每次获取的都是同一个对象。Spring IOC中的单例模式,就是典型的注册登记式单例。

/**
 * @author jiangsj
 * 注册登记式——map容器单例
 */
public class RegisterSingletonMap {

    // 1.私有化类构造器
    private RegisterSingletonMap() {}

    // 2.定义静态私有的类对象容器
    private static Map<String, RegisterSingletonMap> map = new ConcurrentHashMap<String, RegisterSingletonMap>();

    // 3.提供公共静态的获取该私有类对象的方法
    public static RegisterSingletonMap getInstance(String name) {
        String className = RegisterSingletonMap.class.getName();

        if (!className.equals(name)) {
            name = className;
        }

        synchronized (RegisterSingletonMap.class) {
            if (!map.containsKey(name)) {
                map.put(name, new RegisterSingletonMap());
            }
        }

        return map.get(name);
    }
}
注册登记式还有一种写法,枚举型单例
/**
 * @author jiangsj
 * 注册登记式——枚举单例
 */
public class RegisterSingletonEnum {
    // 1.私有化类构造器
    private RegisterSingletonEnum() {}

    // 2.提供公共静态的获取该私有类对象的方法
    public static RegisterSingletonEnum getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    // 3.定义静态私有的枚举对象,利用枚举特性获得类的单实例
    private static enum Singleton {
        INSTANCE;

        private RegisterSingletonEnum singleton;

        // JVM保证此方法只调用一次
        private Singleton() {
            singleton = new RegisterSingletonEnum();
        }

        public RegisterSingletonEnum getInstance() {
            return singleton;
        }
    }
}

序列化与反序列化式

序列化与反序列化式单例,需要重写readResolve()方法。

/**
 * @author jiangsj
 * 序列化与反序列化式单例
 */
public class SeriableSingleton implements Serializable {
    // 1.私有化类构造器
    private SeriableSingleton() {}

    // 2.定义静态私有的类对象
    private static final SeriableSingleton INSTANCE = new SeriableSingleton();

    // 3.提供公共静态的获取该私有类对象的方法
    public static SeriableSingleton getInstance() {
        return INSTANCE;
    }

    // 4.重写readResolve()方法,保证反序列化生成对象时获得的是同一个对象
    private Object readResolve() throws ObjectStreamException {
        return INSTANCE;
    }
}
序列化与反序列化式单例的测试类
/**
 * @author jiangsj
 * 序列化与反序列化式单例测试类
 */
public class SeriableSingletonTest {

    public static void main(String[] args) {
        SeriableSingleton s1 = SeriableSingleton.getInstance();
        SeriableSingleton s2 = null;

        // 序列化,持久化到磁盘
        try {
            FileOutputStream fos = new FileOutputStream(new File("SeriableSingleton.txt"));
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s1);
            oos.close();
            fos.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 从磁盘反序列化,重新生成对象
        try {
            FileInputStream fis = new FileInputStream(new File("SeriableSingleton.txt"));
            ObjectInputStream ois = new ObjectInputStream(fis);
            s2 = (SeriableSingleton) ois.readObject();
            ois.close();
            fis.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        System.out.println("序列化时对象 s1 : " + s1);
        System.out.println("反序列化后对象 s2 : " + s2);
        System.out.println(s1 == s2);
    }

}
测试结果如下
E:\tts\JDK\JDK1.8\bin\java "-javaagent:E:\Intellij IDEA\IntelliJ IDEA 2017.3\lib\idea_rt.jar=49947:E:\Intellij IDEA\IntelliJ IDEA 2017.3\bin" -Dfile.encoding=UTF-8 -classpath "E:\tts\JDK\JDK1.8\jre\lib\charsets.jar;E:\tts\JDK\JDK1.8\jre\lib\deploy.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\access-bridge-64.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\cldrdata.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\dnsns.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\jaccess.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\jfxrt.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\localedata.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\nashorn.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\sunec.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\sunjce_provider.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\sunmscapi.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\sunpkcs11.jar;E:\tts\JDK\JDK1.8\jre\lib\ext\zipfs.jar;E:\tts\JDK\JDK1.8\jre\lib\javaws.jar;E:\tts\JDK\JDK1.8\jre\lib\jce.jar;E:\tts\JDK\JDK1.8\jre\lib\jfr.jar;E:\tts\JDK\JDK1.8\jre\lib\jfxswt.jar;E:\tts\JDK\JDK1.8\jre\lib\jsse.jar;E:\tts\JDK\JDK1.8\jre\lib\management-agent.jar;E:\tts\JDK\JDK1.8\jre\lib\plugin.jar;E:\tts\JDK\JDK1.8\jre\lib\resources.jar;E:\tts\JDK\JDK1.8\jre\lib\rt.jar;E:\Intellij IDEA\idea_workspace\SpringStudyProject\out\production\SpringStudyProject" study.patterns.singleton.test.SeriableSingletonTest
序列化时对象 s1 : study.patterns.singleton.seriable.SeriableSingleton@135fbaa4
反序列化后对象 s2 : study.patterns.singleton.seriable.SeriableSingleton@135fbaa4
true

Process finished with exit code 0
饿汉式、懒汉式、注册登记式单例的测试类
/**
 * @author jiangsj
 */
public class SingletonTest {

    public static void main(String[] args) {
        // 模拟并发的线程数
        int count = 200;

        // 发令枪
        CountDownLatch latch = new CountDownLatch(count);

        long start = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            new Thread(){
                @Override
                public void run() {
                    try{

                        try {
                            // 阻塞,count = 0 就会释放所有的共享锁,模拟多线程并发
                            latch.await();
                        } catch (Exception e){
                            e.printStackTrace();
                        }

                        /*
                         * 可能会有很多线程同时去访问getInstance()
                         * 这里的RegisterSingletonEnum可替换成饿汉式、懒汉式、注册登记式的单例类来进行测试
                         */
                        RegisterSingletonEnum obj = RegisterSingletonEnum.getInstance();
                        System.out.println(System.currentTimeMillis() + ":" + obj);

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }.start();

            // 每循环一次,就启动一个线程,每次启动一个线程,count --
            latch.countDown();
        }
        long end = System.currentTimeMillis();

        // 让主线程延时,该延时期间内,模拟的多线程会去并发访问获取单例的方法
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("总耗时:" + (end - start));
    }

}

结束语

以上便是我对Java单例模式学习的总结,如有疏漏或错误之处,欢迎大家留言指正。

相关文章

网友评论

      本文标题:Java设计模式之——单例模式

      本文链接:https://www.haomeiwen.com/subject/qmqefftx.html