JAVA设计模式之——单例模式


一、类型

创建型模式


二、定义

某个类只能存在一个对象实例,可以自行实例化,并存在一个获取这个唯实例的静态方法。


三、UML类图

在这里插入图片描述

说明:单例类 Singleton 中有一个私有静态成员变量 instance ,可以通过公共的静态方法 getInstance() 来获取。


四、示例

单例模式有如下八种:

  • 饿汉式(静态常量)
  • 饿汉式(静态代码块)
  • 懒汉式(线程不安全)
  • 懒汉式(线程安全,同步方法)
  • 懒汉式(线程不安全,同步代码块)
  • 双重检测
  • 静态内部类
  • 枚举

饿汉式是指:单例类在被加载的时候,就实例化一个对象交给引用。
懒汉式是指:只有在 第一次 调用 getInstance() 方法时,才会实例化一个对象。

4.1 饿汉式(静态常量)

步骤

  • 创建静态的私有对象
  • 创建私有化的构造器,防止外部通过 new 来实例化对象
  • 提供一个获取对象的 public 类型的静态方法

单例类

public class Singleton {
    // 1.声明私有的静态成员变量,并进行实例化
    private static final Singleton instance = new Singleton();

    // 2.私有化的构造器,外部无法通过new来创建对象
    private Singleton() {
    }

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

优点 : 写法简单,在类装载阶段就完成了实例化, 没有线程同步的问题
缺点 : 由于在类装载时就完成了实例化,所以 无法达到懒加载(Lazy loading) 的效果。如果一直都没有使用这个实例,则会造成 内存浪费
结论 : 可以使用,但会造成内存浪费。

4.2 饿汉式(静态代码块)

步骤

  • 声明静态的私有成员变量 instance
  • 在 static 代码块中实例化成员变量 instance
  • 创建私有化的构造器,防止外部通过 new 来实例化对象
  • 提供一个获取对象的 public 类型的静态方法

单例类

public class Singleton {
    // 1.声明私有的静态成员变量
    private static Singleton instance = null;

    // 2.通过静态代码块实例化单例对象
    static {
        instance = new Singleton();
    }

    // 3.私有化的构造器,外部无法通过new来创建对象
    private Singleton() {
    }

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

饿汉式(静态代码块)和饿汉式(静态常量)是类似的,只不过把单例对象实例化的过程放到了 static 代码块中,在进行类装载的时候执行 static 代码块中的代码,单例对象。
优点 : 写法简单,在类装载阶段就完成了实例化, 没有线程同步的问题
缺点 : 由于在类装载时就完成了实例化,所以 无法达到懒加载(Lazy loading) 的效果。如果一直都没有使用这个实例,则会造成 内存浪费
结论:可以使用,但会造成内存浪费。

4.3 懒汉式(线程不安全)

步骤

  • 声明静态成员变量
  • 创建私有的构造器,防止外部通过 new 来实例化对象
  • 提供一个 public 的静态方法,用于返回实例对象。当使用到该方法,且 instance 为 null (未被初始化)时,才去初始化 instance 对象

单例类

public class Singleton {
    // 1.声明静态成员变量
    private static Singleton instance;

    // 2.创建私有的构造器,防止外部通过new来实例化对象
    private Singleton() {
    }

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

优点 : 达到了 懒加载(Lazy loading) 的效果
缺点 : 线程不安全 ,只能在单线程下使用,在多线程环境中会产生多个实例。在多线程下,若线程1进入了 if (instance == null) {...} 代码块,但还未开始创建 instance 对象,此时线程2也通过了这个判断进行了代码块中,这时线程1和2都会创建对象,就会 产生多个实例 (若一个个系统中出现了多个id生成器,可能会导致id重复)。
结论 : 不要使用这种方式。

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

步骤

  • 声明静态成员变量 instance
  • 创建私有构造器,防止外部通过 new 来实例化
  • 提供一个 public 的静态方法,用于返回实例对象。同时使用 synchronized 关键字,使得每次只有一个线程执行这个方法
public class Singleton {
    // 1.声明静态成员变量instance
    private static Singleton instance;

    // 2.创建私有构造器,防止外部通过new来实例化
    private Singleton() {
    }

    // 3.提供一个public的静态方法,用于返回实例对象
    // 使用synchronized关键字,使得每次只有一个线程执行这个方法
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

优点 : 达到了 懒加载(Lazy loading) 的效果; 线程安全 ,不会返回初始化到一半的对象,也不会创建多个实例对象。
缺点 : 效率太低 。这个方法只要执行一次实例化就够了,但如果多个线程同时获取这个单例对象,并执行 getInstance() 方法时都要进行同步,导致效率低下。
结论 : 实际开发中,不推荐使用。

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

步骤

  • 声明私有的静态成员变量 instance
  • 创建私有的构造器,防止外部使用 new 创建对象
  • 提供一个 public 的静态方法,用于返回实例对象。使用 synchronized 同步代码块,使得多个线程可以同时进入方法,但是只有一个线程可以创建对象
public class Singleton {
    // 1.声明私有的静态成员变量instance
    private static Singleton instance;

    // 2.创建私有的构造器,防止外部使用new创建对象
    private Singleton() {
    }

