玩转单例模式

news/2024/5/19 3:41:11 标签: java, 设计模式, 单例模式, 枚举类, 反射

Singleton

      • Eager Mode
      • Lazy Mode
      • Thread-safe Lazy Mode
      • Static Inner Class
      • destroy everything by reflection
      • Enum

单例模式,就是一个类只能有一个实例。

单例模式的关键在于构造器私有化,这样就不能在外面new。


Eager Mode

java">/**
 * eager mode of singleton
 * load the instance as soon as the class file is loaded by classloader
 */
public class EagerMode {

    private static EagerMode eagerMode = new EagerMode();

    private EagerMode(){}

    public static EagerMode getEagerMode(){
        return eagerMode;
    }

}

类被加载时,实例就会创建,这叫饿汉模式。

对于轻量级的对象,倒是可以这么做。

如果从头到尾都没有用到这个对象,那么又是一种对资源的浪费。

那么,我们就懒加载吧。

Lazy Mode

懒加载就是,你要实例的时候我才new出来,不是你要不要我都new。

java">package singletonPattern;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        if(lazyMode == null){
            lazyMode = new LazyMode();
        }

        return lazyMode;
    }
}

代码本身没有问题,但是它经不住多线程的压力。

我在getLazyMode里面打印一句话:

java">package singletonPattern;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        if(lazyMode == null){
            System.out.println("the instance is null");
            lazyMode = new LazyMode();
        }

        return lazyMode;
    }
}

多个线程的情况,如果是安全的话,那么打印只会执行一次:

为了加强效果,我让所有到达getLazyMode入口的线程都睡300毫秒:

java">package singletonPattern;

import java.util.concurrent.TimeUnit;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(lazyMode == null){
            System.out.println("the instance is null");
            lazyMode = new LazyMode();
        }

        return lazyMode;
    }
}

测试方法:

java">@Test
    public void test02() {
        for (int i = 0; i < 100; i++) {
            new Thread(()->{

                LazyMode.getLazyMode();
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

结果:

java">the instance is null
the instance is null
the instance is null
the instance is null
the instance is null
the instance is null
the instance is null
the instance is null
the instance is null



每打印一句the instance is null,就会new一个对象,所以,单例模式就被破坏了。

解决的办法是:加锁。


Thread-safe Lazy Mode

java">package singletonPattern;

import java.util.concurrent.TimeUnit;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public synchronized static LazyMode getLazyMode(){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(lazyMode == null){
            System.out.println("the instance is null");
            lazyMode = new LazyMode();
        }

        return lazyMode;
    }
}

这铁定是没有问题的,但是,100个线程先后在getLazyMode这里排队,太慢了。

问题的关键是,真正走进if(lazyMode == null)的只有一个线程,其他的直接return实例就行了。

因此我们可以用synchronize代码块:

java">package singletonPattern;

import java.util.concurrent.TimeUnit;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(lazyMode == null){
            synchronized (LazyMode.class){
                System.out.println("create the instance!");
                lazyMode = new LazyMode();
            }
        }

        return lazyMode;
    }
}

如果lazyMode是null,那就去new,否则的话,你就直接return。

这比在方法上加synchronize效率高多了。

但是,如果有两个线程同时走到if(lazyMode == null),同样会有线程安全问题。经过测试也测到了打印两句create the instance!的情况。

于是,我们还要加一层判断:

java"> public static LazyMode getLazyMode(){
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //to make the code efficient
        if(lazyMode == null){
            synchronized (LazyMode.class){
                //to make thread safe
                if(lazyMode == null){
                    System.out.println("create the instance!");
                    lazyMode = new LazyMode();
                }
            }
        }

        return lazyMode;
    }

这叫Double Check Lock,双重判空。

第一层判空,是为了让代码更有效、更快。

第二层判空,是为了线程安全。想象一下,如果有两个线程进了if(lazyMode == null),其中一个线程先进synchronize块,使得lazyMode有值,这样第二个线程就进不了第二层的if(lazyMode == null)

这似乎已经完美了,但还有一个问题,那就是new LazyMode()并非原子性的。

原子性,就是一个单一操作,new一个对象,有三个操作:

1.给LazyMode实例分配内存空间
2.执行LazyMode的构造方法
3.使变量lazyMode指向堆中的LazyMode对象。

因为JVM有指令重排,所以最后的顺序可能不是1-2-3,而是1-3-2。

假设线程A走了1-3,这时lazyMode已经不是null了,所以线程B会直接return,这就出错了。

虽然概率小,但是我们还是要想办法避免。

java">package singletonPattern;

import java.util.concurrent.TimeUnit;

/**
 * simple version of lazy mode of singleton
 */
public class LazyMode {
    private static volatile LazyMode lazyMode = null;

    private LazyMode(){

    }

    public static LazyMode getLazyMode(){
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //to make the code efficient
        if(lazyMode == null){
            synchronized (LazyMode.class){
                //to make thread safe
                if(lazyMode == null){
                    System.out.println("create the instance!");
                    //not atomic
                    lazyMode = new LazyMode();
                }
            }
        }

        return lazyMode;
    }
}

lazyMode加上volatile修饰,就会防止指令重排,读操作一定后于写操作,所以线程B拿到的对象没有问题。

至此,线程安全的、懒加载的单例模式就成型了。

Static Inner Class

还有一种写法,就是静态内部类的写法:

java">package singletonPattern;

/**
 * we use static inner class to restore the instance
 * 
 */
public class Outer {
    private Outer(){

    }

    private static class Inner{
        private static final Outer INSTANCE = new Outer();
    }

    public static Outer getInstance(){
        return Inner.INSTANCE;
    }
}

这对比饿汉模式的好处是,它是懒加载的。

