设计模式【3】-对象创建型-单例模式

这节讲一个单例模式,通过多个源码的改进、讲解来进展,本文会结合JMM相关知识,讲单例模式讲透。

. 单例模式的定义

保证只有一个实例存在的类就叫单例对象的类

 

. 分类

  1. 应用场景----多线程环境和单线程环境
        ------ 在一般情况下,其实单线程下,你随便怎么写这个单例都是没问题的;问题出在多线程的情况下。
  2. 初始化时机---类装载时和实例第一次被创建时
        ------ 一般来说,要想资源最大化利用,我们只在实例被需要的时候创建。这两者还是有点差别的。这个就不细说了。


三. 源码解析

 方式:实例第一次被创建时

  1. 最简单的单例

    public class SingleTon1 {

        private static SingleTon1 INSTANCE;
        public static SingleTon1 getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new SingleTon1();
            }

            return INSTANCE;
        }
    }

   评析:这是最简单的做法。
   问题:
        1:类SingleTon1是可以被外部访问的----这里要改成private

  2. 改进1

public class SingleTon2 {

        private static SingleTon2 INSTANCE;

        private SingleTon2(){}
        public static SingleTon2 getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new SingleTon2();
            }

            return INSTANCE;
        }
    }

    评析:这样外部类就不能通过SingleTon结构体直接实例化了。
    问题:

        1:在多线程模式下,INSTANCE == null的判断并没有被同步,INSTANCE可能被
           多次实例化

  3. 改进2

     增加同步机制,先用个

    public class SingleTon3 {

        private static SingleTon3 INSTANCE;

        private SingleTon3(){}
        public static synchronized SingleTon3 getInstance() {
            if (INSTANCE == null) {
                INSTANCE = new SingleTon3();
            }

            return INSTANCE;
        }
    }

    评析:通过给getInstance添加关键字synchronized,让所有调用getInstance的都能依次
          排队。
    问题:

        1getInstance方法在添加synchronized方法后,所有线程都在同时调用时,只
           能有一个在运行,这将很大的降低吞吐率。

  4. 改进3

     增加同步机制,先用个

    public class SingleTon4 {

        private static SingleTon4 INSTANCE;

        Private SingleTon4(){}
        public static synchronized SingleTon4 getInstance() {

            if (INSTANCE == null) {
                synchronized(SingleTon4.class) {
                    INSTANCE = new SingleTon4();

                }
            }

            return INSTANCE;
        }
    }

    评析:将synchronized移到实例化SingleTon的时候再判断,减少了getInstance时的线
          程堵塞。
    问题:

        1:有可能会有两个线程同时通过了INSTANCE == null的判断。还是有多个实例。

  5. 改进4

     增加同步机制,先用个

    public class SingleTon5 {

        private static SingleTon5 INSTANCE;

        Private SingleTon5(){}
        public static SingleTon5 getInstance() {

            if (INSTANCE == null) {
                synchronized(SingleTon5.class) {
                    if(INSTANCE == null) {
                       INSTANCE = new SingleTon5();

                    }

                }
            }

            return INSTANCE;
        }
    }

    评析:二次判断INSTANCE,确保了INSTANCE只会有一个实例。
    问题:

        1JMM模型中讲到两个概念,原子性,有序性。
    知识点补充:
    原子性:指的是操作原子性。
    JVM的内存模型确保了几点,
    一、所有变量储存在主内存
    二、每条线程有自己的工作内存(分内存)
    三、线程间无法访问对方的内存
    四、线程间通信通过主内存完成
    如下图:

    
    讲几个范例
    1. M = 1; --- 原子操作,M已经被开辟出来,剩下只有一个赋值操作
    2. Int M = 1: --- 非原子操作,这里包含两个操作
                 ① M先声明,被开辟出来
                 ②1赋值给M

