单例模式

Java单例模式

单例模式的核心是保证同一个类 只有一个实例,并且提供一个访问实例的全局访问点。

一、单例模式优点

由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决
单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有的数据表的映射处理

二、单例的实现方式

实现方式优缺点
饿汉式线程安全,调用效率高,但是不能延迟加载
懒汉式线程安全,调用率不高,能延迟加载
双重检测锁式由于JVM底层内部模型原因,偶尔会出现问题,不建议使用
静态内部类式线程安全,资源利用率高,可以延时加载
枚举单例线程安全,调用效率高,但是不能延迟加载

1.饿汉式

概念:就是在类加载的时候立即实例化对象,对外提供唯一的静态入口方法

public class SingletonInstance1 {
	// 声明此类型的变量,并实例化,当该类被加载的时候就完成了实例化并保存在了内存中
	private static SingletonInstance1 instance = new SingletonInstance1();

	// 私有化所有的构造方法,防止直接通过new关键字实例化
	private SingletonInstance1(){}
	// 对外提供一个获取实例的静态方法
	public static SingletonInstance1 getInstance(){
		return instance;
	}
}

饿汉式的单例模式代码中,static变量会在类加载时初始化,此时也不会涉及多个线程对象方法该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字
问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!!!

2.懒汉式

/**
 * 单例模式:懒汉式
 *
 */
public class SingletonInstance2 {
	// 声明此类型的变量,但没有实例化
	private static SingletonInstance2 instance = null;

	// 私有化所有的构造方法,防止直接通过new关键字实例化
	private SingletonInstance2(){}
	// 对外提供一个获取实例的静态方法,为了数据安全添加synchronized关键字
	public static synchronized SingletonInstance2 getInstance(){
		if(instance == null){
			// 当instance不为空的时候才实例化
			instance = new SingletonInstance2();
		}
		return instance;
	}
}

此种方式在类加载后如果我们一直没有调用getInstance方法,那么就不会实例化对象。实现了延迟加载,但是因为在方法上添加了synchronized关键字,每次调用getInstance方法都会同步,所以对性能的影响比较大。

3.双重检测锁式

/**
 * 单例模式:懒汉式
 * 双重检测机制
 *
 */
public class SingletonInstance3 {
	// 声明此类型的变量,但没有实例化
	private static SingletonInstance3 instance = null;

	// 私有化所有的构造方法,防止直接通过new关键字实例化
	private SingletonInstance3(){}
	// 对外提供一个获取实例的静态方法,
	public static  SingletonInstance3 getInstance(){
		if(instance == null){
			SingletonInstance3 s3 = null;
			synchronized(SingletonInstance3.class){
				s3 = instance;
				if(s3 == null){
					synchronized(SingletonInstance3.class){
						if(s3 == null){
							s3 = new SingletonInstance3();
						}
					}
				}
				instance = s3;
			}
		}
		return instance;
	}
}

模式将同步内容下方到if内部,提高了执行的效率不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。
问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出问题,不建议使用。

4.静态内部类式

/**
 * 静态内部类实现方式
 *
 */
public class SingletonInstance4 {
	// 静态内部类
	public static class SingletonClassInstance{
		// 声明外部类型的静态常量
		public static final SingletonInstance4 instance = new SingletonInstance4();
	}
	// 私有化构造方法
	private SingletonInstance4(){}
	
	// 对外提供的唯一获取实例的方法
	public static SingletonInstance4 getInstance(){
		return SingletonClassInstance.instance;
	}
}

注意点:
1.外部类没有static属性,则不会像饿汉式那样立即加载对象。
2.只有真正调用getInstance(),才会加载静态内部类。加载类时是线程安全的instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。
3.兼容了并发高效调用和延迟加载的优势!

5.枚举单例

/**
 * 单例模式:枚举方式实现
 *
 */
public enum SingletonInstance5 {

	// 定义一个枚举元素,则这个元素就代表了SingletonInstance5的实例
	INSTANCE;
	
	public void singletonOperation(){
		// 功能处理
	}
}


测试代码:
public static void main(String[] args) {
	SingletonInstance5 s1  = SingletonInstance5.INSTANCE;
	SingletonInstance5 s2  = SingletonInstance5.INSTANCE;
	System.out.println(s1 == s2); // 输出的是 true
}

优点:
1.实现简单
2.枚举本身就是单例模式,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞
缺点:
无延迟加载

三、单例模式的漏洞

1.通过反射的方式我们依然可用多个实例(除了枚举的方式)

public static void main(String[] args) throws Exception, IllegalAccessException {
	SingletonInstance1 s1 = SingletonInstance1.getInstance();
	// 反射方式获取实例
	Class c1 = SingletonInstance1.class;
	Constructor constructor = c1.getDeclaredConstructor(null);
	constructor.setAccessible(true);
	SingletonInstance1 s2 = (SingletonInstance1)constructor.newInstance(null);
	System.out.println(s1);
	System.out.println(s2);
}

输出结果:
在这里插入图片描述
产生了两个对象,和单例的设计初衷违背了。
解决的方式是 在无参构造方法中手动抛出异常控制

// 私有化所有的构造方法,防止直接通过new关键字实例化
private SingletonInstance2(){
	if(instance != null){
		// 只能有一个实例存在,如果再次调用该构造方法就抛出异常,防止反射方式实例化
		throw new RuntimeException("单例模式只能创建一个对象");
	}
}

在这里插入图片描述

2.通过反序列化的方式也可以破坏上面几种方式(除了枚举的方式)

public static void main(String[] args) throws Exception, IllegalAccessException {
	SingletonInstance2 s1 = SingletonInstance2.getInstance();
	// 将实例对象序列化到文件中
	ObjectOutputStream oos = new ObjectOutputStream(
			new FileOutputStream("c:/tools/a.txt"));
	oos.writeObject(s1);
	oos.flush();
	oos.close();
	// 将实例从文件中反序列化出来
	ObjectInputStream ois = new ObjectInputStream(
			new FileInputStream("c:/tools/a.txt"));
	SingletonInstance2 s2 = (SingletonInstance2) ois.readObject();
	ois.close();
	System.out.println(s1);
	System.out.println(s2);
}

输出结果:
在这里插入图片描述
是两个不同的对象,同样破坏了单例模式,这种情况怎么解决呢
我们只需要在单例类中重写readResolve方法并在该方法中返回单例对象即可,如下:

package com.dpb.single;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 单例模式:懒汉式
 * @author 波波烤鸭
 *
 */
public class SingletonInstance2 implements Serializable{
	
	// 声明此类型的变量,但没有实例化
	private static SingletonInstance2 instance = null;

	// 私有化所有的构造方法,防止直接通过new关键字实例化
	private SingletonInstance2(){
		if(instance != null){
			// 只能有一个实例存在,如果再次调用该构造方法就抛出异常,防止反射方式实例化
			throw new RuntimeException("单例模式只能创建一个对象");
		}
	}
	// 对外提供一个获取实例的静态方法,为了数据安全添加synchronized关键字
	public static synchronized SingletonInstance2 getInstance(){
		if(instance == null){
			// 当instance不为空的时候才实例化
			instance = new SingletonInstance2();
		}
		return instance;
	}
	// 重写该方法,防止序列化和反序列化获取实例
	private Object readResolve() throws ObjectStreamException{
		return instance;
	}
}

在这里插入图片描述
说明:readResolve方法是基于回调的,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要在创建新的对象!

四、总结:

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值