目录
前言
饿汉式
懒汉式
Double_check
volatile + double_check
Holder方式
枚举
前言
单例设计模式GOF23中设计模式中最常用的设计模式之一, 单例设计模式提供了多线程环境下的保证唯一实例性的解决方案, 虽然简单, 但是实现单例模式的方式多种多样, 因此需要从多个维度去评价:
- 线程安全
- 性能
- 懒加载
懒加载的好处是啥?
提高页面加载速度:通过仅在需要时加载资源,懒加载可以减少初次加载时的资源量,使页面更快地呈现给用户。
节省带宽:只有当用户真正需要时才加载资源,这样可以节省不必要的带宽使用,特别是对于那些需要大量数据或高分辨率图片的应用。
提升用户体验:通过延迟加载,可以让用户更快地看到页面的主要内容,从而提高用户的满意度和体验。
减少服务器负载:懒加载可以减少一次性加载大量资源的需求,从而减少服务器的负载,优化服务器性能。
优化移动端性能:对于移动设备,网络连接通常较慢且不稳定,懒加载可以显著提高移动端的页面加载速度和性能。
节省系统资源:懒加载可以避免不必要的资源消耗,减少浏览器和设备的内存使用,尤其是在处理大量数据或复杂页面时。
饿汉式
public class TestMain {
// 定义的时候直接初始化
private static TestMain instance = new TestMain ();
// 提供get方法
public static TestMain getInstance() {
return instance;
}
// 私有化构造方法
private TestMain() {
}
}
在类加载过程中提到过, instance作为类变量, 在类的初始化的过程中, 会被收集到<cinit>()反方法中, 该方法每个类只会执行一次, 100%保证同步, 因此在使用instance的过程中, instance不可能会被实例化两次
但是, 这也正是因为是在类初始化的时候就已经存在, 那么就表明, 这个instance实例在完成初始化之后, 并存储在内存空间之后, 并不会立即就会被使用到, 那么就会在内存中驻留一个时间间隔, 这个时间间隔带来的成本增加需要通过量化, 不能一概而论
例如如果instance实例的内存消耗小, 那么饿汉式也未免不是一种良好的解决方案, 虽然他不支持懒加载.
对于这个getInstance方法, 没有加任何锁, 因此没有其他的性能消耗, 速度快
懒汉式
所谓懒汉, 就是我很懒, 我只有在使用类实例的时候才去创建, 可以避免类在初始化的时候就提前去创建.
代码如下:
public final class Test {
private static Test instance = null;
private Test() {
}
public synchronized static Test getInstance() {
if (null == instance) {
instance = new Test ();
}
return instance;
}
}
当前方法的getInstace, 每次进入这个方法之前都会获取这个类的Class对象, 然后加锁, 但是我每次调用getIsntace都需要拿到一次锁, 这样性能不高. 同一个时刻只能由一个线程能拿到这个instance实例.
为了避免上述情况, 我们考虑降低这个锁的粒度:
你似乎可以这样写:
public final class Test {
private static Test instance = new Test ();
private Test() {
}
public static Test getInstance() {
synchronized (Test.class) {
if (instance == null) {
instance = new Test ();
}
return instance;
}
}
}
但是这样写, 好像每次getInstance获取实例还是需要拿到锁, 还是会产生额外的锁竞争消耗 , 但是你仔细分析其实不难得知, 我除了第一次拿的时候需要初始化, 其他的时候, 都不需要初始化, 直接返回就行了, 于是你写出了这样的代码:
public final class Test {
private static Test instance = new Test ();
private Test() {
}
public static Test getInstance() {
if (instance == null) {
instance = new Test ();
}
return instance;
}
}
}
但是第一次的时候会存在多线程风险, 也就是可能会存在多次给instance赋值的情况....
于是为了线程安全, 就给他加了锁, 如下:
public final class Test {
private static Test instance = new Test ();
private Test() {
}
public static Test getInstance() {
if (instance == null) {
synchronized (Test.class) {
instance = new Test ();
}
}
return instance;
}
}
但是这样似乎并不安全, 如果有两个线程都进入了这个if判断中, 同时判断了这个instance为null都准备拿锁进行实例化, 假设是线程1拿到锁, 之后instance 被初始化, 然后被返回, 释放锁后, 线程2又走了一遍实例化的过程, 因此这个instance就被实例化了两次, 也就没有保障单列模式.
于是就有了下面这种写法.
Double_check
不再在方法上加锁, 我们在方法内部使用锁块来进行加锁, 降低粒度. 如下:
public final class Test {
private static Test instance = null;
private Test() {
}
public static Test getInstance() {
if (instance == null) {
synchronized (Test.class) {
if (instance == null)
instance = new Test ();
}
}
return instance;
}
}
volatile + double_check
public final class Test {
private static Test instance = null;
Object var1;
Object var2;
private Test() {
}
public static Test getInstance() {
if (instance == null) {
synchronized (Test.class) {
if (instance == null)
instance = new Test ();
}
}
return instance;
}
public static void setInstance(Test instance) {
Test.instance = instance;
}
public Object getVar1() {
return var1;
}
public Object getVar2() {
return var2;
}
}
但是同时这个方法还存在一个问题, 都知道java的内存模型中每个线程都有自己的工作内存,. 变量都是暂存在工作内存中, 对工作内存中修改变量, 如果没有的就去内公共内存中去读取, 那么档期使用这种方法更新的时候, 其实其他线程是看不到第一个线程对其的修改.
除此之外, 上述的doublecheck也不一定安全, 看看上面的一个案例
在没有volatile的情况下,编译器可能会将instance = new Test();这行代码拆分为多个操作:1) 分配内存给instance;2) 调用Test的构造函数来初始化对象;3) 将instance指向分配的内存。如果没有适当的同步措施,这些操作的执行顺序可能会被重排序,比如先执行步骤1和3,再执行步骤2。这样,在步骤2完成之前,其他线程就可能看到非空的instance引用,但此时对象可能还未被完全初始化,从而导致程序出错。
代码如下:
public final class Test {
private volatile static Test instance = null;
Object var1;
Object var2;
private Test() {
}
public static Object getInstance() {
if (instance == null) {
synchronized (Test.class) {
if (instance == null)
instance = new Test();
}
}
return instance;
}
public static void setInstance(Test instance) {
Test.instance = instance;
}
public Object getVar1() {
return var1;
}
public Object getVar2() {
return var2;
}
}
Holder方式
在讲解java的jvm类加载机制的时候提到过:
可以利用这个特性, 在需要实现单例模式的类中 添加一个静态内部类, 此时只要使用到了这个静态内部类里面的静态变量, 就会执行这个类的初始化, 但是后面无论如何 访问多少次, 就不再初始化了, 这就保证了单例模式的线程安全, 同时实现了懒加载, 重复获取也没有锁竞争.
public final class Test {
private Test() {}
private static class Holder {
private static final Test INSTANCE = new Test();
}
public static Test getInstance() {
return Holder.INSTANCE;
}
}
枚举
effect java力荐方式:
在Java中,使用枚举(Enum)来实现单例模式是一种既简单又高效的方法。枚举类型在Java中是一种特殊的类,它默认就是单例的,并且线程安全。这是因为JVM保证了枚举实例的唯一性,并且在枚举的任何地方访问枚举实例时,都不需要同步
public enum SingletonEnum {
INSTANCE;
// 可以在这里添加方法
public void doSomething() {
System.out.println("Doing something...");
}
// 示例:添加私有字段和方法
private Singleton instance;
public void setData(Singleton instance) {
this.instance= instance;
}
public Singleton getData() {
return instance;
}
}
其中:
SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;
instance1和instance2是相同的实例.
当然这种方式不是懒加载的, 如果你想使用懒加载, 那么你可以使用静态内部类类似的加载机制:
java的枚举类型和静态内部类一样, 只有在第一次被引用时会被加载和初始化,因此 EnumHolder 只有在调用 getInstance() 方法时才会被加载
public final class Test {
private Test() {}
private enum EnumHolder {
INSTANCE;
private Test test;
EnumHolder() {
test = new Test();
}
private Test getTest() {
return test;
}
}
// 调用该方法会主动使用 EnumHolder.INSTANCE, EnumHolder.INSTANCE
public static Test getInstance() {
return EnumHolder.INSTANCE.getTest();
}
}