目录
1. 饿汉式
2. 懒汉式
3. volatile解决指令重排序
4. 反射破坏单例模式
5. 枚举实现单例模式
6. 枚举实现单例模式的好处
7.尝试反射破坏枚举
所谓单例模式,就是是某个类的实例对象只能被创建一次,单例模式两种实现:饿汉式和懒汉式。
1. 饿汉式
所谓饿汉式,顾名思义,很饿,迫不及待,就是在类加载时就已经创建好了对象。优点是没有线程安全问题。缺点是浪费资源空间。不管用不用,对象都会被提前创建出来。
代码演示
// 饿汉模式
class Singleton {
// 私有构造方法
private Singleton() {}
private static final Singleton singleton = new Singleton();
public static Singleton getInstance() {
return singleton;
}
}
2. 懒汉式
所谓懒汉式,就是在当方法调用时才会去创建对象,优点是方法调用时才创建,不浪费空间,缺点是有线程安全问题。
class Singleton {
private Singleton() {}
private static Singleton singleton = null;
public static Singleton getInstance() {
// 判断对象是否创建
if(singleton==null) {
singleton = new Singleton();
}
return singleton;
}
}
代码测试(创建10个线程):
public class Demo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> Singleton.getInstance()).start();
}
}
}
class Singleton {
private Singleton() {
System.out.println(Thread.currentThread().getName()+"创建了对象");
}
private static Singleton singleton;
public static Singleton getInstance() {
if(singleton==null) {
singleton = new Singleton();
}
return singleton;
}
}
结果:
如上代码,先判断对象是否为空,再创建对象。这个过程在多线程中就会出现多个线程同时判断为空并创建对象的情况。那既然判断是否为空这个过程会有多个线程同时执行,我们可以通过加锁解决。
class Singleton {
private Singleton() {}
private static Singleton singleton;
public static Singleton getInstance() {
// 加锁
synchronized (Singleton.class) {
if(singleton==null) {
singleton = new Singleton();
}
}
return singleton;
}
}
代码测试(10个线程):
public class Demo {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> Singleton.getInstance()).start();
}
}
}
class Singleton {
private Singleton() {
System.out.println(Thread.currentThread().getName()+"创建了对象");
}
private static Singleton singleton;
public static Singleton getInstance() {
synchronized (Singleton.class) {
if(singleton==null) {
singleton = new Singleton();
}
}
return singleton;
}
}
结果:
通过测试,懒汉模式创建多个对象问题已经解决,但是又引入了一个新问题,那就是性能问题。上面通过加锁确实解决了多线程情况下创建多个实例对象问题。但是这种代码逻辑每个线程都要去获取锁或者没获取到阻塞等待,性能大大降低。不妨在加锁外面再包裹一层判断对象是否为空。这样判断不为空的线程就可以直接返回对象,无需再去获取锁或阻塞等待了。
class Singleton {
private Singleton() {}
private static Singleton singleton;
public static Singleton getInstance() {
if(singleton==null) {
synchronized (Singleton.class) {
if(singleton==null) {
singleton = new Singleton(); // 不是一个原子性操作
}
}
}
return singleton;
}
}
除此之外,上面代码还有一个问题,那就是new一个实例对象时并不是一个原子性操作。总共需要三步:
- 先分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向这个空间
正常创建对象步骤是1->2->3,但有时JVM为了提高性能,会将这三个步骤调整,即指令重排序,这种情况下就会出现1->3->2,如果是是后者,假如有两个线程A,B,若A线程正在创建对象的第二步(按132,把对象指向这个空间,此时对象还没有初始化),此时若线程B正在判断最外层的对象是否为空时就会认为不为空,立即返回该对象。实际上该对象还没有被构造,只是提前分配占用了这块内存空间。
指令重排序是JVM为了提高性能,并且在保证最终结果正确的前提下,对代码执行顺序进行了调整。
3. volatile解决指令重排序
volatile虽然不能解决原子性问题,但是可以禁止指令重排序和内存可见性。
最终的代码如下所示:
class Singleton {
private Singleton() {}
private volatile static Singleton singleton;
public static Singleton getInstance() {
if(singleton==null) {
synchronized (Singleton.class) {
if(singleton==null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
说好了玩转单例模式,怎么能到这就结束呢。上面的单例模式看似已经优化的很好了,但是由于反射的存在,可以将构造方法从private变成public。这种情况下就直接可以通过构造方法创建,不用去调用静态方法。
4. 反射破坏单例模式
public class Demo {
public static void main(String[] args) throws Exception {
// null表示空参构造,获取空参构造器
Constructor<Singleton> declaredConstructor =
Singleton.class.getDeclaredConstructor(null);
// 设置为true会无视私有构造器
declaredConstructor.setAccessible(true);
Singleton singleton = declaredConstructor.newInstance();
Singleton singleton1 = declaredConstructor.newInstance();
System.out.println(singleton);
System.out.println(singleton1);
}
}
class Singleton {
private Singleton() {}
private volatile static Singleton singleton;
public static Singleton getInstance() {
synchronized (Singleton.class) {
if(singleton==null) {
singleton = new Singleton();
}
}
return singleton;
}
}
可以看出通过反射确实创建出了多个实例对象。
解决办法也很多,比如在私有构造方法里加锁判断对象是否为空,或者设置一个标志位来判断对象是否为空。但是这两种方法使用反射也还是都可以破坏,比如我可以通过反射每次修改标志位。通过查看反射创建实例对象的源码可以发现,如果使用反射去创建枚举类的实例对象就会报错。这说明是不能通过反射来创建枚举对象的。
5. 枚举实现单例模式
public enum Single {
INSTANCE;
public Single getInstance() {
return INSTANCE;
}
}
由于使用反射的newInstance()方法创建枚举对象时,这个方法内会判断当前类是否是枚举类,是的话直接就抛异常了。当然可能还有其他原因,正是这样,反射是不能创建枚举对象的。
6. 枚举实现单例模式的好处
1.代码简洁。对比常规的创建单例模式,需要双重校验锁。
2.线程安全。枚举实现的单例天然是线程安全的,因为在底层,其使用static,final修饰,且在初始化过程是线程安全的。
3.反射不能破坏。通常使用反射破坏单例是先通过反射先获取构造器实例,然后设置为允许访问私有构造方法,最后通过newInstance()方法创建枚举对象。但枚举在最后一步是会抛出异常的。因为在newInstance()方法的源码中会检查当前类是否是枚举,是枚举就会抛出异常。
7.尝试反射破坏枚举
如果我们通过反射破坏枚举就一定会抛出newInstance()方法里的异常吗?答案是不一定,我们来尝试使用反射来破坏单例的枚举类,看会发生什么。
如下,通过编译生成的class类以及反编译中均显示有私有无参构造。那么我们从无参构造出发,来通过反射创建枚举对象。代码如下:
@Test
void test5() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Single single = declaredConstructor.newInstance();
Single single1 = declaredConstructor.newInstance();
System.out.println(single);
System.out.println(single1);
}
结果如下:
但是结果并不尽人意,报错意思是没有这个空参构造方法,这就奇了怪了,从上面的class文件以及反编译中都显示有啊,难道显示的骗了我们吗。难道没有这个无参构造吗?
再换一个jad反编译工具。
原来并不是无参构造,而是这个隐藏其中的有参构造呀。接下来按照这个有参构造来通过反射创建对象。
@Test
void test5() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<Single> declaredConstructor = Single.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
Single single = declaredConstructor.newInstance();
Single single1 = declaredConstructor.newInstance();
System.out.println(single);
System.out.println(single1);
}
结果
这个结果跟之前源码中反射创建枚举实例对象的异常一样。这还真说明说明java居然骗了我们哈哈。