深入浅出单例模式(全网最详细且通俗易懂讲解)

一、什么是单例模式
单例模式是一种设计代码的思想,这个思想就是保证一个类只有一个实例,并且提供了一个访问它的全局访问点。

二、单例模式的作用
1、单例模式其实主要就是为了解决一个全局使用的类被频繁地创建与销毁的问题。
我们都知道,通过类实例化的对象一般都是放在堆内存中的,频繁的创建对象会使得堆内存不够用,进而触发垃圾回收,这会影响整个系统的性能。(比如说你在屋里吃东西,垃圾扔的到处都是,你妈进来收拾屋子,肯定就让你先别吃了,然后清理。吃零食就好比创建对象,所以垃圾清理时JVM中的进程会先停止工作(stop-the-world),反映到用户层面就是系统卡顿了)
而单例模式就解决了这个问题,单例模式保证了这个类只有一个实例,并且提供了一个访问它的全局访问点,也就是说提供的这个实例可以被反复使用,就不必去频繁地创建和销毁实例了。

2、另外,单例模式很显然也可以大大减少内存资源消耗。

三、单例模式主要的应用场景
1、频繁地创建和销毁对象
2、频繁访问IO资源的对象,例如数据库连接或者访问配置文件
3、某些对象被创建时会消耗大量的资源,但又经常使用的对象

四、单例模式代码实现具体思路
1、确保对象实例只有一个
只对类进行一次实例化,所有人都只能使用这个第一次实例化的对象。

2、构造方法私有化
保证外界不能通过new来创建一个新对象,而是只能使用上面第一次实例化的对象。

3、提供静态方法供外界来使用这个唯一的对象实例
因为外界不能new新对象了,所以我们必须提供一个类方法才能让外界访问到这个唯一的对象实例。

五、单例模式的五种常见代码实现方式

1、饿汉式

public class Singleton { 
	private static Singleton instance = new Singleton(); 
	private Singleton (){} 
	public static Singleton getInstance() { 
		return instance; 
	} 
}

由类加载机制我们可以知道,类在整个生命周期只会被加载一次,类变量也同样只会被初始化一次,所以在上述类被加载时就会创建一个实例,并且只有一个实例。

优点:在类装载的时候就完成了实例化,也就创建了唯一的实例,避免了线程同步问题
缺点:在类装载时就创建了实例,可能导致资源浪费,因为过早地创建了实例,这个实例可能压根就用不到,最好时是什么时候需要什么时候创建,实现懒加载

2、懒汉式

public class Singleton { 
	private static Singleton instance; //和饿汉式不同,这里没有进行实例化 
	private Singleton (){} 
	public static Singleton getInstance() { 
		if (instance == null) { 
			instance = new Singleton(); //实例化 
			}
 		return instance;
	  } 
  }

从上述代码可以看到,懒汉式是在需要实例的时候才进行的实例化,这样虽然实现了懒加载,但是这样会出现线程安全的问题,试想如果在多线程场景下,一个线程进入了if(instance == null)语句,但未完成实例化,同时另一个线程也到了if (instance == null)语句,因为之前未完成实例化,所以第二个线程也会进入到if语句,这样就创建了多个实例。

优点:实现了懒加载,需要时才创建实例
缺点:多线程场景下存在线程安全问题,只能在单线程场景中使用

为了解决线程安全问题,可以考虑加锁,代码如下:

public class Singleton { 
	private static Singleton instance; //和饿汉式不同,这里没有进行实例化 
	private Singleton (){} 
	public static synchronized Singleton getInstance() { 
		if (instance == null) { 
			instance = new Singleton(); //实例化 
			}
 		return instance;
	  } 
  }

这样做的缺点也很明显,就是synchronized会影响程序的效率,我们可以使用双重校验锁机制进一步优化。

3、双重校验锁
加锁的懒汉式,虽然实现了懒加载,但加锁肯定会影响效率,为了解决这个问题,可以再加一层校验,如下

