在使用的时候才去检查有没有实例,有就返回,没有就初始化一个。
存在线程安全问题,使用双端检索机制解决。
class Single{ private Single(){} private static Single single = null; public static Single getInstance(){ if(single == null){ single = new Single(); } reutrn single; } }一开始就初始化好实例对象,好处是线程安全,坏处是浪费内存。
class Single{ private Single(){} private static final Single single = new Single(); public static Single getInstance(){ reutrn single; } }双检锁 又叫双重校验锁,综合了懒汉式和饿汉式两者的优点和缺点整合而成。特点是在synchronized关键字内外都加上了一层if判断 这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。
volatile的一个语义是禁止指令重排序优化,所以用volatile保证初始化的时候执行保证顺序是正确的。
class Single{ private static volatile Single single; private Single(){} public static Single getInstance(){ if(single == null){ syschroncized(Single.class){ if(single == null){ single = new Single(); } } } return single; } }加了一层判空,除了第一次为空会访问到同步代码块,其他都不会访问到,直接返回实例,提高了程序性能。
不过还需要考虑一种情况,假如两个线程A、B,A执行了if (single== null)语句,它会认为单例对象没有创建,此时线程切到B也执行了同样的语句,B也认为单例对象没有创建,然后两个线程依次执行同步代码块,并分别创建了一个单例对象。为了解决这个问题,还需要在同步代码块中增加if (single == null)语句,也就是上面看到的代码中的校验2。
由于指令重排优化的存在,导致初始化Single将对象地址赋给single字段的顺序是不确定的。
在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。
此时就可以将分配的内存地址赋值给single字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。
静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。 class Single{ private Single(){} private static class Single{ private static final Single single = new Single(); } public static Single getInstance(){ return Single.single; } }上面的类Resource是我们要应用单例模式的资源,具体可以表现为网络连接,数据库连接,线程池等等。
获取资源的方式很简单,只要 SomeThing.INSTANCE.getInstance() 即可获得所要实例。
下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法。同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。 class Resource{ } public enum SomeThing { INSTANCE; private Resource instance; private SomeThing() { instance = new Resource(); } public Resource getInstance() { return instance; } }