23中设计模式-----单例模式

前言

今天听完狂神说java的单例模式,感慨万分。希望通过这篇文章跟大家一起学习。

1. 饿汉式

[1] 一个私有的构造器
[2] 一个静态变量
[3] 一个静态方法返回对象

饿汉式单例是直接使用静态变量的方式生成这个单例对象(不管是否调用) , 所以缺点比较明显就是占用空间。

package com.gs.juc.单例模式;

//饿汉式单例
public class Hungry {

    //可能会造成浪费空间(假设此时有相应的变量生成)
    //private byte[] data1 = new byte[1024*1024];
  
  	// 1. 一个私有的构造器
    private Hungry(){

    }
    //2.一个静态变量
    private final static Hungry HUNGRY = new Hungry();
	
	//3.一个静态方法
    public static Hungry getInstance(){
        return HUNGRY;
    }
}
2. 懒汉式

懒汉式:表示使用的时候再去创建对象,又叫做DCL(Double Check Lock即双重检测锁模式)

public class LazyMan {
   
    private LazyMan(){
		System.out.println(Thread.currentThread().getName()+" ok");
    }
  
    private static LazyMan lazyMan;

    //双重检测锁模式(DCL双重检测锁模式)
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                //多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
                if(lazyMan==null){
                    lazyMan = new LazyMan();  //不是一个原子操作
                }
            }
        }
        return lazyMan;
    }

注意:最后送大家十套2020最新Java架构实战教程+大厂面试题库,进裙 783802103 在裙文件下载一起交流进步哦!
<1> 那么我们就会有疑问了,这种方式在多线程的环境下就已经安全了吗?
其实并不然,因为这个lazyMan = new LazyMan();的操作并不是一个原子操作,它底层是分为三步去执行的。
[1] 分配内存空间
[2] 执行构造方法,初始化对象
[3] 把这个对象指向内存空间
我们期待的执行顺序当然是[1][2][3],可是计算机底层为了提高效率,存在一种指令重排的操作。在不影响这个操作最终结果的前提下,它可以对这三个执行步骤进行重新排序,也就是它可能会变成[1][3][2],那这种情况会带来什么影响呢?如果我们在多线程的环境下执行,当A线程执行了[1] [3] 操作后,B线程进来了,因为这时lazyMan已经有相应的对象了,那么B线程不会走判断的步骤,而是直接返回 lazyMan这个对象,可是A线程并没有初始化完成,这时返回的就是一个空对象。
解决方法:为了防止指令重排其实很简单,就是在变量上加上一个 volatile关键字(volatile可以通过内存屏障防止指令重排)
只需改变上面的一行代码即可:

  private volatile static LazyMan lazyMan;

<2> 看一看这个单例其实已经很完美了,可是我们却忘记了反射这个操作,在反射面前似乎又并不安全。

   
  public class LazyMan {
   
    private LazyMan(){
		System.out.println(Thread.currentThread().getName()+" ok");
    }
  
    private static LazyMan lazyMan;

    //双重检测锁模式(DCL双重检测锁模式)
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                //多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
                if(lazyMan==null){
                    lazyMan = new LazyMan();  //不是一个原子操作
                }
            }
        }
        return lazyMan;
    }

    //-------------在上面的基础上增加的代码-----------
    //通过反射破坏单例
    public static void main(String[] args) throws Exception {
    	//我们先通过单例的方法获取一个实例
        LazyMan instance = LazyMan.getInstance();
        //再通过一个反射获取另外一个实例
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //设置权限
        declaredConstructor.setAccessible(true);
        LazyMan instance1 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance1);

    }
 }

结果:这时你会发现单例模式被破坏了,因为生成了两个不同的对象
在这里插入图片描述
解决方式:可以在构造器的时候,引入一个判断(三重判断), 检查lazyMan这个对象是否已经存在

 public class LazyMan {
   	// -------和上面代码有所区别的地方---------
    private LazyMan() {
      synchronized (LazyMan.class){
        if(lazyMan==null){
            System.out.println(Thread.currentThread().getName() + " ok");
        }else{
            throw new RuntimeException("不要试图通过反射破坏单例");
        }
      }
        
    }
    private static LazyMan lazyMan;

    //双重检测锁模式(DCL双重检测锁模式)
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                //多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
                if(lazyMan==null){
                    lazyMan = new LazyMan();  //不是一个原子操作
                }
            }
        }
        return lazyMan;
    }


    //通过反射破坏单例
    public static void main(String[] args) throws Exception {
    	//我们先通过单例的方法获取一个实例
        LazyMan instance = LazyMan.getInstance();
        //再通过一个反射获取另外一个实例
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //设置权限
        declaredConstructor.setAccessible(true);
        LazyMan instance1 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance1);

    }
 }

