前言
在java中,单例模式应该是大家较为熟悉的设计模式了,但其实,并不是所有的java开发人员都能写出安全高效的单例模式,由于自己理解也不是很深刻,所以以下算是本人学习的一段经历吧~~
下面分享一下我学习单例模式的历程。
什么是单例?
一个类在整个系统的运行过程中,只有一个实例,而且这个实例只能在类的内部通过private构造。外部不能直接调用其构造方法,只能获取他的实例。
单例模式的特点:
1. 构造方法私有化。
2. 以静态方法或者枚举返回实例。
3. 确保实例只有一个,尤其是多线程环境。
4. 确保反序列换时不会重新构建对象。
单例模式和静态类的区别
什么是静态类,静态类就是一个类里面都是静态的方法和静态的域,并且构造器被private修饰无法由外部创建,只能通过类名直接调用。单例模式则是提供一个可实例化的对象,当对象比较重的时候 还可以通过懒加载减少没必要的消耗。如果只是想使用一些方法不关心对象,可以使用静态类,而且静态类是在编译期进行绑定的比单例模式快。如果要维护一些状态信息或者资源的时候应该用单例模式。
维基百科上对单例的定义:单例对象的类必须保证只有一个实例存在
解读一下这句话,其实单例模式的所有代码都是为了让程序能够更高效安全的获得这个类的唯一实例化对象,那么最重要的就是私有化构造方法。
自然而然的我们最先会想到一种的实现方法就是饿汉式单例。让我们来看看实现:
public class Singleton1 {
private Singleton1(){}
private static Singleton1 instancs = new Singleton1();
public static Singleton1 getInstance(){
return instancs;
}
}
代码很简单,既然你要求只有一个实例,那么我写个私有化构造方法,然后在内部创建好实例化对象,并且提供一个方法供外界访问,好像已经完美的解决了问题,但是聪明的小伙伴就会想到,某些用不到类对象的情况下,紧紧加载了类,就创建了对象,那么会让程序容易多出垃圾,并且在加载类的时候会很慢,这种情况只适用于占用资源消少,要求初始化快的情况。说到这里 聪明的小伙伴又会想到另一种方式,你不用我,那我就不赋值不就行了吗,没错,那就是懒汉式单例,上代码:
public class Singleton2 {
private Singleton2(){}
private static Singleton2 instance;
public static Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
还是一样先私有化构造方法,但是先不给对象实例化,在getInstance方法被调用时 判断是否被实例化过再返回实例化对象,这样是不是就解决了上面的问题呢,但是有的同学已经发现了,如果存在多线程并发的情况,试想一种情况 instance为null时,A线程进入到if判断中,A sleep()了,然后B线程进入也到if判断中然后B执行完new了一个实例对象返回后,A也继续执行也new了一个实例化对象返回,那么就会导致返回对象不一致,违背了单例的规则。那有的同学说了,那我加个锁不就行了吗。
public class Singleton2 {
private Singleton2(){}
private static Singleton2 instance;
public static synchronized Singleton2 getInstance(){
if(instance == null){
instance = new Singleton2();
}
return instance;
}
}
但是这样会导致效率降低的问题,如果A进来获得了锁,那么B必须等到A释放了锁,并且B得到了锁才能继续往下进行判断,说到这,终极的懒汉模式就要诞生了,那就是双重锁判断的饿汉模式来了,amazing~~~ 话不多说上代码:
public class Singleton2 {
private Singleton2(){}
private static volatile Singleton2 instance;
public static Singleton2 getInstance(){
if(instance == null){
synchronized(Singleton2.class){
if (instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
}
看一下思路,和上面有所改进的就是如果实例化好之后,那么就不会再出现锁抢夺导致堵塞的情况,但是还加了一个volatile,jdk1.5以前,volatile保证了对象的可见性,这里的Singleton2对象是在Singleton.class内是可见的,但是不能保证在其他内存中是实时可见的,这样会导致别的线程依旧能进入null判断从而创建不同的实例对象,jdk1.5以后,volatile多了一个功能保证指令不会被重新排序,因为java中new一个对象并不是原子操作,分为了很多步骤,其中的内存分配是无法被指令重排的,但是 引用赋值和初始化对象是可以被重排的,那么就会导致一个问题,如果先执行了引用赋值,那么其他线程抢占了cpu后就不是null了,会直接返回一个未被初始化的对象,导致程序报错。这里可能将的没有特别的详细,具体有兴趣的话可以自行翻看其他大佬的博客。
看到这儿你可能以为结束了,但是NO!!!
再分享两个来自《Effective Java》一书中大佬的方法。(此书强烈建议购买,不看的话,用来压泡面也是不错滴。
方法一 静态内部类
public class Singleton3 {
private static class SingletonHolder{
private static final Singleton3 instance = new Singleton3();
}
private Singleton3(){}
public static final Singleton3 getIntance(){
return SingletonHolder.instanc;
}
}
这种写法首先只有当getIntance第一次被调用时,静态类得以创建,从外部看是懒汉模式,从内部看 SingletonHolder是饿汉模式,ClassLoader保证了SingletonHolder在初始化的时候instance是一个真正的单例,同时又能然后开发者控制类的加载实际,属实妙不可言。
JVM保证了类的静态成员只能被加载一次,所以从JVM层面保证了单例的特性。一个类被加载的时候,当且仅当该类的某个静态成员(静态域,构造方法,静态方法)被调用时发生。此处还百度了一下 。
“即使没有显式地使用static关键字,构造器实际上也是静态方法”—摘自《Thinking in Java 4》
方法二 枚举实现单例
书中说道 —— 单元素的枚举类型已经成为实现Singleton的最佳方法。
上代码!
public enum Singleton4 {
INSTACNE;
}
JVM保证了INSTACNE对象只会被实例化一次并且多线程安全,还抗反序列化,防反射,堪称无敌~~~
那么枚举类是怎么做到的呢,学习了一下其他大佬的博客,下面是转载的链接
[link]https://blog.youkuaiyun.com/moakun/article/details/80688851
除了枚举方法能够实现防序列化和反序列化出现多实例的问题外,其他方法可以通过添加
public Object readResolve() {
return instence;
}
不添加该方法则会出现 反序列化时出现多个实例的问题
防反射
private Singleton(){
if(instance !=null){
throw new RuntimeException("正在实例化对象!") ;
}
}