您的位置:首页 > 其它

设计模式之单例模式(一)

2018-02-07 23:25 316 查看

简介

         安全的单例模式在开发过程中,保障一个类仅有一个实例,并提供一个访问它的全局访问点。可以让类自己负责保存它唯一的实例,这个类可以保证没有其他实例可以被创建,并且提供一个访问该实例的方法。单例可以说是所有设计模式中最简单的模式了。

实现方式

         单例模式的实现方式有两种(懒汉模式和饿汉模式),具体在项目中的使用情况需要根据实际来判断。

饿汉模式

         饿汉模式相对于懒汉模式来说对于安全性更加容易保证,因为饿汉模式的对象实例化是随项目启动就创建的。不会有线程安全性问题。下面是饿汉模式代码示例:
package com.rabbit.pattern.singteton;

/**
* Created by vip on 2018/1/30.
*/
public class SingletonPattern {

//静态常亮,项目启动就创建好对象
private static final SingletonPattern sp = new SingletonPattern();

//私有构造函数不允许外部对象创建
private SingletonPattern() {}

//提供公有方法返回唯一实例
public static SingletonPattern newInstances() {
return sp;
}

//对象的其他方法等

}
饿汉模式优缺点:由于饿汉模式在项目启动时候就创建了对象,因此不会涉及多线程安全性问题,但是在启动过程中需要额外的资源。如果初始化的过程中需要大量的资源的话,可能会占用内存,如果只是简单的初始化可以使用者中模式,因为线程安全性容易控制。

懒汉模式

懒汉模式对象的初始化并不是在项目启动,而是在线程第一次访问的时候。因此,如果是高并发情况下会出现线程安全问题。下面将一步一步介绍:
初始代码:package com.rabbit.pattern.singteton;

/**
* Created by vip on 2018/1/30.
*/
public class SingletonPattern {

//定义全局变量
private static SingletonPattern sp = null;

//私有构造函数不允许外部对象创建
private SingletonPattern() {}

//提供公有方法返回唯一实例
public static SingletonPattern newInstances() {
if (sp == null) {//1
sp = new SingletonPattern();//2
}//3
return sp;
}

//对象的其他方法等

}
这里的代码有线程安全性问题在于:如果A和B两个线程访问,线程A执行代码1处的代码,这时候判断sp==null准备去执行2处的代码创建的时候,被线程B抢夺执行,这时候线程B执行1处的代码,判断sp==null然后执行了2处的代码创建了sp对象并返回。然后线程A继续执行2处的代码也创建了一个sp对象并返回,所有这里线程安全有问题。
 
同步机制代码:同步机制代码:
package com.rabbit.pattern.singteton;

/**
* Created by vip on 2018/1/30.
*/
public class SingletonPattern {

//定义全局变量
private static SingletonPattern sp = null;

//私有构造函数不允许外部对象创建
private SingletonPattern() {}

//提供公有方法返回唯一实例
public static SingletonPattern newInstances() {
synchronized (SingletonPattern.class) {
if (sp == null) {//1
sp = new SingletonPattern();//2
}//3
}
return sp;
}

//对象的其他方法等

}
这种代码存在性能方面的问题,为什么呢?因为每次线程过来都需要等待前面的线程执行完毕释放锁。当线程A和线程B同时执行的时候,线程A先获取到锁然后执行1处的代码判断sp==null是否成立,如果为true则创建对象并返回,如果为false则直接跳过2处的代码直接返回对象并是否锁,线程B获取到锁并继续执行。
 
双重检测机制代码:package com.rabbit.pattern.singteton;

/**
* Created by vip on 2018/1/30.
*/
public class SingletonPattern {

//定义全局变量
private static SingletonPattern sp = null;

//私有构造函数不允许外部对象创建
private SingletonPattern() {}

//提供公有方法返回唯一实例
public SingletonPattern newInstances() {
if (sp == null) {//1
synchronized (SingletonPattern.class) {//2
if (sp == null) {//3
sp = new SingletonPattern();//4
}//5
}//6
}//7
return sp;
}

//对象的其他方法等

}
双重检测机制代码就解决了同步机制代码带来的性能问题。当线程A通过了1,2,3处的代码执行4处创建的时候,线程B抢夺到CPU执行了1处的代码,在2处的时候等待,当线程A创建执行完毕,线程B获取到锁执行3处的代码直接返回对象。可能有人觉得这样也要等待锁,不是和同步机制一样的问题了吗?我们试试下面的情景,假如sp对象已经创建完毕了,线程A和线程B都来执行,当执行到1处的代码就判断到了sp对象已经创建就可以直接返回了,而不用继续等待锁。
 
终极1版代码:
即使是双重检测机制版的代码也会引发问题。原因在于sf = new SingletonPattern();这个代码中,在JVM中创建对象的过程会涉及3个步骤:1)分配对象的内存空间。2)初始化对象。3)设置sf指向刚分配的内存地址。由于这3个步骤经过JVM和CPU的优化,可能出现指令重拍的问题,即指向步骤可能不是1,2,3而是1,3,2。在“双重检测机制”的代码中如果出现指令重排:当线程A执行完毕了1,2,3处的代码在执行4处的代码准备去创建的时候执行到了1)分配对象的内存空间。3)设置sf指向刚分配的内存地址。还没来的及执行初始化,线程B抢夺CPU资源执行1处的代码,sf==null返回false,从而返回一个没有初始化的sf对象。
避免JVM和CPU出现指令重排的情况,可以使用volatile关键字修饰sf对象。

package com.rabbit.pattern.singteton;

/**
* Created by vip on 2018/1/30.
*/
public class SingletonPattern {

//定义全局变量
private volatile SingletonPattern sp = null;

//私有构造函数不允许外部对象创建
private SingletonPattern() {}

//提供公有方法返回唯一实例
public SingletonPattern newInstances() {
if (sp == null) {//1
synchronized (SingletonPattern.class) {//2
if (sp == null) {//3
sp = new SingletonPattern();//4
}//5
}//6
}//7
return sp;
}

//对象的其他方法等

}

反射引发的单例模式安全性问题

package com.rabbit.pattern.singteton;

import java.lang.reflect.Constructor;

/**
* Created by vip on 2018/1/30.
*/
public class Test {

@org.junit.Test
public void test1() throws Exception {
//获取反射对象
Class<SingletonPattern> clazz = SingletonPattern.class;
//获取默认构造函数,由于私有需要设置可以访问
Constructor<SingletonPattern> constructor = clazz.getDeclaredConstructor();
//设置可以访问
constructor.setAccessible(true);
SingletonPattern s1 = constructor.newInstance();
SingletonPattern s2 = constructor.newInstance();
System.out.println(s1.equals(s2));
}
}
以上代码输出结果是false,说明即使是单例模式也可以通过反射创建多个对象,这样就无法确保唯一性。

枚举实现单例模式:

通过枚举获取SingletonPattern对象,JVM会阻止反射获取枚举的私有构造函数。同时可以保证线程安全性。单例对象在枚举类被加载的时候就初始化。package com.rabbit.pattern.singteton;

/**
* Created by vip on 2018/1/30.
*/
public enum SingletonEnum {

SingletonPattern;

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息