3. M++; --- 非原子操作,M++等价于M=M+1;
                 这里包含了两个操作
                 ① 先执行M+1
                 ② M+1赋值给M
    所以,原子性意味着 一条语句在JVM中也只需要一条指令就完成。

    有序性:指的是指令按顺序操作,1234这样。
            实际上,计算机为了提高执行效率,会对指令的执行  自动优化,
            指令1234--->1423.
    用范例说话
    int M = 1;//#这里是两条指令
    M = 5;//#这里是一条指令
    M++;//#这里是两条指令
    M--;//#这里是两条指令

    那么,JVM会将这段代码拆成7条指令才执行,1234567,但是为了执行效率,可能会
    变成231476,或者4536217
    大概是这么个意思,当然操作执行的结果依旧是不变的。

 

  6. 再看 改进4

    public class SingleTon5 {

        ......
        public static SingleTon5 getInstance() {

            if (INSTANCE == null) {
                synchronized(SingleTon5.class) {
                    if(INSTANCE == null) {
                       INSTANCE = new SingleTon5();

                    }

                }
            }

            return INSTANCE;
        }
    }

评析:INSTANCE = new SingleTon5();并不具备原子性。而且有序性也无法得到保障。
    INSTANCE可以分为3
    ① INSTANCE分配内存,设置初始值null(这应该先new SingleTon5()完成)
    ②调用SingleTon5的结构体初始化
    ③INSTANCE指向new SingleTon5对象分配的内存空间
    那么有序性这一项,其指令执行可能是1-2-3或者1-3-2.
    问题:
    在多线程环境下,如果有线程刚好执行到if (INSTANCE == null),而如果是1-3-2
    那么INSTANCE已经不为null了,只是并未初始化其结构体,然后就有Fatal Exception

  7. 改进5

     利用volatile

public class SingleTon6 {

        private static volatileSingleTon6 INSTANCE;

        Private SingleTon6(){}
        public static SingleTon6 getInstance() {

            if (INSTANCE == null) {
                synchronized(SingleTon6.class) {
                    if(INSTANCE == null) {
                       INSTANCE = new SingleTon6();

                    }

                }
            }

            return INSTANCE;
        }
    }

    评析:volatile的功效有2个。
          创建内存屏障(可以理解成工作内存的变量与主内存的变量会强制同步)
                    --- 原子性解决
          ②禁止 指令重排 ---有序性解决

 方式:类装载时

  1. 先看源码

    public class SingleTonB {

        private static final SingleTonB INSTANCE = new SingleTonB();

        Private SingleTonB(){}
        public static SingleTon6 getInstance() {

            return INSTANCE;
        }
    }

    评析:INSTANCE在类加载时就被ClassLoader初始化了。
    问题:

        1:初始化太早,资源浪费,而且拖慢启动
        2:如果初始化里头的变量有依赖,可能会有其他问题,因为被依赖的数据还没初
           始化呢

  2.再看一个源码:利用内部类的创建时机

    public class SingleTonC {

        private static classSingleTonHolder {
            private static final SingleTonC INSTANCE = new SingleTonC();

        }

        Private SingleTonC(){}
        public static SingleTonC getInstance() {

            return SingleTonHolder.INSTANCE;
        }
    }

   评析:内部类的创建依赖外部类的创建时间,在类加载时,ClassLoader会去初始化
          SingleTonC的变量,这时SingleTonCSingleTonHolder里头才会被初始化。也顺
          利达到了目的。

  3.再看另一个源码:利用枚举机制

   File:SingleTon.java

   package enumSingleton;

   public enum SingleTon {

       INSTANCE;

       private SingleTonD instance;

       SingleTon() {

           instance = new SingleTonD();

       }

       public SingleTonD getInstance() {

           return instance;

       }

   }

   File:SingleTonD.java

   package enumSingleton;

   public class SingleTonD {

       SingleTonD(){

        System.out.println("i am singtonD");

       }

   }

   File:testEnumSingleTonMain.java

   package enumSingleton;

   public class testEnumSingleTonMain {

       public static void main(String[] args) {

            SingleTon.INSTANCE.getInstance();

            SingleTon.INSTANCE.getInstance();

            SingleTon.INSTANCE.getInstance();

            SingleTon.INSTANCE.getInstance();

            SingleTon.INSTANCE.getInstance();

            SingleTon.INSTANCE.getInstance();

            SingleTon.INSTANCE.getInstance();

            SingleTon.INSTANCE.getInstance();

            SingleTon.INSTANCE.getInstance();

        }

   }

   输出结果:

   i am singtonD

   评析:每个枚举实例都是static final类型的,也就表明只能被初始化一次。只有在枚举
         被访问时才会被实例化。这个结构相当简洁。注意观察,其实 枚举单例 利用
         内部类的创建时机 还是有殊途同归的意思在。
         

  

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值