设计模式(10)——对象性能模式(1)——单例模式(8种写法)

本文详细介绍了单例模式的8种实现方式,包括饿汉式、懒汉式、双重检查+volatile、静态内部类和枚举等,并分析了各自的线程安全、内存浪费和性能特点。最后,探讨了单例模式在实际开发中的应用和选择建议。
摘要由CSDN通过智能技术生成

目录

0.对象性能模式

1.基本介绍

2.八种写法解读

2.1 饿汉式(静态常量)

2.2 饿汉式(静态代码块)

2.3 懒汉式(线程不安全)

2.4 懒汉式(线程安全,同步方法)

2.5 懒汉式(线程不安全,同步代码块)

2.6.双重检查+volatile

2.7 静态内部类

2.8 枚举

3.单例模式在JDK源码中应用的分析

4.总结


0.对象性能模式

  • 面向对象很好地解决了“抽象”的问题,但是必不可免地要付出一定的代价,对于通常情况来讲,面向对象的成本大都可以忽略不计,但是某些情况,面向对象所带来的成本必须谨慎处理
  • 典型模式
    • 单例模式
    • 享元模式

1.基本介绍

  • 简单来说,就是采用一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
  • 单例模式有8种写法:
    • 1.饿汉式(静态常量)
    • 2.饿汉式(静态代码块)
    • 3.懒汉式(线程不安全)
    • 4.懒汉式(线程安全,同步方法)
    • 5.懒汉式(线程安全,同步代码块)
    • 6.双重检查
    • 7.静态内部类
    • 8.枚举

2.八种写法解读

2.1 饿汉式(静态常量)

(1)创建步骤

  • 1.构造函数私有化
  • 2.类的内部创建对象
  • 3.向外暴露一个静态的公共方法:getInstance

(2)代码实现

public class Singleton01 {

    public static void main(String[] args){
        Singleton instance = Singleton.getInstance();
        Singleton instance01 = Singleton.getInstance();

        System.out.println(instance == instance01);
        System.out.println("Instance.hashCode="+instance.hashCode());
        System.out.println("Instance.hashCode1="+instance01.hashCode());
    }

}


class Singleton {

    //1.构造器私有化,以保证外部不能new对象
    private Singleton(){

    }

    //2.在本类内部创建对象实例(在类加载的时候就创建对象实例)
    private final static Singleton instance = new Singleton();

    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance(){
        return instance;
    }
}

分析此写法优缺点:

  • 优点:
    • 在类装载的时候就完成实例化,避免了线程同步问题
  • 缺点:
    • 在这种方式基于类加载机制避免了多线程的同步问题,不过,instance在类加载时就实例化,在单例模式中大多数都是会调用getinstance方法,但是导致类加载的原因有很多种,因此不能确定是否有其他的方式(或其他的静态方法)导致类加载,这时候初始化instance就没有达到懒加载的效果,造成内存的浪费
  • 结论:
    • 这种单例模式可用,可能造成内存浪费

2.2 饿汉式(静态代码块)

代码实现:

public class Singleton02 {

    public static void main(String[] args){
        Singleton instance = Singleton.getInstance();
        Singleton instance01 = Singleton.getInstance();

        System.out.println(instance == instance01);
        System.out.println("Instance.hashCode="+instance.hashCode());
        System.out.println("Instance.hashCode1="+instance01.hashCode());
    }

}


class Singleton {

    //1.构造器私有化,以保证外部不能new对象
    private Singleton(){

    }

    //2.在本类内部创建对象实例(在类加载的时候就创建对象实例)
    private  static Singleton instance;

    static {  //在静态代码块中,创建单例对象
        instance = new Singleton();
    }

    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance(){
        return instance;
    }
}

分析优缺点:

  • 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类加载的时候,就执行静态代码块中的代码,初始化类的实例,优缺点和上面是一样的

结论:

  • 这种单例模式可用,但是可能造成内存浪费

2.3 懒汉式(线程不安全)

代码实现:

package cn.cqu.Singleton.type1;

public class Singleton03 {

    public static void main(String[] args){

        System.out.println("懒汉式1:线程不安全");
        Singleton instance = Singleton.getInstance();
        Singleton instance01 = Singleton.getInstance();

        System.out.println(instance == instance01);
        System.out.println("Instance.hashCode="+instance.hashCode());
        System.out.println("Instance.hashCode1="+instance01.hashCode());
    }

}


