Java并发编程——单例模式+单例模式不安全

一、饿汉式单例

描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

//饿汉式单例
public class Hungry {

    //构造器私有化
    private Hungry(){
        System.out.println(Thread.currentThread().getName()+" ok");
    }
    private final static Hungry hungry=new Hungry();
    public static Hungry getInstance(){
        return hungry;
    }
}

二、懒汉式单例(DLC)

描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

//懒汉式单例DLC
public class LazyMan {
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+" ok");
    }

    //双重检测锁+原子性操作
    private volatile static LazyMan lazyMan;

    //双重检测锁模式的懒汉式单例 DCL懒汉式
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();//若不用synchronized修饰这个类,就不是原子性操作
                }
            }
        }
        return lazyMan;
    }
    //多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazyMan.getInstance();
            }).start();
        }
    }
}

lazyMan = new LazyMan()时
1.分配内存空间
2.执行构造方法,初始化对象
3.把这个对象指向这个空间
我们希望的是按照1,2,3的顺序执行
但是会进行指令重排,可能会变成1,3,2
对于一个对象A来说:此时1,3,2的顺序不会影响对象A的创建
但是当A创建完后再创建对象B时,由于把对象B指向他的空间会使lazyman!=null,故而使得对象B还没有完成构造!
所以要用volatile修饰lazyman来禁止指令重排。(private volatile static LazyMan lazyMan)

三、静态内部类实现单例

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

//静态内部类实现
public class Holder {
    private Holder(){

    }

    public static Holder getInstance(){
        return InterClass.holder;
    }

    public static class InterClass{
        private static final Holder holder = new Holder();
    }
}

四、单例模式不安全

通过反射破坏单例模式:

package com.alchemy.single;

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

//懒汉式单例DLC
public class LazyMan {
    private static boolean flag=false;
    
    private LazyMan(){
        System.out.println(Thread.currentThread().getName()+" ok");
    }

    //双重检测锁+原子性操作
    private volatile static LazyMan lazyMan;

    //双重检测锁模式的懒汉式单例 DCL懒汉式
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();//不是原子性操作
                }
            }
        }
        return lazyMan;
    }
    
    public static void main(String[] args) throws Exception {
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan instance = declaredConstructor.newInstance();
        LazyMan instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);
    }
}

在这里插入图片描述
应对方法:
定义今天静态标志位
在这里插入图片描述
在私有化构造器中添加:
在这里插入图片描述

//懒汉式单例DLC
public class LazyMan {
    private static boolean flag=false;

    private LazyMan(){

        synchronized(LazyMan.class){
            if(flag==false){
                flag=true;
            }else {
                throw new RuntimeException("不要试图通过反射破坏异常");
            }
        }
        System.out.println(Thread.currentThread().getName()+" ok");
    }

    //双重检测锁+原子性操作
    private volatile static LazyMan lazyMan;

    //双重检测锁模式的懒汉式单例 DCL懒汉式
    public static LazyMan getInstance(){
        if (lazyMan==null){
            synchronized (LazyMan.class){
                if (lazyMan==null){
                    lazyMan = new LazyMan();//不是原子性操作
                }
            }
        }
        return lazyMan;
    }

    public static void main(String[] args) throws Exception {
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);
        LazyMan instance = declaredConstructor.newInstance();
        LazyMan instance2 = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(instance2);
    }
}

在这里插入图片描述
但是如果通过特别的方式知道了我们定义的标志位,单例模式仍然不安全

public static void main(String[] args) throws Exception {
    Field flag = LazyMan.class.getDeclaredField("flag");
    flag.setAccessible(true);
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);
    LazyMan instance = declaredConstructor.newInstance();
    flag.set(instance,false);
    LazyMan instance2 = declaredConstructor.newInstance();

    System.out.println(instance);
    System.out.println(instance2);
}

在这里插入图片描述
所以目前无论怎么操作,该方法创建的单例模式始终不安全!
故而应通过枚举类型创建单例模式

五、枚举类型创建单例模式(安全)

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

//enum本身也是一个class类
public enum EnumSingle {
    INSTANCE;
    public EnumSingle getInstance(){
        return INSTANCE;
    }
}
class Test{
    public static void main(String[] args) throws Exception {
        EnumSingle instance1 = EnumSingle.INSTANCE;
        Constructor<EnumSingle> declaringConstructor=EnumSingle.class.getDeclaredConstructor(String.class,int.class);
        declaringConstructor.setAccessible(true);
        EnumSingle instance2=declaringConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);

    }
}

在这里插入图片描述
枚举类型最终反编译源码

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingle.java
package com.kuang.single;
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/kuang/single/EnumSingle, name);
}
private EnumSingle(String s, int i)
{
super(s, i);
}
public EnumSingle getInstance()
{
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alchemy_Ding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值