JAVA设计模式(1) – 单例模式

  • Post author:
  • Post category:java




该文章首发于:

个人博客-JAVA设计模式

,欢迎关注




介绍



定义

​ 某个类对象只有一个实例,并提供一个公共构造方法,使其项目内有且只能访问这个唯一的对象。



结构

单例模式,其中单例类为Server,访问类为Client



特点

  • 一个类只能有一个实例
  • 类本身有公共构造方法,提供全局使用
  • 不能作为抽象类



应用

​ 项目中,遇到以下情况可以考虑用单例模式:


  • 项目只需要一个实例对象:

    全局都要用到的工具类,管理类,服务类,具体如”任务管理服务”,“打印管理服务”,”环境监测工具”等。

  • 项目中一个类需要共享:

    共享对象可以节约内存,并加快访问速度,如项目中的”全局配置类”,”数据库连接池”等。

  • 某个类需要频繁创建和销毁对象:

    运用单例类可以减轻内存抖动,把操作控制在最小范围内,如”网络连接池”,”考勤管理类”等。



懒汉模式

​ 该类加载时没有生成单例,只有当第一次调用

getInstance()

时才会去创建这个单例,如下:

public class LazySingleton {
   
    private static LazySingleton instance = null;
    private LazySingleton(){};
    
    public static LazySingleton getInstance() {
        if (instance == null) instance = new LazySingleton();
        return instance;
    }
}

​ 上面这段代码不能保证

线程安全

,因此可以加上

synchronized



volatile

关键字来保证安全,但每次访问该方法都会上锁同步,影响了性能且消耗更多资源,这也是懒汉模式的缺点。



饿汉模式

​ 该类加载时就同时创建好了静态对象,以后不再改变,且是线程安全的,如下:

public class HungrySingleton {
   
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton(){};
   
    public static  HungrySingleton getInstance() {
        return instance;
    }
}

​ 上面这段代码表示,类初始化的时候就已经同步创建好了静态对象供系统调用。



总结



优点

  • 一个类只有一个实例,避免了内存的多余开销,也方便项目代码的管理
  • 避免资源的多重占用问题



缺点

  • 职责过重,类内部管理逻辑复杂
  • 没有抽象层,限制了扩展性



优化

​ 上文提到的懒汉模式是比较常用的单例类型,但存在

线程安全

的问题,多线程的情况下可能出现多个线程读取到的

instance

都为null,就会创建多个对象,从而导致单例模式失效。

​ 因此,我们可以锁机制,给实例方法加锁:

public class LazySingleton {
    
    private static LazySingleton instance = null;
    private LazySingleton(){};
    // 给方法本身加锁
    public static synchronized LazySingleton getInstance() {
        if (instance == null) instance = new LazySingleton();
        return instance;
    }
}

​ 给方法本身上锁,就很好地避免了多线程同步带来的困扰,但也同时损耗了很多性能,因为给整个方法上锁,无论是否要同步都得先经过锁机制。

​ 同步抢占一个对象的情况很少发生,所以我们引入了双重校验锁机制:

public class LazySingleton {

    private static LazySingleton instance = null;
    private LazySingleton(){};
	// 双重校验锁
    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) instance = new LazySingleton();
            }
        }
        return instance;
    }
}

​ 以上方法通过

同步代码块

而不是

同步方法

的方案来改进性能,只有在未初始化时才会加锁创建对象,否则就直接返回对象。

​ 但该方法还有弊端,在某些编译器上,两个线程同时引用

getInsntance()

方法,其中一个线程依然有可能获取到尚未构造完成的对象。如代码中

 instance = new LazySingleton();

编译器翻译成字节码,里面包含了四个步骤:

A:申请内存空间

B:赋予默认值

C:执行构造器方法

D:连接引用和实例

其中执行的步骤顺序有可能是ABCD,也有可能是ABDC,那如果是后者,就会导致另一个线程获取到的对象是未构造完成的。因此我们继续优化,给

instance

加上

volatile

关键字,禁止重排序,确保编译器是执行的步骤是ABCD,也就完善了我们的双重校验锁机制:

public class LazySingleton {
	// 加上了 volatile 关键字
    private static volatile LazySingleton instance = null;
    private LazySingleton(){};
	// 双重校验锁
    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) instance = new LazySingleton();
            }
        }
        return instance;
    }
}

​ 以上代码很好地解决了多个线程同步导致的对象抢占问题,但还有一个弊端,那就是在对象可被序列化时,在序列化的途中,编译器会通过反射的方式来在内部调用一个构造方法,从而生成新对象,破坏单例结构。

​ 因此,我们可以继续优化,比如在类内部添加一个

readReslove()

,但在JDK-1.5以后,我们可以通过定义枚举的方式,同时解决”线程安全”和”序列化破坏单例”的问题,如下:

public enum Singleton {
    INSTANCE;
    public void xxx() {};
}

​ 为什么以上代码就能同时解决”线程安全“和”序列化破坏单例“的问题呢,因为编译器在编译枚举类型时,各个枚举项都编译为

static final

类型,即这个枚举项在类加载的时候就被初始化了,而我们知道,JAVA类的加载和初始化就是线程安全的,底层已经包我们做好了这套机制,因此可以认为

枚举是线程安全的

​ 那为什么枚举类型又可以避免序列化导致的单例破坏呢?我们可以在

oracle文档

里找到答案:



1.12 Serialization of Enum Constants

Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant,

ObjectOutputStream

writes the value returned by the enum constant’s

name

method. To deserialize an enum constant,

ObjectInputStream

reads the constant name from the stream; the deserialized constant is then obtained by calling the

java.lang.Enum.valueOf

method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.

The process by which enum constants are serialized cannot be customized: any class-specific

writeObject

,

readObject

,

readObjectNoData

,

writeReplace

, and

readResolve

methods defined by enum types are ignored during serialization and deserialization. Similarly, any

serialPersistentFields

or

serialVersionUID

field declarations are also ignored–all enum types have a fixed

serialVersionUID

of

0L

. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.

大概意思就是:枚举不同于常规的类型,序列化的时候仅输出枚举的名字,并没有真正的局部值,而同时反序列化的时候,也是通过内部定义的

valueof()

方法来通过名字找到枚举对象。同时常规的序列化方法如

writeObject()

,

readObject()

,

readResove()

等 也都被禁用。即枚举内部的序列化与反序列化独立于常规机制,内部做了规范,因此可以认为

枚举类是可以解决反射破坏单例问题的