class Singleton {

    //1.构造器私有化,以保证外部不能new对象
    private Singleton(){

    }

    //2.在本类内部创建对象实例
    private  static Singleton instance;


    //3.提供一个公有的静态方法,返回实例对象
    //  当使用到该方法时,才实例化对象
    public static Singleton getInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

分析优缺点:

优点:

  • 起到了懒加载的效果

缺点:

  • 线程不安全,只能在单线程下使用
  • 如果在多线程下,一个线程进入了判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,所以在多线程环境下不可使用这种方式

结论:

  • 在实际开发中,不要使用这种方式

2.4 懒汉式(线程安全,同步方法)

代码实现:

public class Singleton04 {

    public static void main(String[] args){

        System.out.println("懒汉式2:线程安全,同步方法");
        Singleton instance = Singleton.getInstance();
        Singleton instance01 = Singleton.getInstance();

        System.out.println(instance == instance01);
        System.out.println("Instance.hashCode="+instance.hashCode());
        System.out.println("Instance.hashCode1="+instance01.hashCode());
    }

}


class Singleton {

    //1.构造器私有化,以保证外部不能new对象
    private Singleton(){

    }

    //2.在本类内部创建对象实例
    private  static Singleton instance;


    //3.提供一个公有的静态方法,返回实例对象,并且为该方法加入了同步处理的代码
    //  当使用到该方法时,才实例化对象
    public static synchronized Singleton getInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

分析优缺点:

优点:

  • 解决了线程不安全问题

缺点:

  • 效率太低,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步,而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了,方法进行同步效率太低

结论:

  • 在实际开发中,不推荐使用这种方式

2.5 懒汉式(线程不安全,同步代码块)

代码实现:

class Singleton {

    //1.构造器私有化,以保证外部不能new对象
    private Singleton(){

    }

    //2.在本类内部创建对象实例
    private  static Singleton instance;


    //3.提供一个公有的静态方法,返回实例对象
    //  当使用到该方法时,才实例化对象
    public static  Singleton getInstance(){
        if(null == instance){
            synchronized(Singleton.class){
                instance = new Singleton();
            }
        }
        return instance;
    }
}

缺点:

  • 这种方式,本意是想对第四种方式实现方式的改进,因为前面同步方法效率太低改为同步产生实例化的代码块
  • 但是这种同步并不能起到线程同步的作用,跟第三种方式遇到的情形一致,假如一个线程进入了还未来得及往下执行,另一个线程也通过这个判断语句,这时便会产生多个实例

结论:

  • 在实际开发中,不能使用这种方式

2.6.双重检查+volatile

代码实现:

public class Singleton06 {

    public static void main(String[] args){

        System.out.println("懒汉式4:线程安全,双重检查");
        Singleton instance = Singleton.getInstance();
        Singleton instance01 = Singleton.getInstance();

        System.out.println(instance == instance01);
        System.out.println("Instance.hashCode="+instance.hashCode());
        System.out.println("Instance.hashCode1="+instance01.hashCode());
    }

}


class Singleton {

    //1.构造器私有化,以保证外部不能new对象
    private Singleton(){

    }

    //2.在本类内部创建对象实例
    private  static volatile Singleton instance;


