喜欢就关注我们吧!
单例模式说是简单,却又不简单。单说单例模式的创建方式,就有7种。
单例模式供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
应用实例:
优点:
1)饿汉模式(多线程安全)
饿汉模式,所谓饿汉就是在类装载的时候就创建,不管你用不用,先创建了再说,如果一直没有被使用,便浪费了空间,典型的空间换时间,每次调用的时候,就不需要再判断,节省了运行时间。
1. class Singleton {
2. private static Singleton instance = new Singleton(); // 实例化对象
3. //让构造函数为 private,这样该类就不会被实例化
4. private Singleton(){}
6. // 创建唯一的对象
7. public static Singleton getInstance(){
8. return instance;
9. }
11. public void show(){
12. System.out.println("Singleton show---");
13. }
14. }
16. public class Main {
17. public static void main(String[] args) {
18. Singleton instance = Singleton.getInstance();
19. instance.show();
20. }
21. }
「饿汉式」是最简单的实现方式,这种实现方式适合那些在初始化时就要用到单例的情况,这种方式简单粗暴,如果单例对象初始化非常快,而且占用内存非常小的时候这种方式是比较合适的,可以直接在应用启动时加载并初始化。
2)懒汉模式(线程不安全)
懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作。在多线程访问的时候,很可能会造成多次实例化,就不再是单例了。
1. class Singleton {
2. private static Singleton instance;
4. private Singleton(){}
6. public static Singleton getInstance(){
7. if(instance == null){
8. instance = new Singleton();
9. }
10. return instance;
11. }
13. public void show(){
14. System.out.println("Singleton show---");
15. }
16. }
18. public class Main {
19. public static void main(String[] args) {
20. Singleton instance = Singleton.getInstance();
21. instance.show();
22. }
23. }
3)懒汉模式(线程安全)
对创建对象的加锁,适用于多线程中,但是效率不高
因为这种方式在getInstance()
方法上加了同步锁,所以在多线程情况下会造成线程阻塞,把大量的线程锁在外面,只有一个线程执行完毕才会执行下一个线程。
1. class Singleton {
2. private static Singleton instance;
4. private Singleton(){}
6. public static synchronized Singleton getInstance(){ // 对创建对象加速
7. if(instance == null){
8. instance = new Singleton();
9. }
10. return instance;
11. }
13. public void show(){
14. System.out.println("Singleton show---");
15. }
16. }
18. public class Main {
19. public static void main(String[] args) {
20. Singleton instance = Singleton.getInstance();
21. instance.show();
22. }
23. }
4)双重校验锁 - DCL(线程安全)【推荐使用】
懒汉式(线程安全)」毫无疑问存在性能的问题 — 如果存在很多次getInstance()的调用,那性能问题就不得不考虑了
为什么双重校验锁可以保证线程安全,原因就是检测null的操作和创建对象的操作分离了。如果这两个操作能够原子地进行,那么单例就已经保证了。
1. class Singleton {
2. private volatile static Singleton instance;
4. private Singleton(){}
6. public static synchronized Singleton getInstance(){ // 对创建对象加速
7. if(instance == null){
8. synchronized(Singleton.class){
9. if (instance == null){
10. instance = new Singleton();
11. }
12. }
13. }
14. return instance;
15. }
17. public void show(){
18. System.out.println("Singleton show---");
19. }
20. }
22. public class Main {
23. public static void main(String[] args) {
24. Singleton instance = Singleton.getInstance();
25. instance.show();
26. }
27. }
1. 被volatile修饰的变量的值,将不会被本地线程缓存,
2. 所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
getSingleton
,才会初始化signleton
实例。singleton
是否初始化,减少不必要的同步开销。singleton
使用volatile
修饰。为什么要使用volatile修饰?
虽然已经使用synchronized进行同步,但在第4步创建对象时,会有下面的伪代码:
1. memory=allocate(); //1:分配内存空间
2. ctorInstance(); //2:初始化对象
3. singleton=memory; //3:设置singleton指向刚排序的内存空间
4. 复制代码
当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以JVM是允许的。如果此时伪代码发生重排序,步骤变为1->3->2,线程A执行到第3步时,线程B调用getsingleton
方法,在判断singleton==null
时不为null
,则返回singleton
。但此时singleton
并还没初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!
「双重校验锁」:既可以达到线程安全,也可以使性能不受很大的影响,换句话说在保证线程安全的前提下,既节省空间也节省了时间,集合了「饿汉式」和两种「懒汉式」的优点,取其精华,去其槽粕。
对于volatile关键字,还是存在很多争议的。由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。
5)静态内部类(线程安全)【推荐使用】
在很多情况下JVM已经为我们提供了同步控制,比如:
static {...}
区块中初始化的数据在JVM进行类加载的时候他会保证数据是同步的,我们可以这样实现:**采用内部类,在这个内部类里面去创建对象实例。**这样的话,只要应用中不使用内部类 JVM 就不会去加载这个单例类,也就不会创建单例对象,从而实现「懒汉式」的延迟加载和线程安全。
1. class Singleton {
3. private Singleton(){}
5. public static Singleton getInstance(){
6. return Inner.instance;
7. }
9. // 内部静态类
10. private static class Inner{
11. private final static Singleton instance = new Singleton();
12. }
14. public void show(){
15. System.out.println("Singleton show---");
16. }
17. }
19. public class Main {
20. public static void main(String[] args) {
21. Singleton instance = Singleton.getInstance();
22. instance.show();
23. }
24. }
内部类的实现方法有以下优点:
- 实现代码简洁。和双重检查单例对比,静态内部类单例实现代码真的是太简洁,又清晰明了。
- 延迟初始化。调用
getSingleton
才初始化Singleton
对象。- 线程安全。JVM在执行类的初始化阶段,会获得一个可以同步多个线程对同一个类的初始化的锁。
6)使用枚举
枚举单例的优点就是简单,但是大部分应用开发很少用枚举,可读性并不是很高,不建议用。
1. enum Singleton {
3. INSTANCE;
4. public void show(){
5. System.out.println("Singleton show---");
6. }
7. }
9. public class Main {
10. public static void main(String[] args) {
11. Singleton instance = Singleton.INSTANCE;
12. instance.show();
13. }
14. }
7)使用容器
在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
1. public class SingletonManager {
2. private static Map objMap = new HashMap();3. private Singleton() { 4. }5. public static void registerService(String key, Objectinstance) {
6. if (!objMap.containsKey(key) ) {
7. objMap.put(key, instance) ;8. }9. }10. public static Object getService(String key) {
11. return objMap.get(key) ;12. }13. }
往期推荐
计算机TCP/IP面试10连问,你能顶住几道?
看了那么多的开源商城项目,最想推荐的还是这一个
穷极一生,我们究竟在追寻什么
真香!大佬总结了一份 Spring Boot 项目搭建模板
21届校招应届生Offer薪资曝光:年薪35万+,严重倒挂老员工是互联网行业常态?