设计模式:单例模式

单例模式

 某个类只能存在一个实例对象。

饿汉式:

类加载就创建了,有点着急,有可能没用就创建了,用静态属性是对象也会创建。通常是重量级对象,饿汉式可能造成创建了对象但没有使用,造成资源浪费。避免了多线程的同步问题。

法一:静态成员变量位置创建
public class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton(){}
	public Singleton getInstance () {
		return instance;
	}
}
法二:静态代码块中创建
public class Singleton {
	private static Singleton instance;
	static {
	instance = new Singleton;
	}
	private Singleton(){}
	public Singleton getInstance () {
		return instance;
	}
}

因为类加载就创建一个对象,所以不存在线程安全问题

懒汉式:

类加载不会创建对象,调用静态属性不会调用构造器。只要调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象。如果是多线程环境下,每个线程抢占Singleton类的对象资源,但是可能会发生对个线程同时请求对象实例的问题,这个时候就有可能创建多个对象,从而导致数据不一致,就会出现线程安全问题。

法一:线程不安全懒加载
public class Singleton{
	private static Singleton instance;
	private Singleton(){}
	public static Singleton getInstance(){
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
法二:线程安全,效率低
public class Singleton{
	private static Singleton instance;
	private Singleton(){}
	public static synchronized Singleton getInstance(){
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
法三:双重检查锁
public class Singleton{
	//JVM在实例化对象的时候会进行优化和指令重排,在多线程情况下,可能会出现空指针。
	/**对象创建主要分为三个部分
	1.分配对象内存空间
	2.初始化对象
	3.设置instance指针指向对象所在的内存空间
	线程a得到锁之后,还没初始化对象,就设置了指针,然后归还了锁。线程b检查到对象不为空,就返回了线程a创建的对象,但是线程a还没有完成初始化,所以会导致了线程b拿到的对象时一个空对象,就会出现空指针问题
	解决方法:使用volatile关键字,保证可见性和有序性,禁止了对当前修饰的变量上下文重排。保证了方法的可靠性。
	*/
	private static volatile Singleton instance;
	private Singleton(){}
	public static Singleton getInstance(){
		//没有给整个获取方法上锁,提高效率
		if (instance == null) {
		//第一个判断之后仍会有线程并发安全问题
		//问题是多个线程并发判空成功,多个线程可以排队获得锁,导致创建多个实例,第二次判空很有必要。
			synchronized(Singleton.class) {
				//需要进行第二次判空
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}
法四:静态内部类
/**
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 
只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。
静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
*/
public class Singleton{
	private Singleton(){}
	private static class SingletonHolder{
		private static final Singleton INSTANCE = new SingletonHolder();
	}
	public static Singleton getInstance(){
		return SingletonHolder.INSTANCE;
	}
}
/**
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder
并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
​
所以静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。
在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
*/
法五:枚举方式
/**
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚
举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
*/
public enum Singleton{
	INSTANCE;
}
//没错,正如你所看到,枚举正是你所看到的这样就是这么简单,实际上枚举类型是在Java的语法糖。

单例模式可以被破坏

上述定义的单例中正常使用的情况下只可以同时只有一个对象存在,但存在着某一些操作可以破坏这种现象,使得其可以创建多个对象。(枚举除外)。
这两种方式,分别是序列化和反序列化:

  • 序列化反序列化破坏单例模式
    使用性能较好的内部类的方式的单例模式,来检验一下,序列化和反序列化会不会创建不同的对象

代码示例

Singleton类:

public class Singleton implements Serializable {
    private Singleton(){}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

Test类:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Singleton singleton = Singleton.getInstance();
        writeObject(singleton);
        Singleton s1 = readObject();
        Singleton s2 = readObject();
        System.out.println(singleton == s1);
        System.out.println(s1 == s2);
    }

    private static Singleton readObject() throws IOException, ClassNotFoundException {
        ObjectInputStream input = new ObjectInputStream(new FileInputStream("E:\\桌面\\object.txt"));
        Singleton obj = (Singleton)input.readObject();
        return obj;
    }

    private static void writeObject(Singleton singleton) throws IOException {
        ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("E:\\桌面\\object.txt"));
        output.writeObject(singleton);
    }

}

上面代码运行结果都是false,表明序列化和反序列化已经破坏了单例模式只有一个对象存在的原则。

  • 反射破坏单例模式
public class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Class<Singleton> singletonClass = Singleton.class;
        Constructor con = singletonClass.getDeclaredConstructor();
        con.setAccessible(true);
        Singleton s1 = (Singleton)con.newInstance();
        Singleton s2 = (Singleton)con.newInstance();
        System.out.println(s1 == s2);
    }

上面代码运行结果也都是false,表明反射也破坏了单例模式。

原因分析:

实际上对于序列化与反序列化破坏单例模式的问题,主要是通过readObject()方法,出现了破坏单例模式的现象,主要是因为这个方法最后会通过反射调用无参数的构造方法创建一个新的对象,从而每次返回的对象都不一致。

对于反射破坏单例模式是因为单例模式通过setAccessible(true)指示反射的对象在使用时,取消了 Java 语言访问检查,使得私有的构造函数能够被访问,

而单例模式的设计在于只保留一个公有静态函数来获取唯一的实例,其他方法(构造函数)或字段为私有,外界不能访问。

而反射破坏了这一原则,它突破了构造函数私有的限制,可以获取单例类的私有构造函数并使用其创建多个对象。

问题解决

  • 序列化、反序列化方式破坏单例模式的解决方法

ReadObject()方法底层,通过反射的方式调用要被反序列化的类的readResolve方法。
所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

代码实例

public class Singleton implements Serializable {

    private Singleton(){}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}
  • 反射破解单例模式的解决方法

因为反射是一种暴力获取对象实例的方法,因为他可以直接访问private修饰的构造函数,所以在对于反射方式破坏单例模式的问题上我们只能采取被动的防御,

既然你能访问我的构造函数,我就在我的构造函数中建立防御机制,不让你通过我的构造函数创建多个实例对象。

代码示例:

public class Singleton implements Serializable {
    private static volatile boolean flag = false;

    private Singleton(){
    	//加锁,防止多个线程同时创建
        synchronized (Singleton.class) {
        	//使得构造函数只能创建一个实例对象
            if (flag) {
                throw new RuntimeException();
            }
            flag = true;
        }
    }

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

关于枚举类单例模式为什么不会被破坏

其实是因为java的底层做了很多的方法来防止我们的枚举类单例模式被破坏,其中一点就是我们无法创建枚举类的实例。

本文参考文章链接: https://blog.csdn.net/weixin_63834061/article/details/130286426

  • 30
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值