    //3.提供一个公有的静态方法,加入双重检查的代码,返回实例对象
    //  当使用到该方法时,才实例化对象
    public static  Singleton getInstance(){
        if(null == instance){
            synchronized(Singleton.class){
                if(null == instance){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

分析:

  • 双检查机制是多线程开发中经常使用到的,如代码中所示,我们进行了两次的检查,这样可以保证线程安全了
    • 当第一次判断不通过(即instance已经被实例化过)的时候,线程直接跳到return语句,避免了反复进行方法同步,保证了效率
    • 当通过第一次判断进入同步代码块中的第二次判断时,这时候再次检查保证了,多个都通过第一次检查的线程不会重复创建多个实例,解决了线程安全的问题
  • 线程安全,懒加载,效率高

结论:

  • 在实际开发中推荐使用

注意:

  • 双检查必须将实例引用instance定义为volatile才是线程安全的,下面是仅有双检查锁,无volatile的分析:
class Singleton {

    //1.构造器私有化,以保证外部不能new对象
    private Singleton(){

    }

    //2.在本类内部创建对象实例
    private  static Singleton instance;


    //3.提供一个公有的静态方法,加入双重检查的代码,返回实例对象
    //  当使用到该方法时,才实例化对象
    public static  Singleton getInstance(){
        if(null == instance){
            synchronized(Singleton.class){
                if(null == instance){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

首先此种使用双检查锁,不使用volatile的写法有问题——内存读写指令重排序(reorder)不安全(关于指令重排序的解释,见我的另一篇博客:volatile详解

原因解释:

  • 某一个线程执行到第一次检测时,读取到的instance不为null时,instance的引用对象可能没有完成初始化
  • instance = new Singleton();在底层执行时可以分为以下三个步骤(伪代码):
memory = allocate();  //1.分配对象内存空间
instance(memory);     //2.为该内存空间初始化对象
instance = memory;    //3.将对象引用指向刚分配并初始化对象的内存 

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的,所以可能出现以下优化顺序:

memory = allocate();  //1.分配对象内存空间
instance = memory;    //3.将对象引用指向刚分配并初始化对象的内存
instance(memory);     //2.为该内存空间初始化对象

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关系多线程间的语义一致性

  • 所以当一条线程正在new对象的时候,但它的底层按照上述1,3,2的顺序执行,
  • 当执行完步骤3时,这时CPU调度另一个线程来创建对象,
  • 此时访问instance不为null,直接返回该instance,
  • 但是实际该instance实例并未完成初始化,此时如果被返回直接使用,就会造成异常,即造成了线程安全问题

而volatile能禁止指令重排序,所以使用volatile可以保证线程安全,这种正确的写法是双检查锁+volatile

2.7 静态内部类

代码实现:

public class Singleton07 {

    public static void main(String[] args){

        System.out.println("懒汉式5:线程安全,静态内部类");
        Singleton instance = Singleton.getInstance();
        Singleton instance01 = Singleton.getInstance();

        System.out.println(instance == instance01);
        System.out.println("Instance.hashCode="+instance.hashCode());
        System.out.println("Instance.hashCode1="+instance01.hashCode());
    }

}

class Singleton {

    //1.构造器私有化,以保证外部不能new对象
    private Singleton(){

    }

    //2.创建一个静态内部类,并且它的属性为Singleton实例
    private static class SingletonInstance{
        private static final Singleton instance = new Singleton();
    }


    //3.提供一个公有的静态方法,返回实例对象
    //  当使用到该方法时,才加载SingletonInstance,进行实例化对象
    public static  Singleton getInstance(){
        return SingletonInstance.instance;
    }
}

分析:

静态内部类的特点说明:

  • 1.上述的外部类Singleton被加载的时候,静态内部类SingletonInstance不会被加载
  • 2.当我们使用getInstance方法时,使用到内部类SingletonInstance的属性instance时,该内部类才会被加载,并且在类加载时,线程是安全的(JVM底层的加载机制保证了线程安全,在类进行初始化的时候,别的线程是无法进入的)

通过静态内部类的特点,保证了线程安全,实现了懒加载,效率高

结论:

  • 在实际开发中,推荐使用

2.8 枚举

代码实现:

public class Singleton08 {

    public static void main(String[] args){

        System.out.println("枚举方式实现");
        Singleton instance = Singleton.INSTANCE;
        Singleton instance01 = Singleton.INSTANCE;

        System.out.println(instance == instance01);
        System.out.println("Instance.hashCode="+instance.hashCode());
        System.out.println("Instance.hashCode1="+instance01.hashCode());

        instance.sayOK();
    }

}

enum  Singleton {
    INSTANCE;//属性

    public void sayOK(){
        System.out.println("ok!");
    }
}

分析:

  • 1.这借助JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步的问题,而且还能防止反序列化重新创建新的对象
  • 2.这种方式是Effective Java的作者提倡的方式

结论:

  • 在实际开发中,推荐使用

3.单例模式在JDK源码中应用的分析

4.总结

  • 1.单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能
  • 2.当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new
  • 3.单例模式使用的场景:
    • 需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即,重量级对象),但又经常用到的对象、
    • 工具类对象、
    • 频繁访问数据库或文件的对象(比如数据源、session工厂等)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值