只有调用Inner.INSTANCE时,才会去new对象。关于这一点,debug一下就能看到。

同时,它又是线程安全的。JVM在类的初始化阶段会获取一个锁,这个锁将保证完成实例化的线程只有一个。

因此,这也是很棒的写法。

destroy everything by reflection

上面的东西好像很厉害,但是,反射技术击破一切。

拿eager mode开刀吧(其他都一样的):

java">  @Test
    public void test04() throws Exception {
        Constructor<EagerMode> declaredConstructor = EagerMode.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        EagerMode instance1 = declaredConstructor.newInstance();
        EagerMode instance2 = declaredConstructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }

只要用declaredConstructor.setAccessible(true);,构造器的private就不起作用了。结果是:

java">singletonPattern.EagerMode@5b80350b
singletonPattern.EagerMode@5d6f64b1

两个实例。

不过我们可以在构造器上加异常来防止反射

java">  private EagerMode(){
        if(eagerMode != null){
            throw new RuntimeException("Don't use reflection, stupid ass!");
        }
    }

但是,对于LazyMode,这一招并不管用,由于是懒加载,实例一开始确实是null,于是就不会抛出异常。

那么,怎么做才能对反射免疫呢?


Enum

我们看到,在newInstance方法里:

java">  @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

如果构造器存在于枚举类的话,直接报错。

我们试一下:

枚举类单例模式

java">package singletonPattern;

public enum Singleton {
    INSTANCE;
}

搞它:

java"> @Test
    public void test06() throws Exception{
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println(singleton);

    }

报的错是:

java">java.lang.NoSuchMethodException: singletonPattern.Singleton.<init>()

是构造方法调的不对。

我们最好能看到枚举类反编译的结果:

使用jad反编译Singleton.class:

java">// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   Singleton.java

package singletonPattern;


public final class Singleton extends Enum
{

    public static Singleton[] values()
    {
        return (Singleton[])$VALUES.clone();
    }

    public static Singleton valueOf(String name)
    {
        return (Singleton)Enum.valueOf(singletonPattern/Singleton, name);
    }

    private Singleton(String s, int i)
    {
        super(s, i);
    }

    public static final Singleton INSTANCE;
    private static final Singleton $VALUES[];

    static 
    {
        INSTANCE = new Singleton("INSTANCE", 0);
        $VALUES = (new Singleton[] {
            INSTANCE
        });
    }
}

构造器里面还需要两个参数:

java">    @Test
    public void test06() throws Exception{
        Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        Singleton singleton = declaredConstructor.newInstance();
        System.out.println(singleton);

    }

这时候报的错就是我们预料的了:

java">java.lang.IllegalArgumentException: Cannot reflectively create enum objects

至于为什么需要两个参数,因为每个枚举类都继承了Enum:

java">protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

使用super调用了父类构造器。


单例模式最终使用枚举类的方式最强悍!


http://www.niftyadmin.cn/n/1312416.html

相关文章

android-01背景、编译运行、开发快捷键、插件

计算机的发展是以信息智能化与小型化为进化路线。 无论是编程语言还是开发工具&#xff0c;变化的都是技术实现手段&#xff0c;而不是人类愿景和系统原理。人类愿景是让生活更加便捷、让娱乐更加丰富&#xff0c;系统原理是让软件界面更加美观、让运行速度更加流畅。 Androi…

android-02物理像素、设备独立像素、像素密度

in 1英寸 2.54cm 5、5.5对角线长度 px 物理像素 1920*1280 dpi像素密度 每单位长度像素个数 dp dip设备独立像素 160dpi 320dpi 同一dp显示一样大

android-03颜色

六位十六进制数字和八位十六进制数字 八位中前两位是透明度 FF表示完全不透明&#xff0c;00表示完全透明&#xff08;看不见&#xff09; 后六位表示红绿蓝颜色&#xff0c;数字越大越浓越亮&#xff0c;亮到极致是白色&#xff0c;数字越小越暗&#xff0c;按到极致是黑色 XM…

No mapping for GET……

很久没有玩spring mvc了。这次一玩&#xff0c;出问题了。 我发现对于静态页面&#xff08;*.html&#xff09;&#xff0c;dispatcher servlet处理不了&#xff0c;会报No mapping for GET/url的错误。 我先把环境说一下&#xff1a; AppInitializer.java 配置dispatcher s…

android-04 简单控件、跑马灯、文字图片按钮、截图

View是Android的基本视图&#xff0c;所有控件和布局都是由view类直接或间接派生而来的。 ViewGroup 所有的布局类视图由它派生而来。 FrameLayout 重叠显示的场合 foreground指定框架布局的前景图像&#xff0c;永远处于最顶层&#xff0c;不会被覆盖 Layout_gravity 该视图…

线程池阻塞

线程池的阻塞问题线程池的知识回归问题问题 今天碰到一个有意思的问题&#xff0c;我把它抽取出来&#xff1a; package thread_pool;import java.util.concurrent.*;public class TestThreadPool {public static void main(String[] args) {ExecutorService threadPoolExecu…

项目集成管理工程师

信息的概念 客观事物状态、运动特征的一种普遍描述 维纳 信息不是物质也不是能量 香农 信息消除不确定性 信息的质量 精确性、完整性、及时性、可靠性、可验证性、安全性、经济性 信源->编码->信道&#xff08;噪声&#xff09;->解码->信宿 信息系统 目的性、健…

OC语言

import 避免重复 ""当前目录 工程目录 系统目录 用户文件 &#xff1f; <>编译目录 系统目录 库文件 &#xff1f; 引入头文件 可以用其中的函数 面向对象 用可以做某件事的对象调用某方法 谁对某方法最清楚&#xff0c;方法属于谁 定义OC类 定义对象方…