什么是单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
饿汉式单例模式
//饿汉式单例
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();
private static Hungry getInstance(){
return HUNGRY;
}
}
懒汉式单例模式
//懒汉式单例
public class LazyMan {
private LazyMan(){
}
//先不初始化
private static LazyMan LAZY_MAN;
//只有为空的时候初始化
private static LazyMan getInstance(){
if (LAZY_MAN==null){
LAZY_MAN=new LazyMan();
}
return LAZY_MAN;
}
}
这样的单利在单线程下是可以的,但是在多线程时就会失效。
测试用例
public static void main(String[] args) {
for (int i = 0; i <10 ; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
DCL懒汉式单例模式
由于在多线程条件下,上面的懒汉式不安全,所以我们可以加一层锁,把class锁住,这样就只有一个class了。这就是双重检测锁懒汉式,又叫DCL懒汉式。
//双重检测锁
private static LazyMan getInstance(){
if (LAZY_MAN==null){//第一层检测
synchronized (LazyMan.class){//锁
if (LAZY_MAN==null){//第二层检测
LAZY_MAN=new LazyMan();
}
}
}
return LAZY_MAN;
}
volatile
上面这样就真的安全了吗?
LAZY_MAN=new LazyMan();
我们知道new一个对象不算原子性操作,是由三步组成的。
1、分配内存空间 2、执行构造方法,初始化对象 3、把这个对象指向分配的空间。
正常情况,CPU是按123的顺序执行的,但是也有可能是132的顺序。如果线程A的执行顺序是132,在执行完13后,线程B开启,判断LAZY_MAN不为已经存在,但是指向的空间是没有初始化的,就出现了问题。
所以,我们最好是在定义LAZY_MAN时加上volatile关键字。
private volatile static LazyMan LAZY_MAN;
静态内部类实现单例模式
除了以上的基本方式,我们还可以使用静态内部类实现单例模式,但是也不安全,不推荐使用。(可以用于装B和炫技)
//静态内部类实现
public class Holder {
private Holder(){
}
private static Holder getInstance(){
return InnerHolder.HOLDER;
}
//写一个内部类创造HOLDER
private static class InnerHolder{
private final static Holder HOLDER=new Holder();
}
}
反射破解DCL单例
只要有反射,任何代码都是不安全的。
DCL单例在多线程下也是安全的,不会被破坏,但是我们还是要问一句
== 这样就真的安全了吗 ==
//反射破解DCL单例
public static void main(String[] args) throws Exception{
LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance2 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
那遇到这种情况,我们又该怎么解决呢?
由于反射是通过得到我们构造器来创造对象,所以我们可以在构造器里面加一个锁!确定只有一个构造器被初始化。
private LazyMan(){
synchronized (LazyMan.class){
if (LAZY_MAN!=null)
throw new RuntimeException("不要试图破坏单例!!!");
}
}
这样就真的安全了吗?
上面我们是new了一个lazyman的对象,所以在构造器可以检测到,但是如果两个对象都是用反射创建的,这样的单例还安全吗?
//反射破解DCL单例
public static void main(String[] args) throws Exception{
//LazyMan instance = LazyMan.getInstance();
Constructor<LazyMan> constructor = LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance = constructor.newInstance();
LazyMan instance2 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
遇到这种情况又该如何解决呢?
我们可以额外的设置一个变量,设置一个初始值,如何在构造器里改变这个值,如果这个值改变了,就说明已经执行过了,就抛出异常。
//定义一个随机变量,变量名一般比较复杂,我这里用“樱岛麻衣”
private static boolean yingdaomayi=false;
private LazyMan(){
if (!yingdaomayi)
yingdaomayi=true;
else {
throw new RuntimeException("不要试图破坏单例!!!");
}
}
这样就真的安全了吗?
如果有人对你的源码进行了反编译,得到了这个变量名(无论你设置的在复杂,都会有破解的办法),不就可以在创建完第一个对象后,再把这个变量设置为初始化,这样单例还安全吗?
//反射破解DCL单例
public static void main(String[] args) throws Exception{
//LazyMan instance = LazyMan.getInstance();
Field yingdaomayi = LazyMan.class.getDeclaredField("yingdaomayi");
yingdaomayi.setAccessible(true);
Constructor<LazyMan> constructor =LazyMan.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
LazyMan instance = constructor.newInstance();
yingdaomayi.set(instance,false);
LazyMan instance2 = constructor.newInstance();
System.out.println(instance);
System.out.println(instance2);
}
遇到这种情况又要怎么解决呢?
我们进入instance源码可知,可知枚举是不能用反射破坏的。所以我们要在好好地认识一下枚举!
枚举
我们先创建一个enum测试一下她的单例是不是真的不能被反射破坏。
//enum本身也是一个class类
public enum EnumSingle {
ENUM_SINGLE;
private EnumSingle getInstance(){
return ENUM_SINGLE;
}
}
class Test {
public static void main(String[] args) {
EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
System.out.println(enumSingle);
System.out.println(enumSingle2);
}
}
我们运行后看一下target下面的class文件,发现生成的构造器是无参的,然后我们可以用反射测试一下。
public static void main(String[] args) throws Exception {
EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
//EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
EnumSingle enumSingle2 = constructor.newInstance();
System.out.println(enumSingle);
System.out.println(enumSingle2);
}
但是运行测试后出现了问题,找不到无参构造器。
运行结果是不会骗人的,所以只能是idea骗了我。
经过反编译工具,我们看到了真正的源码
发现并不是无参构造器,而是有参构造,类型是string和int。所以我们修改了我们的测试代码
public static void main(String[] args) throws Exception {
EnumSingle enumSingle = EnumSingle.ENUM_SINGLE;
//EnumSingle enumSingle2 = EnumSingle.ENUM_SINGLE;
Constructor<EnumSingle> constructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumSingle enumSingle2 = constructor.newInstance();
System.out.println(enumSingle);
System.out.println(enumSingle2);
}
终于出现了应该的结果,枚举果然不能被反射破坏单例。
但是枚举真的不能被破解吗?
序列化也是可以破解的,这里就不深究了其实我也不会