单例模式的核心为构造器私有化,无法通过new创建,需要通过调用类对外提供的方法获取实例
单例模式分为饿汉式与懒汉式,下文分别说明
饿汉式
在类被加载进内存后就直接创建对象实例 可能会造成内存浪费
示例代码
public class HungryMan {
//构造器私有 无法通过new创建对象实例
private HungryMan(){
}
private final static HungryMan HUNGRY_MAN = new HungryMan();
public static HungryMan getInstance(){
return HUNGRY_MAN;
}
}
DCL懒汉式(Double Check Lock)
只有需要对象实例的时候才创建对象实例 不会造成内存浪费
示例代码
注意使用
public class LazyMan {
//构造器私有 无法通过new创建对象实例
private LazyMan(){
System.out.println(Thread.currentThread().getName() + " 通过 getInstance() 创建了LazyMan对象实例");
}
/*
为什么需要添加volatile以避免指令重排 原因如下:
new LazyMan()并不是原子性操作 可细分为:
1.分配内存空间
2.执行构造方法 初始化对象
3.将空间分配给该对象
若发生指令重排 如1->3->2
若执行完3正在执行2时 恰巧有其他线程调用getInstance()方法 此时其他线程会认为lazyMan!=null(已分配内存空间) 会返回lazyMan
但此时lazyMan尚未初始化!
*/
private volatile static LazyMan lazyMan;
//DCL懒汉式 即double check lock
public static LazyMan getInstance(){
//为什么要使用double check?
//1.若第一层不进行if判断 则每次调用方法都会进行一次锁的争抢 会消耗不必要的资源
//2.若第二层不进行if判断 则可能同时有多个线程通过第一个if判断进入if代码块(线程均认为lazyMan==null) 会导致线程均等待获取锁 造成重复创建单例
if (lazyMan==null){
synchronized (LazyMan.class){
if (lazyMan==null) lazyMan = new LazyMan();
}
}
return lazyMan;
}
}
测试代码
public class Test {
public static void main(String[] args){
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
测试结果
可以看到,LazyMan类仅被创建了一个实例(构造方法仅执行了一次)
但是!以上的单例都可通过反射破坏,所以可通过枚举类型(enum)来解决该问题
示例代码
public enum Test{
INSTANCE;
public void method(){
System.out.println("method");
}
public static Test getInstance(){
return INSTANCE;
}
}
public class Test2 {
public static void main(String[] args) {
Test.getInstance().method();
try {
//获取私有构造方法
Constructor<Test> constructor = Test.class.getDeclaredConstructor(String.class, int.class);
//关闭安全验证(无视访问权限进行访问)
constructor.setAccessible(true);
//调用构造方法创建实例
constructor.newInstance();
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
测试结果
可以看到,无法通过反射破坏枚举类型的单例