    // 3.提供一个public的静态方法,用于返回实例对象
    // 使用synchronized同步代码块,使得多个线程可以同时进入方法
    // 但是只有一个线程可以创建对象
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

懒汉式(同步代码块)和上面的懒汉式(同步方法)类似,只是把同步方法改为了同步代码块。
优点 : 达到了 懒加载(Lazy loading) 的效果
缺点 : 效率低 。instance 对象只要执行一次实例化就够了,但如果多个线程同时获取这个单例对象,进入 getInstance() 方法后并判断 instance==null 后,只有一个线程可以进入同步代码块并初始化对象,第一个线程在初始化对象后,后面的线程仍然在等待进入同步代码块,它们进入同步代码块后,会重复创建对象,这导致效率低下。
结论 : 实际开发中, 不推荐 使用。

4.6 双重检测

步骤

  • 声明私有的静态成员变量 instance,并使用 volatile 关键字进行修饰,防止指令重排序
  • 创建私有的构造器,防止外部通过 new 关键字创建对象
  • 提供 public 类型的静态方法,用于返回实例对象,使用双重检测,使得线程进入同步代码块后再判断一下对象是否已经创建,防止重复创建对象
public class Singleton {
    // 1.声明私有的静态成员变量instance
    // 并使用volatile关键字进行修饰,防止指令重排序
    private static volatile Singleton instance;

    // 2.创建私有的构造器,防止外部通过new关键字创建对象
    private Singleton() {
    }

    // 3.提供public的静态方法,用于返回实例对象
    // 使用双重检测,进入同步代码块后再判断一下对象是否为null,防止重复创建对象
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

为什么使用volatile关键字?
一般创建对象的步骤如下所示:
1、申请一块内存空间
2、对其进行初始化
3、分配一个指针指向这块内存
因为指令重排序的问题(编译器优化的问题),没有规定谁先谁后,可能会出现线程 A 初始化对象时,先申请了一块内存,然后分配了一个指针指向内存,最后对其进行初始化,步骤顺序从1、2、3就变为了1、3、2
而线程 A 在执行完1、3、2中的3后,另一个线程 B 调用了 getInstance() 方法,此时 instance 不为空,就将这个对象的引用返回,然而这个实例并没有构造完成,从而造成线程安全问题。

优点:

  • 线程安全 :使用了两次检测,防止多次实例化;使用 volatile 关键字,防止返回未构造完成的对象。
  • 效率高 :实例化代码只用执行一次,后面的代码再次访问时,经过 if (instance == null) 的判断,就可以直接返回实例化的对象,无需频繁进行同步。

结论 : 推荐使用。

4.7 静态内部类

步骤

  • 声明一个私有的静态变量 instance
  • 创建私有构造器,防止外部通过 new 创建对象
  • 创建私有的静态内部类,内部创建一个静态私有的对象 INSTANCE
  • 创建要给 public 的静态方法,用于获取实例
public class Singleton {
    // 1.声明一个私有的静态变量 instance
    private static Singleton instance;

    // 2.创建私有构造器,防止外部通过new创建对象
    private Singleton() {
    }

    // 3.创建私有的静态内部类,内部创建一个静态私有的对象INSTANCE
    private static class SingletonClass {
        private static Singleton INSTANCE = new Singleton();
    }

    // 4.创建要给public的静态方法,用于获取实例
    public static Singleton getInstance() {
        return SingletonClass.INSTANCE;
    }
}

优点:

  • 懒加载 : 静态内部类的方式在 Singleton 类被装载时并不会立即实例化,而是在调用 getInstance() 方法时,才会装载 SingletonClass 类,从而完成 Singleton 的实例化。
  • 线程安全 : 类的静态属性只有在第一次加载类的时候初始化,所以这里时 JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

结论 : 推荐使用。

4.8 枚举

单例类

public enum Singleton {
    INSTANCE;

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

客户端调用并测试

public class Client {
    public static void main(String[] args) {
        Singleton instance01 = Singleton.INSTANCE;
        Singleton instance02 = Singleton.INSTANCE;
        // 查看两对象的地址是否相同,来判断它们是否是同一个实例
        System.out.println("hashCode of instance01 : " + instance01.hashCode());
        System.out.println("hashCode of instance02 : " + instance02.hashCode());
        System.out.println("两个对象的地址是否相同 : " + (instance01 == instance02));
    }
}

测试结果
在这里插入图片描述

优点:

  • 线程安全
  • 防止反序列化创建新的对象

结论 : 推荐使用


五、注意事项和细节说明

优点

  • 保证了系统内存中只存在一个单例类的对象,节省了系统资源;
  • 对于需要频繁创建和销毁的对象,使用单例模式可以提高系统性能。

相关场景

  • 需要频繁创建和销毁的对象;
  • 创建对象时耗时过多或消耗资源过多(重量级对象),但又经常使用的对象;
  • 工具类对象;
  • 频繁访问数据库或文件的对象(比如:数据源、session工厂等)。

注意事项

  • 只能使用单例类提供的方法得到单例对象, 不要使用反射 ,否则会实例化得到一个新对象;
  • 不要断开单例类对象与类中静态引用,由于单例对象未被引用,可能会被JVM当作垃圾进行回收;
  • 多线程使用单例进行资源共享时,要注意线程的安全问题。

参考文章

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值