java 单例模式

**

一、饿汉式(在多线程情况是线程安全的)

**

/** 饿汉式单例模式
 * @author harry
 * @date 2021/11/29 22:19
 */
public class Hungry {
    // 无参构造器私有化
    private Hungry(){
        System.out.println(Thread.currentThread().getName());
    }

    private static final Hungry instance = new Hungry();

    public static Hungry getInstance(){
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                Hungry.getInstance();
            }).start();
        }
        Hungry instance = Hungry.getInstance();
        System.out.println(instance);
        // main
        // com.harry.demo.Hungry@246b179d
        // 创建的实例都是同一份 且当前仅有一个线程进来并走了Hungry的无参构造 打印了main主线程
    }
}

通过结果可以看到:我这里给它new了1000个线程去启动 但是最后执行Hungry无参构造方法输出的只有一个主线程main,说明饿汉式这种单例模式的加载方式 对于多线程它是安全的。
注意:这里为什么不是Thread-getName()
因为当前是main主线程去执行的Hungry的实例 main是静态的
private static final Hungry instance = new Hungry(); 也是静态的 这里new了Hungry的实例 所以会去执行
Hungry的无参构造 然后打印当前线程是main 后面再去执行for循环1000次 都不会执行Hungry.getInstance();因为当前在堆中已经给你分配好了一份唯一的且有地址指向的Hungry实例。

二、懒汉式(多线程下不是线程安全的)

/**
 * 懒汉式单例模式
 *
 * @author harry
 * @date 2021/11/29 22:31
 */
public class Lazy {
    // 无参构造私有化
    private Lazy() {
        System.out.println(Thread.currentThread().getName());
    }

    // 此处是没有final关键字修饰的 所以每次创建一个实例就会分配一个新的给它
    private static Lazy instance;

    public static Lazy getInstance() {
        if (instance == null) {
            instance = new Lazy();
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                Lazy.getInstance();
            }).start();
        }
        Lazy instance = Lazy.getInstance();
        System.out.println(instance);
        // Thread-0
        //Thread-4
        //Thread-3
        //Thread-2
        //Thread-1
        //com.harry.demo.Lazy@246b179d
        // 懒汉式加载方式 针对多线程是无效的
    }
}

通过结果可以看到:我这里给它new了1000个线程去启动 但是最后执行Hungry无参构造方法输出的有多个线程的存在,说明饿汉式这种单例模式的加载方式 对于多线程它是不安全的。
这里没有出现main的线程打印 是因为 在main主线程执行的时候 去执行静态private static Lazy instance;但是这里没有对Lazy进行实例化 故不会走无参构造方法去打印出main主线程。因为在第一次main加载的时候没有创建对象 分配地址 初始化值 所以 在多个线程进来的时候 都会去找instance为null的情况,故存在多个线程的打印。
解决办法:
基于instance 判断是否为空 再去实例化Lazy对象,故衍生出来了一种解决办法:双重检测锁机制:
代码修改为:

// 双重检测锁  DCL懒汉式模式
    public static Lazy getInstance() {
        if (instance == null) {
            synchronized (Lazy.class){
                if (instance == null) {
                    instance = new Lazy();
                }
            }
        }
        return instance;
    }

虽然双重检测锁能保证线程的顺序性,但是又会衍生出一个新的问题。在 instance = new Lazy();的时候,可能并不会保证一个原子性的一个操作安全。
new Lazy():有三个步骤:

  1. 在堆中开辟一个内存空间,分配内存地址
  2. 执行Lazy的构造方法,初始化对象
  3. 将堆中对象的地址赋值给栈中定义的Lazy instance引用变量 instance

在多线程并发的情况下,有可能出现指令重排的情况 (执行顺序有123 132)如果是132 就会出现第二个线程进来获取instance 此时instance !=null就会出现问题。
解决办法
对instance类实例加volatile关键字就可以防止指令重排序

高阶玩法:
此时如果在main线程中 利用反射对Lazy的private构造方法进行破坏,通过构造方法的方式去new实例就会出现多个实例的情况。

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Lazy instance = Lazy.getInstance();
        Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        Lazy instance1 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }

此时 解决办法:

// 无参构造私有化
    private Lazy() {
        synchronized (Lazy.class) {
            if (instance != null) {
                throw new RuntimeException("请不要破坏私有的构造方法");
            }
        }
    }

这样的话控制台就会抛出咱们自定义的一个运行时异常的语句

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.harry.demo.Lazy.main(Lazy.java:41)
Caused by: java.lang.RuntimeException: 请不要破坏私有的构造方法
	at com.harry.demo.Lazy.<init>(Lazy.java:17)
	... 5 more

三、枚举类

/** 探究枚举类为啥是线程安全的
 * @author harry
 * @date 2021/11/30 0:24
 */
public enum Single {
    INSTANCE;
    public static Single getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Single instance = Single.getInstance();
        Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Single instance1 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }
}

这里也是直接根据枚举获取一个instance和通过反射去破坏private构造器去获取另外一个instance实例
但是当前控制台报

Exception in thread "main" java.lang.NoSuchMethodException: harry.demo.Single.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
Disconnected from the target VM, address: '127.0.0.1:49927', transport: 'socket'
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.harry.demo.Single.main(Single.java:18)

没有无参构造 通过反编译工具 查看后 其实jdk1.5以后枚举默认就有一个

private Single(String s,int i){
	super(s,i);
}

的一个有参构造方法,于是修改我们的代码。

 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Single instance = Single.getInstance();
        Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        Single instance1 = declaredConstructor.newInstance();
        System.out.println(instance);
        System.out.println(instance1);
    }

此时控制台输出的结果为:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.harry.demo.Single.main(Single.java:20)

这句话的意思就是无法以反射方式创建枚举对象。所以枚举对象是线程安全的。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值