1、饿汉式
实现:
public class Hungry {
// 可能会浪费空间
private byte[] data1 = new byte[1024 * 1024];
private byte[] data2 = new byte[1024 * 1024];
private byte[] data3 = new byte[1024 * 1024];
private byte[] data4 = new byte[1024 * 1024];
private Hungry() {
}
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance() {
return HUNGRY;
}
}
存在的问题:如果实例从头到尾就没用过,而这个实例对象又占用了较大内存,容易造成内存泄漏;
改进:改成需要使用实例对象时再创建。
2、饱汉式(懒加载)
最简单的实现:
public class LazyMan {
private LazyMan() {
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
}
对于单线程,这种实现毫无问题,但是对于多线程来说,如果线程 A 判断完 if 但是还没来得及执行lazyMan = new LazyMan();
,此时线程 B 获取 CPU 资源,也来到 if ,也判断为 null,并创建一个实例对象进行返回。接着线程 A 苏醒,又执行了一次创建对象。
测试代码:
public class Test{
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(()->{
LazyMan.getInstance();
});
Thread B = new Thread(()->{
LazyMan.getInstance();
});
A.start();
B.start();
}
}
运行结果:
加个延时:
public class Test{
public static void main(String[] args) throws InterruptedException {
Thread A = new Thread(()->{
LazyMan.getInstance();
});
Thread B = new Thread(()->{
LazyMan.getInstance();
});
A.start();
TimeUnit.SECONDS.sleep(1);
B.start();
}
}
运行结果:
可见,多线程下,这种方式的单例模式是有问题的。
解决方式:使用双重检测锁。
3、DCL懒加载
DCL:Double Check Lock。
代码实现:
public class LazyMan {
private LazyMan() {
System.out.println("LazyMan 创建了");
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
无论多少线程,都可以发现只创建了一个实例。
你可能会问:为什么不干脆在getInstance
方法直接加synchronized
关键字呢?这是因为,getInstance
方法调用是很频繁的,假设已经有了实例,多个线程来获取实例时,总是在判断锁,导致多个线程同时要拿实例时,变得要阻塞了。
这种 DCL 方式是否一定 OK 呢?答案是 :No!
我们学习 volatile 时了解了指令重排的问题。对于语句lazyMan = new LazyMan();
我们看起来以为只是一行,其实它底层有三步:
1、分配内存空间
2、执行构造方法,初始化对象
3、把这个对象指向这个空间
如果是正常的123顺序,那么就没问题。如果来个指令重排,变成了132,就容易出现问题了。
假设指令重排变成了132。线程 A 进入lazyMan = new LazyMan();
语句的3,即对象已经分配了空间,并且引用也指向了这个对象,但是还没有执行构造函数初始化对象。这时,线程 A 暂停,线程 B 进入了获取实例的方法,线程 B 判断出来的是对象不为空,于是线程 B 拿到了一个没有初始化的对象。
解决方法就是对实例对象加 volatile 关键字。于是,完整的DCL懒加载如下所示:
public class LazyMan {
private LazyMan() {
System.out.println("LazyMan 创建了");
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
4、使用枚举实现单例模式
正所谓道高一尺魔高一丈,我们知道 Java 是有反射机制的,如果有人这样操作:
public class Test{
public static void main(String[] args) throws Exception {
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance1 = constructor.newInstance();
LazyMan instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
运行结果:
此时发现,DCL的单例模式被反射给破坏了。
解决方法:在构造器中使用标志变量。
public class LazyMan {
private static boolean flag = false;
private LazyMan() {
synchronized (LazyMan.class) {
if (flag){
throw new RuntimeException("我知道已经有了实例你还在创建实例,不要试图使用反射破坏异常");
}else {
System.out.println("LazyMan 创建了");
flag = true;
}
}
}
private static LazyMan lazyMan;
public static LazyMan getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
}
测试代码:
public class Test{
public static void main(String[] args) throws Exception {
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance1 = constructor.newInstance();
LazyMan instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
运行结果:
可见,有了实例对象的前提下,如果再调用构造函数,我们就可以知道它是打算破坏单例模式,但是被我们的代码给遏制了。
但是,这样就完全安全了吗?答案还是:No!
如果有人这样做:
public class Test{
public static void main(String[] args) throws Exception {
Field flag = LazyMan.class.getDeclaredField("flag");
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance1 = constructor.newInstance();
flag.set(instance1,false);
LazyMan instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
运行结果:
也就是说,创建了一个实例对象后,本来 flag 已经变成 true 了,可是被反射获取后又强行改成了 false,导致还能再成功调用一次构造器。
我靠,这样搞下去,没法玩了。
如果不是被人为反射搞破坏,其实DCL懒加载已经够用,既然能被反射搞成这样,那我就从反射本身去想办法。
查看反射获取的构造器进行创建实例对象的源码:
终于找到敌人的弱点了,反射不能搞枚举类型,哈哈哈哈。
使用枚举实现单例模式:
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
使用反射来获取一下:
class Test{
public static void main(String[] args)throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> constructor = EnumSingle.class.getConstructor();
constructor.setAccessible(true);
EnumSingle instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
运行结果:
不对啊,不是应该说枚举不能被反射么,这里的异常显示是找不到构造函数??
查看一下这个枚举类型的反编译结果:
这不是有空参构造么?
换一个反编译工具 jad ,对 EnumSingle.class 进行反编译。
jad -sjava EnumSingle.class
打开反编译的结果:
首先可以确定它确实是一个 class,并且继承了 Enum。
它并没有空参构造,只有一个有参构造private EnumSingle(String s, int i)
,那我们就从这里突破:
class Test {
public static void main(String[] args) throws Exception {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
运行结果:
说明枚举类型真的不能被反射进行创建实例。