什么是单例模式
单例:只允许创建一个该类的对象
实现单例三个步骤
- 私有化构造方法
- 在类内部创建一个对象
- 在类中添加一个公开的方法,返回单例对象
饿汉式
类加载时创建,线程安全
-
优点:线程安全
-
缺点:生命周期长,浪费空间
public class SingleTon {
// 1 私有化构造方法
private SingleTon() {
}
// 2 在类内部创建一个对象
private static final SingleTon instance = new SingleTon();
// 3 在类中添加一个公开的方法,返回该对象
public static SingleTon getInstance() {
return instance;
}
}
懒汉式
使用时创建
- 优点:生命周期短,节省空间
- 缺点:线程不安全,需要加同步
public class SingleTon2 {
// 1 私有化构造方法
private SingleTon2() {
}
// 2 在类内部创建一个对象
private static SingleTon2 instance;
// 3 通过公开方法返回该对象
public static SingleTon2 getInstance() {
if (instance == null) {
instance = new SingleTon2();
}
return instance;
}
}
测试
public class TestSingleTon {
public static void main(String[] args) {
new Thread(() -> {
for(int i = 0; i < 10; i++) {
System.out.println(SingleTon2.getInstance().hashCode());
}
}).start();
}
}
结果
出现哈希值不相同的对象,说明创建了多个对象,程序出错
解决
在多线程下,需要使用同步方法使每个线程同步执行
public class SingleTon2 {
// 1 私有化构造方法
private SingleTon2() {
}
// 2 在类内部创建一个对象
private static SingleTon2 instance;
// 3 通过公开方法返回该对象
public static SingleTon2 getInstance() {
synchronized (SingleTon2.class) {
if (instance == null) {
instance = new SingleTon2();
}
return instance;
}
}
}
添加synchronized后,就不会创建多个对象了,但是synchronized是重量级锁,需要调用操作系统的内核态,所以效率低
那么如何解决呢?
假如共有100个线程,第一个线程已经创建了对象,那么剩下99个线程无需再去获取锁了,所以应该在获取锁的外层再加一次判断:如果此对象为空,则再进行后续操作,如果对象不为空,说明已经实例化了对象,则无需再进行后续操作,提高了效率
此方法也就是:单例模式中的双重检测加锁机制
public class SingleTon2 {
// 1 私有化构造方法
private SingleTon2() {
}
// 2 在类内部创建一个对象
private static SingleTon2 instance;
// 3 通过公开方法返回该对象
public static SingleTon2 getInstance() {
if (instance == null) {
synchronized (SingleTon2.class) {
if (instance == null) {
instance = new SingleTon2();
}
}
}
return instance;
}
}
但是在new对象的时候,在JVM中分为4步:(1)new (2)dup (3)invokespecial (4)astore,第三步和第四步在编译时可能被JVM优化,交换顺序执行:1、2、4、3,在单线程中没有问题,但是在多线程中就会出现问题
那么为了解决指令重排问题,需要在声明对象的时候,添加volatile关键字,就可以防止指令重排
private volatile static SingleTon2 instance;
那么现在就一定是单例模式了吗?
使用反射来创建对象,测试一下
public class TestSingleTon {
public static void main(String[] args) throws Exception {
Class<?> aClass = Class.forName("com.robot.reflect.pattern.SingleTon2");
Constructor<?> con = aClass.getDeclaredConstructor();
con.setAccessible(true);
System.out.println(con.newInstance().hashCode());
System.out.println(con.newInstance().hashCode());
System.out.println(con.newInstance().hashCode());
}
}
结果
1625635731
1580066828
491044090
三个对象的哈希值并不同,说明创建了三个对象
此方式也就是使用反射来破解单例模式
那么如何来防止破解呢?
添加一个boolean型变量flag,当第一次创建对象后,flag=true,在构造方法中判断,如果flag=true,说明对象已经创建过了,抛出异常
public class SingleTon2 {
private static boolean flag = false;
// 1 私有化构造方法
private SingleTon2() {
if (flag) {
throw new RuntimeException("不能反射破解");
}
}
// 2 在类内部创建一个对象
private volatile static SingleTon2 instance;
// 3 通过公开方法返回该对象
public static SingleTon2 getInstance() {
if (instance == null) {
synchronized (SingleTon2.class) {
if (instance == null) {
instance = new SingleTon2();
flag = true;
}
}
}
return instance;
}
}
测试
需要先正常创建一个对象,然后使用反射来破解
public class TestSingleTon {
public static void main(String[] args) throws Exception {
// 先正常创建一次
SingleTon2.getInstance();
// 破解
Class<?> aClass = Class.forName("com.robot.reflect.pattern.SingleTon2");
Constructor<?> con = aClass.getDeclaredConstructor();
con.setAccessible(true);
System.out.println(con.newInstance().hashCode());
System.out.println(con.newInstance().hashCode());
System.out.println(con.newInstance().hashCode());
}
}
结果
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 com.robot.reflect.pattern.TestSingleTon.main(TestSingleTon.java:19)
Caused by: java.lang.RuntimeException: 不能反射破解
at com.robot.reflect.pattern.SingleTon2.<init>(SingleTon2.java:12)
... 5 more
抛出异常,说明反射破解失败
既然是通过变量来标识是否创建了对象,而通过反射也可以修改属性变量的值,所以并没有完全的阻挡反射破解单例模式
所以如何既高效,又安全的实现单例模式呢?
答案是使用静态内部类的方式来实现
静态内部类方式
此方式也属于懒汉式,在使用的时候才会创建
优点:
- 安全:使用类加载保证线程安全
- 生命周期短、节省空间:静态内部类只有在使用的时候才会创建
public class TestSingleTon3 {
private TestSingleTon3() {
}
private static class Holder {
private static final TestSingleTon3 instance = new TestSingleTon3();
}
public static TestSingleTon3 getInstance() {
return Holder.instance;
}
}