饿汉式:
/**
* @Description: 饿汉式单例模式
* @Author: BeforeOne
* @Date: Created in 2021/5/23 9:22
*/
public class Hungry {
// 由于对象始终都会创建,所以可能会导致空间的浪费
private byte[] data = new byte[1024*1024*1024];
private Hungry(){
}
// 始终会初始化对象
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
饿汉式单例模式中,对象始终会被初始化,无论是否使用,容易造成空间浪费。
由于是在类加载的时候初始化对象,所以线程是安全的。
懒汉式
package com.shili.exercise;
/**
* @Description: 懒汉式单例模式
* @Author: BeforeOne
* @Date: Created in 2021/5/23 9:24
*/
public class Lazy {
private Lazy(){
}
private static Lazy lazy;
private static Lazy getInstance(){
// 当对象没有创建的时候才创建
// 线程不安全
if (lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
package com.shili.exercise;
/**
* @Description: 懒汉式单例模式
* @Author: BeforeOne
* @Date: Created in 2021/5/23 9:24
*/
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName() + "OK");
}
private static Lazy lazy;
private static Lazy getInstance(){
if (lazy == null){
lazy = new Lazy();
}
return lazy;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
lazy.getInstance();
}).start();
}
}
}
线程不安全测试:
线程不安全可能通过加锁来保证线程安全:
private static Lazy getInstance(){
//每次对象都需要上锁,繁琐,降低了性能
synchronized (Lazy.class){
if (lazy == null){
lazy = new Lazy();
}
return lazy;
}
}
如果只是简单的加上synchronized,之后每次获取对象都需要获取锁,会导致效率很低,因此可以加上双重验证来提高效率
// 双重检查锁 DCL
private static Lazy getInstance() {
// 在加锁前进行一次检查,避免每次获取对象都需要加上锁,但是在高并发的时候仍然会有多个线程进入
if (lazy == null) {
// 将通过 第一层检查 的多个线程加上锁
// 某一线程获得锁创建一个Lazy对象时,即已有引用指向对象,lazy不为空,从而保证只会创建一个对象
// 假设没有 第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
双重检查锁可以解决并发情况下大多数问题,但是由于 lazy = new Lazy(); 这行操作并不是原子性的,可能会因为指令重排导致一些问题
//通过 volatile 关键字来避免指令重排
private volatile static Lazy lazy;
// 双重检查锁 DCL
private static Lazy getInstance() {
// 在加锁前进行一次检查,避免每次获取对象都需要加上锁,但是在高并发的时候仍然会有多个线程进入
if (lazy == null) {
// 将通过 第一层检查 的多个线程加上锁
// 某一线程获得锁创建一个Lazy对象时,即已有引用指向对象,lazy不为空,从而保证只会创建一个对象
// 假设没有 第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
/*
1.分配内存空间
2.执行构造方法,初始化对象
3.将对象指向该内存空间
重排序1-2-3–》1-3-2
此时若线程2执行getInstance发现单例对象已有指针(不为null),但实际上内存空间可能还未存放对象,导致线程2后序操作报错。
*/
}
}
}
return lazy;
}
静态内部类实现(了解)
package com.shili.exercise;
/**
* @Description: 静态内部类实现单例模式
* @Author: BeforeOne
* @Date: Created in 2021/5/23 10:22
*/
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}
目前,通过反射可以破解上面所有形式的单例模式!
public static void main(String[] args) throws Exception {
//正常创建一个对象
Lazy instance1 = Lazy.getInstance();
//通过反射创建一个对象
// 获取 Lazy的空参构造器
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
// 设置可以访问私有方法
declaredConstructor.setAccessible(true);
Lazy instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
// 在构造器中加入一层检查
private Lazy(){
synchronized (Lazy.class){
if (lazy != null){
throw new RuntimeException("请不要使用反射破坏单例");
}
}
}
这样写有一点效果,但是如果同时使用两次反射破坏
此时lazy对象并没有被调用,为null。构造器里面的判断语句就不好使了。
还有方式可以定义一个额外的变量,在构造器里面进行判断唯一性,但是也可能会被反射获取到并且破解。
所以,通过反射我们可以破解绝大部分的单例模式,但是通过newInstance源码可以得知,枚举类的单例模式无法被反射破坏
枚举类创建单例
package com.study.exercise;
/**
* @Description: 枚举类反射
* @Author: BeforeOne
* @Date: Created in 2021/5/23 11:12
*/
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class TestEnumSingle{
public static void main(String[] args) {
EnumSingle enumSingle1 = EnumSingle.INSTANCE;
EnumSingle enumSingle2 = EnumSingle.INSTANCE;
System.out.println(enumSingle1);
System.out.println(enumSingle2);
}
}
尝试通过反射来破坏
根据输出的文件可以初步得知枚举类使用的是空参构造器
class TestEnumSingle{
public static void main(String[] args) throws Exception {
EnumSingle enumSingle1 = EnumSingle.INSTANCE;
// 通过空参构造器获取对象
Constructor<EnumSingle> enumSingle2 = EnumSingle.class.getDeclaredConstructor(null);
System.out.println(enumSingle1);
System.out.println(enumSingle2);
}
}
通过这个错误可以发现,并没有找到这个空参构造器!
进一步挖掘,终于发现是有参构造
最终实现了JDK内部定义的错误
枚举类中所有属性都被 static final
修饰,天然支持多线程(static 类型的属性会在类加载过程初始化, 当一个 Java 类第一次被真正使用到的时候静态资源被初始化、 Java 类的加载和初始化过程都是线程安全的( 因为虚拟机在加载枚举的类的时候, 会使用 ClassLoader 的 loadClass 方法, 而这个方法使用同步代码块保证了线程安全))