前言
单例模式是一种十分常用但却相对而言比较简单的单例模式。虽然它简单但是包含了关于线程安全、内存模型、类加载机制等一些比较核心的知识点。本章会介绍单例模式的设计思想,会去讲解了几种常见的单例实现方式,如饿汉式、懒汉式、双重检锁、静态内部类、枚举等。
前期回顾:【Java 线程通信】模拟ATM取钱(wait 和 notify机制)
目录
前言
单例模式简介
单例模式设计
饿汉式实现方式
代码分析
代码优劣
代码测试
懒汉式实现方式
代码优化
线程安全
效率低下
双重检锁
内存可见性
完美代码
代码优劣
静态内部类的实现方式
枚举的实现方式
关于反射破坏
单例模式简介
单例模式,顾名思义就是一个运行时域,一个类只有一个实例对象。
那么为什么需要单例模式呢?单例模式的使用场景是什么?
像我们之前写的类的实例对象的创建与销毁对资源来说消耗不大,用不用单例模型其实无所谓。但是有些类的消耗比较大,如果频繁的创建与销毁而且这些类的对象完全可以复用的话,这势必会造成不必要的性能浪费。
举个栗子~
我们要写一个访问数据库类,但是创建数据库链接对象是一个十分耗资源的操作,并且数据库链接是完全可以复用的。那么可以把这个类设置为单例的,这样只需要创建一次对象并且重复使用这个对象就好了,而不用每次去访问数据库都要创建链接对象。
单例模式设计
单例模式有多种写法,比如饿汉式、懒汉式等等。但是不管是哪一种写法其实都要考虑一下三点:
是否线程安全 |
是否懒加载(也叫延迟加载) |
能否反射破坏 |
饿汉式实现方式
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
因为这种方法在 类加载时 就立即创建了实例,就如饿汉看见吃的就 “迫不及待”的去吃的感觉。这里也是如此,类一加载就迫不及待的创建了对象,所以称之为饿汉。
代码分析
(1) 由于单例就是一个类只有一个实例对象的,所以我们并不希望别人能通过 new 直接创建对象,所以我们使用 private 来修饰构造方法
(2) 这个对象由于 static 静态修饰的,所以在类加载的时候就已经创建好了,通过 getInstance 调用只是获取这个对象实例而已,并且这种创建方式是天生线程安全的。
代码优劣
优点:
JVM 在加载这个类的时候就会对它进行初始化, 这里包含对静态变量的初始化,天生线程安全 |
没有加锁,运行效率更高 |
缺点:
类加载时就初始化,若是重启服务的话,会拖慢运行速度 |
类加载时就初始化,如果创建了不使用,会导致内存浪费 |
代码测试
class Test{
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
运行结果
true
所以饿汉的方式创建对象只会创建一个单例,考虑到空间浪费,使用的时候还需权衡优劣。
懒汉式实现方式
以下是只是标准模板(单线程版本),还有很多因素没有考虑 ~
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
以上代码我们可以发现这个对象并不会随着类加载而创建,而是在第一次访问单例类的实例时才去创建(第一次调用 getInstance 方法),我们将这种延迟创建的行为称之为 “懒汉”。
代码优化
线程安全
(1) 首先我们发现以上代码是线程不安全的,在执行以下这条语句时
if (instance == null)
可能会有多个线程已经越过这个语句去创建对象了,所以它是不安全的。
我们需要改进一下:
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
效率低下
(2) 先给这个方法加上 synchronized ,这样就能保证同一时刻只有一个线程访问这个方法了,但是这样又会引入新的问题:其实我们只想要对像构建的时候同步线程,像以上这种代码是每次在获取对象的时候都要进行同步操作,这样对性能影响是是十分大的。所以这种方法并不推荐。
通过以上可以知道要想提升效率,直接在对象构建的时候加同步锁就可以了,而使用对象是不需要同步的,那么我们就可以改成这样。如下图:
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
}
双重检锁
(3) 关于上述代码,getInstance 是不需要参与锁竞争的所有线程都可直接进入,那么现在就开始第二步判断,如果实例对象没有创建,那么所有线程都会去争抢锁,抢到锁的那个线程会开始创建实例对象。实例对象创建了之后,以后所有的 getInstance 操作都是进行到第二步直接跳过,然后返回当前实例对象。这就解决了上述代码的低效问题。
但是以上代码仍然是有问题的:
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
假设 线程A、线程B 同时进入 if 语句,那么线程A拿到锁后创建了一个实例对象后将锁释放了;此时 线程B 拿到锁也可以创建实例对象。此时就可以创建多个实例对象了,所以这也是线程不安全的。有没有一种办法保证线程安全呢?其实我们只要在内部在加上一条 if 判断检查当前对象是否被创建即可。这种方法也被叫做双重检锁。
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
此时 线程B 获得锁以后会进行一个判空,此时 线程A 已经实例过一次了,线程B 自然就不能创建对象了。
内存可见性
(4) 关于上述代码虽然看上去已经很完美了,但是还是有一点瑕疵。这里就要谈到 happens-before 内存可见性原则。简单来讲就是我们简单的一条 Java 语句,内部其实是有多种指令运行完成的。
比如像以下这行代码,由于不是原子操作,虽然只是一条语句,但实际有三个指令在完成操作。
instance = new Singleton();
(1)为对象分配内存 |
(2)初始化对象 |
(3)返回对象指向的内存地址 |
以上的一条语句在非并发也就是单线程中是没有问题的,但是在并发执行时,虚拟机为了效率可能会对指令进行重排。比如说 线程A 的执行顺序是:1->3->2。那么这个线程是先为对象分配好内存,再返回这个对象指向的内存地址,但是由于这个对象还没来得及初始化。此时如果有一个线程 B 进行 if (instance == null) 判空操作就会返回 false 跳过创建对象这个步骤,直接返回这个未初始化的对象。这也是造就了线程安全问题。那么怎么解决呢?我们只需加上 volatile 修饰即可。
完美代码
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
代码优劣
优点
需要这个实例的时候,先判断它是否为空,如果为空,再创建单例对象 |
用到的时候再去创建,避免了创建了对象不去的用而造成浪费 |
缺点
由于懒汉模式经过优化过后已经没有什么缺点了,唯一的缺点就是编写略显复杂。
关于其他的创建方式这里简述一下:
静态内部类的实现方式
JVM在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性、方法被调用时才会被加载,并初始化其静态属性
class StaticInnerSingleton {
private StaticInnerSingleton() {}
public static StaticInnerSingleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static StaticInnerSingleton instance = new StaticInnerSingleton();
}
}
比较推荐这种方式,没有加锁,线程安全。用到时再加载,并发行能高。
枚举的实现方式
枚举单例是最好的单例,有效防止反射
enum EnumSingleton {
// 此枚举类的一个实例, 可以直接通过EnumSingleton.INSTANCE来使用
INSTANCE
}
关于反射破坏
以上的方式除了枚举,其他都能被放射破坏。但是反射是一种人为操作,只有故意去这样操作才会造成反射破坏。
class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
class Test1{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Singleton s1 = Singleton.getInstance();
// 使用反射创建Singleton实例
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
// 通过反射获取的实例
Singleton s2 = declaredConstructor.newInstance();
System.out.println(s1 == s2);
}
}
运行结果:
false
关于如何利用反射破坏单例,请参考以上代码 ~