目录
引言
单例模式,从入门到精通。
单例的6总实现方式!
为什么枚举是最靠谱的单例模式?
从源码一一详细剖析!
简介
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 1、单例类只能有一个实例。
- 2、单例类必须自己创建自己的唯一实例。
- 3、单例类必须给所有其他对象提供这一实例。
一、懒汉式
1.代码
public class LazyMan {
private static LazyMan instance = null;
private LazyMan() {
}
public static LazyMan getInstance(){
if(instance==null){
instance = new LazyMan();
return instance;
}
return instance;
}
//测试
public static void main(String[] args) {
LazyMan instance1 = LazyMan.getInstance();
LazyMan instance2 = LazyMan.getInstance();
System.out.println(instance1==instance2);//true
}
}
2.说明
这种方式简单易懂,有助于新手理解,但严格意义上讲并不是单例模式,因为线程不安全。
二、懒汉式(线程安全)
public class LazyManThreadSecurity {
private static LazyManThreadSecurity instance = null;
private LazyManThreadSecurity() {
}
public static LazyManThreadSecurity getInstance(){
if(instance==null){
//加锁,只能用类的锁才能保证一个类只有一个实例
synchronized (LazyManThreadSecurity.class){
instance = new LazyManThreadSecurity();
return instance;
}
}
return instance;
}
//测试
public static void main(String[] args) {
LazyManThreadSecurity instance1 = LazyManThreadSecurity.getInstance();
LazyManThreadSecurity instance2 = LazyManThreadSecurity.getInstance();
System.out.println(instance1==instance2); //true
}
}
2.说明
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
三、饿汉式
1.代码
public class Hunger {
private static Hunger instance= new Hunger();
//构造器私有
private Hunger() {
}
//提供获取实例方法
public static Hunger getInstance(){
return instance;
}
//测试
public static void main(String[] args) {
Hunger instance1 = Hunger.getInstance();
Hunger instance2 = Hunger.getInstance();
System.out.println(instance1==instance2);
}
}
2.优点
这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
四、双检锁/双重校验锁
1.代码
public class DoubleCheck {
private volatile static DoubleCheck instance = null;
//构造器私有
private DoubleCheck() {
}
//提供获取实例方法
public static DoubleCheck getInstance() {
if(instance==null){
synchronized (DoubleCheck.class){
instance = new DoubleCheck();
return instance;
}
}
return instance;
}
//测试
public static void main(String[] args) {
DoubleCheck instance1 = DoubleCheck.getInstance();
DoubleCheck instance2 = DoubleCheck.getInstance();
System.out.println(instance1==instance2);
}
}
2.说明
这段代码是否看着很熟悉,这其实就是在线程安全的懒汉式上,在instance说明语句上加了volatile关键字
为什么要加volatile呢 ?
因为在执行getInstance方法时,虽然看似是线程安全的,但不一定是安全的。
原因:instance =new DoubleCheck();不是有序性操作。
- instance =new DoubleCheck();实际上分了三步操作
- 1.开辟一个内存空间
- 2.初始化构造器
- 3.将改对象指向这个空间(这时候这个空间就属于这个对象了)
- 若以上三步在执行时,是按顺序执行的,那么就没问题。但是以上三步,在CPU执行时顺序可以不同。
例如:有两条线程A和B,
A线程先进入执行instance =new DoubleCheck();,而因为某些因素,指令执行是按照132,
当A线程指令执行完3还没执行2时,B线程也执行了getInstance方法,此时instance对象已经指向某个内存空间,所以instance不等于null,则直接返回了,此时改对象的构造函数还没执行,所以就引发了错误。
volatile关键字:作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。(保证指令顺序执行)
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。
五、登记式/静态内部类
1.代码
public class StaticSingle {
//静态内部类
private static class StaticInner{
//创建后不可修改
private static final StaticSingle INSTANCE = new StaticSingle();
}
private StaticSingle() {
}
public static StaticSingle getInstance(){
return StaticInner.INSTANCE;
}
}
2.说明
该方法利用了静态内部类的特性,只有在调用时才会对其初始化。
这种方式能达到双检锁方式一样的功效,但实现更简单。
对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。
这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Hunger 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 StaticInner类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 StaticInner类,从而实例化 instance。
想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
六、枚举
1.代码
public enum SingleEnum {
INSTANCE;
}
2.说明
使用时直接,SingleEnum.INSTANCE;就可以得到单例对象了!
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。
它更简洁,自动支持序列化机制,绝对防止多次实例化。(枚举类特性)
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。
总结
经验之谈:一般情况下,不建议使用第 一 种和第 二 种懒汉方式,建议使用第 三 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 五5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 六 种枚举方式。如果有其他特殊的需求,可以考虑使用第 四 种双检锁方式。
炫技时刻
通过反射破坏单例
1.基础
方法一到方法五都有私有构造器,都可以使用反射生成多个实例!
这里就用看起来比较好的方法二举例。
public class ReflectOne {
public static void main(String[] args) throws Exception {
LazyManThreadSecurity instance1 = LazyManThreadSecurity.getInstance();
// 获得无参构造器
Constructor<LazyManThreadSecurity> declaredConstructor = LazyManThreadSecurity.class.getDeclaredConstructor(null);
// 破坏私有构造方法
declaredConstructor.setAccessible(true);
LazyManThreadSecurity instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}
}
输出结果
single.LazyManThreadSecurity@677327b6
single.LazyManThreadSecurity@14ae5a5
falseProcess finished with exit code 0
2.level up!
你决定上面反射的方法是需要通过构造器的,所以你觉得可以设置一个标志位。
当有实例生成之后,标志位就置为某个值,下次再执行构造函数时,就抛出异常,就不会生成新的对象了
代码改造
public class LazyManThreadSecurity2 {
private static LazyManThreadSecurity2 instance = null;
private static boolean created = false;//添加标志位
private LazyManThreadSecurity2() {
if(!created){
created = true;
}else {
throw new RuntimeException("别乱搞哦!");
}
}
public static LazyManThreadSecurity2 getInstance(){
if(instance==null){
//加锁,只能用类的锁才能保证一个类只有一个实例
synchronized (LazyManThreadSecurity2.class){
instance = new LazyManThreadSecurity2();
return instance;
}
}
return instance;
}
public static void main(String[] args) {
LazyManThreadSecurity2 instance1 = LazyManThreadSecurity2.getInstance();
LazyManThreadSecurity2 instance2 = LazyManThreadSecurity2.getInstance();
System.out.println(instance1==instance2); //true
}
}
测试
public class ReflectOne {
public static void main(String[] args) throws Exception {
LazyManThreadSecurity2 instance1 = LazyManThreadSecurity2.getInstance();
// 获得无参构造器
Constructor<LazyManThreadSecurity2> declaredConstructor = LazyManThreadSecurity2.class.getDeclaredConstructor(null);
// 破坏私有构造方法
declaredConstructor.setAccessible(true);
LazyManThreadSecurity2 instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}
}
输出
Exception in thread “main” java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at single.promote.ReflectOne.main(ReflectOne.java:19)
Caused by: java.lang.RuntimeException: 别乱搞哦!
at single.promote.LazyManThreadSecurity2.(LazyManThreadSecurity2.java:18)
… 5 more
3.level up!!
增加标志位后就没办法了吗?
反射这么强大怎么可能没办法呢,通过反射将标志位设置一下就好了(可以通过反编译看源码,标志位可能是一个通过加密的值,所以需要多种手段结合)
public class ReflectOne {
public static void main(String[] args) throws Exception {
LazyManThreadSecurity2 instance1 = LazyManThreadSecurity2.getInstance();
//改变标志位
Field created = LazyManThreadSecurity2.class.getDeclaredField("created");
created.setAccessible(true);
created.set("create",false);
// 获得无参构造器
Constructor<LazyManThreadSecurity2> declaredConstructor = LazyManThreadSecurity2.class.getDeclaredConstructor(null);
// 破坏私有构造方法
declaredConstructor.setAccessible(true);
LazyManThreadSecurity2 instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
System.out.println(instance1 == instance2);
}
}
输出
single.promote.LazyManThreadSecurity2@7f31245a
single.promote.LazyManThreadSecurity2@6d6f6e28
falseProcess finished with exit code 0
枚举类为什么就是安全的?
尝试通过反射破坏单例
1.通过编译后的结果看到改类有一个私有构造器
public class EnumReflect {
public static void main(String[] args) throws Exception {
SingleEnum instance1 = SingleEnum.INSTANCE;
//尝试通过私有构造器新建实例
Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
SingleEnum instance2 = declaredConstructor.newInstance();
System.out.println(instance1==instance2);
}
}
输出结果:
Exception in thread “main” java.lang.NoSuchMethodException: single.SingleEnum.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at single.promote.EnumReflect.main(EnumReflect.java:18)
- 报错:“没有这样的构造器”,我们明明可以看到编译后确实有该构造器,且我们已经破坏其private属性。
- 此时只有一个可能,idea骗了我们!
2.idea骗了我们,那么就用javap命令反编译一下
- ”右键“该类,选择”Show in Explorer“到该类所在目录下
- 在上方输入栏输入”cmd“并回车
- 输入命令
#格式:java -p 类名
javap -p SingleEnum.class
可以看到此时也只是有一个无参构造方法
3.用专业工具反编译
jad:把.class文件反编译 成.java文件的专业工具
进入页面,点击”Download Jad“
选择适合自己的版本,以win10为例,就选择以下的版本
下载不了的可以用以下的仓库下载
下载完成后
选择1:把jad配置到环境变量Path中,以后在任何地方都可以用jad命令
选择2:把jad.exe复制到要反编译的文件的同目录下,再执行jad命令
我这里采用方法2
# 在命令行中输入以下命令
jad
4.反编译
①复制到相同目录下,并打开命令行切换到该窗口(快捷方式:回车栏cmd)
要反编译这个SingleEnum.class
②执行反编译命令
#jad 文件名.class
jad SingleEnum.class
编译成功会看到如下输出
Parsing SingleEnum.class… Generating SingleEnum.jad
同时在该目录下看到一个SingleEnum.jad文件,可以改成.java为后缀后打开(有颜色)或者直接用记事本打开也好。
惊人的一幕发生了!!!该类竟然没有无参构造器,而是一个带两个参数的私有构造器!!!
5.通过该构造器继续尝试用反射的方法破坏单例
public class EnumReflect {
public static void main(String[] args) throws Exception {
SingleEnum instance1 = SingleEnum.INSTANCE;
Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
SingleEnum instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);
}
}
输出结果:
Cannot reflectively create enum objects
6.通过源码究原因
①查看newInstance();方法的源码
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
关键代码
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException(“Cannot reflectively create enum objects”);如果是枚举,直接就抛异常了!
jdk从根本上拒绝了从反射获得枚举类实例
②Enum的clone和序列化方法
直接抛异常(狠)
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
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");
}
通过以上的分析就可以得知为什么枚举类是最靠谱实现单例的模式!
补充:
实现单例的最简单的方法就是私有化构造器,但这个方法可以用clone,序列化,反射破坏单例!
学完,一个字,爽!!!