目录
前言:
怎样设计单例模式?
三条原则:
具体实现:
1. 懒汉式,线程不安全
2. 懒汉式,线程安全
3. 饿汉式
4. 饿汉式
5. 双检锁/双重校验锁(DCL,即 double-checked locking)
6. 静态内部类
7. 枚举
7.1 原型模式中的序列化克隆原型对象
8.JDK中的单例模式使用
在java面试过程中(当然了,不能只是出于面试的目的去学设计模式,要做一个有理想的程序员),设计模式是一定会问到的一环,而其中的单例模式因为比较简单,而且涉及到的面比较广,线程安全、内存模型、类加载机制、反射等基础知识点,比较好拿出来考察面试者,值得深入理解。
单例模式是什么?为什么需要单例模式?
单例模式是指系统在运行过程中,某个类自始至终只有一个实例对象。
因为类的对象的创建和销毁是需要消耗资源的,有的类频繁的创建和销毁不会消耗多少资源,例如String类,但是如果有的类的对象庞大而复杂(比较重),那么多次创建和销毁,并且这些对象是完全可以复用的,那么就会造成不必要的性能浪费,例如访问数据库时(比如datasource数据源->数据库连接池、session工厂等),需要创建数据库连接对象,这是一个耗资源的操作,并且这个对象是可以复用的,那么就可以将这个对象设计为单例模式,因为如果频繁的创建销毁这个对象对系统性能影响是很大的。
虽然实现单例模式有很多种方式,但是要考虑下面的这三条原则,每种具体的实现都要尽量满足下面的三条原则,因为反射是程序员自己设计的,所以可以不考虑,实际就是满足前两条原则。
单例模式的实现有多种方式,分为下面几类
关键代码:构造函数是私有的
是否 Lazy 初始化-懒加载:是
是否线程安全:否
实现难度:易
描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class Singleton {
private static Singleton instance;
private Singleton (){} //一定是private,不能使用new创建新的instance,那样就不是单例了
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
是否 Lazy 初始化-懒加载:是
是否线程安全:是
实现难度:易
优点:懒加载,第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
这种方式具备很好的 lazy loading,也能够在多线程中很好的工作,但是效率太低了,每个线程想要获取这个单例,执行getInstance()方法的时候都需要同步,而实际上只要这个单例创建出来以后,后面的线程就不需要再进行同步了,判断instance不为null,直接return一个就好了,所以对方法级别进行同步,效率太低。
public class Singleton {
private static Singleton instance;
private Singleton (){} //一定是private,不能使用new创建新的instance,那样就不是单例了
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
是否 Lazy 初始化-懒加载:否
是否线程安全:是
实现难度:易
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制,保证初始化 instance 时只有一个线程(类加载过程中的初始化阶段),避免了多线程的同步问题,加载->链接(验证,准备,解析等)->初始化,在类加载子系统的初始化阶段,如果类中有静态变量或者静态代码块,则classloader会自动生成一个叫做
init和clinit方法的区别:
init是对象构造器方法,就是在new 一个对象的初始化阶段(初始化对象的非静态成员变量,以及调用该类的 constructor 方法)才会执行init方法。而clinit是类构造器方法,也就是在jvm进行类加载—–链接(验证,准备,解析等)—–初始化 当中的初始化阶段,jvm会调用clinit方法。
不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton {
private final static Singleton instance = new Singleton(); //不管是不是加了final,都是在类加载的初始化阶段进行赋值,因为用到了new,需要执行代码,只有在初始化阶段才开始执行类的一些代码
private Singleton (){} //一定是private,不能使用new创建新的instance,那样就不是单例了
public static Singleton getInstance() {
return instance;
}
}
与上面的饿汉式几乎一样,只是把初始化代码放到了静态代码块中
public class Singleton {
private static Singleton instance; //跟上面相比,去掉了final
static {
instance = new Singleton();
}
private Singleton (){} //一定是private,不能使用new创建新的instance,那样就不是单例了
public static Singleton getInstance() {
return instance;
}
}
是否 Lazy 初始化-懒加载:是
是否多线程安全:是
实现难度:较复杂
这种方式采用双重校验机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
Note: 在多线程环境下,为了提高效率,double-check是经常被使用的。
public class Singleton {
private volatile static Singleton singleton; //为什么使用volatile ??? 这里就不进行解释了,自行查询相关知识。
private Singleton (){} //一定是private,不能使用new创建新的instance,那样就不是单例了
public static Singleton getSingleton() {
if (singleton == null) { //双重校验1
synchronized (Singleton.class) {
if (singleton == null) { //双重校验2
singleton = new Singleton();
}
}
}
return singleton;
}
}
为什么使用volatile ? 请参考文章 全面理解Java内存模型(JMM)及volatile关键字_zejian_的博客-CSDN博客,以及https://blog.csdn.net/wdquan19851029/article/details/115378751。 说简单点儿就是因为singleton = new Singleton(); 对象的实例化要分为多步,不是原子性操作,正常来说应该是对象初始化完成之后,才将其赋值给singleton引用变量,这样能保证多线程情况下,对象在没有实例化完成的情况下,双重校验1 if (singleton == null) 始终成立。但是由于有指令重排优化的原因,先将分配的内存地址赋值给instance变量,然后再去初始化对象,导致对象没有实例化完成,instance却不为空,多线程情况下,其它线程可能拿到没有实例化完成的对象,并且去使用。这叫做多线程下的有序性问题,可以通过加上volatile关键字 private volatile static Singleton singleton; ,禁止singleton = new Singleton(); 实例化相关的指令重排优化。
是否 Lazy 初始化-懒加载:是
是否线程安全:是
实现难度:一般
静态内部类:在外部的类被加载的时候,静态内部类不会随着被一起加载,只有用到它的时候,才会被装载,而且只会装载一次,所以能保证懒加载,并且是线程安全的。
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(clinit方法给静态变量赋值,从而创建了一个实例,没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载(例如其它的地方,调用了Singleton.count这个静态属性,则Singleton类需要被加载,但是这个时候不需要实例化,还用不到这个实例),那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class Singleton {
public static count = 0;
private static class SingletonHolder { //静态内部类
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){} 一定是private,不能使用new创建新的instance,那样就不是单例了
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
是否 Lazy 初始化-懒加载:否
是否线程安全:是
实现难度:易
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
public enum Singleton {
INSTANCE;
public void sayHello() {
System.out.println("Hello world!");
}
}
//测试
public class SingletonTest{
public static void main(String[] args){
Singleton instance = Singleton.INSTANCE;
Singleton instance1 = Singleton.INSTANCE;
System.out.println(instance==instance1);
System.out.println(instance.hashCode());
System.out.println(instance1.hashCode());
instance.sayHello();
instance1.sayHello();
}
}
通过单例类只能获取到用一个单例对象,但是在获得单例对象后,我们可以通过序列化的方式,将单例对象克隆一个,这样就变成2个及2个以上,破坏了单例模式。
下面类中的public Object deepClone()方法,给出了使用序列化方式克隆原型对象的例子。
package com.prototype;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class DeepClone implements Cloneable, Serializable{
/**
*
*/
private static final long serialVersionUID = -8474514933067441515L;
private String name;
private String weight;
private int age;
private String address;
public DeepClone(String name, String weight, int age, String address) {
super();
this.name = name;
this.weight = weight;
this.age = age;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getWeight() {
return weight;
}
public void setWeight(String weight) {
this.weight = weight;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "Sheep [name=" + name + ", weight=" + weight + ", age=" + age
+ ", address=" + address + "]";
}
//对象浅拷贝,不拷贝对象中的引用类型,只拷贝基本数据类型和String类型。
@Override
protected Object clone(){
DeepClone deepClone = null;
try {
deepClone = (DeepClone) super.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return deepClone;
}
//对象深拷贝,使用序列化的方式克隆一个原型对象
public Object deepClone(){
ObjectOutputStream ooStream;
ByteArrayOutputStream baOutStream;
ObjectInputStream oiStream;
ByteArrayInputStream baInputStream;
try {
baOutStream = new ByteArrayOutputStream();
ooStream = new ObjectOutputStream(baOutStream);
ooStream.writeObject(this);
baInputStream = new ByteArrayInputStream(baOutStream.toByteArray());
oiStream = new ObjectInputStream(baInputStream);
DeepClone cloneObject;
try {
cloneObject = (DeepClone) oiStream.readObject();
return cloneObject;
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
1.在JDK中,java.lang.Runtime是经典的单例模式,而且是采用了饿汉式单例模式。
2.Spring容器中bean管理,可以选择单例模式或者原型模式,当然了,这种单例模式只是对于一个spring容器来说的,如果有两个或者两个以上的spring容器,那么就不是标准的单例模式了,标准的单例模式是指在JVM中仅有一个instance。
3.java1.8版本中,CAS底层实现类Unsafe,使用饿汉式提供了一个单例,theUnsafe,private static final Unsafe theUnsafe = new Unsafe(); ,在类加载的初始化阶段,这个Unsafe字段就已经被赋值。
// 通过反射得到theUnsafe对应的Field对象
// 使用饿汉式提供了一个单例theUnsafe,private static final Unsafe theUnsafe = new Unsafe(); ,在类加载的初始化阶段,这个Unsafe字段就已经被赋值。
// 通过反射直接得到Unsafe的引用值,就是一个Unsafe实例。
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置该Field为可访问
field.setAccessible(true);
// 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe);
从java11开始,Unsafe类提供了一个静态方法getUnsafe(),通过它可以直接获取到Unsafe单例,不需要再用反射去获取单例。
4.java类加载,每个类被加载到内存中,仅有一个对应的Class对象。