简述
一个类仅有一个实例,由自己创建并对外提供一个实例获取的入口,外部类可以通过这个入口直接获取该实例对象。
场景
很多时候整个应用只能提供一个全局的对象,为了保证唯一性,这个全局的对象的引用不能再次被更改。比如在某个应用程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例类统一读取并实例化到全局仅有的唯一对象中,然后应用中的其他对象再通过这个单例对象获取这些配置信息。
如 Spring容器中的对象,Windows任务管理器,垃圾回收站,打印机打印。
实现思路
1. 将该类的构造方法定义为私有方法,这样其他的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
class Person {
private Person() {
//此类外部无法使用new关键字进行实例化
}
}
2. 在该类内提供一个静态方法,当我们调用这个方法时,返回该类实例的引用。
public static Person getInstance() {
return new Person();
}
饿汉式写法
/**
* 饿汉式单例写法
*/
public class Singleton1 {
//私有构造
private Singleton1(){}
private static Singleton1 instance = new Singleton1();
//静态工厂方法
public static Singleton1 getInstance(){
return instance;
}
}
调用并输出日志
public class Main {
public static void main(String[] args) {
Singleton1 instance1 = Singleton1.getInstance();
System.out.println(instance1);
Singleton1 instance2 = Singleton1.getInstance();
System.out.println(instance2);
}
}
打印输出:
com.t.Singleton1@4554617c
com.t.Singleton1@4554617c
调动两次,输出的是同一个对象,没有重新创建。
饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全。
缺点:Java反射机制支持访问private属性,所以可通过反射来破解构造方法,产生多个实例。
懒汉式写法
/**
* 懒汉式单例写法
*/
public class Singleton2 {
//比较懒,一开始不加载该对象,只有使用的时候才会实例化该对象
private static Singleton2 instance = null;
//私有构造
private Singleton2(){}
public static Singleton2 getInstance(){
if (instance == null){
instance = new Singleton2();
}
return instance;
}
}
调用并输出日志
public static void main(String[] args) {
Singleton2 instance1 = Singleton2.getInstance();
System.out.println(instance1);
Singleton2 instance2 = Singleton2.getInstance();
System.out.println(instance2);
}
打印输出
com.t.Singleton2@4554617c
com.t.Singleton2@4554617c
同样,这里也是调动两次,输出的是同一个对象,没有重新创建。
懒汉式属于延迟加载范畴,好处是当第一次使用到时才会进行实例化,但缺点是在多线程环境下面会产生多个single对象,现在我们使用多线程来进行懒汉式单例的破解。
多线程破解
//多线程破解懒汉式单例加载
class BreakThread extends Thread{
@Override
public void run() {
Singleton2 instance = Singleton2.getInstance();
System.out.println(instance);
}
}
调用并输出日志
public class Main {
public static void main(String[] args) {
new BreakThread().start();
new BreakThread().start();
}
}
打印输出
com.t.Singleton2@5169ce60
com.t.Singleton2@655a2155
Singleton2 对象创建了两次,那问什么会出现这样的情况呢?来看代码
public static Singleton2 getInstance(){
if (instance == null){
//线程是异步执行的,第一个线程还没有实例化Singleton2对象,
// instance还是null,紧接着第二个线程就执行进来了
instance = new Singleton2();
}
return instance;
}
此时懒汉式在多线程模式下将不堪一击,产生了多个实例,该如何解决呢?接下来我们看双检锁写法
双检锁写法
懒汉式多线程改进
/**
* 双检锁
* 懒汉式单利多线程改进
*/
public class Singleton3 {
private Singleton3(){}
private static Singleton3 instance = null;
public static Singleton3 getInstance(){
//等同于public synchronized static Singleton3 getInstance()
if (instance == null){
synchronized (Singleton3.class){
if (instance == null){
instance = new Singleton3();
}
}
}
return instance;
}
}
调用并输出日志
public class Main {
public static void main(String[] args) {
new BreakThread().start();
new BreakThread().start();
}
}
//多线程破解懒汉式单例加载
class BreakThread extends Thread{
@Override
public void run() {
Singleton3 instance = Singleton3.getInstance();
System.out.println(instance);
}
}
打印输出
com.t.Singleton3@5169ce60
com.t.Singleton3@5169ce60
加上双检锁之后,只有等线程1执行完毕,才会执行线程2,实例只会创建一个,这种方式是目前企业中使用较多的单例写法。synchronized 之前进行一次非空判断解决了每次访问加锁的问题,
但我们仍然可以通过反射来访问private构造方法,破坏实例化规则,产生多个实例,所以枚举写法应运而生。
枚举写法
/**
* 单例枚举写法
*/
public enum Singleton4 {
INSTANCE;
public void something() {
//做你想做的
}
}
上面是推荐写法,但我们看完就懵圈了,完全不知道怎么回事,怎么用?
其实枚举写法并不是要求你获取某个单例对象, 而是通过枚举类直接去做你想做的事情,简单例子如下
/**
* 单例枚举写法
*/
public enum Singleton4 {
INSTANCE;
private Singleton4() {
System.out.println("枚举构造方法");
}
public String[] getProperties() {
final String[] properties = new String[3];
properties[0] = "属性1";
properties[1] = "属性2";
properties[2] = "属性3";
return properties;
}
}
调用并输出日志
public class Main {
public static void main(String[] args) {
String[] properties1 = Singleton4.INSTANCE.getProperties();
System.out.println(Arrays.asList(properties1));
String[] properties2 = Singleton4.INSTANCE.getProperties();
System.out.println(Arrays.asList(properties2));
}
}
打印输出
枚举构造方法
[属性1, 属性2, 属性3]
[属性1, 属性2, 属性3]
可见Singleton4实例只执行了一次。
枚举单例写法来源于Effective Java 这本书
书中描述:单元素的枚举的枚举类型已经成为实现Singleton的最佳方法
思考:为什么枚举单例模式是最好的单例模式?
1. 写法简单,简单明了
2. JDK定义枚举类的构造方法为private的。每个枚举实例都是static final 类型的,也就表明只能被实例化一次
3. 创建枚举默认就是线程安全的,所以不必担心线程问题
4. enum 是来自于Enum类的,通过JDK我们可以看到Emun类的构造,可以看到枚举类提供了序列化机制
public abstract class Enum<E extends Enum<E>> implements Comparable<E>,
Serializable
其能够阻止默认的反序列化,方法声明如下:
/**
* 阻止默认的反序列化操作
* prevent default deserialization
*/
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
throw new InvalidObjectException("can't deserialize enum");
}
private void readObjectNoData() throws ObjectStreamException {
throw new InvalidObjectException("can't deserialize enum");
}
5. 反射中的newInstance中,禁止对枚举进行实例化,详见Constructor类416行
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException{
//...省略
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
//...省略
}
最后,不管采取何种方案,请时刻牢记单例的三大要点:
- 线程安全
- 延迟加载
- 序列化与反序列安全