Java设计模式(第五章:单例模式)

第五章:单例设计模式

5.1 单例设计模式介绍

单例设计模式:就是采取一定的方法保证在整个的软件系统种,对某个类只能存在一个对象实例,并且该类对系统只提供一个取得其对象实例的方法(静态方法)。

5.2 单例设计模式要点:

5.2.1 第一点:某个类只能有一个实例

某个类只能有一个实例 ------> 构造器私有化

5.2.2 第二点:它必须自行创建这个实例

它必须自行创建这个实例 ------> 含有一个该类的静态变量来保存这个唯一的实例

5.2.3 第三点:它必须自行向整个系统提供这个实例

它必须自行向整个系统提供这个实例 ------> 两种方法

对外提供获取该实例对象的方式:①直接暴露(变量权限为public),②用静态变量的get方法获取(变量权限为private,方法权限为public)。

5.2.4 第四点:强调这是一个单例

强调这是一个单例 ------> 用final来修饰

5.3 单例设计模式八种方式

  1. 饿汉式(静态常量)
  2. 饿汉式(静态代码块)
  3. 懒汉式(线程不安全)
  4. 懒汉式(线程安全,同步方法)
  5. 懒汉式(线程安全,同步代码块)
  6. 双重检查
  7. 静态内部类
  8. 枚举

5.4 饿汉式(静态常量):

步骤:构造器私有化 ------> ②类内部创建对象 ------> ③向外暴露一个静态公共方法getInstance()

public class Singleton1 {
    private static final Singleton1 INSTANCE_1 = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return INSTANCE_1;
    }
}
//测试,发现instance和instance2的哈希值是一致的,这两个变量都是同一个对象。
public void test1(){
    Singleton1 instance = Singleton1.getInstance();
    Singleton1 instance2 = Singleton1.getInstance();
    System.out.println(instance == instance2);//true
}

优缺点说明:

  • 优点:写法简单,类加载的时候就完成实例化,避免了线程同步的问题。

  • 缺点:在类加载的时候就完成实例化,没有达到Lazy Loading懒加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

  • 这种方式基于ClassLoader类装载机制避免了多线程的同步问题,不过instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他方式(其他的静态方法)导致类加载,这时候初始化instance就没有达到Lazy Loading的效果。

总结:这种单例模式可用,不过有可能造成内存的浪费。

5.5 饿汉式(静态代码块):

public class Singleton2 {
    private static final Singleton2 INSTANCE_2;
    private String info;

    static {
        try {
            Properties properties = new Properties();
            ClassLoader classLoader = Singleton2.class.getClassLoader();
            InputStream is = classLoader.getResourceAsStream("single.properties");
            properties.load(is);
            INSTANCE_2 = new Singleton2(properties.getProperty("info"));
        } catch (IOException e) {
            throw new RuntimeException();
        }
    }

    private Singleton2(String info) {
        this.info = info;
    }
    
    public void setInfo(String info) {
        this.info = info;
    }

    @Override
    public String toString() {
        return "Singleton2{info='" + info + "'}";
    }

    public static Singleton2 getInstance(){
        return INSTANCE_2;
    }
}
//测试:修改了实例的内容后,可以发现,两个变量实际上是指向同一个内存空间的,哈希值也未曾发生改变
public void testSingleton2(){
    Singleton2 s = Singleton2.getInstance();
    Singleton2 s2 = Singleton2.getInstance();
    System.out.println(s);
    System.out.println(s.hashCode());
    s2.setInfo("abcde");
    System.out.println(s);
    System.out.println(s2.hashCode());
}

优缺点说明:

  • 优点:单例类在创建实例时可能需要调用其他地方的变量进行实例化,所以需要使用静态代码块来做处理。
  • 缺点:与5.4饿汉式静态常量创建一致。

总结:这种单例模式可用,不过有可能造成内存的浪费。

5.6 关于饿汉式是否需要加final的分析

如果存在释放资源的情况下,就不能加final修饰了

public void releaseInstance(){ 
    if(INSTANCE_1 != null){ 
        INSTANCE_1 = null; 
    } 
}

释放资源之后,如果需要重新使用这个单例,就必须存在重新初始化的过程,所以不能加final。对于不需要释放资源的情况,可以加final。总而言之,要不要加final修饰,可以根据情况而定。
在这里插入图片描述

