`

单例模式与双重检测

阅读更多

        首先要解释一下什么是延迟加载,延迟加载就是等到真真使用的时候才去创建实例,不用时不要去创建。

        从速度和反应时间角度来讲,非延迟加载(又称饿汉式)好;从资源利用效率上说,延迟加载(又称懒汉式)好。

         下面看看几种常见的单例的设计方式:

第一种:非延迟加载单例类

public class Singleton {
 private Singleton() {}
 private static final Singleton instance = new Singleton();
 public static Singleton getInstance() {
  return instance;
 }
}

 

第二种:同步延迟加载

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

 

第三种:双重检测同步延迟加载 

        为处理原版非延迟加载方式瓶颈问题,我们需要对 instance 进行第二次检查,目的是避开过多的同步(因为这里的同步只需在第一次创建实例时才同步,一旦创建成功,以后获取实例时就不需要同获取锁了),但在Java中行不通,因为同步块外面的if (instance == null)可能看到已存在,但不完整的实例。JDK5.0以后版本若instance为volatile则可行:

public class Singleton {
 private volatile static Singleton instance = null;
 private Singleton() {}
 public static Singleton getInstance() {
  if (instance == null) {
   synchronized (Singleton.class) {// 1
    if (instance == null) {// 2
     instance = new Singleton();// 3
    }
   }
  }
  return instance;
 }
}

        双重检测锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是失败的一个主要原因。

        下面进一步谈谈无序写入:

        为解释该问题,需要重新考察上述清单中的 //3 行。此行代码创建了一个 Singleton 对象并初始化变量 instance 来引用此对象。这行代码的问题是:在 Singleton 构造函数体执行之前,变量 instance 可能成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程得到的是一个还会初始化的对象,这样会导致系统崩溃。

        什么?这一说法可能让您始料未及,但事实确实如此。在解释这个现象如何发生前,请先暂时接受这一事实,我们先来考察一下双重检查锁定是如何被破坏的。假设代码执行以下事件序列:

        1.线程 1 进入 getInstance() 方法。

        2.由于 instance 为 null,线程 1 在 //1 处进入 synchronized 块。 

        3.线程 1 前进到 //3 处,但在构造函数执行之前,使实例成为非 null。 

        4.线程 1 被线程 2 预占。

        5.线程 2 检查实例是否为 null。因为实例不为 null,线程 2 将 instance 引用返回给一个构造完整但部分初始化了的 Singleton 对象。 

        6.线程 2 被线程 1 预占。

        7.线程 1 通过运行 Singleton 对象的构造函数并将引用返回给它,来完成对该对象的初始化。

        为展示此事件的发生情况,假设代码行 instance =new Singleton(); 执行了下列伪代码:

mem = allocate();             //为单例对象分配内存空间.
instance = mem;               //注意,instance 引用现在是非空,但还未初始化
ctorSingleton(instance);    //为单例对象通过instance调用构造函数

        这段伪代码不仅是可能的,而且是一些 JIT 编译器上真实发生的。执行的顺序是颠倒的,但鉴于当前的内存模型,这也是允许发生的。JIT 编译器的这一行为使双重检查锁定的问题只不过是一次学术实践而已。

        另一篇详细分析文章:http://www.iteye.com/topic/260515

 

第四种:使用ThreadLocal修复双重检测

        借助于ThreadLocal,将临界资源(需要同步的资源)线程局部化,具体到本例就是将双重检测的第一层检测条件 if (instance == null) 转换为了线程局部范围内来作。这里的ThreadLocal也只是用作标示而已,用来标示每个线程是否已访问过,如果访问过,则不再需要走同步块,这样就提高了一定的效率。但是ThreadLocal在1.4以前的版本都较慢,但这与volatile相比却是安全的。

public class Singleton {
 private static final ThreadLocal perThreadInstance = new ThreadLocal();
 private static Singleton singleton ;
 private Singleton() {}
 
 public static Singleton  getInstance() {
  if (perThreadInstance.get() == null){
   // 每个线程第一次都会调用
   createInstance();
  }
  return singleton;
 }

 private static  final void createInstance() {
  synchronized (Singleton.class) {
   if (singleton == null){
    singleton = new Singleton();
   }
  }
  perThreadInstance.set(singleton);
 }
}

 

第五种:使用内部类实现延迟加载

        为了做到真真的延迟加载,双重检测在Java中是行不通的,所以只能借助于另一类的类加载加延迟加载:

public class Singleton {
 private Singleton() {}
 public static class Holder {
  // 这里的私有没有什么意义
  /* private */static Singleton instance = new Singleton();
 }
 public static Singleton getInstance() {
  // 外围类能直接访问内部类(不管是否是静态的)的私有变量
  return Holder.instance;
 }
}

        

单例测试

        下面是测试单例的框架,采用了类加载器与反射。

        注,为了测试是否为真真的单例,我自己写了一个类加载器,且其父加载器设置为根加载器,这样确保Singleton由MyClassLoader加载,如果不设置为根加载器为父加载器,则默认为系统加载器,则Singleton会由系统加载器去加载,但这样我们无法卸载类加载器,如果加载Singleton的类加载器卸载不掉的话,那么第二次就不能重新加载Singleton的Class了,这样Class不能加载则最终导致Singleton类中的静态变量重新初始化,这样就无法测试了。

        下面测试类延迟加载的结果是可行的,同样也可用于其他单例的测试:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;

public class Singleton {
    
    private Singleton() {
    }

    public static class Holder {
        // 这里的私有没有什么意义
        /* private */static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        // 外围类能直接访问内部类(不管是否是静态的)的私有变量
        return Holder.instance;
    }
}

class CreateThread extends Thread {
    
    Object singleton;
    ClassLoader cl;
    
    public CreateThread(ClassLoader cl) {
        this.cl = cl;
    }

    public void run() {
        Class c;
        try {
            c = cl.loadClass("Singleton");
            // 当两个不同命名空间内的类相互不可见时,可采用反射机制来访问对方实例的属性和方法
            Method m = c.getMethod("getInstance", new Class[] {});
            // 调用静态方法时,传递的第一个参数为class对象
            singleton = m.invoke(c, new Object[] {});
            c = null;
            cl = null;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyClassLoader extends ClassLoader {
    
    private String loadPath;

    MyClassLoader(ClassLoader cl) {
        super(cl);
    }

    public void setPath(String path) {
        this.loadPath = path;
    }

    protected Class findClass(String className) throws ClassNotFoundException {
        
        FileInputStream fis = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;

        try {
            fis = new FileInputStream(new File(loadPath + className.replaceAll("\\.", "\\\\") + ".class"));
            baos = new ByteArrayOutputStream();
            int tmpByte = 0;
            while ((tmpByte = fis.read()) != -1) {
                baos.write(tmpByte);
            }
            data = baos.toByteArray();
        } catch (IOException e) {
            throw new ClassNotFoundException("class is not found:" + className, e);
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
                if (fis != null) {
                    baos.close();
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return defineClass(className, data, 0, data.length);
    }
}
/**
 * yield:如果知道已经完成了在run()方法的循环的一次迭代过程中所需要的工作,就可以给线程调度一个机制暗示:我的工作已经做的差不多了,可以让给别的线程使用CPU了。通过调用yield()来实现。
 * 当调用yield时,你也是在建议具有相同优先级的其他线程可以运行。
 * 对于任何重要的控制或在调整应用时,都不恩那个依赖于yield。实际上,yield经常被误用。
 *(yield并不意味着退出和暂停,只是,告诉线程调度如果有人需要,可以先拿去,我过会再执行,没人需要,我继续执行)
 * 调用yield的时候锁并没有被释放
 */
public class SingleTest {
    
    public static void main(String[] args) throws Exception {
        while (true) {
            // 不能让系统加载器直接或间接的成为父加载器
            MyClassLoader loader = new MyClassLoader(null);
            loader.setPath("D:\\depEnv\\workspace\\Study\\bin\\");
            CreateThread ct1 = new CreateThread(loader);
            CreateThread ct2 = new CreateThread(loader);
            ct1.start();
            ct2.start();
            ct1.join();
            ct2.join();
            if (ct1.singleton != ct2.singleton) {
                System.out.println(ct1.singleton + " " + ct2.singleton);
            }
            System.out.println(ct1.singleton + " " + ct2.singleton);
            ct1.singleton = null;
            ct2.singleton = null;
            Thread.yield();
        }
    }
}

运行结果:

Singleton@10b30a7 Singleton@10b30a7
Singleton@530daa Singleton@530daa
Singleton@66848c Singleton@66848c
Singleton@de6f34 Singleton@de6f34
Singleton@e0e1c6 Singleton@e0e1c6
Singleton@c20e24 Singleton@c20e24
Singleton@141d683 Singleton@141d683
Singleton@1bf73fa Singleton@1bf73fa
Singleton@1cf8583 Singleton@1cf8583
Singleton@665753 Singleton@665753

 

文章来源:http://www.iteye.com/topic/652440

分享到:
评论

相关推荐

    单例模式详解

    ### 单例模式详解 #### 一、单例模式简介 单例模式(Singleton Pattern)是一种常用的软件设计模式,属于创建型模式之一。其目的是确保某个类只有一个实例,并提供一个全局访问点。单例模式的核心在于确保在系统...

    41丨单例模式(上):为什么说支持懒加载的双重检测不比饿汉式更优?1

    它与静态类的区别在于,静态类不支持实例化,而单例模式可以通过`getInstance()`方法获取单个实例。静态类通常用于提供工具方法或全局变量,而单例模式则用于管理共享资源。 替代单例模式的解决方案可以考虑依赖...

    java 多线程单例模式详解

    ### Java多线程单例模式详解 #### 一、单例模式概述 单例模式(Singleton Pattern)是一种常用的软件设计模式,它确保一个类仅有一个实例,并提供一个全局访问点。这种模式通常用于那些需要频繁实例化然后销毁的...

    Android线程池+单例模式+webService

    综合以上,这个项目可能是一个实现Android应用与服务器通信的示例,利用线程池优化后台任务执行,采用单例模式管理网络请求,并通过WebService接口进行数据交换。理解并熟练掌握这些技术,对于提升Android开发能力...

    android也架构之单例模式

    单例模式中有区分了懒汉式和饿汉式,懒汉式主要是用时间来换空间,饿汉式则是用空间来换时间。饿汉式是线程安全的,懒汉式是非线程安全的,如果要实现懒汉式的非线程安全,则可以再访问点添加synchronized关键字声明...

    单例模式窗口_单例窗口_窗口单例_C#_pdf查看_c#单例窗口_

    单例模式的实现方式有很多,比如饿汉式、懒汉式以及双重检查锁定(Double-Check Locking)等。在Windows Forms应用中,我们可能会希望主窗口只打开一次,这就是单例窗口的用武之地。 在描述中提到的"实现单例窗口,...

    单例设计模式个人总结+摘录

    3. **双重检测锁模式** - **定义**:改进版的懒汉式单例模式,通过双重检查锁定来提高效率。 - **实现**:首先在静态方法中检查实例是否已经被创建,如果未创建,则同步该方法并再次检查实例是否存在,不存在则...

    单例设计模式

    3. 双重检测锁式:结合了饿汉式的延迟加载和懒汉式的线程安全,但在某些JVM环境下可能会出现问题,不推荐使用。 4. 静态内部类式:利用类加载机制保证线程安全,延迟加载,效率较高。 5. 枚举单例:最安全、简洁的...

    设计模式之创建型模式实验.docx

    实验中可以实现饿汉式单例和双重检测锁单例两种方式。 通过以上实验,你可以深入理解每种创建型设计模式的核心思想,掌握其结构、优缺点以及适用场景,并能够使用Java编程语言实现这些模式。这不仅提升了编程技能,...

    设计模式.pdf

    3. **双重检测锁式**:双重检查锁定,线程安全,可以实现延迟加载,但偶尔会因为JVM底层内部模型原因出现问题。 4. **静态内部类式**:静态内部类加载时初始化对象,线程安全,可以实现延迟加载,调用效率高。 5. ...

    java多线程设计模式

    常见的线程安全单例模式有双重检查锁定(DCL)、静态内部类和枚举方式。 8. 线程通信:wait()、notify()和notifyAll()是Java Object类提供的方法,用于线程间的通信,配合synchronized使用,可以使线程在特定条件下...

    java简易版开心农场源码-GOF23:一起来学习设计模式吧~

    双重检测锁式(由于 JVM 底层内部模型原因, 偶尔会出问题, 不建议使用) 静态内部类式(线程安全, 调用效率高, 但是可以延时加载) 枚举单例(线程安全, 调用效率高, 不能延时加载, 并且天然地防止反

    day24-多线程-设计模式.7z

    3. **单例模式**:单例模式确保一个类只有一个实例,并提供全局访问点。在多线程环境中,实现线程安全的单例模式尤为重要,防止多个线程同时创建单例对象,保证了资源的有效利用和一致性。常见的实现方式有双重检查...

    实验3指导书1

    - **单例模式(Singleton Pattern)**:在游戏管理器(GameManager)或游戏控制器(GameController)中,可能需要确保只有一个实例存在,以全局管理游戏状态和资源。可以使用饿汉式(静态常量引用单例)、懒汉式...

    300Java设计模式部分学习笔记

    笔记中列出了饿汉式、懒汉式、双重检测锁式、静态内部类和枚举类实现单例的方法,并探讨了防止反射和反序列化破解单例的方法。 2. 工厂模式(Factory Pattern):包括简单工厂模式、工厂方法模式和抽象工厂模式,...

    java多线程设计模式详解(PDF及源码)

    2. **单例模式**:在多线程环境中,确保一个类只有一个实例并提供全局访问点是非常重要的。Java中可以使用双重检查锁定(Double-Checked Locking)或静态内部类来实现线程安全的单例。 3. **线程池模式**:通过`...

    222018321062006-宋行健-实验31

    在这个实验中,宋行健同学深入研究了创建型模式,包括原型模式和单例模式,并对碰撞检测进行了优化,同时提出了一些改进方案。 1. **原型模式**:在Coin类中应用了原型模式,以实现对象的深复制。GameManager类中,...

Global site tag (gtag.js) - Google Analytics