public class Singleton {
	private volatile static Singleton instance; 
	private Singleton (){}
	public static Singleton getInstance() { 
	if (instance == null) { //第一次 
		synchronized (Singleton.class) { 
			if (instance == null) { //第二次
				instance = new Singleton(); 
			}
		} 
	}
	return instance;
	}
}

与加锁的懒汉式相比,我们先去判断是否实例化,如果没有实例化再进行加锁,如果已经实例化就直接返回实例,不再进行加锁。这样双重校验锁机制就缩小了锁的范围,大大减少了加锁的次数。

这里又有两个常问的问题:
(1)判断一次instance是否等于null不可以吗,为什么还要判断第二次呢?
如果缺少第一个if判断语句,那么就变成上面的加锁的懒汉式了,效率就比较低了。
如果缺少第二个if判断语句,那么当有两个线程同时进入第一个if语句,但此时没有第二个if语句,那就会创建出两个instance实例。

(2)为什么要用volatile修饰instance?
这里volatile的作用主要就是禁止指令重排序。戳我了解什么是指令重排序
上面的instance = new Singleton();并不是一个原子操作,而是分为3步执行:
①在栈上为instance引用分配内存
②初始化堆中new出来的Singleton对象
③将Singleton对象的内存地址赋值给instance引用(此时instance的值才从默认值null变为Singleton对象的内存地址)
由于JVM会进行指令重排序,简单来说就是,不是按照①②③顺序执行的,这在单线程情况下不会存在太大问题,但在多线程下就会出现问题,假设有两个线程T1和T2,T1执行到了instance = new Singleton();,但是是按照①③②的顺序执行的,刚执行完①③两步,这时T2执行第一个if语句,发现instance不等于null,直接return instance;了,但这时T1还未初始化instance。
而通过volatile修饰则可以避免指令重排序,确保上面的代码按照①②③的顺序执行,从而解决上面的问题。

4、静态内部类
通过静态内部类实现单例也是一种比较好的方法,既可以保证线程安全,又能保证懒加载,而且相比于双重校验锁实现更加简单。

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

(1)当Singleton类被加载到内存中时,内部类Singleton2并不会被加载,只有在return Singleton2.INSTANCE;后,Singleton2才会被加载,实现了懒加载。

(2)通过static修饰,可以保证INSTANCE只被初始化一次,避免了多线程的线程安全问题 。

(3)整个过程没有加锁,代码执行效率也比双重校验锁的实现方式更高。

5、枚举

public enum EnumSingleton { 
	INSTANCE; 
	public EnumSingleton getInstance(){ 
		return INSTANCE; 
	} 
}

(1)虽然通过枚举实现的单例非常简单,但这确实是单例的最佳实现方式,因为枚举方式实现的单例不但可以避免线程安全问题,还可以避免反射和反序列化对单例模式的破坏。

(2)这里简单说下反射和反序列化是如何破坏单例模式的:
①单例模式主要的特点有以下三点: 实例化的变量引用私有化、构造方法私有化、获取实例的方法公有
②这些特点都是为了保证了一个类只有一个实例,并且提供了一个访问它的全局访问点。
③构造函数私有化的原因很简单,就是为了防止其他类通过 new Singleton()的方式来获取实例,而必须调用getInstance(),这才保证了获取的都是同一个实例,但反射可以获取类的构造函数,并通过setAccessible(true)取消 Java言访问检查,可以正常调用私有的构造函数。

class Singleton { 
	private static Singleton instance = new Singleton(); 
	private Singleton (){} 
	public static Singleton getInstance() { 
		return instance; 
	}
	public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { 
		Singleton singleton = Singleton.getInstance(); 
		//通过反射创建实例 
		Constructor constructor = Singleton.class.getDeclaredConstructor(); 	
		constructor.setAccessible(true); //取消Java语言访问检查
		Singleton singleton1 = constructor.newInstance(); 
		//测试是否是同一个实例
		System.out.println(singleton.hashCode() == singleton1.hashCode()); 
		//结果输出为false,说明两者并非同一实例
	} 
}
  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值