您的位置:首页 > 其它

[置顶] 【设计模式】深入理解单例&懒汉饿汉&双重锁定

2017-08-23 18:07 507 查看
       在实际的开发中,我们需要某个类只有唯一一个实例,比如在Windows中我们打开任务管理器,即使点击多次,每次也只是有一个窗口。我们一起看下单例模式的定义:

       单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

1.示例代码

public class Person {
private String name;
private static Person person;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

private Person() {
}

public static Person getPerson() {
if(person == null) {
person = new Person();
}
return person;
}
}


(1)为了确保Person实例的唯一性,我们需要禁止类的外部直接使用new来创建对象,因为每次使用new实例化Person类时将产生一个新对象,我们需要将person的构造函数的可见性改为private

private Person() {
}


(2)将构造函数改为private之后类的外部无法再使用new来创建对象,但是Person的内部还是可以创建的,在类内进行创建对象。我们在Person中创建并保存这个唯一实例,为了让外界可以访问这个唯一实例,需要在Person中定义一个静态的person类型的私有成员变量。就是如下代码:

private static Person person;


并且增加一个公有的静态方法。首先判断person对象是否存在,如果不存在则创建一个Person类型的person对象,再返回新创建的person对象,否则返回已有的person对象。

public static Person getPerson() {
if(person == null) {
person = new Person();
}
return person;
}


在类外无法直接创建新的Person对象,但是可以通过代码Person.getPerson()来访问实例对象,第一次调用getPerson()方法时将创建唯一实例,再次调用时将返回第一次创建的实例,从而确保实例对象的唯一性。

2.懒汉模式和饿汉模式

(1)饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不在改变。
懒汉式是延时加载,在需要的时候才创建对象
(2)饿汉式是线程安全的,懒汉式如果在创建实例对象时不加上synchronized则会导致对对象的访问不是线程安全的。
 饿汉式在虚拟机启动的时候就会创建,无需关注多线程问题、写法简单明了、能用则用。但是它是加载类时创建实例所以如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用一块创建。
所以开始部分的示例代码是懒汉模式,如下代码为饿汉模式。
public class Person {
public static final Person person = new Person();//在类创建的同时就已经创建好一个静态的对象供系统使用
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

private Person() { }

public static Person getPerson() {
return person;
}

3.懒汉模式的线程安全问题
在多线程的环境下,当一个线程判断person为null正在进行new person()还未实例化完成时,这时又一个线程判断person这时还为null,又再次进行实例化,所以这时就会存在创建两次或多次实例的情况,存在问题了,所以我们可以考虑在getPerson方法上加上synchronized关键字。
public class Person {
private String name;
private static Person person;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

private Person() { }

public static synchronized Person getPerson() {
if(person == null) {
person = new Person();
}
return person;
}
}

4.双重检查锁定(Double-Check Locking)
上面的代码通过synchronized关键字虽然可以解决线程安全问题,但是每次调用getPerson时都要进行线程锁定判断,在多线程高并发的情况下,会导致系统性能大大降低。所以继续改进,不对整个getPerson方法进行锁定,只对person=new Person进行锁定。
if(person == null) {
synchronized (Person.class) {
person = new Person();
}
}
但是这样还是存在问题的,假如在某一瞬间线程A和线程B两个线程都在调用getPerson()方法,此时person对象为null值,均能通过person== null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(person== null)判断,这种方式就是我们要介绍的双重检查锁定。使用双重检查锁定实现的懒汉式单例类完整代码如下所示:
if(person == null) {
synchronized (Person.class) {
if(person == null) {
person = new Person4();
}
}

}

5.使用volatile关键字 上面我们双重锁定表面上看没有什么问题了,但是从根本上说还是有一定问题的。具体详情请点击。如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。
public class Singleton{
private volatile static Singleton singleton;

private Singleton(){}

public static Singleton getSingleton(){
if(singleton == null){
synchronized (Singleton.class){
if(singleton == null){
singleton=new Singleton();
}
}
}
}
}


由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: