单例模式的定义与特点
单例模式(Singleton)的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
单例模式的优点和缺点
单例模式的优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
单例模式的缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
单例模式的结构与实现
单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
1. 单例模式的结构
单例模式的主要角色如下。
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
从上图可以得知:
- 单例类私有化了构造函数,这是为了防止在外部进行 new 操作实例化;
- 单例类含有一个公开的 getInstance() 方法用于获取单例类的实例;
2. 单例模式的实现
单例模式的实现方式有以下8种方式:
- 饿汉式(静态常量);
- 饿汉式(静态代码块);
- 懒汉式(线程不安全);
- 懒汉式(线程安全,同步方法);
- 懒汉式(线程安全,同步代码块);
- 双重检查;
- 静态内部类
- 枚举
2.1 饿汉式——静态常量
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
//1 声明一个当前类的静态常量,并初始化为类的实例
private static final Singleton INSTANCE = new Singleton();
//2 私有化构造函数,阻止在类的外部进行实例化
private Singleton() {
}
//3 提供共有的获取实例的方法
public static Singleton getInstance() {
return INSTANCE;
}
}
优缺点说明:
- 优点:这种写法比较简单,就是在类加载的时候就完成了实例化,避免了线程同步的问题,在多线程环境中是安全的。
- 缺点:在类加载的时候就完成了实例化,没有达到懒加载的效果,如果从未使用该实例,就会造成内存的浪费;
- 这种方式在基于classloader机制避免了多线的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance()方法,但是导致类装载的原因有很多,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance就没有达到 Lazy Loading的效果;
- 会造成内存的浪费
2.2 饿汉式——静态代码块
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
static {
INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return INSTANCE;
}
}
优缺点说明:
- 与静态常量的实现方式其实是一样的,静态代码块也是在类加载时执行的
- 会造成内存浪费
2.3 懒汉式——非线程安全
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
//1 声明一个静态变量
private static Singleton INSTANCE;
//私有化构造方法,阻止在外部new
private Singleton() {
}
// 在使用时才去实例化
public static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
优缺点说明:
- 起到了懒加载的效果,但是只能在单线程环境中使用,多线程环境中会造成线程不安全;
- 在开发中,不建议使用这种方式。
2.4 懒汉式——线程安全,同步方法
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
优缺点说明:
- 解决了线程安全问题
- 效率太低,每一个线程在想要获取类的实例时,都需要同步执行getInstance()进行同步,如果线程数量很多,就会造成大量线程堆积在一起等待锁的释放,严重影响性能;
2.5 懒汉式——线程安全、同步代码块
我们发现上述同步方法的实现方式虽然可以解决线程安全问题,但是效率过低,并且在getInstance()方法中,我们只会对INSTANCE进行一次实例化操作,因此,我们可以将同步的操作降低到实例化语句上,这样就可以进一步优化同步的性能;
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
优缺点分析:
- 这种方式本意是要解决同步方法性能过低的问题的,但是其实这种方式连线程安全都没解决,因为只要线程进入了if (INSTANCE == null) 语句里面以后,就无法保证线程安全问题了,因此这种方式其实是一种错误的实现方式;
- 实际开发中,不能使用这种方式;
2.6 双重检查(DCI)
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
优缺点说明:
- 即解决了线程安全问题,又达到了懒加载的问题,效率较高
- 在实际开发中,推荐这种方式来实现;
2.7 静态内部类
静态内部类有两个特点:当外部类被装载的时候,静态内部类不会被立即装载;当通过外部类的方法调用静态内部类时,才会对静态内部类进行装载,而类在装载时是线程安全的;
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton1 == singleton2);
}
}
class Singleton {
//私有化构造器,阻止在外部new
private Singleton() {
}
//定义一个静态内部类
private static class SingletonHandler{
//声明一个外部类的静态常量
private static final Singleton INSTANCE = new Singleton();
}
//提供一个公共的获取实例的静态方法
public static Singleton getInstance() {
return SingletonHandler.INSTANCE;
}
}
优缺点说明:
- 这种方式采用了类装载的机制来保证实例化时只有一个线程,解决了线程安全问题;
- 静态内部类方式在Singleton被装载时不会被装载,只有在调用getInstance()方法时,才会被类加载器装载;
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类初始化时,别的线程是进不来的;
- 即解决了线程安全问题,又达到了懒加载的目的
- 实际开发中,推荐使用这种方式
2.8 枚举
public class SingletonTest {
public static void main(String[] args) {
Singleton singleton1 = Singleton.INSTANCE;
Singleton singleton2 = Singleton.INSTANCE;
System.out.println(singleton1 == singleton2);
}
}
enum Singleton {
INSTANCE;
}
优缺点说明:
- 借助枚举不仅可以解决线程安全问题,还可以防止反序列化创建实例导致单例失效的问题;
- 实际开发中,推荐使用