单例模式---懒汉式的优化

本文深入解析单例模式的实现方式,包括懒汉式和饿汉式的优缺点,并探讨了线程安全问题及解决方案,最后讨论了反射机制对单例模式的影响。

单例模式中最重要的思想是:构造器私有,因此能保证我们的内存中只有一个对象。

单例模式分为懒汉式和饿汉式

1.饿汉式:顾名思义,很饿,上来就吃。

package cn.com;

//饿汉式单例,在程序运行时,无论是否需要 都会创建对象,因此  可能会浪费空间
public class HungryMan {

    //构造器私有,无法new 这个对象
    private HungryMan() {

    }
    private  static HungryMan HUNGRY = new HungryMan();

    private static HungryMan getInstance() {
        return HUNGRY;
    }

    //测试后 得到的 对象的hashcode 一致,说明是同一个对象
    public static void main(String[] args) {
        HungryMan instance1 = HungryMan.getInstance();
        HungryMan instance2 = HungryMan.getInstance();
        HungryMan instance3 = HungryMan.getInstance();
        // 打印hashcode
        System.out.println(instance1);
        System.out.println(instance1);
        System.out.println(instance1);
    }
}
运行结果: 三个对象的hashcode 值一样,说明是指向了同一个对象
cn.com.Hungry@1b6d3586
cn.com.Hungry@1b6d3586
cn.com.Hungry@1b6d3586

2.懒汉式单例

package cn.com;

public class LazyMan1 {
    //构造器私有
    private LazyMan1() {
        System.out.println(Thread.currentThread().getName() + "  正在运行!");
    }

    //先不创建 ,等使用的时候再去创建
    private static LazyMan1 man;

    public static LazyMan1 getInstance() {
        if (man == null) {
            man = new LazyMan1();
        }
        return man;
    }

    /**
     * 此代码是线程不安全的,只在单线程下可以
     * 例如
     */
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                LazyMan1.getInstance();
            }).start();
        }
    }
}
运行结果

Thread-0  正在运行!
Thread-3  正在运行!
Thread-2  正在运行!
Thread-1  正在运行!

单例模式下,存在多个线程,明显不符合单例模式要求。下面对代码进行改进

3.懒汉式单例 改进

注: new LazyMan();不是原子性操作,实际底层有三步,

1.分配内存空间 

2.执行构造方法,初始化对象

 3.把对象指向这个空间 

当A线程执行方法时,可能在执行 1---3---2 ;如果此时B线程进入,由于A线程已经将空对象指向内存空间,因此B将会得到一个null的对象。所以,必须使用volatile  修饰符 避免 被指令重排

package cn.com;

//懒汉式单例
public class LazyMan {

    //构造器私有
    private LazyMan() {
        System.out.println(Thread.currentThread().getName() + "is ok ");
    }

    //volatile 避免被指令重排
    private volatile static LazyMan lanzMan;

    //双重检测锁 模式的懒汉式单例   ----DCL 懒汉式
    public static LazyMan getInstance() {
        if (lanzMan == null) {
            synchronized (LazyMan.class) {
                if (lanzMan == null) {
                    lanzMan = new LazyMan();
                }
            }
        }
        return lanzMan;
    }

    //多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                LazyMan.getInstance();
            }).start();
        }
    }
}
多次运行结果均为Thread-0is ok ;说明在多线程下是安全的。

4.尽管我们使用了双重检测锁,但是由于反射的存在,上述代码仍然是不安全的。例如,我们通过反射机制来破坏单例模式。

package cn.com;

import java.lang.reflect.Constructor;

//懒汉式单例
public class LazyMan {

    //构造器私有
    private LazyMan() {
        System.out.println(Thread.currentThread().getName() + "is ok ");
    }

    //volatile 避免被指令重排
    private volatile static LazyMan lanzMan;

    //双重检测锁 模式的懒汉式单例   ----DCL 懒汉式
    public static LazyMan getInstance() {
        if (lanzMan == null) {
            synchronized (LazyMan.class) {
                if (lanzMan == null) {
                    lanzMan = new LazyMan();
                    /**
                     *  new LazyMan();不是原子性操作;
                     * 1.分配内存空间
                     * 2.执行构造方法,初始化对象
                     * 3.把对象指向这个空间
                     */
                }
            }
        }
        return lanzMan;
    }

    //由于反射的存在,即使 使用双重检测锁 ,在多线程下也是不安全的
    public static void main(String[] args) throws Exception {
        //实例1
        LazyMan lazyMan1 = LazyMan.getInstance();

        /**
         * 通过反射破坏单例模式
         */
        //获取空参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);// 可以无视私有构造器
        LazyMan lazyMan2 = declaredConstructor.newInstance();
        System.out.println(lazyMan1.hashCode());
        System.out.println(lazyMan2.hashCode());
    }
}
运行结果

460141958
1163157884

由此可见,单例模式下 我们获取的是两个不同的对象。

解决办法:在构造器加synchronized,又双重检测锁升级为三重检测锁

//构造器私有
private LazyMan() {
        synchronized (LazyMan.class) {
            if (lanzMan != null) {
                throw new RuntimeException("不要来搞破坏");
            }
        }
}

再执行代码,结果:

 

上述解决办法可行吗?再来。。。

我们将获取对象的方式全改为 反射获取

//由于反射的存在,即使 使用双重检测锁 ,在多线程下也是不安全的
public static void main(String[] args) throws Exception {
 
    Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
    declaredConstructor.setAccessible(true);// 可以无视私有构造器
    LazyMan lazyMan1 = declaredConstructor.newInstance();
    LazyMan lazyMan2 = declaredConstructor.newInstance();
    System.out.println(lazyMan1.hashCode());
    System.out.println(lazyMan2.hashCode());
}

执行结果:  发现 又获取到了不同对象。。。。。

解决办法:通过在构造器增加标志位

package cn.com;

import sun.nio.cs.FastCharsetProvider;

import java.lang.reflect.Constructor;

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

    //构造器私有
    private LazyMan() {
        synchronized (LazyMan.class) {
            if (str == false) {
                str = true;
            } else {
                    throw new RuntimeException("不要来搞破坏");
            }
        }
    }
    private volatile static LazyMan lanzMan;

    public static LazyMan getInstance() {
        if (lanzMan == null) {
            synchronized (LazyMan.class) {
                if (lanzMan == null) {
                    lanzMan = new LazyMan(); 
                }
            }
        }
        return lanzMan;
    }

    public static void main(String[] args) throws Exception {
        //获取空参构造器
        Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);// 可以无视私有构造器
        LazyMan lazyMan1 = declaredConstructor.newInstance();
        LazyMan lazyMan2 = declaredConstructor.newInstance();
        System.out.println(lazyMan1.hashCode());
        System.out.println(lazyMan2.hashCode());
    }
}
执行结果,问题已解决。  我想说,,,其实这种方法仍然是不安全的,后面讲如何通过枚举类解决此问题

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值