在java中单例模式是经常被使用的,比如spring中所创建的bean都是单例的
那么单例模式有什么好处呢?
- 单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
- 因为类控制了实例化过程,所以类可以灵活更改实例化过程。
简单的来说就是向外暴露唯一的对象由开发者进行操作,同时有有利于开发者更改创建对象的过程
那么当我们需要使用单例模式的时候自己怎么去实现单例模式呢?经过前人的摸索大概有以下的几种方法。
-
饿汉式
public class Mrg01 { private static final Mrg01 MRG_01 = new Mrg01(); private Mrg01(){} public static Mrg01 getMrg01(){ return MRG_01; } }饿汉式的原理是将构造函数变成私有的,保证在外部不能够实例化此对象,然后将创建对象的过程设置成静态的,当类加载到内存之后就会实例化一个实例,且jvm会保证这个过程的线程安全。
但是他的缺点也是很明显的,那就是无论这个对象使用不使用他都会不实例化,这就造成了一定的资源浪费,
所以在在这个基础上就提出了懒汉式单例
-
懒汉式
public class Mrg02 { private static Mrg02 MRG_02 ; private Mrg02(){} public static Mrg02 getMrg02(){ if (MRG_02==null){ MRG_02 = new Mrg02(); } return MRG_02; } }懒汉式就是将实例化对象的时机改成在使用的时候实例化,如果不使用就不实例化,这样就不会造成资源的浪费了。但是这种写法有很大的问题,比如在这个时候使用多线程获取单例对象的时候,就可能会造成对象不是单例,如下图打印出来的对象的地址并不是同一个对象
public class Mrg02 { private static Mrg02 MRG_02 ; private Mrg02(){} public static Mrg02 getMrg02(){ if (MRG_02==null){ // 让这个线程沉睡10ms可以看的更清晰下 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } // 在一个线程判断对象存在的同时 第二个线程也进入到了这里 // 如果第二个对象争夺到了资源的使用权那么他会优先创建对象 // 然后等另外一个获取锁之后会再次创建对象 MRG_02 = new Mrg02(); } return MRG_02; } public static void main(String[] args){ for (int i=0;i<100;i++){ new Thread(()->{ System.out.println(Mrg02.getMrg02()); }).start(); } } }当然我们可以在方法上添加关键字synchronized 来解决这个问题,但是同时带来的还有效率下降。
除了在方法上添加synchronized 关键字之外,还可以通过双重检查来解决线程的问题,但是要注意的是
需要在private static Mrg02 MRG_02 ;上加上volatile,原因是防止代码重排序导致对象未初始化便已经分配好内存空间,导致返回的对象在使用的时候报空指针异常
public class Mrg02 { private static volatile Mrg02 MRG_02 ; private Mrg02(){} public static Mrg02 getMrg02(){ if (MRG_02==null){ synchronized(Mrg02.class){ if (MRG_02 == null){ MRG_02 = new Mrg02(); } } } return MRG_02; } public static void main(String[] args){ for (int i=0;i<100;i++){ new Thread(()->{ System.out.println(Mrg02.getMrg02()); }).start(); } } }如果需要彻底解决效率问题,那就可以使用静态内部类的方式创建单例
-
静态内部类方式
public class Mrg03 { private Mrg03(){ } private static class Mrg03Holder{ private final static Mrg03 MRG_03 = new Mrg03(); } public static Mrg03 getInstance(){ return Mrg03Holder.MRG_03; } public static void main(String[] args){ for (int i=0;i<100;i++){ new Thread(()->{ System.out.println(Mrg03.getInstance()); }).start(); } } }静态内部类创建单例的方式是最完美的方法,它不仅可以解决资源问题还可以解决线程问题。如果需要在代码中实现单例的化我推荐使用此方式。
-
枚举方式
public enum Mrg04 { INSTANCE; public static void main(String[] args){ for (int i=0;i<100;i++){ new Thread(()->{ System.out.println(Mrg04.INSTANCE.hashCode()); }).start(); } } }也可以通过枚举的方式去实现单例模式,而且这种方式也是effect Java中推荐的方式,effect Java中是对于这种方法这样说:这种方法在功能上与公有域方法相似,但是更加简洁,无偿的提供了序列化机制,绝对防止多次实例化,即使是在面对复杂的序列化或者反射攻击的时候,虽然这种方式还没有广泛采用,但是氮元素的枚举类型经常成为时间singleton的最佳方法。注意,如果singleton必须扩展一个超类,而不是扩展enum的时候,则不宜使用这个方法。
其实这种方式就是为了更好的防止反序列化和反射攻击,因为通过反序列化和反射可以产生当前对象的副本,可以通过副本去继续操作程序。
-
枚举方式外的防止反序列化攻击的方式
我们可以提供一个readResolve方法,此方法允许我们使用readObjec创建实例来代替另一个实例,当反序列化之后,新建对象上的readResolve就会被调用,然后该方法返回的对像引用将被返回,取代新建的对象。
private Object readResolve(){ return INSTANCE; }