概述
单例模式是比较简单的设计模式,使用也是非常的广泛。看一下关于单例模式的定义:
确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。
它的定义包含了三个要点:
- 一个类只有一个实例:外部程序无法通过new关键字来创建实例,构造方法用private修饰
- 自行实例化:由类本身创建实例对象,为了外界可以访问这个实例,需要定义成静态的私有成员变量。
- 提供实例: 因为无法提供new来床架对象,只能提供一个公有静态方法,返回创建的对象实例。
来看一下单例模式的通用代码:
饿汉式单例
class Singleton {
private static final Singleton singletonTest = new Singleton(); //创建实例
private Singleton() {} //构造方法私有
public static Singleton getInstance(){
return singletonTest ; //返回实例
}
}
public class SingletonTest{
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance() ;
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); //true
}
}
上面这种写法是最简单的写法,但是有一个问题,就是在类加载的时候就创建了实例,而不管是否需要创建这个实例,没有做到延迟加载 ,增加了负载。
懒汉式单例
为了解决上面的问题,对上面的通用代码进行如下改进,当系统需要该实例的时候,才产生实例对象,并返回,代码如下:
class LazySingleton{
private static LazySingleton singleton = null ;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(singleton == null ){
singleton = new LazySingleton() ;
}
return singleton ;
}
}
通过上面的代码发现,在类加载的时候并没有进行类的初始化操作,只有在第一次调用getInstance()方法时,才进行了实例化操作,实现了延迟加载的功能。那么问题又来了,啥问题问题,上面的类在单线程环境是没有问题的,但是在多线程环境中是不安全的无法保证实例的唯一性,那么在改进一下,看下面代码:
class LazySingleton{
private static LazySingleton singleton = null ;
private LazySingleton(){}
public static synchronized LazySingleton getInstance(){
if(singleton == null ){
singleton = new LazySingleton() ;
}
return singleton ;
}
}
在getInstance的方法上添加了synchronized关键字,来保证线程之间的同步,确保实例的唯一性。看起来已经解决了上面的问题,但是新的问题其实又出现了,什么问题呢? 就是每次访问getInstance()方法时都进进行线程锁定的判断,在多线程高并发的环境下,使得系统的性能大大的降低了,那么有什么办法来解决这个问题吗? 当然有,看下面代码:
class LazySingleton{
private static LazySingleton singleton = null ;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(singleton == null ){
synchronized (LazySingleton.class){
singleton = new LazySingleton() ;
}
}
return singleton ;
}
}
最初为了保证线程之间的同步问题,给getInstance()的整个方法加了锁,其实要保证同步的罪魁祸首其实是 singleton=new LazySingleton()这句,只要保证了它的同步,产生的实例就是唯一的,所以使用synchronized块,来保证线程之间的同步。 这样在完成了singleton初始化之后,便不会在进入synchronized中,系统的性能便不会受影响了。 程序看起来已经很完美了,事实确实是这样吗? 当然不是,再次被打脸了,分析一下,假设现在有两个线程A ,B 都执行到了 if() 出,线程A先执行了,获得了锁,此时完成了singleton的初始化操作,释放锁 ,B开始执行,因为B并不知道此时singleton已经完成了实例的创建,会再次创建一个实例。 好的,我们在改进一下 :
class LazySingleton{
private volatile static LazySingleton singleton = null ;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(singleton == null ){
synchronized (LazySingleton.class){
if(singleton == null){
singleton = new LazySingleton() ;
}
}
}
return singleton ;
}
}
在synchronized块中,又进行了一次条件判断,这种方式称为双重检查锁定。 另外在变量的声明中添加了volatile关键字。关于volatile简单说两句,由于singleton使用了volatile修饰,一旦一个线程对改变做了修改,会马上由工作内存写回到主内存中,被其他线程所读取,工作内存可以理解为被线程独享,主内存可以理解为线程共享,被volatile修饰的成员变量可以确保多个线程都能够正确的处理,但是该代码只能在jdk1.5及以上的版本中才能正确执行。现在9都快要发布了, 相信现在还运行在1.5以下的程序应该不是很多了,应该影响不大, 这里只是提一下。 volatile关键字会禁止指令重排序优化,屏蔽到java虚拟机所做的一下代码优化,可能会导致运行效率降低, 这看起来也不是一个非常完美的实现方式。
饿汉式单例和懒汉式单例的比较
- 饿汉式单例在类加载时就完成了初始化操作,因此无须考虑多线程的并发访问问题,正是由于它一开始就进行了实例化操作,从资源的利用和加载的时间上考虑可能比懒汉式要差一点
- 懒汉式单例实现了延迟加载,但是在多线程并发访问的情况下对性能还是有一定的影响。
既然饿汉式和懒汉式都存在自己的问题,有没有一种方式能解决它们的问题呢,当然有,看下面代码:
通过使用静态内部类实现单例
public class StaticInnerClass {
private StaticInnerClass(){}
private static class InnerClass{
private static final StaticInnerClass singleton = new StaticInnerClass() ;
}
public static StaticInnerClass getInstance(){
return InnerClass.singleton ;
}
}
调用getInstance()方法时,才进行实例的初始化操作,由于是static变量只会进行一次初始化操作,有JVM来保证线程的安全性。 即实现了延迟加载,有可以保证线程的安全。
枚举实现单例
//单例类
class Resource{
public Resource(){
System.out.println("resouce 构造方法");
}
}
//枚举类生成Resource的单个实例
public enum EnumSingleton{
INSTANCE ;
private Resource resource ;
private EnumSingleton(){
System.out.println("EnumSingleton 构造方法执行");
resource = new Resource() ;
}
public Resource getInstance(){
return resource ;
}
}
//测试类及运行结果
public class Test {
public static void main(String[] args) {
Resource s1 = EnumSingleton.INSTANCE.getInstance() ;
Resource s4 = EnumSingleton.INSTANCE.getInstance() ;
Resource s3 = EnumSingleton.INSTANCE.getInstance() ;
Resource s2 = EnumSingleton.INSTANCE.getInstance() ;
if(s1==s2 && s2 ==s3 && s3==s4 && s1 ==s4){
System.out.println("同一个引用");
}
}
}
//运行结果
EnumSingleton 构造方法执行
resouce 构造方法
同一个引用
在枚举类中构造方法私有化,外部无法访问,在访问枚举实例时会执行构造方法,实例化Resouce对象,枚举实例都是static final类型的,只能被实例化一次。这也是effective java中推荐的方式。
单例模式的优点:
- 在内存中只有一个实例,减少了内存的开支,无须频繁的创建、销毁
- 减少了系统的性能开销,避免了资源的多重占用
缺点:
- 没有接口,扩展相对比较困难,违背了单一职责的原则
使用场景:
要求一个类有且仅有一个对象,只允许有一个公共的访问节点。 可以使用单例模式。
少年听雨歌楼上,红烛昏罗帐。
壮年听雨客舟中,江阔云低,断雁叫西风。
感谢支持!
---起个名忒难
网友评论