单例模式

痴心易碎 提交于 2020-02-28 19:51:24

这篇文章中我会用8种写法来对单例模式进行优化,以达到最完美的效果

但是说实话在平常我们进行代码编写的时候用不着那么完美

单例模式

什么是单例模式?

简单的说就是只能new出来一个实例

第一种写法

饿汉式:

  优点:简单实用

  缺点:不论该对象是否会被用到,都提前将对象实例化

1.首先我们创建出一个静态的不可更改的变量Instance

2.我们将该类的构造方法的权限设置为private,防止其他类new对象

3.设置该对象的get方法

 1 /**
 2  * 饿汉式
 3  * 类加载到内存后,就实例化一个单例,JVM保证线程安全
 4  * (JVM保证每一个class只会露到内存一次,那么static变量在class露到内存之后马上进行初始化,所以static变量也保证初始化这一次)
 5  * 简单实用,推荐使用
 6  * 唯一缺点:不管用到与否,类加载时就完成实例化
 7  */
 8 public class Mgr01 {
 9     private static final Mgr01 Instance = new Mgr01();
10 
11     private Mgr01(){}
12 
13     public static Mgr01 getInstance(){return Instance;}
14 
15     public static void main(String[] args) {
16         /**
17          * 调用静态方法常用的两种方式
18          *   1.new对象调用静态方法
19          *   2.类名打点调用静态方法
20          *   这里构造方法设为私有访问控制符,不能new对象
21          */
22         Mgr01 m1 = Mgr01.getInstance();
23         Mgr01 m2 = Mgr01.getInstance();
24         System.out.println(m1 == m2);
25     }
26 }

第二种写法

第二种写法与第一种写法几乎没有区别,只是使用了静态代码块进行对象的初始化但是该写法仍然没有解决类加载时初始化的问题
 1 /**
 2  * 与Mgr01意思相同
 3  */
 4 public class Mgr02 {
 5     //这里加上final没有初始化,但是在下面必须加static静态代码块进行初始化
 6     private static final Mgr02 Instance;
 7 
 8     static{
 9         Instance = new Mgr02();
10     }
11 
12     private Mgr02(){}
13 
14     public static Mgr02 getInstance(){return Instance;}
15 
16     public static void main(String[] args) {
17         Mgr02 m1 = Mgr02.getInstance();
18         Mgr02 m2 = Mgr02.getInstance();
19         System.out.println(m1 == m2);
20     }
21 }

第三种写法

懒汉式:

  优点:在需要的时候进行对象初始化,解决了以上两种写法的缺点

  缺点:带来了线程不安全的问题

在进行判断是否存在Instance实例时,我们假设有现后两个线程,一号线程刚判断完发现没有实例,正准备进行new时,二号线程突然进入,判断结果同样为没有Instance实例,这时就会有两个线程先后执行new操作

 1 /**
 2  * 懒汉式
 3  * 达到了按需初始化的目的,但是带来了线程不安全的问题
 4  */
 5 public class Mgr03 {
 6     //这里不能加final,因为加上final就必须进行初始化new对象
 7     private static Mgr03 Instance;
 8 
 9     private Mgr03(){}
10 
11     public static Mgr03 getInstance(){
12         if (Instance == null){
13             //多线程同时打入的时候容易在这里产生误差
14             Instance = new Mgr03();
15         }
16         return Instance;
17     }
18 
19     public static void main(String[] args) {
20         Mgr03 m1 = Mgr03.getInstance();
21         Mgr03 m2 = Mgr03.getInstance();
22         System.out.println(m1 == m2);
23     }
24 }

第四种写法

在getInstance方法上加锁来保证线程安全,但是如果每个线程在执行getInstance时都进行加锁操作,那么就会降低程序执行效率

 1 /**
 2  * 增加了线程的安全性,但是降低了程序执行效率
 3  */
 4 public class Mgr04 {
 5     private static Mgr04 Instance;
 6 
 7     private Mgr04(){}
 8 
 9     public static synchronized Mgr04 getInstance(){
10         if (Instance == null){
11             Instance = new Mgr04();
12         }
13         return Instance;
14     }
15     public static void main(String[] args) {
16         Mgr04 m1 = Mgr04.getInstance();
17         Mgr04 m2 = Mgr04.getInstance();
18         System.out.println(m1 == m2);
19     }
20 }

第五种写法

试图通过同步代码块的方式在保证线程安全的前提下提高效率

