单例模式(Singleton Pattern)的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。本质:控制实例的数目。
1、饿汉模式
public class HungrySingletonTest {
public static void main(String[] args) {
// 判断两次获取的实例是否相同
System.out.println(HungrySingleton.getInstance() == HungrySingleton.getInstance());
}
}
class HungrySingleton {
// 直接在这里创建对象,只能创建一次
private static HungrySingleton hungrySingleton = new HungrySingleton();
// 私有化构造方法,可以在内部控制创建实例的数目
private HungrySingleton() {
}
// 定义一个方法来为客户端提供实例
public static HungrySingleton getInstance() {
// 直接返回已经创建好的实例
return hungrySingleton;
}
}
运行结果:
true
饿汉模式是典型的空间换时间
2、懒汉模式
public class LazySingletonTest {
public static void main(String[] args) {
// 判断两次获取的实例是否相同
System.out.println(LazySingleton.getInstance() == LazySingleton.getInstance());
}
}
class LazySingleton {
// 定义一个变量来存储创建好的实例
private static LazySingleton singleton;
public static LazySingleton getInstance() {
// 判断存储实例的变量是否有值
if (null == singleton) {
// 没有,就创建一个实例,并把值赋给存储类实例的变量
singleton = new LazySingleton();
}
// 有值就直接使用
return singleton;
}
}
运行结果:
true
但是如果我们对上面的代码稍做改动,如下:
public class LazySingletonTest {
public static void main(String[] args) {
new Thread(() -> {
LazySingleton s1 = LazySingleton.getInstance();
System.out.println("s1:"+s1);
}).start();
new Thread(() -> {
LazySingleton s2 = LazySingleton.getInstance();
System.out.println("s2:"+s2);
}).start();
}
}
class LazySingleton {
// 定义一个变量来存储创建好的实例
private static LazySingleton singleton;
public static LazySingleton getInstance() {
// 判断存储实例的变量是否有值
if (null == singleton) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 没有,就创建一个实例,并把值赋给存储类实例的变量
singleton = new LazySingleton();
}
// 有值就直接使用
return singleton;
}
}
运行结果:
s1:LazySingleton@61553549
s2:LazySingleton@670db704
以上代码就会出现并发了,会创建两个不同的实例了。
说明懒汉模式是非线程安全的。那如何实现懒汉模式的线程安全呢?只要加上synchronized即可,如下:
public static synchronized LazySingleton getInstance(){...}
3、双重校验加锁
public class LazySingletonTest {
public static void main(String[] args) {
new Thread(() -> {
LazySingleton s1 = LazySingleton.getInstance();
System.out.println("s1:"+s1);
}).start();
new Thread(() -> {
LazySingleton s2 = LazySingleton.getInstance();
System.out.println("s2:"+s2);
}).start();
}
}
class LazySingleton {
// 定义一个变量来存储创建好的实例
private volatile static LazySingleton singleton;
public static synchronized LazySingleton getInstance() {
// 判断存储实例的变量是否有值,对实例的变量添加volatile的修饰
if (null == singleton) {
// 同步块,线程安全的创建实例
synchronized (LazySingleton.class){
// 再次检查实例是否存在,如果不存在才真正的创建实例
if(null == singleton){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 没有,就创建一个实例,并把值赋给存储类实例的变量
singleton = new LazySingleton();
}
}
}
// 有值就直接使用
return singleton;
}
}
运行结果:
s1:LazySingleton@670db704
s2:LazySingleton@670db704
volatile关键字保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)。禁止进行指令重排序。(实现有序性)。volatile 只能保证对单次读/写的原子性。
由于volatile关键字可能会屏蔽掉虚拟机的一些不必要的代码优化,所以运行的效率并不是很高。我们建议没有特殊的需要,不要使用“双重检查加锁”机制来实现线程安全的单例。
4、类级内部类
有没有一种方案,既可以实现延迟加载,又可以实现线程安全呢?这种方案就是综合使用了Java的类级内部类和多线程缺省同步锁的只是,很巧妙地同时实现延迟加载和线程安全。
public class InnerclassSingletonTest {
public static void main(String[] args) {
System.out.println(InnerclassSingleton.getInstance() == InnerclassSingleton.getInstance());
}
}
class InnerclassSingleton {
private static class InnerclassSingletonHolder {
private static InnerclassSingleton singleton = new InnerclassSingleton();
}
private InnerclassSingleton() {
}
public static InnerclassSingleton getInstance() {
return InnerclassSingletonHolder.singleton;
}
}
运行结果:
true
类级内部类是怎么实现延迟加载和线程安全的呢?
类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。
现在我们对上面的代码做如下修改:
public static void main(String[] args) throws Exception{
Constructor<InnerclassSingleton> declaredConstructor = InnerclassSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
InnerclassSingleton s1 = declaredConstructor.newInstance();
InnerclassSingleton s2 = declaredConstructor.newInstance();
System.out.println(s1 == s2);
}
运行结果:
false
运行结果两个实例并不是是一个对象。这里实际就是单例模式受到反射的攻击。当然,我们还可以通过序列化实现Serializable。一个串行化的对象在每次返串行化的时候,都会创建一个新的对象,而不仅仅是一个对原有对象的引用。为了防止这种情况,可以在单例类中加入readResolve 方法。
5、单元素枚举实现单例
public enum EnumSingleton {
INSTANCE;
private void method() {
System.out.println("hello world!");
}
public static void main(String[] args) {
System.out.println(EnumSingleton.INSTANCE == EnumSingleton.INSTANCE);
EnumSingleton.INSTANCE.method();
}
}
运行结果:
true
hello world!
- java的枚举类型实质上是功能齐全的类
- java枚举类型的基本思想是通过公有的静态final域为每个枚举常量导出实例的类。
- 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举。
使用枚举来实现控制会更加简洁,而且无偿地提供了序列化的机制,并由JVM从根本上提供保障,防止多次实例化,更是简洁、高效、安全的实现单例的方式。
单元素枚举实现单例,怎么防止反射攻击的呢?我们可以看下通过getDeclaredConstructor获取到的构建器会发现并没有我们所需的无参构造器,只有参数为(String.class,int.class)构造器。每个枚举对象拥有两个唯一的属性:String name 和 int ordinal,name就是我们在声明枚举变量是的名字(比如INSTANCE),ordinal就是声明的顺序(比如INSTANCE是第一个声明的,所以为0)。源码如下:
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
*
* @param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is assigned
* an ordinal of zero).
*/
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
单元素枚举实现单例,怎么防止反序列化攻击呢?我们看下枚举抽象类的valueOf方法
public static <T extends Enum<T>> T valueOf(Class<T> enumType,
String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null)
return result;
if (name == null)
throw new NullPointerException("Name is null");
throw new IllegalArgumentException(
"No enum constant " + enumType.getCanonicalName() + "." + name);
}
再看enumConstantDirectory方法
Map<String, T> enumConstantDirectory() {
if (enumConstantDirectory == null) {
T[] universe = getEnumConstantsShared();
if (universe == null)
throw new IllegalArgumentException(
getName() + " is not an enum type");
Map<String, T> m = new HashMap<>(2 * universe.length);
for (T constant : universe)
m.put(((Enum<?>)constant).name(), constant);
enumConstantDirectory = m;
}
return enumConstantDirectory;
}
我们再看getEnumConstantsSharded方法
T[] getEnumConstantsShared() {
if (enumConstants == null) {
if (!isEnum()) return null;
try {
final Method values = getMethod("values");
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<Void>() {
public Void run() {
values.setAccessible(true);
return null;
}
});
@SuppressWarnings("unchecked")
T[] temporaryConstants = (T[])values.invoke(null);
enumConstants = temporaryConstants;
}
// These can happen when users concoct enum-like classes
// that don't comply with the enum spec.
catch (InvocationTargetException | NoSuchMethodException |
IllegalAccessException ex) { return null; }
}
return enumConstants;
}
getEnumConstantsShared()方法获取枚举类的values()方法,然后得到枚举类所创建的所有枚举对象。
之前提到过,每个枚举对象都有一个唯一的name属性。序列化只是将name属性序列化,在反序列化的时候,通过创建一个Map(key,value),搭建起name和与之对应的对象之间的联系,然后通过索引key来获得枚举对象。
总结
建议在如下情况时,选用单例模式。
当需要控制一个类的实例只能有一个,并且客户只能从一个全局访问点访问它时,可以采用单例模式。