文章目录
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()则直接返回此方法指定的对象,而不需要在创建新的对象!