23种设计模式笔记第二篇单例模式

狂神的单例模式是真的讲的非常好理解,这里附上链接:

【狂神说Java】单例模式-23种设计模式系列_哔哩哔哩_bilibili

单例模式就是让一个类只能创建一个对象

单例中最重要的就是构造方法私有(一旦构造方法私有,别人就无法new这个对象了)

是23种设计模式中最简单的

目录

单例模式的概念及优缺点

单例模式分类

饿汉式单例

实现方式1(静态变量方式)

实现方式2(静态代码块)

懒汉式单例

方式1(线程不安全)

方式2:线程安全

方式3:双重检测模式的懒汉式单例(DCL懒汉式)

方式4:静态内部类实现(推荐)

反射(单例不安全)

解决方式

 序列化和反序列化也能破坏单例

解决方式

枚举方式实现单例模式(恶汉式)最优


单例模式的概念及优缺点

单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式包含角色:Singleton

这样的模式有几个好处:

1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。节约系统资源

2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。

缺点:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。在一定程度是违背了“开闭原则”。

在以下情况下可以使用单例模式:

系统只需要一个实例对象 ,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
客户调用类的单个实例 只允许使用一个公共访问点 ,除了该公共访问点,不能通过其他途径访问该实例。
在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式。(要学会变通)

单例模式分类

饿汉式单例:类加载就会导致该单实例对象被创建

懒汉式单例:类加载不会导致改单实例对象被创建,而是首次使用该对象时才会创建

饿汉式单例

一开始就将所有东西全部加载进来,相当于对象已经存在了,非常占用内存资源

实现方式1(静态变量方式)

//饿汉式单例
public class Hungry{
    //可能会浪费空间
    private byte[] data1 = new byte[1024*1024];
    private byte[] data2 = new byte[1024*1024];
    private byte[] data3 = new byte[1024*1024];
    private byte[] data4 = new byte[1024*1024];
    
    //构造方法私有:就是让外界不能创建该类的对象
    private Hungry(){
    }

    //在本类中创建本类对象
    private final static Hungry HUNGRY = new Hungry();
   
    //提供一个公共的访问方式,让外界获取该对象
    public static Hungry getInstance(){
        return HUNGRY;
    }
}
//测试
public class Client{
    public static void main(String []args){
        //创建Hungry类的对象
        Hungry hungry = Hungry.getInstance();
        
        Hungry hungry1 = Hungry.getInstance();
        
        //判断获取到的两个是否是同一个对象
        //利用==号,比较的是两个对象在内存中的位置
        System.out.println(hungry == hungry1);
        //结果为true,保证了Hungry类只能创建一个对象
    }
}

实现方式2(静态代码块)

public class Hungry{
    //私有构造方法
    private Hungry(){}

    //声明Hungry类型的变量,之所以是静态是因为要通过类名直接访问
    private static Hungry instance;//初始值为null
    
    //在静态代码块中进行赋值
    static {
        instance = new Hungry();
    }

    //对外提供获取该类对象的方法
    public static Hungry getInstance(){
        return instance;
    }
}
//测试
public class Client{
    public static void main(String []args){
        //创建Hungry类的对象
        Hungry hungry = Hungry.getInstance();
        
        Hungry hungry1 = Hungry.getInstance();
        
        //判断获取到的两个是否是同一个对象
        //利用==号,比较的是两个对象在内存中的位置
        System.out.println(hungry == hungry1);
        //结果为true,保证了Hungry类只能创建一个对象
    }
}

懒汉式单例

对象是用的时候才去加载,而不是一开始就加载

方式1(线程不安全)

//懒汉式单例
public class LazyMan{
    //私有构造方法
    private LazyMan(){    
    }
    //声明LazyMan类型的变量
    private static LazyMan LAZYMAN;//只是声明,没有创建
    //对外提供访问方式
    public static LazyMan getInstance(){
        //判断是否创建了LAZYMAN对象
        if(LAZYMAN == null){
            LAZYMAN = new LazyMan();
        }
        return LAZYMAN;
    }
}
//测试
public class Client{
    public static void main(String[] args){
        LazyMan instance = LazyMan.getInstance();
        LazyMan instance1 = LazyMan.getInstance();
        
        //判断两次获取到的LazyMan对象是否是同一个对象
        System.out.println(instance == instance1);
        //结果为ture
    }
}

这个方式的单线程下是OK的,在多线程并发中是会出现问题的:

//懒汉式单例
public class LazyMan{
    private LazyMan(){
        //打印线程名称
        System.out.println(Thread.currentThread().getName());
    }

