23种设计模式笔记第二篇单例模式(参考狂神视频)

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

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

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

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

目录

单例模式的概念及优缺点

饿汉式单例

懒汉式单例

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

静态内部类实现(推荐)

反射(单例不安全)

扫描二维码关注公众号,回复: 13988049 查看本文章

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


单例模式的概念及优缺点

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

单例模式包含角色:Singleton

这样的模式有几个好处:

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

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

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

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

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

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

饿汉式单例

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

//饿汉式单例
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()l;
    
    public static Hungry getInstance(){
        return HUNGRY;
    }
}

懒汉式单例

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

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

    private static LazyMan LAZYMAN;

    public static LazyMan getInstance(){
        if(LAZYMAN == null){
            LAZYMAN = new LazyMan();
        }
        return LAZYMAN;
    }
}

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

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

    private static LazyMan LAZYMAN;

    public static LazyMan getInstance(){
        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();
        }
    }
}

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

 针对多线程,添加锁模式

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

注意volatile和synchronized的区别:前者保证有序性,后者保证原子性

volatile只能保证单个线程的可见性,不能保证多个线程的可见性

之所以去加volatile的原因就是因为实例化时不时原子性操作

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

    private volatile static LazyMan LAZYMAN;

    public static LazyMan getInstance(){
        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();
        }
    }
}

静态内部类实现(推荐)

//静态内部类
public class Holder{
	
	private Holder() {
		
	}
	//获取实例
	public static Holder getInstance() {
		return InnerClass.HOLDER;
	}
	
	//在内部类中创建对象
	private static class InnerClass{
		private static final Holder HOLDER = new Holder();
	}
}

反射(单例不安全)

上述三种单例都是不安全的,只要有反射,任何代码都是不安全的

反射:可以通过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)

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

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

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

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

可以添加方法解决这个问题:

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

猜你喜欢

转载自blog.csdn.net/qq_44709970/article/details/124368288