饥汉模式(积极加载)
public class Hungry {
byte[] byts1 = new byte[1024*1024];
byte[] byts2 = new byte[1024*1024];
byte[] byts3 = new byte[1024*1024];
byte[] byts4 = new byte[1024*1024];
private Hungry(){}//构造器私有化
private static Hungry instance = new Hungry();//加载类模板时,已经创建实例
public Hungry getInstance() {//公有化get方法,提供实例
return instance;
}
}
从上面实例,我们可以看出饥汉模式的弊端就是,当这个类会占用相当的内存时, 未被使用,就已经实例化,会造成相当的内存浪费! 于是有了 懒汉模式。
懒汉模式(懒加载,使用时才加载)
public clacc Lazy {
private Lazy(){}
private static Lazy instance;
public Lazy getInstance(){
instance = new Lazy();//方法被执行时才创建实例
return instance;
}
}
//以上的写法真的是单例吗? 单线程下是ok的, 但是我们看看多线程情况下
class Test{
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()-> Lazy.getInstance()).start();
}
}
}
显而易见的,创建了多个实例,违背了单例!
针对以上情况,我们可以加锁,来保证多线程情况下也是单例的安全
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName() + "已被创建!");
}
private static Lazy instance;
public static Lazy getInstance(){
if(instance == null) {
//这个对象还未被创建,我们不能锁null,所以给它的类模板对象上锁。
//new对象是根据类模板来new的
synchronized (Lazy.class) {
if (instance == null) {
instance = new Lazy();
}
}
}
return instance;
}
}
这样似乎是完善了,不,并没有 看如下
public class Lazy {
private Lazy(){
System.out.println(Thread.currentThread().getName() + "已被创建!");
}
//所以我们要加volatile关键字告诉jvm它是易变的 不要优化策略而进行指令重排
private static volatile Lazy instance;
public static Lazy getInstance(){
if(instance == null) {
synchronized (Lazy.class) {
if (instance == null) {
instance = new Lazy();//这不是一个原子型操作
/**
* 1.分配内存空间
* 2.执行构造方法
* 3.引用变量指向内存空间
* 虚拟机jvm执行时,可能会产生指令重排的现象 类似 132 的顺序
* 这将导致并发场景下的另一线程想要获取实例时,
* 锁之前的判空就会认为不为空了
* 则会返回 指向未知内存的引用(因为实际上未执行构造方法)
*/
}
}
}
return instance;
}
}
以上,就是较为完善的 DCL懒汉式单例。
静态内部类的 单例实现
public class Holder {
private Holder(){}
public static class Innerc {
private static final Holder holder = new Holder();
}
public static Holder getInstance() {
return Innerc.holder;
}
}
静态内部类实现单例有哪些好处呢?
静态内部类不会在外部类被加载时就加载,所以也具备懒汉的特质。
另外jvm在多个线程同时调用同一个类模板的cInit方法时,总会有某个线程最先执行,那么其它线程会进入阻塞态。那个真正执行cInit方法的线程完成执行后,其它线程将不再会去执行这个已经完成了模板初始化的类的cInit方法。所以保证了 多线程情况下是安全的。
听起来似乎很厉害? 但在得到这个单例的同时,确实不太好传入外部参数。
up主觉得,可以在得到单例后强行设置嘛~~~
public class Holder {
private String arg;
@Override
public String toString() {
return "Holder{" +
"arg='" + arg + '\'' +
'}';
}
public void setArg(String arg) {
this.arg = arg;
}
private Holder(){}
public static class Innerc {
private static final Holder holder = new Holder();
}
public static Holder getInstance() {
return Innerc.holder;
}
}
class Test0 {
public static void main(String[] args) {
Holder instance = Holder.getInstance();
instance.setArg("哈哈");
System.out.println(instance);
}
}
然而事情依然没有结束!!!
为什么需要枚举的单例实现
我们知道Java中存在一项技术,叫做反射!!
它可以改变类的构成元素,包括访问修饰符的限制!! 例如
import java.lang.reflect.Constructor;
public class ReflectBrokeSingle {
public static void main(String[] args) throws Exception {
//首先我们获取一个单例对象
Lazy lazy1 = Lazy.getInstance();
//利用反射,得到它的构造器
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);//修改构造器的对外访问权限
Lazy lazy2 = declaredConstructor.newInstance(null);
System.out.println(lazy1.hashCode());
System.out.println(lazy2.hashCode());
}
}
我们发现它又创建了2个不同的实例!!!
然后一个颇具针对性的解决方案,解决这种反射式破坏
public class Lazy {
private Lazy(){
synchronized (Lazy.class) {
if (instance != null) {
throw new RuntimeException("不要试图通过反射破坏单例!");
}
}
}
//所以我们要加volatile关键字告诉jvm它是易变的 不要优化策略 进行指令重排
private static volatile Lazy instance;
public static Lazy getInstance(){
if(instance == null) {
synchronized (Lazy.class) {
if (instance == null) {
instance = new Lazy();
}
}
return instance;
}
}
上面那种破坏被针对了,但完全无济于事。因为构造方法中判断的实例,是通过静态方法getInstance赋值的。 所以我们依然可以极其轻松的用反射破坏它!
public class ReflectBrokeSingle {
public static void main(String[] args) throws Exception {
//首先我们获取一个单例对象
// Lazy lazy1 = Lazy.getInstance();
//利用反射,得到它的构造器
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);//修改构造器的对外访问权限
Lazy lazy2 = declaredConstructor.newInstance();
Lazy lazy3 = declaredConstructor.newInstance();
System.out.println(lazy3.hashCode());
System.out.println(lazy2.hashCode());
}
}
不去执行的他自己的getInstance()方法,这样它的判断条件永远不成立。直接通过反射可以一直创建不同的实例!!
死循环了,特么的,怎么解决,总能想法子破坏, 反射毒瘤!
心病还须心药医,解铃还须系铃人
我们看下反射创建实例方法的源码
原来,java是提供了这个保护机制,那就是 通过枚举来阻止反射去 创建实例!
所以我们需要枚举的单例!
这里只给出骨架,实际应用场景,自行丰富
public enum EnumSingle {
instance;
public EnumSingle getInstance() {
return instance;
}
}
这样终究是,把单例模式画上了比较圆满的句号。 单例:我真的不容易!