本文为多线程编程核心技术第6章,单例模式与多线程
跳过中间章节来进行本章节的知识总结,其原因在于面试中被面试官问了一个看似简单的问题,如何创建单例模式,这个当然很简单,但是当面试官问到如何创建一个安全的单例模式时,一脸懵逼。。。。so,面试结束赶快充电,暂定为这样理解题目……..
本章核心内容:如何使单例模式在遇到多线程时是安全的,正确的!!!
一、单例模式
①.立即加载/饿汉模式
理解:立即加载就是使用类时,已经将对象创建完毕。也就是说立即加载在调用方法前,实例已经被创建了
public class FastSingleton {
//立即模式,在调用getSingleton()方法前实例已经创建
private static FastSingleton fastSingleton = new FastSingleton();
private FastSingleton(){
}
public static FastSingleton getSingleton() {
return fastSingleton;
}
}
因为构造函数是私有的,所以立即加载可以确保在多线程的环境下不会出现多个实例的错误,但是,静态实例会在类加载的时候就会被创建,如果该实例占用内存较大,或者在某个特定场景才会用到时,就不适合使用该方式创建单例,应使用延迟加载的方式。
②、延迟加载/懒汉模式
延迟加载就是在调用get()方法时,实例才被创建,常见的方法就是在get()中进行new实例化
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton(){
}
public static LazySingleton getSingleton() {
//延迟加载
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
虽然立即加载和延迟加载都可以实现获取一个实例,但是在多线程的环境下,延迟加载就会出现问题,因为多线程可能会同步调用get(),导致多个实例被创建。
二、解决方法:
①、声明Synchronized关键字,对get()加锁,但是变为同步方法会极大的降低效率,因为下一个线程想要取得对象,就必须等上一个线程释放锁,才可以继续
synchronized public static LazySingleton getSingleton() {
try {
//延迟加载
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}catch (Exception e){
e.printStackTrace();
}
return lazySingleton;
}
②、同步代码块
和①同样的缺点:效率低,因为还是同步的方式
public static LazySingleton getSingleton() {
try {
//延迟加载
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}catch (Exception e){
e.printStackTrace();
}
return lazySingleton;
}
③、同步重要代码
在多线程时会由于并发而出现错误。
public static LazySingleton getSingleton() {
try {
//延迟加载
if (lazySingleton == null) {
synchronized (LazySingleton.class) {
lazySingleton = new LazySingleton();
}
}
}catch (Exception e){
e.printStackTrace();
}
return lazySingleton;
}
④、使用DCL双检查锁(Double-Check-Lock)机制
public class LazySingletonDCL {
private volatile static LazySingletonDCL lazySingletonDCL;
private LazySingletonDCL(){
}
public static LazySingletonDCL getLazySingleton(){
try{
if (lazySingletonDCL == null){
Thread.sleep(3000);
synchronized (LazySingletonDCL.class){
if (lazySingletonDCL == null){
lazySingletonDCL = new LazySingletonDCL();
}
}
}
}catch (Exception e){
e.printStackTrace();
}
return lazySingletonDCL;
}
}
解释下为什么被称为双检查锁:
首先,在声明实例时,使用了volatile关键字,而volatile关键字的作用就是线程在对同一个变量值取值时,强迫其在共享内存中重新取值,而任何对成员变量进行修改的操作结果,都会被强迫写回共享内存中。也就是说,无论多少个线程,他们看到的都是同一个变量值。这就是volatile变化可见的功能。
在java中,允许线程保存共享内存中变量的私有拷贝,只有在离开或进入到同步代码块时才会和共享内存中进行值比较。所以,使用volatile也会降低效率
在DCL机制中,当进入synchronized代码块时,会进行一次值比较的操作,所以不会出现③的错误。
PS:在大多数的延迟加载的环境中,都会使用DCL机制解决线程不安全的问题!
三、其他的解决方案:
①、静态内部类:
public class SingletonStaticInnerClass {
//利用类加载机制实现,和立即加载原理相同,但是当不使用内部类时,实例不会被创建
//实现了延迟加载和线程安全
private static class SingletonStaticInnerClassHandler{
private static SingletonStaticInnerClass singletonStaticInnerClass = new SingletonStaticInnerClass();
}
private SingletonStaticInnerClass(){
}
public static SingletonStaticInnerClass getInstance(){
return SingletonStaticInnerClassHandler.singletonStaticInnerClass;
}
}
②、static代码块
public class SingletonStaticBlock {
private static SingletonStaticBlock singletonStaticBlock=null;
private SingletonStaticBlock(){}
//静态代码块中的代码在使用类时就已经被执行
static {
singletonStaticBlock = new SingletonStaticBlock();
}
public static SingletonStaticBlock getInstance(){
return singletonStaticBlock;
}
}
③、序列化与反序列化的单例实现
使用内部静态类可以实现安全单例,但遇到序列化对象时,使用默认的方式运行得到的还是多例的
解决办法:在反序列化时使用readResolve()
public class SingletonSerial implements Serializable{
private static final long serialVersion = 888L;
private static class SingletonSerialHandler{
private static final SingletonSerial singletonSerial = new SingletonSerial();
}
private SingletonSerial(){}
public static SingletonSerial getInstance(){
return SingletonSerialHandler.singletonSerial;
}
//使用readResolve()
protected Object readResolve() throws ObjectStreamException{
return SingletonSerialHandler.singletonSerial;
}
}
在反序列化时抛出ObjectStreamException异常
④、使用枚举类实现
JDK1.5以后出现了枚举
枚举类:
枚举的本质是类,有自己的成员变量,方法和构造函数
public enum EnumClass {
A("a",1),
B("b",2);
private String name;
private int index;
private EnumClass(String name,int index){
this.name= name;
this.index = index;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
}
枚举屏蔽了枚举值的类型信息,不像在用public static final定义常量必须指定类型
枚举是用来构建常量数据结构的模板,这个模板可扩展。枚举的使用增强了程序的健壮性。
我们可以将相关的常量分配到一个枚举类中
通过枚举类来创建一个线程安全的单例模式是十分简单的
class Resource{
//需要使用的资源
}
public enum SingletonEnum {
INSTANCE;
private Resource instance;
SingletonEnum(){
instance = new Resource();
}
public Resource getInstance(){
return instance;
}
}
获取资源的方式很简单,通过Singleton.INSTANCE.getInstance()即可获得单例。
保证单例:
构造函数的私有特性,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。
也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
最重要的是,enum类拥有完善的序列化的机制,可以防止反序列化时的多例出现!
此处感谢Java 利用枚举实现单例模式的详细讲解!
多种设计模式用的最多的算是单例了,但是之前很少会考虑单例的不安全性,希望以该问题引以为戒,学习知识多纵向思考,深入挖掘!