    private static LazyMan LAZYMAN;

    public static LazyMan getInstance(){
        //线程1等待,线程2获取CPU的执行权,也会进到该判断中
        if(LAZYMAN == null){
            LAZYMAN = new LazyMan();
        }
        return LAZYMAN;
    }
    
    //多线程并发
    public static void main(String[] args){
        for(int i=0;i<10;i++){
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

运行结果(偶尔成功,偶尔失败,结果随机):

方式2:线程安全

 针对多线程,添加锁模式synchronized,缺点:执行效果特别低

//懒汉式单例
public class LazyMan{
    //私有构造方法
    private LazyMan(){    
    }
    //声明LazyMan类型的变量
    private static LazyMan LAZYMAN;//只是声明,没有创建
    //对外提供访问方式
    public static synchronized LazyMan getInstance(){
        //判断是否创建了LAZYMAN对象
        if(LAZYMAN == null){
            LAZYMAN = new LazyMan();
        }
        return LAZYMAN;
    }
}

方式3:双重检测模式的懒汉式单例(DCL懒汉式)

懒汉式方式2中加锁的问题,对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以没必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机,由此出现了:双重检查锁模式。

volatile:保证可见性和有序性

注意:volatile和synchronized的区别:前者保证有序性(变量的创建),后者保证原子性(同步)

volatile能保证线程的可见性,但是并不能替代synchronized保证原子性

        ps:原因是volatile无法保证线程安全问题,比如i++操作,在底层是需要三步完成的(cpu读取内存中的值——cpu将值+1——cpu将值写回内存)。假设一个场景:i = 0,线程A先执行,要写回时CPU资源被线程B抢夺,此时线程A没有执行完成,线程B取到的还是i = 1,然后执行i++并写回(i=2),假设这个时候线程A又被执行,由于之前已经读取过值了,就不会再从内存中读取,而是直接通过线程的上下文切换机制,获取到的就是线程A之前没有执行完毕的i值,i = 1,然后执行完毕并写回内存,结果依旧是i=1,线程不安全

之所以去加volatile的原因就是因为实例化时不是原子性操作,避免指令重排

//懒汉式单例,双重检查锁方式
public class LazyMan{
    private LazyMan(){
        //打印线程名称
        System.out.println(Thread.currentThread().getName());
    }

    private volatile static LazyMan LAZYMAN;

    public static LazyMan getInstance(){
        //第一次判断,如果值不为null,不需要抢占锁,直接返回对象
        if(LAZYMAN == null){
        	synchronized (LazyMan.class){
                //第二次判断
        		if(LAZYMAN == null) {
        			LAZYMAN = new LazyMan();//不是原子性操作
                    /**
                     * 会进行的操作:
                     * 1,分配内存空间
                     * 2,执行构造方法,初始化对象
                     * 3,将这个对象指向这个空间
                     * 容易出现顺序乱的情况,比如132等
                     *  
                     * 添加volatile关键字和synchronized加锁
                     */
        		}
        	}
        }
        return LAZYMAN;
    }
    
    //多线程并发
    public static void main(String[] args){
        for(int i=0;i<10;i++){
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

方式4:静态内部类实现(推荐)

在本方式中,实例右内部类创建,由于jvm在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性、方法别调用时才会被下载,并初始化其静态属性。

静态属性由于被static修饰,保证只被实例化一次,并且严格保证实例化顺序

//静态内部类方式
public class Holder{
	//私有构造方法
	private Holder() {	
	}

	//定义一个静态内部类
	private static class InnerClass{
        //在内部类中创建外部类对象
		private static final Holder HOLDER = new Holder();//final防止外部对它进行修改
	}

	//获取实例
	public static Holder getInstance() {
		return InnerClass.HOLDER;
	}
	
}

//测试
public class Client{
    public static void main(String[] args){
        Holder instance = Holder.getInstance();
        Holder instance1 = Holder.getInstance();
        
        //判断两次获取到的LazyMan对象是否是同一个对象
        System.out.println(instance == instance1);
        //结果为ture
    }
}

反射(单例不安全)

破坏单例模式就是创建多个对象

上述单例实现方式都是不安全的,只要有反射,任何代码都是不安全的

反射:可以通过class模板得到你写的所有方法和属性

在懒汉式单例中

import java.lang.reflect.Constructor;

//懒汉式单例
public class LazyMan{
    private LazyMan(){
    	
    }

    private volatile static LazyMan LAZYMAN;

    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();
      //添加null,无参构造器
      Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
      //setAccessible是一个非常霸道的方法,会无视私有的构造器,通过反射来创建对象
      declaredConstructor.setAccessible(true);
      LazyMan instance2 = declaredConstructor.newInstance();
      
      System.out.println(instance);
      System.out.println(instance2);
    }
}

可以看到结果是已经破坏了。

解决方式

在私有的构造方法中加锁,不允许破坏

 private LazyMan(){
    	synchronized (LazyMan.class) {
    		if(LAZYMAN != null){
                //抛一个运行时异常
    			throw new RuntimeException("不要试图使用反射破坏异常");
    		}
    	}
    }

再次运行就会报错

但是如果两次都用反射破坏呢?这要怎么解决呢?可以设置一个“红绿灯”,也就是定义一个都不知道的布尔变量,外部通过反射的情况是找不到这个变量的。

import java.lang.reflect.Constructor;

//懒汉式单例
public class LazyMan{
	//设置一个“红绿灯”
	private static boolean pipisong = false;
    private LazyMan(){
    	synchronized (LazyMan.class) {
    		if(pipisong == false) {
    			pipisong = true;
    		}else {
    			throw new RuntimeException("不要试图使用反射破坏异常");
    		}
    	}
    }

    private volatile static LazyMan LAZYMAN;

    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{
      //添加null,无参构造器,获取无参构造方法对象
      Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
      //setAccessible是一个非常霸道的方法,会无视私有的构造器,通过反射来创建对象
      declaredConstructor.setAccessible(true);
      LazyMan instance = declaredConstructor.newInstance();
      LazyMan instance2 = declaredConstructor.newInstance();
      System.out.println(instance);
      System.out.println(instance2);
    }
}

实际上,别人也可以获取到我们设置的“红绿灯”,并且将其破坏掉。所以程序没有办法做到绝对的安全,所谓“道高一尺,魔高一丈”。

Field pipisong = LazyMan.class.getDeclaredField("pipisong");
pipisong.setAccessible(true);

枚举enum与反射_A person,A fool的博客-CSDN博客

所以为了单例安全,我们可以考虑使用枚举,但是需要注意的是枚举中没有无参构造,而是有两个参数(String,int)

 序列化和反序列化也能破坏单例

首先我们要知道什么是序列化和反序列化:

序列化:把对象转换为字节序列的过程称为对象的序列化

反序列化:把字节序列恢复为对象的过程称为对象的反序列化

测试一下序列化破坏单例

//测试用反射破坏单例
//以静态内部类为例
//Holder类要实现序列化接口Serializable
public class Client{
    public static void main(String[] args) throws Exception{
            writeObject2File();
            readObjectFromFile();
            readObjectFromFile();
    }
    
    //向文件中写数据(对象)
    public static void writeObject2File() throws Exception{
        //1、获取单例对象
        Holder instance = Holder.getInstance();
        //2、创建对象输出流对象,该路径会生成一个文件
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("文件路径"));
        //3、写对象
        oos.writeObject(instance);
        //4、释放资源
        oos.close();
    }
    
    //从文件中读数据(对象)
    public static void readObjectFromFile() throws Exception{
        //1、创建对象输出流对象
        ObjectInputStream ois = new ObjectInputStream(new FileOutputStream("文件路径"));
        //2、读取对象
        Holder instance = (Holder)ois.readObject();
        System.out.println(instance);//如果输出的两次打印的对象地址是否一样,如果一样说明没有破坏单例模式,如果不一样说明破坏了单例模式
        //结果输出是不一样的,已经破坏了
        //3、释放资源
        ois.close();
    }
}

解决方式

可以在Holder类中添加readResolve方法解决这个问题:

 /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    public Object readResolve() {  
        return getInstance();  
    }  

解释:在源码中,ObjectinputStream类中,可以看到readOrdinaryObject()方法调用的是hasReadResolveMethod()判断是否有readResolve()方法,如果有,就执行这个方法,如果没有这个方法,就new一个新对象

枚举方式实现单例模式(恶汉式)最优

枚举类型是线程安全的,并且只会装载一次。是所有单例实现中,唯一一种不会被破坏的单例实现模式。

//枚举:写法简单
public enum Singleton{
    INSTANCE;
}

//测试
public class Client{
    public static void main(String[] args){
        Singleton instance = Singleton.INSTANCE;
        Singleton instance1 = Singleton.INSTANCE;
        
        //判断两次获取到的LazyMan对象是否是同一个对象
        System.out.println(instance == instance1);
        //结果为ture
    }
}

在不考虑内存空间的情况下,首选枚举方式实现单例模式

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Stella呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值