5.7 懒汉式(线程不安全)

public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}

优缺点说明:

  • 优点:起到了Lazy Loading懒加载的效果,但是只能在单线程下使用。
  • 缺点:在多线程下,如果一个线程进入了if(instance == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这个时候便会产生多个实例。所以说在多线程环境下不可以使用这种方式。

总结:在实际开发中,不要使用这种方式。

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

public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {
    }

    public static synchronized Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}

优缺点说明:

  • 优点:解决了线程安全的问题。
  • 缺点:效率低,每个线程在想要获取到类实例的时候,执行getInstance方法都需要进行同步。而这个方法只执行一次实例化代码就够了,后面的想获得该类的实例,直接return就行了。方法进行同步效率太低。

总结:在实际开发中,不推荐这种方式。

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

public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {
    }

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

总结:与同步方法其实是一致的,无差别。

5.10 双重检查

public class Singleton6 {
    private static volatile Singleton6 instance;

    private Singleton6() {
    }

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

优缺点说明:

  • 优点:
    • Double-Check双重检查概念是多线程开发种常使用到的,由代码中可见我们进行了两次if(instance == null)检查,这样就可以保证线程安全了。
    • 这样,实例化代码只用执行一次,后面再次访问时,判断if(instance == null),直接return实例化对象,也避免的反复进行方法同步。
    • 线程安全,延迟加载,效率较高

总结:在实际开发中,推荐使用这种单例设计模式。

5.11 关于双重检查是否需要加volatile的分析

当一个变量定义为volatile之后,它将具备两种特性。

  1. 保证此变量对所有线程的可见性
    • 解释:当一个线程修改了volatile变量之后,它先写入它的工作内存中,然后立刻写入主内存,并且刷新其他线程中的工作内存,这样其他线程再去读取他们工作内存中的变量时,确保能够拿到最新的。
  2. 禁止指令重排序优化
    • 解释:计算机由于cpu的执行速度与内存的读取速度差异太大,而又无法从硬件上获得突破,所以cpu的乱序执行出现了,在指令没有依赖的情况下,由cpu自动的对代码进行优化,提高cpu的执行效率。使用volatile后可以阻碍指令的重排序

JVM中instance的创建过程:instance=new Singleton6()不是一个原子操作,分为三个指令操作。

  1. 给instance分配内存
  2. 调用构造函数来初始化成员变量,形成实例
  3. 将instance对象指向分配的内存空间

经过指令重排序后,如果先执行1,3指令,再执行2指令。instance已经不为null了,但是它还没有初始化这个实例,那么其他线程这个时候执行返回的instance就是错误的了。所以需要加上volatile关键字。

5.12 静态内部类

静态内部类不会自动随着外部类的加载和初始化而初始化,它是要单独去加载和初始化的。

public class Singleton7 {
    private Singleton7() {
    }

    private static class SingletonInstance {
        private static final Singleton7 INSTANCE_7 = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return SingletonInstance.INSTANCE_7;
    }
}

优缺点说明:

  • 优点:
    • 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
    • 静态内部类方式在Singleton7类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法会装载SingletonInstance类,从而完成Singleton7的实例化。
    • 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了现场的安全性,在类进行初始化时,别的线程时无法进入的。
    • 线程安全,利用静态内部类特点实现延迟加载,效率较高

总结:在实际开发中,推荐使用这种单例设计模式。

5.13 枚举

枚举类型:表示该类型的对象是有限的几个

我们可以限定为一个,就成了单例

public enum Singleton8 {
    INSTANCE_8;

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

优缺点说明:

  • 优点:
    • 这借助了JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

总结:在实际开发中,推荐使用这种单例设计模式。

5.14 单例模式在JDK应用的源码分析

JDK中的Runtime就是经典的单例模式(饿汉式)
在这里插入图片描述

5.15 单例模式注意事项和细节说明

  1. 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
  2. 当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是new
  3. 单例模式使用的场景:需要频繁的进行创建和销毁的对象,创建对象时耗时过多或耗费资源过多(即重量级对象),但又经常用到的对象,工具类对象,频繁访问数据库或文件的对象(比如数据源session工厂等)
  4. 如果是饿汉式,枚举形式最简单。如果是懒汉式,静态内部类形式最简单
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值