单例模式的几种创建方式

饿汉式

package com.li.single;

public class Single1 {

        // 私有实例,类初始化就加载
        private static Single1 instance = new Single1();

        // 私有构造方法
        private Single1() {}

        // 公共的、静态的获取实例方法
        public static Single1 getInstance() {
            return instance;
        }

    public static void main(String[] args) {
        Single instance = Single.getInstance();
        Single instance1 = Single.getInstance();
        System.out.println(instance);
        System.out.println(instance1);

    }
    }

饿汉式:

 1. 类加载时就初始化,浪费内存,不能延迟加载;
 2. 基于 classloader 机制避免了多线程的同步问题,线程安全;
 3. 没有加锁,调用效率高。

懒汉式

public class SingletonDemoInLazy {
    
    // 私有实例,初始化的时候不加载(延迟加载)
    private static SingletonDemoInLazy instance;
    
    // 私有构造
    private SingletonDemoInLazy() {}
    
    // 公共获取实例方法(线程不安全)
    public static SingletonDemoInLazy getInstance() {
        if(instance == null ) { // 使用的时候加载
            instance = new SingletonDemoInLazy();
        }
        return instance;
    }
}

上面这种写法,是线程不安全的,但是可以做到延迟加载

public class SingletonDemoInLazy {
    
    // 私有实例,初始化的时候不加载(延迟加载)
    private static SingletonDemoInLazy instance;
    
    // 私有构造
    private SingletonDemoInLazy() {}
    
    // 公共获取实例方法(线程安全,调用效率低)
    public synchronized static SingletonDemoInLazy getInstance() {
        if(instance == null ) {
            instance = new SingletonDemoInLazy();
        }
        return instance;
    }
}

 上面代码中,通过关键字synchronized声明公共的获取实例的方法getInstance(),可以确保线程安全,能做到延迟加载,但是效率不高

double-checked locking(双重检查锁) 

package com.li.single;

import lombok.SneakyThrows;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;

public class Single {
    // 私有实例,volatile关键字,禁止指令重排。
    private volatile static Single instance;
    private static boolean flag = false;

    // 私有构造 这里在双重锁的基础上再次加锁,为了避免反射破坏,但是使用反射可以获取字段名并更改值,所以还是可以使用反射破坏单例
    private Single() {
        synchronized (Single.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("使用反射破坏单例");
            }
        }
    }

    // 公共获取实例方法(线程安全)
    public static Single getInstance() {
        if (instance == null) { // 一重检查
            synchronized (Single.class) {
                if (instance == null) { // 二重检查
                    instance = new Single();
                }
            }
        }
        return instance;
    }

    @SneakyThrows
    public static void main(String[] args) {
        Single instance = Single.getInstance();
        System.out.println(instance);
//使用反射的空参构造获取对象
        Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
//使用反射获取字段
        Field flag = Single.class.getDeclaredField("flag");
        flag.setAccessible(true);
//更改字段的值
        flag.set(declaredConstructor,false);
        Single single = declaredConstructor.newInstance();
        System.out.println(single);
    }
}

在加锁之前判断是否为空,可以确保 instance 不为空的情况下,不用加锁,可以直接返回。

加锁之后,还需要判断 instance 是否为空,是为了防止在多线程并发的情况下,会实例化多个对象。例如:线程 a 和线程 b 同时调用 getInstance 方法,假如同时判断 instance 都为空,这时会同时进行抢锁。假如线程 a 先抢到锁,开始执行 synchronized 关键字包含的代码,此时线程 b 处于等待状态。线程 a 创建完新实例了,释放锁了,此时线程 b 拿到锁,进入 synchronized 关键字包含的代码,如果没有再判断一次 instance 是否为空,则可能会重复创建实例

加volatile关键字原因

 这是因为编译器在编译时会进行指令重排,而 volatile 可以禁止指令重排
对象的创建大概有这么三个步骤(从指令层面来看啊,从JVM来讲具体还有很多步骤):

  1. 分配内存空间
  2. 执行构造方法,初始化对象
  3. 把这个对象指向这个空间

正常是按 123 的顺序执行,但由于指令重排的存在,可能会存在 A 线程按照132 执行,当执行到 3 时,线程 B 来取对象,就会得到空值。因此必须给单例对象加上 volatile 关键字。 

双重检查锁:

 1. 双重判断,延迟加载;
 2. 线程安全;
 3. JDK 版本要求1.5起

也叫DCL模式

静态内部类

public class SingletonDemoInStaticInnerClass {
    
    // 静态内部类
    private static class InnerClass{
        // 初始化实例
        private final static SingletonDemoInStaticInnerClass INSTANCE = new SingletonDemoInStaticInnerClass();
    }
    
    // 私有构造
    private SingletonDemoInStaticInnerClass() {}
    
    // 公关获取实例方法(线程安全,延迟加载)
    public static SingletonDemoInStaticInnerClass getInstance() {
        return InnerClass.INSTANCE;
    }
}

静态内部类:

 1. 利用了classloader机制来保证初始化 instance 时只有一个线程,线程安全
 2. 只有通过显式调用 getInstance 方法时,才会显式装载静态内部类,从而实例化instance,延迟加载

枚举

public enum SingletonEnum {
    
    // 枚举元素本身就是单例 
    INSTANCE;
    
    
    // 其他要执行的方法
    public void sayHello() {
        System.out.println("你好");
    }
    ......
}

 枚举:这是实现单例模式的最佳方法。它更简洁,不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。但是不是延迟加载的,是不可以使用反射破坏单例机制的

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值