以下均为个人学习知识,可能存在错误,请辩证阅读
一、简介
单例模式目的在于控制整个系统中只存在一个实例并且该类自己创建自己的一个唯一的实例供系统全局调用,避免实例频繁的创建和销毁,具体的主要实现便是构造方法私有化,(反射可以破坏,具体最后说)。单例模式的应用:系统配置文件, SLF4j,log4j等项目中只存在一个实例。
二、实现
单例模式实现的方法有饿汉式,懒汉式,静态内部类。
当然了,还有一个枚举的方法(enum),该方法是由《effective java》的作者提出来的,enum是jdk1.5版本出现的,用enum方法来实现单例模式是最佳的方法,不仅简洁,支持序列化机制 而且可以防止反射的破坏( reflection attack),即用反射来调用你的私有构造方法。
1、饿汉式
根据我的理解哈,就是一个汉子很饿,那么他一上来就吃,在代码中的实现就是初始化的时候就把实例new 出来。
package GOF23.simple;
//饿汉式
public class Hungury {
private static String[] Garbage = new String[1024*1024*1024];
private static Hungury HUNGURY = new Hungury();
private Hungury() {
System.out.println("初始化.....");
}
public static Hungury getInstance() {
return HUNGURY;
}
}
个人分析: 饿汉式没有加锁,所以执行的效率会高些,但是容易产生垃圾对象,如上代码中的Garbage 变量。当这样的变量很多的时候呢。
2、懒汉式
根据我的理解,就是这个汉子很懒,在获取该实例也就是调用getInstance方法的时候才去new 这个实例,那么代码实现的时候就需要对该对象进行判空操作。
package GOF23.simple;
//懒汉式
public class LazyMan {
private static LazyMan LAZYMAN;
private LazyMan(){}
private static LazyMan getInstance(){
if(LAZYMAN == null) {
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
}
但是这个实现有一个问题,现在基本系统都是多线程,那么多线程的情况下就存在并发的问题,当并非的时候就可能导致实例不唯一。
package GOF23.simple;
//懒汉式
public class LazyMan {
private static LazyMan LAZYMAN;
private LazyMan(){
}
private static LazyMan getInstance(){
if(LAZYMAN == null) {
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
public static void main(String[] args) {
for (int i= 0; i< 10; i++) {
new Thread(() -> {
System.out.println(LazyMan.getInstance());
}).start();
}
}
}
如上所示: 10个线程打印出来的对象结果有时是不一致的 由此证明这个方法不是线程安全的方法,那么想要线程安全的话就需要对其进行加锁(同步锁synchronized)操作;
package GOF23.simple;
//懒汉式
public class LazyMan {
private static LazyMan LAZYMAN;
private LazyMan() {
}
private static LazyMan getInstance() {
if(LAZYMAN == null) {
synchronized (LazyMan.class) {
if (LAZYMAN == null) {
LAZYMAN = new LazyMan();
}
}
}
return LAZYMAN;
}
}
个人分析: 为什么要双重校验呢,当并发发生的时候首先判断LAZYMAN 对象是否为空,如果为空再对其进行加锁操作,如果是先加锁的话,那么每次获取实例的时候都会进行加锁那么就会使得系统开销变大。(为什么加锁之后还要判空呢??暂时不知道原因)
如上的代码仍然存在一个问题,那就是新建实例的时候不是一个原子操作,系统在new 一个对象的时候有三步,
1、对象赋值
2、开辟空间
3、将对象指向这个空间
那么就存在一个问题:线程A 执行顺序是123 线程B执行顺序是132 那么就存在可能导致线程不安全,因此需要给对象加上原子性的定义volatile 。最终代码如下:
package GOF23.simple;
//懒汉式
public class LazyMan {
private volatile static LazyMan LAZYMAN;
private LazyMan() {
}
private static LazyMan getInstance() {
if(LAZYMAN == null) {
synchronized (LazyMan.class) {
if (LAZYMAN == null) {
LAZYMAN = new LazyMan();
}
}
}
return LAZYMAN;
}
}
3、静态内部类
package GOF23.simple;
//静态内部类实现
public class InnerClass {
private static class SingleInnerClass { //内部类 在内部类中定义final实例
private static final InnerClass INSTANCE = new InnerClass();
}
private InnerClass() {
}
public static final InnerClass getInstance() {
return SingleInnerClass.INSTANCE; //内部类.变量
}
}
4、枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
以下来自菜鸟教程:
经验之谈:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
三、反射
待更新.....