结果:很nice,确实做到防止反射破坏单例了,这时我又觉得自己行了。
在这里插入图片描述
<3> 可是如果细想,如果两个实例都通过反射获得,那么自然不会存在lazyMan这个判断的阻挠。我们把代码往下改:

 public class LazyMan {
  
    private LazyMan() {
      synchronized (LazyMan.class){	
        if(lazyMan==null){
            System.out.println(Thread.currentThread().getName() + " ok");
        }else{
            throw new RuntimeException("不要试图通过反射破坏单例");
        }
       }
        
    }
    private static LazyMan lazyMan;

    //双重检测锁模式(DCL双重检测锁模式)
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                //多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
                if(lazyMan==null){
                    lazyMan = new LazyMan();  //不是一个原子操作
                }
            }
        }
        return lazyMan;
    }


    //----------与上面有所区别的地方---------
    public static void main(String[] args) throws Exception {

        //两个对象都通过反射获得
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //设置权限
        declaredConstructor.setAccessible(true);
        LazyMan instance1 = declaredConstructor.newInstance();
        LazyMan instance  = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance1);

    }
 }

结果:gg, 还有这么赖的方式创建对象,那我们的单例不是又被破坏了吗
在这里插入图片描述
解决方式:我们在学习并发编程的时候,学过一种信号灯的方式,简单的说就是设置一个标志位flag为false,当有线程走了LazyMan()这个私有化构造器先判断这个标志位,若为false,则放行,并把标志位置为ture。下次有线程进来就直接被拦截了。

 public class LazyMan {

  	  // --------- 和上面有所区别的地方------------
	 //设置一个标志位
    private static boolean flag = false;

    private LazyMan() {
        synchronized (LazyMan.class) {
            //优化4:通过标志位去避免两次反射创建对象的情况
            if (flag == false) {
                flag = true;
                System.out.println(Thread.currentThread().getName()+" ok");
            } else {
                throw new RuntimeException("不要试图破坏反射");
            }
        }

    }

    private static LazyMan lazyMan;

    //双重检测锁模式(DCL双重检测锁模式)
    public static LazyMan getInstance(){
        if(lazyMan == null){
            synchronized (LazyMan.class){
                //多一层判断是防止多个线程同时进入了第一个判断,导致生成多个对象
                if(lazyMan==null){
                    lazyMan = new LazyMan();  //不是一个原子操作
                }
            }
        }
        return lazyMan;
    }


    //----------与上面有所区别的地方---------
    public static void main(String[] args) throws Exception {

        //两个对象都通过反射获得
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        //设置权限
        declaredConstructor.setAccessible(true);
        LazyMan instance1 = declaredConstructor.newInstance();
        LazyMan instance  = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance1);

    }
 }

在这里插入图片描述
我好像又可以了,可是反射就不能获得你标志位的属性吗?停,刚精快醒醒。别试了,在反射面前所有的属性和方法都是透明,所以理论上都是不安全的。在java中有一个类天生就是单例安全的,没错它就是枚举类。为什么它安全呢?因为它可以避免反射的操作。
反射中newInstance() 上对枚举类的说明:
在这里插入图片描述

3. 枚举类的单例
package com.gs.juc.单例模式;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;


//枚举是一个什么? 本身也是一个Class类
public enum  EnumSingle {

    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class Test{
    public static void main(String[] args) throws Exception {

        //1.通过枚举
		EnumSingle instance1 = EnumSingle.INSTANCE;
		EnumSingle instance2 = EnumSingle.INSTANCE;

	    System.out.println(instance1.hashCode());
        System.out.println(instance2.hashCode());
    }
}

结果:观察两个对象的hash值,确实是单例安全的。
在这里插入图片描述

4. 静态内部类实现单例模式

说明:这个方式实现的单例和前面的懒汉式和饿汉式都是线程不安全的

package com.gs.juc.单例模式;

//静态内部类
public class Holder {

    private Holder(){

    }
    public static Holder getInstance(){
        return InnerClass.HOLDER;
    }

    public static class InnerClass{
        private static final Holder HOLDER = new Holder();
    }
}

注意:最后送大家十套2020最新Java架构实战教程+大厂面试题库,进裙 783802103 在裙文件下载一起交流进步哦!

小结:

单例模式的这4种实现方式,本质上各有各的优势。其中我们使用懒汉式的单例时,一般只要实现到避免指令重排那一步即可。因为深究反射可以让类中的方法和属性都透明化。(后面关于反射的优化很多只是为了面试时能多说一点)使用饿汉式的单例时,虽然占用内存,不过实现简单。

  • 14
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值