单例模式的测试 – 热爱改变生活
我的GitHub GitHub |     登录
  • If you can't fly, then run; if you can't run, then walk; if you can't walk, then crawl
  • but whatever you do, you have to keep moving forward。
  • “你骗得了我有什么用,这是你自己的人生”
  • 曾有伤心之地,入梦如听 此歌

单例模式的测试

模式 sinvader 2007℃ 0评论

啥叫单例模式嘞?

单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

实现单例模式的思路是:一个类能返回对象一个引用 (永远是同一个) 和一个获得该实例的方法(必须是静态方法,通常使用 getInstance 这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁 (虽然这样会降低效率)。

构建方式
通常单例模式在 Java 语言中,有两种构建方式:

  • 懒汉方式。指全局的单例实例在第一次被使用时构建。
  • 饿汉方式。指全局的单例实例在类装载时构建。

以上内容来自于 维基百科

为什么需要单例

来设想一个场景:有一个图书馆,里面书不多,只有 303 本,设置一个管理员 A,假设他能够记住所有的书籍有没有借出,那么他可以将图书馆管理的仅仅有条。但是后来经费太多,于是又招了一个图书管理员 B,那么这时候问题就来了。
一位读者来了,向 A 借《悲惨世界》,A 从书架上拿出了书借给了这位读者,读者拿回家看书去了,而 A 有事暂时离开了图书馆。过了一会儿,来了另一位读者,向 B 借同样的一本书《悲惨世界》,因为上一位读者借书的时候没有经过 B,所以 B 并不知道这本书已经借出去了,他以为这本书还在书架上,于是他答应了这位读者,然后去书架上找书,当然他是找不到的,于是在给读者书的时候出现了错误。

将这个问题对应到程序中,假设那本《悲惨世界》是一块儿内存,而这两个管理员是对应两个线程中的两个对象,他们都有管理这块儿内存的权利,A 对象使用了这块儿内存,放入了信息,B 在使用的时候也放入了信息,那么 B 放入的信息就覆盖了 A 的信息,当 A 要使用这块儿内存的时候就出现了错误。

针对这种情况,这本书或者这块儿内存必须要有统一的一个人来分配,于是,就出现了单例模式。

单例模式的实现、破坏以及维护

单例,就是要保证当前类只有一个对象。那要如何实现对象的单一呢?

在 Java 中,如果我们要创建一个对象,一般会用到两种方法:

  • 1. 使用 new 关键字
  • 2. 使用反射

在使用 new 关键字的时候,我们会去调用类中的构造方法,如果构造方法的修饰符是 private,那我们就不能在类的外部获得这个类的对象了,但是我们仍然可以在类的内部获得当前类的实例,我们可以通过类中的一个静态方法,将类内部的实例传递到类的外部,这样,所有想要获得类实例的,都必须通过这个静态方法来获得,而获得的这个,就由我们类里面的代码来控制了。

在使用反射的时候,通过反射获得实例我们并不能阻止,但是我们可以在构造方法中设置一个标志位,实例化一次就加 1,如果已经是 1 了,就抛出一个异常。

啥叫饿汉模式嘞

public class Singleton {
    private final static Singleton INSTANCE = new Singleton();
  
    // Private constructor suppresses   
    private Singleton() {}
 
    // default public constructor
    public static Singleton getInstance() {
        return INSTANCE;
    }
  }

太饿了,我希望想吃的时候就有。结果大厨(类)在刚开饭店(实例化)的时候就把饭(对象)给我做好了。

啥叫懒汉模式嘞

懒汉模式是相对于饿汉模式来说的,在饿汉模式中,类在初始化的时候就已经创建了对象,无论我是否会使用到这个对象。这就对资源造成了一种浪费。

先看一个简单的懒汉模式的代码:

public class LazyCase {
    private static LazyCase mInstance;
    public static LazyCase getInstace() {
        if (mInstance == null) {
            mInstance = new LazyCase();
        }
        return mInstance;
    }
    private int age = 0;
    private LazyCase() {    }
}

从上面的静态方法中我们可以看到,在外部想要获得类的对象的时候,首先会判断当前类中保存的对象是否是空的,如果是空的,才会去创建对象,这就是它懒的地方。这样做的好处不会创建无用的对象,在这种模式下,只要我创建对象,那就是外部需要这个对象。

但是,这种方法也存在一个问题:多线程怎么办?

public class MainClass {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            createThread();
        }
    }
    private static void createThread() {
        new Thread() {
            public void run() {
                System.out.println(LazyCase.getInstace());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            };
        }.start();
    }
}

