设计模式(1)Singleton单例模式_8种写法

使用场景

内存中只需要一个实例
• 比如各种Mgr
• 比如各种Factory

一共有8种写法,但是只有2种写法是完美无缺的。

写法一:饿汉式(最常见的写法,很实用)

保证只有一个实例—》定义一个静态的实例Instance

/**
 * 饿汉式
 * 类加载到内存后,就实例化一个单例,JVM保证线程安全,
 * JVM保证每一个class在load到内存中,只是load一次--->static变量是在load到内存之后,马上就进行初始化一次,就初始化这一次,多线程也没有关系
 * 简单实用,推荐使用!
 * 唯一缺点:不管用到与否,类装载时就马上完成实例化
 * Class.forName("") ----加载一个类,只是把class放到内存中,但是不对其进行实例化
 * (话说你不用的,你装载它干啥)
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {};  //构造方法是private,使得别的类无法new --->只有一个实例

    public static Mgr01 getInstance() {  //别人要想使用,必须要调用这个方法---》所以就只有这一个实例
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2); //true
    }
}

写法二:和第一个是一个意思

/**
 * 跟01是一个意思
 */
public class Mgr02 {
    private static final Mgr02 INSTANCE;
    static { //静态语句块
        INSTANCE = new Mgr02();
    }

    private Mgr02() {};

    public static Mgr02 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr02 m1 = Mgr02.getInstance();
        Mgr02 m2 = Mgr02.getInstance();
        System.out.println(m1 == m2);
    }
}

写法三:lazy loading

也叫懒汉式,什么时候使用,什么时候初始化

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题(具体来说就是多线程访问的时候会有影响)
 */
public class Mgr03 {
    private static Mgr03 INSTANCE;  //一开始INSTANCE不进行初始化

    private Mgr03() {    //private导致没法new
    }

    public static Mgr03 getInstance() {   //getInstance是初始化的方法
        if (INSTANCE == null) {
            try {  //测试多线程访问的问题,其实多线程访问的时候又可能创建多个Mgr03(实例化了多个)
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();   //判断INSTANCE为空,就初始化
        }
        return INSTANCE; //如果INSTANCE不为空,就不再初始化,仍旧用原先的那个实例
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {  //创建100个线程
            new Thread(()->               //Java8的lambda表达式
                System.out.println(Mgr03.getInstance().hashCode())   //不同对象hashcode是不同的
            ).start();   //不写lambda的话,需要在new Thread()时传入一个new Runnable,其中重写run方法(匿名内部类)
        }
    }
}

补充:lambda表达式就是只有一个方法 的匿名内部类(接口)的对象

说一下上面的执行结果:很多线程的getInstance的哈希值都是相同的,理论上来说应该是不一样的,但是因为线程执行速度过快,所以导致很多hashcode的值都相同,为了模拟实际情况才加入了上面的try-catch来演示结果。(哈希值相同也有可能不是同一个对象)

同一个类的不同对象其哈希值是不同的。上面其实不打印hashCode而直接打印地址也可以

特别注意:最一开始定义的private static Mgr03 INSTANCE; 不能加final因为加了final必须要初始化。

写法四:懒汉式(加锁synchronized)

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 解决:可以通过synchronized解决,但也带来效率下降
 */
public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {
    }

    public static synchronized Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr04.getInstance().hashCode()); //输出的是相同的哈希值
            }).start();
        }
    }
}

static synchronized锁定的是Mgr04.class对象

写法4相对于写法3就多了一个static synchronized,别的没有增加

但是这种方法也有缺陷:内存中的对象比这个大得多,每次用的时候,都要申请锁,加锁,效率就低了。

写法5:

妄图通过减小同步代码块的方式提高效率,然后不可行

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr05 {
    private static Mgr05 INSTANCE;

    private Mgr05() {
    }

    public static Mgr05 getInstance() {
        if (INSTANCE == null) {
            //妄图通过减小同步代码块的方式提高效率,然后不可行
            synchronized (Mgr05.class) {   //在需要的地方加锁
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

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

这种方法并不能保证在多线程访问的时候做到只有一个实例!!
原因:锁的范围太小(不够“原子性”,在if判断的时候和下面加锁的地方多线程访问,导致创建了多个实例)

写法6:双判断(双重检查)

/**
 * lazy loading
 * 也称懒汉式
 * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
 * 可以通过synchronized解决,但也带来效率下降
 */
public class Mgr06 {
    private static volatile Mgr06 INSTANCE; //JIT(这个编译器把Java语言直接编译城本地语言),注意要加上volatile,因为这个牵扯到Java虚拟机内部执行的时候汇编语言的优化(语句重排,指令重排,这个会非常频繁),不加volatile会使得没有初始化的时候就返回INSTANCE

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {   //防止多余上锁
            //双重检查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

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

这种写法是不会出问题的,很完美

写法7:静态内部类方式(完美)

/**
 * 静态内部类方式
 * JVM保证单例
 * 加载外部类时不会加载内部类,这样可以实现懒加载
 */
public class Mgr07 {

    private Mgr07() {
    }

    private static class Mgr07Holder {    //静态内部类,在加载Mgr07的时候,没有初始化这个静态内部类的,这就是优于写法1的地方。只有在调用getInstance方法时,才会加载这个内部类(懒加载也能实现)
        private final static Mgr07 INSTANCE = new Mgr07();  //初始化Mgr07
    }

    public static Mgr07 getInstance() {
        return Mgr07Holder.INSTANCE;  //返回的是静态内部类中的INSTANCE
    }

    public void m() {
        System.out.println("m");
    }

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

上面方法为什么线程安全?是JVM帮我们搞定的。虚拟机在加载一个class的时候只会加载一次。

写Java的大牛还出过一本书叫做“Effective Java”,书中写到了一个单例的写法,就是下面的写法8:

写法8:完美中的完美——枚举单例

/**
 * 不仅可以解决线程同步,还可以防止反序列化。
 */
public enum Mgr08 {//枚举类

    INSTANCE;

    public void m() {}

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

}

实际应用中,其实写法1就够了,没必要追求不必要的完美。

关于序列化和反序列化

Java的反射可以通过一个class文件将整个class load到内存,然后再new一个实例出来---->反序列化,“不可阻挡”

为了防止反序列化,当然用写法8是能够解决这个问题的(网安中有严重 的反序列化漏洞,简单来说一些白帽子或者一些黑客可能会利用反序列化漏洞)。

写法8之所以能够避免反序列化漏洞是因为这个类(枚举类)没有构造方法。所以说写法8是最完美的方法。但是实际中确实方法1用的最多。因为明明应该是一个类,好好的把它搞成枚举,确实有点让人觉得奇怪了。

单例总结

语法上最完美的是写法8,但是实际中用的最多的是写法1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值