Java单例模式

  单例模式应该是我们接触的众多设计模式中的第一个,但是对于单例模式的一些细节地方对于初学者来说并不是很清楚,所以本文就来整理下单例模式。

单例模式

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

单例的使用场景

  1. Spring中bean对象的模式实现方式
  2. servlet中每个servlet的实例
  3. spring mvc和struts1框架中,控制器对象是单例模式
  4. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加
  5. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统
  6. 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
  7. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源

单例模式优点

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

单例的实现方式

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

饿汉式

  也就是类加载的时候立即实例化对象,实现的步骤是先私有化构造方法,对外提供唯一的静态入口方法,实现如下

/**
 * 单例模式:饿汉式
 * @author 波波烤鸭
 *
 */
public class SingletonInstance1 {
	// 声明此类型的变量,并实例化,当该类被加载的时候就完成了实例化并保存在了内存中
	private static SingletonInstance1 instance = new SingletonInstance1();

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

  饿汉式单例模式代码中,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字

问题:如果只是加载本类,而不是要调用getInstance(),甚至永远没有调用,则会造成资源浪费!

懒汉式

/**
 * 单例模式:懒汉式
 * @author 波波烤鸭
 *
 */
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方法都会同步,所以对性能的影响比较大。

双重检测锁式

/**
 * 单例模式:懒汉式
 * 双重检测机制
 * @author 波波烤鸭
 *
 */
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底层内部模型原因,偶尔会出问题。不建议使用。

静态内部类式

/**
 * 静态内部类实现方式
 * @author 波波烤鸭
 *
 */
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. 兼备了并发高效调用和延迟加载的优势!– 兼备了并发高效调用和延迟加载的优势!

枚举单例

/**
 * 单例模式:枚举方式实现
 * @author dengp
 *
 */
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. 无延迟加载

单例模式漏洞

  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);
}

输出结果:

com.dpb.single.SingletonInstance1@15db9742
com.dpb.single.SingletonInstance1@6d06d69c

产生了两个对象,和单例的设计初衷违背了。
解决的方式是在无参构造方法中手动抛出异常控制

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

Java单例模式_第1张图片

  1. 通过反序列化的方式也可以破解上面几种方式(除了枚举的方式)
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);
}

输出结果:

com.dpb.single.SingletonInstance2@5c647e05
com.dpb.single.SingletonInstance2@4c873330

是两个不同的对象,同样破坏了单例模式,这种情况怎么解决呢
我们只需要在单例类中重写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;
	}
}

Java单例模式_第2张图片
说明:readResolve方法是基于回调的,反序列化时,如果定义了readResolve()则直接返回此方法指定的对象,而不需要在创建新的对象!

常见的五种单例模式在多线程环境下的效率测试

大家只要关注相对值即可。在不同的环境下不同的程序测得值完全不一样
Java单例模式_第3张图片

总结

这几种设计模式如何选择

1、单例对象占用资源少,不需要延时加载:

枚举式 好于 饿汉式

2、单例对象占用资源大,需要延时加载:

静态内部类式 好于 懒汉式

你可能感兴趣的:(#,java设计模式)