1.单例模式分为饿汉式单例模式和懒汉式单例模式;
2.饿汉式单例模式的写法有两种:
1).普通的写法:
public class Singleton{
private static Singleton singleton = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
2)静态代码块形式
public class Singleton{
private static Singleton singleton;
static{
singleton = new Singleton();
}
private Singleton(){
}
public static Singleton getInstance(){
return singleton;
}
}
以上两种写法的优缺点都是一样的,只不过静态代码块的实例化过程放在了静态代码块里;
优点:在类装载的时候就完成了实例化,避免了线程同步问题。
缺点:在类装载的时候就完成了实例化,而没有达到懒加载的效果,所以如果从始至终都没有使用到这个实例,就会造成内存的浪费。
3.懒汉式单例模式的写法有四种:
1)普通的懒汉式单例模式写法(只能在单线程环境下使用)
//存在多线程不安全情况
public class Singleton{
private Static Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
2)双重检查锁定式
//可以有效解决多线程安全问题,但是解决不了序列化和反序列化问题
public class Singleton{
//volatile的作用:避免实例未完全实例化(因为singleton = new Singleton()不是一个原子操作, 而是做了三件事:
//1.给singleton分配内存空间;
//2.调用Singleton的构造函数来初始化singleton;
//3.将singleton对象指向分配的内存空间(执行完第三步,singleton就不是null了))
//注意第二步和第三步的顺序(这两步的顺序是不能确定的),存在重排序的优化
private static volatile Singleton singleton;
private Singleton(){
}
public static Singleton getInstance(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton = new Singleton():
}
}
}
return singleton;
}
}
优点:不仅线程安全,并且延迟加载,效率也更高;
常见问题:
为什么要两次check,而不能一次check呢?
假设有两个线程同时调用getInstance方法,并且由于singleton是空的,所以两个线程都通过了第一个if判断,由于有锁机制的存在,会有一个线程先进入第二重if的判断,而另一个线程就在外面等待,而当第一个线程完成实例后,就会推出synchronized保护的区域,这时如果没有第二层singleton==null的话,那么第二个线程就会再次创建实例,这就破坏了单例,这肯定是不行的;
而对于第一个check而言,如果去掉它,那么所有的线程都会串行执行,效率低下,所以两个check都是需要保留的。
3)静态内部类式(由jvm来保证线程安全问题)
public class Singleton{
private Singleton(){
}
private static class SingletonInstance{
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance(){
return SingletonInstance.singleton;
}
}
4)枚举式(最推荐的单例模式的方式)
public enum Singleton{
INSTANCE;
public void whateverMethod(){
}
}
优点:写法简洁,避免多线程同步的问题,而且还能防止反序列化和反射创建新的对象来破坏单例。
原理:通过反编译枚举类可以发现,枚举中的各个枚举项是通过static代码块来定义和初始化的,他们会在类被加载时完成初始化,而java类的加载是由JVM保证线程安全的,所以创建一个Enum的枚举是线程安全的。而对序列化这一件事情,java专门对枚举类的序列化做出了规定,在序列化时,仅仅是将枚举对象的name属性输出到结果中,在反序列化时,就是通过java.lang.Enum的valueOf来根据名字查找对象,而不是创建一个新的对象,所以这就防止了反序列化导致的单例破坏的事件;而对于通过反射破坏单例而言,枚举类同样有防御措施,反射在通过newInstance创建对象时,会检查这个类是否为枚举类,如果是的话,就会抛出IllegalAraumentException("Cannot reflectively create enum objects")这样的异常,反射创建对象失败。
直接通过Singleton.INSTANCE.whateverMethod()的方式调用即可,方便、简洁又安全。
为什么需要单例呢?
原因一:节省内存,节省运算;
原因二:保证结果的正确性以及方便管理。
什么时候可以用到单例模式?
无状态的工具类:日志工具、字符串工具
全局信息类:全局计数、环境变量