单例模式
单例模式定义
单例模式(singleton pattern)是一个比较简单的模式。即任何情况下,一个类在整个系统中都有且仅有一个实例。
单例模式通用类图:
单例模式的优点
-
由于单例模式在内存中只有一个实例,
减少内存开支
,特别是一个对象需要频繁地创建销毁时,而且创建或销毁时性能又无法优化,单例模式就非常明显了 -
由于单例模式只生成一个实例,所以,
减少系统的性能开销
,当一个对象产生需要比较多的资源时,如读取配置,产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
饿汉式——(静态常量、静态代码块)
懒汉式——静态常量
通过三个步骤完成静态常量,饿汉式单例模式。
- 私有化类的构造器,放置通过new关键字创建对象。
- 通过静态常量,在类加载的时候直接new一个自身私有的实例,即为单例。
-
对外提供一个返回该单例的公共方法。
至此,我们就完成了单例模式的实现。
class HungryStaticConstant {
// 1. 私有化构造器。放置外部通过new创建对象。
private HungryStaticConstant() {
}
// 2. 由本来自己创建一个实例,存放在静态常量中。
private static final HungryStaticConstant singleton = new HungryStaticConstant();
// 3. 提供一个公共静态方法,返回唯一的实例。
public static HungryStaticConstant getSingleton() {
return singleton;
}
}
下面我们来测试一下该单例模式。
public class HungryStaticConstantTest {
public static void main(String[] args) throws Exception {
// 1. 正常情况下,单例模式是不会被打破
HungryStaticConstant singleton1 = HungryStaticConstant.getSingleton();
HungryStaticConstant singleton2 = HungryStaticConstant.getSingleton();
// 注意singleton1 和 singleton2的hashCode值是一致的。表示单例模式是成功的。
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
}
执行完成后可以看到打印的hash code值是一致。说明我们的单例模式是成功的。
入侵单例模式——反射
接下来我们通过反射的形式来获取单例的实例,看是否可以打破单例的模式。
public class HungryStaticConstantTest {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, IOException, ClassNotFoundException {
// 1. 正常情况下,单例模式是不会被打破
HungryStaticConstant singleton1 = HungryStaticConstant.getSingleton();
HungryStaticConstant singleton2 = HungryStaticConstant.getSingleton();
// 注意singleton1 和 singleton2的hashCode值是一致的。表示单例模式是成功的。
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
// 2. 通过反射的形式,打破单利模式。
Class<HungryStaticConstant> HungryStaticConstantClass = HungryStaticConstant.class;
Constructor<HungryStaticConstant> declaredConstructor =
HungryStaticConstantClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
HungryStaticConstant singleton3 = declaredConstructor.newInstance();
// 注意singleton3的hashCode值与1和2的是不同的,表明该单例模式已被打破。
System.out.println(singleton3.hashCode());
}
}
通过反射我们获取到了单例类的私有构造器,创建了singleton3对象。
打印结果可以看出singleton1和singleton2的hash值是相等的,但是singleton3是不相等的。
因此,通过反射的方式是可以打破单例模式的。
接下来我们通过在单例类的构造器中加入以下代码,防止单例模式被反射入侵。
再一次运行main方法,我们会发现当单例被反射入侵时,会抛出RuntimeException(“单例模式被入侵!”)异常。即完成了防止反射的入侵。
class HungryStaticConstant {
// 1. 私有化构造器。放置外部通过new创建对象。
private HungryStaticConstant() {
synchronized (HungryStaticConstant.class){
if (singleton == null){
throw new RuntimeException("单例模式被入侵!");
}
}
}
}
入侵单例模式——序列化
在java的有一个序列化的接口Serializable,如果我们让HungryStaticConstant implements Serializable实现这个接口。那么通过序列化的操作也是可以打破单例模式的。
让单例类实现Serializable接口后,我们在main方法中加入新的测试案例。
如所示:
public class HungryStaticConstantTest {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, IOException, ClassNotFoundException {
// 1. 正常情况下,单例模式是不会被打破
HungryStaticConstant singleton1 = HungryStaticConstant.getSingleton();
HungryStaticConstant singleton2 = HungryStaticConstant.getSingleton();
// 注意singleton1 和 singleton2的hashCode值是一致的。表示单例模式是成功的。
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
// 3. 当单例可以被序列化时,在反序列化时打破单例模式。
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(singleton1);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
HungryStaticConstant singleton4 = (HungryStaticConstant)ois.readObject();
System.out.println(singleton4.hashCode());
}
}
通过3部分所示的代码,我们先序列化单例的对象,然后再通过反序列的话方式创建singleton4。
运行结果可以发现singleton4的hash值与singleton1、singleton2是不一致的,单例模式被打破。
要防止可以序列化的单例类被入侵,在单例类中加入以下方法可以解决防止入侵。
加入此方法之后,在运行main方法测试。singleton1、singleton2、singleton3的hash值都是一样的。
class HungryStaticConstant {
private Object readResolve() {
return singleton;
}
}
懒汉式——静态代码块
处理通过静态常量完成饿汉式之外,用静态代码块也是一样。原理与静态常量类似,不做过多说明。
代码如下所示:
class HungryStaticBlock {
// 1. 私有化构造器。放置外部通过new创建对象。
private HungryStaticBlock() {
}
// 2. 由本来自己创建一个实例,存放在静态常量中。创建的过程在静态代码块中实现。
private static HungryStaticBlock singleton = null;
static {
singleton = new HungryStaticBlock();
}
// 3. 提供一个公共静态方法,返回唯一的实例。
public static HungryStaticBlock getSingleton() {
return singleton;
}
}
饿汉式的优缺点:
优点:
- 写法简单,便于理解。通过在类加载时完成实例化。可以避免多线程的问题。
缺点:
- 不能控制加载时机,不能达到懒加载的效果。如果始终未使用该类时,会造成内存浪费。还会加重服务器启动时间。
懒汉式
懒汉式——直接锁
由于饿汉式不能控制加载时机,我们在饿汉式的基础上再做修改。
class Lazy{
private static Lazy singleton;
// 私有化构造器。
private Lazy(){
}
// 增加锁保证线程安全,避免在多线程的情况下创建多个实例,打破单利模式。
public synchronized static Lazy getSingleTon(){
if (singleton == null){
singleton = new Lazy();
}
return singleton;
}
}
从上面的代码中可以看出,我们将单例对象的初始化放到了getSingleTon方法中去。这样我们就将单例对象的加载推迟到了使用时。如果一直不使用不会创建此单例对象。
需要注意的时,为了防止多线程访问时,打破单例这里在方法上面加了synchronized的关键字,会影响性能。咋办呢,接着往下看…
懒汉式——双重检查锁
要解决懒加载锁的性能问题,引入了一个双重检查加锁的机制。
代码如下:
class SingletonDCL {
private static volatile SingletonDCL singletonDCL;
private SingletonDCL() {
}
public static SingletonDCL getSingletonDCL() {
// 第一次校验
if (singletonDCL == null) {
// 锁。
synchronized (SingletonDCL.class) {
// 第二次校验
if (singletonDCL == null) {
singletonDCL = new SingletonDCL();
}
}
}
return singletonDCL;
}
}
我们分析一下getSingletonDCL执行流程,当有多个线程同时访问时。在懒汉式的案例中,不管有没有创建好单例对象都会等待其它线程占用的释放。性能很差。
这里我们先来第一次判断,看是否已经创建了单例对象,如果创建直接返回即可,不存在线程问题,也就避免了性能问题不用等待。
如果没有创建,那么我们进入加锁的代码块。这里需要注意,因为多线程的情况下,都可能进入第二次校验。
此时还没有创建单例对象,而进入的线程排队准备创建案例对象。所以我们此时进行第二次校验,那么只要有一个线程常见成功。由于这里是加锁的,后面的线程顺序执行的时候就不会再创建单例。既可以保证单例模式的安全。
因为这种模式我们只要需要等待一次单例的获取,所以基本上不存在获取单例的性能问题。
懒汉式——静态内部类
除了上述两种方式外,利用静态内部类的延迟加载特性。也可以完成懒汉式的单例模式。
代码如下:
class StaticInnerClass{
// 构造器私有化。
private StaticInnerClass(){
System.out.println("外部内加载。");
}
// 提供一个静态内部类。
private static class Instance{
public Instance(){
System.out.println("内部静态类加载。");
}
private static final StaticInnerClass instance = new StaticInnerClass();
}
// 提供静态的方法返回单例。
public static StaticInnerClass getInstance(){
System.out.println("调用getInstance发方法");
return Instance.instance;
}
}
通过私有的静态内部类来持有单例类的对象,完成对单例的延迟加载。
上述三种延迟加载模式,在实际的场景中,只有第一个是不能使用的存在多线程的安全隐患。
当然,上述三种懒汉模式也存在,之前说的反射和序列化的问题,这里就不在赘述。由于反射和序列化问题是程序员主动的编码问题,所以通常情况下,单例模式可以忽略这两个问题。
枚举单例模式
由于枚举的特殊性,使用枚举来完成单例模式可以很好地满足上述要求。
enum EnumSingleton{
SINGLETON;
private EnumSingleton(){
System.out.println("枚举单例模式初始化");
}
public void doWork(){
System.out.println("枚举单例模式");
}
}
实战
JDK中的单例的运用
jdk中的Runtime类很明显就是一个懒汉式的加载模式。代码节选如下所示:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
实际工作中
如我们需要在A系统中去对接B系统,而此时需要在A系统中完成一些访问B系统的初始化工作,且之后调用B系统过的功能都无需在重复初始化的工作时。我们可以使用单例模式。