目录
写在文章开始
单例一共有如下几种写法:懒汉式(非线程安全)、懒汉式(线程安全)、饥饿式、双重校验式、枚举式、静态内部类、容器类管理。
什么是单例?
单例模式属于工厂模式的特殊情况,只是它不需要输入参数并且始终返回同一对象的引用。单例模式能够保证某一类型对象在系统中的唯一性,即某类在系统中只有一个实例。
即:在单例模式中,首先必须要有一个private访问级别的构造函数,只有这样才能确保单例不会在系统中的其他代码内被实例化。
其次,instance成员变量和getInstance()方法必须是static修饰的。
单例的应用
1、饥饿式
饿汉式,核心关键在于“饿”,不管三七二十一,咱们先做了再说,先把对象创建起来存放在仓库(JVM内存)中,你需要的时候再来调用即可。也就是说,我们如果使用饿汉式的话,就是要在这个系统启动的时候就会去创建这个实例。
/**
* @Author: 遊亦洋
* @Company Name:ulting
* @Description: 普通单例
* @Create Time: 2019-05-27 16:22
**/
public class Singleton {
private static Singleton instance = new Singleton();
public String name="Singleton";
/**
* 单例之所以需要一个无参的构造函数,是因为单例类是不允许其他类直接“new”的,只能通过调用getInstance方法获取当前对象
*/
private Singleton(){}
public static Singleton getInstance(){
return instance;
}
}
不足:此方法无法做到延迟加载。因为instance是static类型的,当JVM加载单例类时,单例对象就会被建立。
因此这个单例类在系统中其他地方还有被引用时,那么在任何地方被引用时都会实例化这个对象。
2、懒汉式(2种)
懒汉式,顾名思义“懒”,不到万不得已的时候就不会去做这个事情。也就是说,我们如果使用懒汉式的话,就是要在这个类被使用的时候才会去创建这个实例。当然,懒汉式的话也有两种方法创建:一种是“线程安全”、一种是“线程不安全的”(线程安全和不安全区别在于是否引入了synchronized ,所以另外一种不安全模式暂时不贴代码了)。
线程安全的创建方法
/**
* @Author: 遊亦洋
* @Company Name:ulting
* @Description: 普通单例
* @Create Time: 2019-05-27 16:22
**/
public class LazySingleton {
private LazySingleton(){
System.out.println("延迟加载开始...");
}
/**
* 先定义一个静态变量instance,并赋值为null。确保在系统加载的时候没有额外的负载。
*/
private static LazySingleton instance = null;
public String name = "LazySingleton";
public static synchronized LazySingleton getInstance(){
if (instance==null){
instance=new LazySingleton();
}
return instance;
}
}
不足:虽然此种方法实现了延迟加载功能,但是和第一种方法相比,它引入了同步关键字synchronized;所以在多线程的环境下它的延时远远大于第一种单例模式。
为了延迟加载而引入了同步关键字反而降低了系统性能,所以很得不偿失。
3、双重校验式
/**
* @Author: 遊亦洋
* @Company Name:ulting
* @Description: 双重校验式单例
* @Create Time: 2019-06-12 2:46
**/
public class DoubleCheckSingleton {
private DoubleCheckSingleton(){
System.out.println("双重校验式单例执行私有构造方法");
}
public String name="张三";
private static DoubleCheckSingleton doubleCheckSingleton = null;
public static DoubleCheckSingleton getInstance(){
//第一次判断,用户是否创建对象
if (doubleCheckSingleton==null){
//如果用户没有创建对象,加锁
synchronized(DoubleCheckSingleton.class){
//当用户获得锁以后,判断初始化是否创建对象
if (doubleCheckSingleton==null){
doubleCheckSingleton = new DoubleCheckSingleton();
}
}
}
return doubleCheckSingleton;
}
}
缺点:在单个线程中没有问题,但多个线程同时访问的时候就可能同时创建多个实例,而且这多个实例不是同一个对象,虽然后面创建的实例会覆盖先创建的实例,但是还是会存在拿到不同对象的情况.
优点:不调用getInstance()就不会实例化,提高效率.
4、静态内部类
/**
* @Author: 遊亦洋
* @Company Name:ulting
* @Description: 内部类加载方法
* @Create Time: 2019-05-27 16:49
**/
public class StaticSingleton {
private StaticSingleton(){
System.out.println("内部类加载方法开始...");
}
public String name = "StaticSingleton";
private static class SingletonHolder{
SingletonHolder(){
System.out.println("内部类初始化开始...");
}
private static StaticSingleton instance = new StaticSingleton();
}
public static StaticSingleton getInstance(){
final SingletonHolder singletonHolder = new SingletonHolder();
return SingletonHolder.instance;
}
}
不足:虽然通过静态内部类形式解决了延迟加载的性能问题,但是外界还是可以通过反射、序列化方式进行暴力破解。
优点:此方法使用内部类来维护单例的实例。只有当我们调用getInstance()方法时我们才能获取到实例对象。其他时候当我们加载StaticSingleton时就不会加载StaticSingleton实例
5、枚举式
/**
* 当我们使用其他方式的单例的时候,容易被通过序列化、反射进行暴力破解
* @Author: 遊亦洋
* @Company Name:ulting
* @Description: 枚举防止暴力破解
* @Create Time: 2019-05-27 16:49
**/
public enum EnumSingleton {
INSTANCE;//INSTANCE代表一个EnumSingleton的对象
// 枚举能够绝对有效的防止实例化多次,和防止反射和序列化破解
public void add() {
System.out.println("枚举的add方法...");
}
}
private static void enumSingletonMain()throws Exception{
EnumSingleton instance1 = EnumSingleton.INSTANCE;
EnumSingleton instance2 = EnumSingleton.INSTANCE;
System.out.println(instance1 == instance2);
//测试是否能暴力破解...(下面抛出异常:Exception in thread "main" java.lang.NoSuchMethodException: com.ulting.enumsingleton.EnumSingleton.<init>())
Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
EnumSingleton v3 = declaredConstructor.newInstance();
System.out.println(v3==instance1);
}
优点:枚举能够绝对有效的防止实例化多次,和防止反射和序列化破解
枚举源码:我们需要通过特殊的Java反编译器.exe工具进行反编译枚举的.class文件才能看到源码。
由图中第7行可以看出,当我们定义一个枚举类的时候,其实我们是继承的一个java的枚举类。
由图中27行可以看出,当我们在枚举中定义参数“INSTANCE”时,其实我们定义的是一个EnumSingleton的变量,所以我们可以在调用本例子的时候,我们可以直接通过EnumSingleton.INSTANCE获取到当前的实例。
6、容器类管理。
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String, Object>();
public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}
public static Object getService(String key) {
if (objMap.containsKey(key)) {
return objMap.get(key);
}
}
}
容器管理,就是通过一个map集合来管理我们要存储我们要实例化的单例对象。
不足:高并发情况下HashMap线程不安全,即便是换成ConcurrentHashMap,因为我们使用的是static方法,所以依旧会存在线程不安全的情况。
攻防
使用反射技术破解单例
/**
* 通过反射的方式破解单例
*/
private static void reflectionCracke()throws Exception{
//1、先获取实例
final LazySingleton instance = LazySingleton.getInstance();
//2、使用java反射初始化对象
final Constructor<LazySingleton> declaredConstructor = LazySingleton.class.getDeclaredConstructor();
//3、设置获取无参构造函数权限
declaredConstructor.setAccessible(true);
//4、执行无参构造函数
final LazySingleton lazySingleton = declaredConstructor.newInstance();
//执行结果不相等,说明执行了两次初始化
System.out.println(instance==lazySingleton);
}
防止被反射暴力破解
private LazySingleton(){
//如果要防止反射暴力破解,我们需要在这里判断对象是否被初始化,如果被初始化了直接抛出异常
synchronized (this){
if(instance!=null){
throw new RuntimeException("不能多次初始化");
}
}
System.out.println("延迟加载开始...");
}
防止序列化暴力破解
//返回序列化获取对象 ,保证为单例
//即如果我们要防止序列化暴力破解,只需要在单例类中添加readResolve()方法即可
public class Singleton implements Serializable{
private Object readResolve(){
return instance; //默认返回 instance 对象,而不是重新生成一个新的对象
}
}
写在末尾
本文参考某蚁教育、java程序性能优化、大话设计模式