单例模式就是确保某一个类只有一个实例,而且自行实例化,并向整个系统提供这个实例
确保某个类只有一个对象的使用场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如,创建一个对象需要消耗过多的资源,如要访问IO和数据库等资源,就应该考虑单例模式了。
/**
* 饿汉式单例模式
* */
public class YuanZhen {
private static final YuanZhen yuanzhen =new YuanZhen();
private YuanZhen(){
}
public static YuanZhen getInstance(){
return yuanzhen;
}
}
饿汉式优点: 不需要加锁,性能优越
饿汉式缺点:一开始就加载好了对象,占用内存
/**
* 懒汉式单例
* */
public class YuanZhen1 {
private static YuanZhen1 yuanZhen1;
private YuanZhen1(){
}
public static synchronized YuanZhen1 getInstance(){
if(yuanZhen1 ==null){
yuanZhen1 =new YuanZhen1();
}
return yuanZhen1;
}
}
懒汉式单例就是在需要的时候去创建对象。
懒汉式单例的优点:单例只有在使用的时候才去加载,一定程度上节约了资源
懒汉式单例的缺点:
1.第一次加载时需要实例化,运行稍慢
2.每次调用synchronized同步锁,消耗资源
3,虽然加了同步锁,但是仍然可能会出现同步问题
/**
* DCL单例
* */
public class YuanZhen2 {
private static YuanZhen2 yuanZhen2;
private YuanZhen2(){
}
public static YuanZhen2 getInstance(){
if(yuanZhen2 == null){
synchronized (YuanZhen2.class){
if(yuanZhen2 ==null){
yuanZhen2 =new YuanZhen2();
}
}
}
return yuanZhen2;
}
}
在getInstance方法中,对yuanzhen2进行了2次判空,第一次判空是为了避免不必要的同步
第二次判空是为了在null的情况下创建实例
当线程A执行到new Yuanzhen2() 这句代码时,它最终会被编译成多条汇编指令:
1,给YuanZhen2的实例分配内存
2,调用YuanZhen2的构造函数,初始化成员变量
3,将yuanzhen2对象指向分配的内存空间
JDK1.5之前,它们在内存中的执行顺序可能1-2-3 也可能是1-3-2.
如果是1-3-2,那么如果执行完3之后,切换到了B线程,那这时yuanzhen2表面上看是非空,实际上是空的。那么后面再使用的时候就会报错,这就是DCL失效。
但是JDK1.5之后,调整了JVM.只需要将YuanZhen2的定义改成如下volatile方式,就能保证YuanZhen2对象每次都是从主内存中读取。
/**
* DCL单例
* */
public class YuanZhen2 {
private static volatile YuanZhen2 yuanZhen2;
private YuanZhen2(){
}
public static YuanZhen2 getInstance(){
if(yuanZhen2 == null){
synchronized (YuanZhen2.class){
if(yuanZhen2 ==null){
yuanZhen2 =new YuanZhen2();
}
}
}
return yuanZhen2;
}
}
volatile会影响性能,但是如果可以避免DCL的失效问题,这点性能几乎可以忽略不计了
DCL的优点:
1,资源利用率高,第一次执行getInstance时单例对象才被初始化
2,避免了多余的同步问题
3,避免了线程的并发问题
DCL的缺点:
1,第一次加载时反省稍慢
2,使用synchronized 和 volaile 关键字,性能有一定消耗
/**
* 静态内部类
* */
public class YuanZhen3 {
private YuanZhen3(){
}
public static YuanZhen3 getInstance(){
return SingletonHolder.yuanzhen3;
}
private static class SingletonHolder{
private static final YuanZhen3 yuanzhen3 =new YuanZhen3();
}
}
当第一次加载YuanZhen3类时,并不会初始化yuanzhen3,只有在第一次调用Yuanzhen3的getInstance方法时才会导致yuanzhen3被初始化,因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单列的实例化,所以这是最推荐使用的方式。
/**
* 枚举方式
* */
public enum YuanZhen4 {
INSTACE;
}
枚举方式是最简单的单例实现方式。而且它也是线程安全的,并且在任何情况下它都是一个单例。
在上述几种方式中,我们可以通过hook反序列化的函数readResolve来重新生成对象,除非我们重写readResolve函数将对象返回。但是枚举并不存在这个问题
/**
* 使用容器
* */
public class YuanZhen5 {
private static Map map =new HashMap<>();
private YuanZhen5(){}
public static void registerInstance(String key,Object instance){
if(!map.containsKey(key)){
map.put(key, instance);
}
}
public static Object getInstance(String key){
return map.get(key);
}
}
使用:
YuanZhen5.registerInstance("1", YuanZhen1.getInstance());
YuanZhen5.registerInstance("2", YuanZhen2.getInstance());
YuanZhen2 yuanzhen2 = (YuanZhen2) YuanZhen5.getInstance("2");
在程序初始化时将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对应类型的单例。
优点:
1,方便管理多种类型的单例
2,使用时可以通过统一的接口进行获取操作,降低了用户的使用成本
3,对用户隐藏了具体实现
4,降低了耦合度
单例模式优点:
1,单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建,销毁时,单例模式的优势就非常明显了
2,由于单例模式只生成一个实例,所以,减少了系统的性能开销,当一个对象的创建需要比较多的资源时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决。
缺点:
1,单例模式一般没有接口,扩展很难
2,单例对象如果持有context,很容易发生内存泄漏,此时需要注意,传递给单例对象的Context最好是Application Context。