上面的代码创建了 10 个线程,每个线程的任务就是输出一下他们通过懒汉单例获得的对象。来看下结果:
\"lazycase_result\"

啥?麻蛋!不是说获得是单例么!是同一个对象么!怎么会有其他的对象呢!

其实,情况是这样的:

我们知道 cpu 是分 [时间片] 来运行的,在一个时间片之内只能运行一个程序。当我们有 10 个线程的时候,对我们的感官来讲,他们是一瞬间就完成的,速度很快,应该是顺序来执行的吧。但实际上,因为这个时间片很小,一个线程可能还没有执行完成就已经切换到了其他的线程上去接着运行了。那么,当第一个线程运行到 if (mInstance == null) { 与 mInstance = new LazyCase(); 之间的时候,他的时间片已经运行完了(他已经判断过了 mInstance 不为空,下一次不会判断),他的活动被保存,开始执行第二个线程,第二个线程从开始走发现 mInstance 并没有具体的对象,所以他实例化了一个对象,并将这个对象返回了出去,运行完成之后,他的时间片也已经用完了。此时,cpu 开始运行线程 1,因为已经判断过 mInstance 是否为空,所以他也会实例化出来一个新的对象。那么这样,线程 1 和线程 2 就得到了不一样的对象。

解决方法

上面我们说到是因为不同的线程同时去访问了静态方法 getInstance 才导致了这个结果,那么解决这个问题及要从这方面去做。

使用:synchronized

Java 语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象 object 中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问 object 的一个加锁代码块时,另一个线程仍然可以访问该 object 中的非加锁代码块。

public class LazyCase {
    private static LazyCase mInstance;
    public static synchronized LazyCase getInstace() {
        if (mInstance == null) {
            mInstance = new LazyCase();
        }
        return mInstance;
    }
    private int age = 0;
    private LazyCase() {

    }
    public void agePlus() {
        System.out.println(\"Lazy:\" + age++);
    }
}

只是加了一个关键字,好了。不过,synchronized 是加在了 getInstance 方法上面,每次在调用 getInstance 的时候都会去同步,会造成不必要的浪费。我们可以换一种写法来优化。

public class LazyCase {
    private static LazyCase mInstance;
    public static LazyCase getInstace() {
        if (mInstance == null) {
            synchronized (LazyCase.class) {
                if (mInstance == null) {
                    mInstance = new LazyCase();
                }
            }
        }
        return mInstance;
    }
    private int age = 0;
    private LazyCase() {

    }
    public void agePlus() {
        System.out.println(\"Lazy:\" + age++);
    }
}

第一次判断 mInstance 为了避免不必要的同步,第二次同步是为了防止时间片段中途运行完,下次进入的时候已经通过了第一次 mInstance 判空然后再次创建对象。

新的问题

写成了上面那样,万事大吉了么?

too young to simple

在 JDK1.5 之前,mInstance=new LazyCase(); 不是一个 [原子操作],他会分成多条代码来执行。

  • 1. 给 mInstance 分配内存
  • 2. 调用 LazyCase 构造方法,实例化里面的成员
  • 3. 将 mInstance 对象指向内存

那么,JDK1.5 之前,cpu 在乱序执行的时候,如果正好顺序是 1-3-2。已经执行了 1 和 3,此时对象已经存在了,但是里面的成员还没有实例化,所以在调用里面的成员的时候就会报错了。

要解决这个问题,可以使用 volatile 关键字,在声明 private static LazyCase mInstance; 的时候使用 private static volatile LazyCase mInstance; 即可。

不过如果你的 jdk 版本够高,这个问题已经不存在了。

静态内部类单例

public class LazyCase {
    private static LazyCase mInstance;

    public static LazyCase getInstace() {

        return singleHolder.instance;
    }

    private static class singleHolder {
        private static final LazyCase instance = new LazyCase();
    }
}

推荐使用这一种方法,效果都有。

¥ 有帮助么?打赏一下~

转载请注明:热爱改变生活.cn » 单例模式的测试


本博客只要没有注明“转”,那么均为原创。 转载请注明链接:sumile.cn » 单例模式的测试

喜欢 (3)
发表我的评论
取消评论
表情

如需邮件形式接收回复,请注册登录

Hi,你需要填写昵称和邮箱~

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址