题目:设计一个类,我们只能生成该类的一个实例
单例模式的特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
实现单例的三要素:
-
私有的构造方法;
-
指向自己实例的私有静态引用;
-
以自己实例为返回值的静态的公有方法。
单例模式分为懒汉式(需要时才去创建对象)和饿汉式(加载类的时候,就创建对象)。从速度和反应时间角度来讲,饿汉式(又称立即加载)要好一些;从资源利用效率上说,懒汉式(又称延迟加载)要好一些。
1 饿汉式(立即加载):在类加载初始化的时,就主动创建实例。即在调用getInstance()方法之前,实例已经被创建好了
在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例,也就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此就说,饿汉式单例天生就是线程安全的。
这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
线程安全的实现:
1.1 利用静态变量
在类加载的时候被初始化,由于类只加载一次。所以instance=new Singleton()只会执行一次。
/**
* 饿汉式(立即加载)。线程安全。因为静态变量在类加载的时候只会被初始化一次。
*/
public class Singleton {
private Singleton() {
}
//静态变量
private static Singleton instance=new Singleton();
public static Singleton getInstance() {
return instance;
}
}
1.2 利用静态代码块
静态代码块只会在用到该类的时候(类加载,调用了静态方法等)被调用唯一的一次
/**
* 饿汉式(立即加载)。线程安全。因为静态代码块只在类加载的时候执行一次。
*/
public class Singleton0 {
private Singleton0() {
}
private static Singleton0 instance;
// 静态代码块
static {
instance = new Singleton0();
}
public static Singleton0 getInstance() {
return instance;
}
}
2 懒汉式(延迟加载): 等到真正使用的时候才去创建实例,不用时不去主动创建。即在调用getInstance()方法时,才去创建实例。
非线程安全的实现:
2.1 当实例为空时,才进行闯将
/**
*非线程安全。懒汉式(延迟加载,用到时才加载)。如果有两个线程同时调用getInstance()方法,则会创建两个实例化对象。
*/
public class Singleton1 {
//私有的构造函数
private Singleton1() {
}
public static Singleton1 instance=null;
public static Singleton1 getInstance() {
//只有在instance为null时,才创建一个实例。避免了重复创建.
if(instance==null) {
instance =new Singleton1();
}
return instance;
}
}
缺点:线程不安全。 如果两个线程同时运行到,判断instacne是否为null的语句,并且instance的确还没有创建时,那么两个线程都会去创建一个实例。那么问题来了,new出了两个instance,这还能叫单例吗?
线程安全的实现
2.2 给方法加同步锁,但是加锁是一个非常耗时的操作。
/**
*线程安全。懒汉式(延迟加载,用到时才加载)。每次调用 getInstance()时,都会加锁。时间消耗很大。
*/
public class Singleton2 {
private Singleton2() {
}
private static Singleton2 instance = null;
// 给方法加同步锁
public synchronized static Singleton2 getInstance() {
if (null == instance) {
instance = new Singleton2();
}
return instance;
}
}
2.3 使用同步块来实现。和上面的方法效果一致
/**
* 线程安全。懒汉式(延迟加载,用到时才加载)。每次调用 getInstance()时,都会加锁。时间消耗很大。
*/
public class Singleton3 {
private Singleton3() {
}
private static Singleton3 instance = null;
public static Singleton3 getInstance() {
// 利用同步块
synchronized (Singleton3.class) {
if (null == instance) {
instance = new Singleton3();
}
}
return instance;
}
}
2.4 【推荐解法1】使用DCL双锁检查机制来实现。避免了反复加锁。
/**
* 线程安全。懒汉式(延迟加载,用到时才加载)。DCL(Double Check Lock)双锁检查。
*/
public class DCL_Singleton {
private DCL_Singleton() {
}
private volatile static DCL_Singleton instance;//必须用volatile来禁止指令重排序。否则,可能会获取到分配了空间但还没初始化完成的对象。
public static DCL_Singleton getInstance() {
if (instance == null) {//只在第一次创建实例时,才需要加同步锁。避免了每次调用getInstance()都加锁。
synchronized (DCL_Singleton.class) {
//每次只有一个线程能进来
if (instance == null) { //只有在实例还没被创建时,才创建实例。避免了实例的重复创建。
instance = new DCL_Singleton();
}
}
}
return instance;
}
}
2.5 【推荐解法2】使用静态内部类
/**
*线程安全。使用静态内部类,来实现延迟加载。在调用getInstance()方法时,会触发StaticInnerClass类的初始化。又因为虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。
*因此StaticInnerClass类只会被加载一次,所以其内部的静态变量instance也只会被初始化一次。
*/
public class Singleton4 {
//静态内部类
private static class StaticInnerClass{
private static final Singleton4 instance=new Singleton4();
}
private Singleton4(){
}
public static Singleton4 getInstance() {
return StaticInnerClass.instance;
}
}
当外部类调用getInstance()方法时,会触发StaticInnerClass类的初始化。由于instance是StaticInnerClass的类成员变量,因此在JVM调用StaticInnerClass类的类构造器对其进行初始化时,虚拟机会保证一个类的类构造器在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器,其他线程都需要阻塞等待,直到活动线程执行方法完毕。在这种情形下,其他线程虽然会被阻塞,但如果执行类构造器方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行类构造器,因为 在同一个类加载器下,一个类型只会被初始化一次,因此就保证了单例。
2.6 枚举实现
public enum Singleton {
INSTANCE;
private String objName;
public String getObjName() {
return objName;
}
public void setObjName(String objName) {
this.objName = objName;
}
public static void main(String[] args) {
// 单例测试
Singleton firstSingleton = Singleton.INSTANCE;
firstSingleton.setObjName("firstName");
System.out.println(firstSingleton.getObjName());
Singleton secondSingleton = Singleton.INSTANCE;
secondSingleton.setObjName("secondName"); //修改单例对象中的内容
System.out.println(firstSingleton.getObjName()); //firstSingleton和secondSingleton 指向同一个单例对象,
System.out.println(secondSingleton.getObjName());
System.out.println("----------------------------------");
// 反射获取实例测试
try {
Singleton[] enumConstants = Singleton.class.getEnumConstants();
for (Singleton enumConstant : enumConstants) {
System.out.println(enumConstant.getObjName());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行结果:
firstName secondName secondName ---------------------------------- secondName
该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法,可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中 添加 防止多次实例化的代码。该实现是由 JVM 保证 只会实例化一次,因此不会出现上述的反射攻击。
该实现在多次序列化和序列化之后,不会得到多个实例。而其它实现需要使用 transient 修饰所有字段,并且实现序列化和反序列化的方法。