您的位置:首页 > 编程语言 > Java开发

Java设计模式——单例模式

2016-12-04 01:12 253 查看
在Java中,单例模式是一种常见的设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。(引用自百度百科)

一·单例模式的特点

单例模式有以下特点:

1. 在一个JVM中只能有一个实例。

2. 单例类需要自己创建一个自己的实例。

3. 单例类需要将自己的实例提供给其他对象使用。

二·最初的版本

针对上面单例模式的特点,首先讨论一下实现的步骤。

1. 只能有一个实例,首先想到的是将构造函数私有化,这样就该类对象的创建过程只能在该类内部完成,相对可控。(至于能否用反射的方式突破这个限制,后面再做讨论)

2. 单例类自己创建一个自己的实例,创建出来之后要存放在某个地方,所以考虑在该类内部定义一个私有的成员变量,用来保存这个实例对象。

3. 如何做到供其他对象使用?由于类的构造函数是私有化的,所以在外部没有办法通过new的方式来创建对象,但可以考虑通过类名.静态方法的方式来获取。

4. 由于静态方法只能访问静态成员变量,所以上面的成员变量也要修改为静态的。

基于上面的讨论,先实现一个版本,代码如下:

public class Singleton {

private static Singleton instance = new Singleton();

private Singleton() {}

public static Singleton getInstance() {
return instance;
}

}


额。。。。。

写完这段代码之后,突然发现这个好像没啥问题呀。那我怎么引出下面的讨论呢??好吧。在大家普遍的讨论里,这种方式的单例叫做饿汉式。我们知道,在一个系统中,对于单例的对象,往往是被多个线程同时使用的,所以单例模式实现的线程安全性是需要考虑的。饿汉式其实并不存在线程安全的问题。饿汉式唯一的不足大概是从一开始就创建对象,浪费了些空间。如果这个对象是重量级选手的话,速度会慢些。因此如果单例占用的内存比较大,或单例只是在某个特定场景下才会用到,使用饿汉模式就不合适了,这时候就需要用到懒汉模式进行延迟加载。

饿汉式为什么线程安全?

这里是因为成员变量是static的,只要了解了一个实例对象初始化的过程,就很容易理解了。静态的成员变量以及静态代码块会在类加载时就执行,由于类的加载只会发生一次,在加载的过程中就把对象创建出来了,所以不会存在线程安全问题。

三·懒汉式单例对象

所谓懒汉式,就是等到其他对象第一次使用该类对象时才初始化生成实例对象。

最初的懒汉式代码

public class Singleton {

private static Singleton instance = null;

private Singleton() {}

public static Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}

}


在多线程环境下,这种写法显然是存在线程安全问题的(因为被共享的成员变量的初始化过程并非原子操作)。接下来需要改造一下这个类。

懒汉式单例 版本2.0

public class Singleton {

private static Singleton instance = null;

private Singleton() {}

//通过synchronized关键字保证线程安全
public synchronized static Singleton getInstance() {
if (null == instance) {
instance = new Singleton();
}
return instance;
}

}


通过在方法上添加synchronized关键字,将getInstance()方法改造成为一个线程安全的方法。继续探讨,在方法上添加synchronized关键字后,每次进入该方法都需要加锁,多次累积调用的性能损耗也是很可观的。所以继续对版本2.0进行改造,即双重校验锁。

懒汉式单例 版本3.0

public class Singleton {

private static Singleton instance = null;

private Singleton() {}

public  static Singleton getInstance() {
if (null == instance) {
synchronized(Singleton.class) {
if (null == instance) { //为何还要加判断?
instance = new Singleton();
}
}
}
return instance;
}
}


通过这次改造,可以保证只有在instance为null的时候才会进入同步代码块并加锁,提升了性能。至于上面为何还要在同步代码块中添加非空判断,是因为进入代码块之前的代码存在线程安全问题。举例说明,线程A和线程B同时进入了第一个非null判断,然后只有一个线程可以拿到锁,这里假设是A线程拿到了锁。B线程由于没拿到锁,所以就会处于阻塞状态,等待A线程释放锁。A线程在同步代码块中顺利的执行了new的操作,然后释放锁。此时B线程获取锁,由于已经通过了第一次的非null判断,所以如果进入到同步代码块后,如果不进行第二次非null判断,B线程就会也执行一次new的操作。单例对象被new了2次,显然不是我们希望看到的。这就是为什么需要加双重校验的原因。

双重校验锁还有隐患吗?

这里先谈谈Java中创建对象的基本过程,基本上分三步:

1. 分配对象的内存空间。

2. 初始化对象
4000


3. 将对象的引用赋值给变量instance。

理论上来说,这个步骤是无懈可击的。但是Java中存在指令重排优化。

所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。

这个就比较尴尬了,虽然没有直接证据表明上面创建对象的过程会被重排指令,但是确实不能排除这种可能性。这里做一个假设,如果指令被重排了,先执行了上面的第3步,然后才执行第2步,这种情况下会出现什么问题呢?

还是线程A和线程B两个线程举例,基于上面的假设,指令被重排了。当线程A进入了同步代码块。开始执行instance = new Singleton()这一句。由于指令被重排,instance已经被赋值,但是还没有初始化。此时线程B来到了第一个非null判断的地方,由于此时instance不为null,所以线程B高高兴兴的获取到一个还没初始化的实例对象返回了。这就是上面双重校验锁存在的隐患。

双重校验锁的这种隐患的原因是什么?

分析上面的假设,问题出在哪里了?其实就是指令重排序的发生。那么jdk中有没有可以禁止指令重排序的机制呢?有,那就是volatile关键字。

volatile有2层语义:

1. 保证线程间变量的内存可见性。

2. 禁止指令重排序。

网上很多对双重校验锁隐患的讨论都止于volatile的内存可见性,其实volatile的内存可见性特点并没有解决这个隐患(因为instance = new Singleton()并非原子操作)。之所以用volatile,不仅是因为内存可见性,还考虑到它的第二重语义:即禁止指令重排序。

关于volatile、指令重排序、内存可见性的更多解释请参见:http://blog.csdn.net/t894690230/article/details/50588129

下面给出利用volatile改造后的单例代码:

public class Singleton {

private static volatile Singleton instance = null;

private Singleton() {}

//通过synchronized关键字保证线程安全
public  static Singleton getInstance() {
if (null == instance) {
synchronized(Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}

}


另一种单例实现: 内部类的方式

代码如下:

public class Singleton {
private Singleton() {}
public  static Singleton getInstance() {
return SingletonContainer.instance;
}

public static class SingletonContainer {
public static Singleton instance = new Singleton();
}
}


这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。

这种方式解决了上面饿汉式代码的弊端。

还有其他方式吗? 试试枚举

代码如下:

public enum Singleton {
INSTANCE
}


下面的描述引用自博客:http://blog.csdn.net/goodlixueyong/article/details/51935526

上面提到的四种实现单例的方式都有共同的缺点:

1. 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。

2. 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。

而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。

扩展讨论

这里想要讨论的问题其实上面在说到枚举方式的时候已经讲过了。就是传统的单例模式到底是不是绝对的单例,上面提到了2种方式突破这个限制:

1. 对象序列化。

2. 反射机制。

不再赘述。

应用场景

单例模式应用的场景一般发现在以下条件下:

  (1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。

  (2)控制资源的情况下,方便资源之间的互相通信。如线程池等。

  

实际场景举例:

1. 数据库连接池设计。

2. 多线程线程池设计。

3. ServletContext。

4. 网站计数器。

5. 应用程序的日志应用。

[TOC]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  设计模式