【设计模式】【单例模式】详解

  单例模式的作用:保证系统内有且只有唯一的一个实例。

  1、饿汉单例模式

  单例的保证交给了java的ClassLoader来完成,由于ClassLoader对于每个类的加载都只有一次,所以保证了唯一性。

public class T01_HungerSingleton {

    private final static T01_HungerSingleton INSTANCE = new T01_HungerSingleton();

    private T01_HungerSingleton() {
    }

    public T01_HungerSingleton getInstance() {
        return INSTANCE;
    }
}

2、由于类加载的过程中就进行了实例化,但是这个实例我可能暂时不会用到,这样不是浪费了系统资源?那能不能改成我需要用的时候再进行实例化?请看如下代码:

package com.khstudy.designer.singleton;

public class T02_LazySingletonV1 {
    private static T02_LazySingletonV1 s;

    private T02_LazySingletonV1() {
    }

    public static T02_LazySingletonV1 getInstance() {
        if(s==null)
        {
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            s=new T02_LazySingletonV1();
        }
        System.out.println(s.hashCode());
        return s;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(T02_LazySingletonV1.getInstance().hashCode());
            }).start();
        }
    }

}

这时候我们可以看到输出结果:(我是通过hashcode来判断是否是同一个对象,这里有一点小问题:不同的对象的hashcode也可能相同,但是可以看到所有的结果基本都不相同,所以可以认为产生了很多对象),所以这种方法不行。产生这个问题的原因在于在多线程运行的情况下多个线程同时进入了s==null,然后又各自new了实例。

3、为了解决上面的问题我们利用synchronized加锁来实现同步,我直接给getInstance()方法加了synchronized。

package com.khstudy.designer.singleton;

public class T03_LazySingletonV2 {
    private static T03_LazySingletonV2 s;

    private T03_LazySingletonV2() {
    }

    public static synchronized T03_LazySingletonV2 getInstance() {
        if(s==null)
        {
            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            s=new T03_LazySingletonV2();
        }
        return s;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(T03_LazySingletonV2.getInstance().hashCode());
            }).start();
        }
    }

}

输出结果一致,表明我们的解决方法有效。但是这个还能不能继续优化呢?在实际的工程中,我们的这个方法可能还会涉及其他的业务逻辑,所以我决定进行锁细化的优化。

4、为了将锁细化,我把类改为了如下:

package com.khstudy.designer.singleton;

public class T04_LazySingletonV3 {
    private static T04_LazySingletonV3 s;

    private T04_LazySingletonV3() {
    }

    public static T04_LazySingletonV3 getInstance() {
        if (s == null) {

            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (T04_LazySingletonV3.class) {
                s = new T04_LazySingletonV3();
            }
        }
        return s;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(T04_LazySingletonV3.getInstance().hashCode());
            }).start();
        }
    }

}

运行结果我们发现又被实例化了很多次,原因就在于当很多个线程都进入到了s==null之后,虽然实例化的代码进行了同步,但是每个线程执行完之后,其他进入s==null的线程会再继续执行而重新实例化。我们再次进行优化。

5、针对上面的问题,我们继续优化,来看下面的代码。在同步代码块中我继续判断s==null,如果已经实例化就不再实例化,即使那些一起进入第一个if的线程也不会再实例化了。问题解决。

package com.khstudy.designer.singleton;

public class T05_LazySingletonV4 {
    private static T05_LazySingletonV4 s;

    private T05_LazySingletonV4() {
    }

    public static T05_LazySingletonV4 getInstance() {
        if (s == null) {

            try {
                Thread.currentThread().sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (T05_LazySingletonV4.class) {
                if (s == null) {
                    s = new T05_LazySingletonV4();
                }
            }
        }
        return s;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(T05_LazySingletonV4.getInstance().hashCode());
            }).start();
        }
    }

}

6、现在已经算是完美了,还有一个问题:就是DCL下还需不需要加volatile?

6.1汇编指令

我们先来看下创建一个实例的需要的汇编语言。我们可以看到一共分为了5个汇编指令,第一个new 汇编指令会在堆内存中申请一个空间,此时m=0;第二步dup,在栈中创建一个指向m=0的指针,并复制一份;第三步,消耗一个指针完成实例的初始化,此时m的值才是8;第四步:消耗掉第三步剩下的一个指针将实例 t 指向m=8;最后一步返回创建完成的对象。