同步代码块只加在需要new实例的时候

这样虽然提高了程序执行效率,但是显然是不能保证线程安全的

假设有两个线程:线程一在if条件判断结束后发现没有实例,正在往下执行但是还没有进入同步代码块时,线程二进入也同样判断没有实例,线程二比线程一提前拿到锁new出来实例后释放锁,等到线程二释放锁之后线程一又执行方法new出来实例。这样就导致了线程的不安全性

 1 public class Mgr05 {
 2     private static Mgr05 Instance;
 3 
 4     private Mgr05(){}
 5 
 6     public static Mgr05 getInstance(){
 7         if (Instance == null){
 8             /**
 9              * 试图通过同步代码块的方式提高效率
10              * 但是这里又很容易造成线程不安全问题
11              */
12             synchronized (Mgr05.class){
13                 Instance = new Mgr05();
14             }
15         }
16         return Instance;
17     }
18     public static void main(String[] args) {
19         Mgr05 m1 = Mgr05.getInstance();
20         Mgr05 m2 = Mgr05.getInstance();
21         System.out.println(m1 == m2);
22     }
23 }

第六种写法(比较完美的写法之一)

双重if判断来保证只有一个实例对象

这样即不会出现线程不安全问题,又保证了不会随着类加载而创建出来实例

 1 public class Mgr06 {
 2     //加volatile主要是为了防止指令重排
 3     private static volatile Mgr06 Instance;
 4 
 5     private Mgr06(){}
 6 
 7     public static Mgr06 getInstance(){
 8         //第一个进入方法的线程进行Instance实例是否存在的判断
 9         if (Instance == null){
10             //同步代码块进行加锁
11             synchronized (Mgr06.class){
12                 //双重锁机制进行判断Instance对象是否被实例
13                 if (Instance == null){
14                     Instance = new Mgr06();
15                 }
16             }
17         }
18         return Instance;
19     }
20     public static void main(String[] args) {
21         Mgr06 m1 = Mgr06.getInstance();
22         Mgr06 m2 = Mgr06.getInstance();
23         System.out.println(m1 == m2);
24     }
25 }

第七种写法(比较完美的写法之一)

使用静态内部类来保证只有一个实例

静态内部类在加载类的时候是不会被加载的,而getInstance方法在执行返回Instance实例调用静态内部类时,静态内部类才会被加载,保证了在使用时才会创建实例

JVM只会加载一次类,同样也只会加载一次静态内部类,这样就保证了线程安全,只会产生一个实例对象

 1 /**
 2  * 使用静态内部类保证单例,同时也保证线程安全
 3  * 线程安全是用过JVM机制来保证的
 4  * JVM只会加载一次Mgr07这个类,只会加载一次Mgr07Holder这个内部类
 5  *      这也就保证了只会生成一个Instance实例
 6  */
 7 public class Mgr07 {
 8     private Mgr07(){}
 9 
10     /**
11      * 使用静态内部类进行实现
12      * 当Mgr07这个类被加载时,里面的内部类是不会被加载的,保证了类加载时不完成实例化
13      * 当调用getInstance方法时内部类才会加载,也保证了只有一个实例
14      */
15     private static class Mgr07Holder{
16         private static final Mgr07 Instance = new Mgr07();
17     }
18 
19     public static Mgr07 getInstance(){
20         return Mgr07Holder.Instance;
21     }
22 
23     public static void main(String[] args) {
24         Mgr07 m1 = Mgr07.getInstance();
25         Mgr07 m2 = Mgr07.getInstance();
26         System.out.println(m1 == m2);
27     }
28 }

第八种写法(最完美的写法)

枚举单例模式,枚举类中没有构造方法,保证了仅仅实例化一个对象

只有在调用Instance实例的时候才会创建实例,保证了在使用时才会创建实例

 1 /**
 2  * 枚举单例模式:通过枚举进行单例模式的实例创建
 3  * 原因:枚举类中没有构造方法
 4  * 不仅可以解决线程同步,还可以防止反序列化
 5  */
 6 public enum Mgr08 {
 7 
 8     Instance;
 9 
10     public static void main(String[] args) {
11         Mgr08 m1 = Mgr08.Instance;
12         Mgr08 m2 = Mgr08.Instance;
13         System.out.println(m1 == m2);
14     }
15 }

总结

单例模式在现在编写代码时使用较少,spring帮我们保证了仅仅初始化一个实例对象,但是我们也应该理解单例模式的实现原理

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!