Java——单例模式和延迟加载
延迟加载
延迟加载(lazy load) (也称为懒加载,也叫延迟实例化,延迟初始化等)主要表达的思想就是:把对象的创建延迟到使⽤的时候创建,⽽不是对象实例化的时候创建。延迟加载机制是为了避免⼀些⽆谓的性能开销⽽提出来的,这种⽅式避免了性能的浪费。所谓延迟加载就是当在真正需要数据的时候,才真正执⾏数据加载操作。
单例模式
单例模式: 因程序需要,有时我们只需要某个类同时保留⼀个对象,不希望有更多对象,此时,我们则应考虑单例模式的设计。
单例模式的好处: 单例模式适合于应⽤中频繁创建的对象,如果是重量级的对象,更应该使⽤单例模式。⽐如配置⽂件,如果不采⽤单例模式的话,每个配置⽂件对象的内容都是⼀样的,创建重复的对象就会浪费宝贵的内存,所以有必要使⽤单例模式,达到性能的提升,减⼩了内存的开销和GC的压⼒。本⽂会⼀步⼀步由浅⼊深的讨论如何实现正确的单例模式。
单例模式常见的⼏种⽤法:
饿汉法
顾名思义,饿汉法就是在第⼀次引⽤该类的时候就创建对象实例,⽽不管实际是否需要创建。代码如下:
public class Singleton {
private static Singleton =new Singleton();
private Singleton(){}
public static getSignleton(){
return singleton;
}
}
这样做的好处是编写简单,但是⽆法做到延迟创建对象。但是我们很多时候都希望对象可以尽可能地延迟加载,从⽽减⼩负载,所以就需要下⾯的懒汉法:
单线程写法
这种写法是最简单的,由私有构造器和⼀个公有静态⼯⼚⽅法构成,在⼯⼚⽅法中对singleton进⾏null判断,如果是null就new⼀个出来,最后返回singleton对象。这种⽅法可以实现延时加载,但是有⼀个致命弱点:线程不安全。如果有两条线程同时调⽤getSingleton()⽅法,就有很⼤可能导致重复创建对象。代码如下:
public class Singleton {
private static Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null) singleton =new Singleton();
return singleton;
}
}
考虑线程安全的写法
这种写法考虑了线程安全,将对singleton的null判断以及new的部分使⽤synchronized进⾏加锁。同时,对singleton对象使⽤volatile关键字进⾏限制,保证其对所有线程的可见性,并且禁⽌对其进⾏指令重排序优化。如此即可从语义上保证这种单例模式写法是线程安全的。注意,这⾥说的是语义上,实际使⽤中还是存在⼩坑的,会在后⽂写到。
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
synchronized(Singleton.class){
if(singleton == null){
singleton =new Singleton();
}
}
return singleton;
}
}
单例模式的几种实现方式兼顾线程安全和效率的写法
虽然上⾯这种写法是可以正确运⾏的,但是其效率低下,还是⽆法实际应⽤。因为每次调⽤getSingleton()⽅法,都必须在synchronized 这⾥进⾏排队,⽽真正遇到需要new的情况是⾮常少的。所以,就诞⽣了第三种写法:
public class Singleton {
private static volatile Singleton singleton = null;
private Singleton(){}
public static Singleton getSingleton(){
if(singleton == null){
synchronized(Singleton.class){
if(singleton == null){
singleton =new Singleton();
}
}
}
return singleton;
}
}
这种写法被称为“双重检查锁”,顾名思义,就是在getSingleton()⽅法中,进⾏两次null检查。看似多此⼀举,但实际上却极⼤提升了并发度,进⽽提升了性能。为什么可以提⾼并发度呢?就像上⽂说的,在单例中new的情况⾮常少,绝⼤多数都是可以并⾏的读操作。因此在加锁前多进⾏⼀次null检查就可以减少绝⼤多数的加锁操作,执⾏效率提⾼的⽬的也就达到了。
那么,这种写法是不是绝对安全呢?前⾯说了,从语义⾓度来看,并没有什么问题。但是其实还是有坑。说这个坑之前我们要先来看看volatile这个关键字。其实这个关键字有两层语义。第⼀层语义相信⼤家都⽐较熟悉,就是可见性。可见性指的是在⼀个线程中对该变量的修改会马上由⼯作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便⼀提,⼯作内存和主内存可以近似理解为实际电脑中的⾼速缓存和主存,⼯作内存是线程独享的,主存是线程共享的。volatile的第⼆层语义是禁⽌指令重排序优化。⼤家知道我们写的代码(尤其是多线程代码),由于编译器优化,在实际执⾏的时候可能与我们编写的顺序不同。编译器只保证程序执⾏结果与源代码相同,却不保证实际指令的顺序与源代码相同。这在单线程看起来没什么问题,然⽽⼀旦引⼊多线程,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题。
注意,前⾯反复提到“从语义上讲是没有问题的”,但是很不幸,禁⽌指令重排优化这条语义直到jdk1.5
以后才能正确⼯作。此前的JDK中即使将变量声明为volatile也⽆法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是⽆法保证线程安全的。
静态内部类法
那么,有没有⼀种延时加载,并且能保证线程安全的简单写法呢?我们可以把Singleton实例放到⼀个静态内部类中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载⼀次,所以这种写法也是线程安全的:
private static class Holder {
private static Singleton singleton =new Singleton();
}
private Singleton(){}
public static Singleton getSingleton(){
return Holder.singleton;
}
}
但是,上⾯提到的所有实现⽅式都有两个共同的缺点:
都需要额外的⼯作(Serializable、transient、readResolve())来实现序列化,否则每次反序列化⼀个序列化的对象实例时都会创建⼀个新的实例。
可能会有⼈使⽤反射强⾏调⽤我们的私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第⼆个实例的时候抛异常)。
枚举写法
当然,还有⼀种更加优雅的⽅法来实现单例模式,那就是枚举写法:
public enum Singleton {
INSTANCE;
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
使⽤枚举除了线程安全和防⽌反射强⾏调⽤构造器之外,还提供了⾃动序列化机制,防⽌反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使⽤枚举来实现单例。
参考博客

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。