6.2指令重排

程序在内存中的执行实际上就是CPU不断执行程序的汇编指令,汇编语言其实就是机器语言的助记符。那为什么会出现指令重排呢?这是对CPU的一种优化,比如说我执行了一条指令(从内存中读取一个数据),在等待返回的这个时间段,我可以继续执行下面与这个返回值无关的指令,这就造成了指令重排。

6.3禁止指令重排

在CPU层面禁止指令重排是通过三种原语(loadfence,storefence,mixedfence)来实现禁止指令重排的。而在JVM规范中是通过load和store的排列组合来实现禁止指令重排的。volatile 的作用有两个:1、保证线程可见性。2、禁止指令重排序

了解了上面几个概念后,我们就来讲讲问题的答案:需要加!原因是:如果在DCL新建对象的过程中发生了指令重排序,先执行了astore_1(先于invokespecial),这时候m=0,这就产生了对象的半初始化状态,当其他线程访问到这个半初始化的状态时,就出错了,所以必须要加volatile来禁止指令重排。

package com.khstudy.designer.singleton;

public class T06_DCLSingleton {
    private static volatile T06_DCLSingleton INSTANCE;

    private T06_DCLSingleton() {
    }

    public T06_DCLSingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (T06_DCLSingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new T06_DCLSingleton();
                }
            }
        }
        return INSTANCE;
    }

}

7、前面我们讲了饿汉模式、懒汉模式的单例,现在我们来讲一下另外一种实现方法:利用静态内部类实现单例,通过输出结果我们可以看到,我们实现了单例。

7.1内部类和静态内部类的区别

(1)静态类的属性和方法在方法加载的时候就会加载到内存中,对于非静态内部类来说,只有实例化外部类之后才会加载。如果外部类还没有加载,内部类中存在静态方法或属性,这时候类还没有加载却要访问它的属性和方法就产生冲突了,所以非静态内部类不能有非静态方法和属性,但是静态内部类既可以拥有非静态的属性方法,也可以拥有静态的属性方法。

(2)实例化的方式不同。非静态内部类的实例化的方法:A.B b=new A().new B(),而静态内部类 A.B b=new A.B();

(3)非静态内部类可以访问外部类的非静态和静态属性方法,而静态内部类只能访问外部类的静态属性方法。

package com.khstudy.designer.singleton;

/**
 * 静态内部类方法
 * 当jvm加载类时之后加载外部类,内部类只有在调用时才会初始化
 * jvm保证了单例
 */
public class T08_StaticInnerClassSingleton {
    private T08_StaticInnerClassSingleton() {
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(new T08_StaticInnerClassSingleton().getInstance());
            }).start();

        }
    }

    //静态内部类实例化方法
    T08_StaticInnerClassSingleton.InnerClass b=new T08_StaticInnerClassSingleton.InnerClass();
    //非静态内部类实例化方法
    T08_StaticInnerClassSingleton.NInnerClass n=new T08_StaticInnerClassSingleton().new NInnerClass();

    public T08_StaticInnerClassSingleton getInstance() {
        return InnerClass.INSTANCE;
    }

    /**
     * 非静态内部类
     * 不能拥有静态属性方法,因为非静态内部类需要依赖外部类,需要外部类先实例化
     * 静态属性和方法是在类加载的时候就加载进内存,当外部类还没实例化,直接访问静态属性和方法就冲突了
     * 所以不能拥有静态属性方法
     */
    class NInnerClass{

    }

    /**
     * 静态内部类
     * 既可以有静态属性方法也可以拥有非静态属性方法
     */
    static class InnerClass {
        private static T08_StaticInnerClassSingleton INSTANCE = new T08_StaticInnerClassSingleton();
        private int a=10;
    }
}

8、介绍了饿汉、懒汉、静态内部类、接下来我们来讲单例模式的最后一种实现方法:枚举的方式

package com.khstudy.designer.singleton;

/**
 * 不仅可以解决线程同步,还可以防止反序列化
 */
public enum T08_SingletonEnum {
    INSTANCE;

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{
                System.out.println(T08_SingletonEnum.INSTANCE.hashCode());
            }).start();
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

echo.T

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

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

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

打赏作者

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

抵扣说明:

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

余额充值