Java 实现单例的难点

澳门新浦京娱乐游戏 7

有差非常少又飞快的形式能够兑现单例情势,但绝非一种方法能在任何情状下都保障单例的完整性。

澳门新浦京娱乐游戏,过多设计情势类别的篇章开篇大都是 { 单例情势 }
初试锋芒,标注了单例是最简易的设计格局之一,作者也循行业内部准则,说一说轻巧的单例方式。

单例情势是指某些类只被实例化壹次,用来代表全局或种类范围的构件。单例方式常用于日志记录、工厂、窗口管理器和平台组件管理等。笔者觉着要尽量幸免使用单例情势,因为假如完成就很难改造或重载,何况会以致编写测量试验用例困难、代码结构不佳等主题素材。其余,上边作品中的单例情势是不安全的。

它真的轻巧吗?

      此为单例方式的类图:二个常见类 Singleton,类中带有了二个 Singleton
类型的个人静态字段 instance、受保证的布局器以致二个重返值为 Singleton
的国有静态方法 getInstance(State of Qatar 。so
easy?澳门新浦京娱乐游戏 1

先来贯彻一个简易的“精粹“单例形式(为啥打上引号,你懂的!):

public class ClassicSingleton {
    private static ClassicSingleton instance = null;

    protected ClassicSingleton() {

    }

    public static ClassicSingleton getInstance() {
        if (instance == null) {
            instance = new ClassicSingleton();
        }
        return instance;
    }
}

如上代码假使在面试中冒出,那么面试官的第一展现就是:这些年轻人要学的东西还超多哟!too
young too
simple!热心点的面试官也许会以各样反问句的秘籍来分解此段代码的各类坑,不意志力的或者就直接pass 了……

大家花大批量的生命力商讨怎么更加好地贯彻单例格局,但有一种轻易高效的得以达成格局。可是,未有一种形式能在其它意况下都保障单例的完整性。阅读下文,看看您是或不是认可。

说好的私家布局器呢?

      在 Java 语言专门的学业中,protected
的结构方法能够被其子类以致在同叁个包中的任何类调用来实例化类,由此何谈类的单实例!这些坑还相比好填,改革单例类布局器为
private,如此构造器只好在单例类内部调用,子类以致同三个包中的别的类便不能调用,保险了大局只可以通过调用getInstance
方法这一种渠道来收获单例类的实例。Better
better,best!此种意况下即使表明类为 final
类,那样意图分明且使用了编译器的一点品质优化,何乐不为!澳门新浦京娱乐游戏 2

Final字段

这种办法将布局函数私有化,向外提供四个国有的static final对象:

public class FooSingleton {
    public final static FooSingleton INSTANCE = new FooSingleton();
    private FooSingleton() { }
    public void bar() { }
}

类加载时,static对象被初阶化,那个时候个人的构造函数被第贰遍也是终极二遍调用。就算在类初阶化前有八个线程调用此类,JVM也能确认保证线程继续运维时该类已总体开始化。但是,使用反射和setAccessible(true卡塔尔(قطر‎方法,能够创立别的新的实例:

Constructor[] constructors = FooSingleton.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton spuriousFoo = (FooSingleton) constructor.newInstance(new Object[0]);

咱俩必要修改结构函数,使其免于多次调用,举个例子当它被另行调用时抛出极其。如下那样匡正FooSingleton布局函数,可以幸免此类攻击:

public class FooSingleton2 {
    private static boolean INSTANCE_CREATED;
    public final static FooSingleton2 INSTANCE = new FooSingleton2();
    private FooSingleton2() {
        if (INSTANCE_CREATED) {
            throw new IllegalStateException("You must only create one instance of this class");
        } else {
            INSTANCE_CREATED = true;
        }
    }
    public void bar() { }
}

那样看起来安全一些了,但事实上要创造新的实例照旧雷同轻便。大家只需修正INSTANCE_CREATED字段,再玩形似的杂技就能够了:

Field f = FooSingleton2.class.getDeclaredField("INSTANCE_CREATED");
f.setAccessible(true);
f.set(null, false);
Constructor[] constructors = FooSingleton2.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton2 spuriousFoo = (FooSingleton2) constructor.newInstance(new Object[0]);

咱俩接收的别样防备措施都可能被绕过,所以此方案并不中用。

四线程因素的思索

     
经过如上修改后的单例已初具雏形,不过在十二线程蒙受下还留存线程安全主题材料。为啥如此说呢?来看上海教室豆水泥灰部分的
if 语句块,这段代码对 instance
那些临界财富开展读写访谈,假诺此刻设有三个线程,线程 1 通过了 if
剖断步入语句块中希图对 instance 赋值,无独有偶当时发出线程切换(instance
还未有赋上值仍是 null),调解程序调整线程 2 实践这段指令,因为 instance
那时仍是空,那么它也经过了 if 判别进到了语句块,之后的结果正是线程 1 和
2
分别实例化了八个单列类。那样的结果可不是大家愿意看见的!澳门新浦京娱乐游戏 3

对此二十四线程程序的测量试验也得经过一番倒卖,来看上面包车型大巴代码段:

先是步:对 Singleton 类进行改换,参加simulateRandomActivity(卡塔尔,模拟将步入 if 语句块的线程 1 古人工睡眠 50ms
以使线程 2 有丰裕时间去管理 if 决断

public final class Singleton {
    private static Singleton instance = null;

    private static Logger logger = Logger.getRootLogger();
    private static boolean firstThread = true;

    private Singleton() {
        logger.info("----->singleton's .ctor...");
    }

    public static Singleton getInstance() {
        if (instance == null) {
            simulateRandomActivity();
            instance = new Singleton();
        }
        logger.info("created singleton: " + instance);
        return instance;
    }

    private static void simulateRandomActivity() {
        try {
            if (firstThread) {
                firstThread = false;
                logger.info("first thread sleeping...");
                // 让线程 1 睡眠 50ms 使线程 2 有足够时间走到 if 判断
                Thread.currentThread().sleep(50);
            }
        } catch (InterruptedException ex) {
            logger.warn("Sleep interrupted");
        }
    }
}

其次步:编写测量检验案例,模拟五个线程同步调用单例类的 getInstance(State of Qatar 方法

public class SingletonTest extends TestCase {
    private static Singleton singleton = null;

    public SingletonTest(String name) {
        super(name);
    }

    public void setUp() {
        singleton = null;
    }

    public void testUnique() throws InterruptedException {
        Thread threadOne = new Thread(new SingletonTestRunnable()), 
                threadTwo = new Thread(new SingletonTestRunnable());
        threadOne.start();
        threadTwo.start();

        threadOne.join();
        threadTwo.join();
    }

    private static class SingletonTestRunnable implements Runnable {
        public void run() {
            Singleton instance = Singleton.getInstance();

            // 同步两个测试线程 保证对 singleton 变量的访问线程安全
            synchronized (SingletonTest.class) {
                if (singleton == null)
                    singleton = instance;
            }
            Assert.assertEquals(true, instance == singleton);
        }
    }
}

如上测量试验类的运作结果如下:

澳门新浦京娱乐游戏 4

静态工厂

利用这种情势,公有的分子相同静态工厂:

public class FooSingleton3 {
    public final static FooSingleton3 INSTANCE = new FooSingleton3();
    private FooSingleton3() { }
    public static FooSingleton3 getInstance() { return INSTANCE; }
    public void bar() { }
}

getInstance(卡塔尔方法重临的永世是同三个指标引用。纵然这些方案也力不胜任防护反射,但还是有它的有的优点。举例,能够在不改善API的情形下,退换单例的实现。getInstance(State of Qatar出今后大致全体的单例完结中,它也标识着这实乃三个单例方式。

同步

     
要消除如上叙述的线程安全题材,将在学习线程的一道机制,本篇器重不在那就不赘述了,详见“并发编程”博文连串。Java在越来越高的层系上包裹了管程和锁的思虑,提供了
synchronized
关键字用于缓慢解决线程同步。那么对代码进行如下改正便能够缓慢解决如上创造七个单例的处境:

public synchronized static Singleton getInstance() {
    if (instance == null) {
        simulateRandomActivity();
        instance = new Singleton();
    }
    logger.info("created singleton: " + instance);
    return instance;
}

在同步化 getInstance
方法之后运营测量检验案例能够取得运营结果:澳门新浦京娱乐游戏 5

延期加载的单例方式

(译者注:在软件工程中,Initialization-on-demand holder
那一个习语指的正是延迟加载的单例形式,参见维基百科)

比如愿意尽量延迟单例的始建(懒汉式加载),能够应用延缓伊始化方法,当getInstance(卡塔尔方法第叁回调用时线程安全地开创单例。比较在此以前的方案当第叁次援用该类时就创办单例(饿汉式加载),那是叁个向上。如下:

public class FooSingleton4 {
    private FooSingleton4() {
    }
    public static FooSingleton4 getInstance() {
        return FooSingleton4Holder.INSTANCE;
    }
    private static class FooSingleton4Holder {
        private static final FooSingleton4 INSTANCE = new FooSingleton4();
    }
}

主题材料解决了呢?

      synchronized
关键字在解决线程安全难点是足以说是“万能”的,抢先49%的面世调整操作都能动用
synchronized
来完成。可是越来越“万能”的产出调节,平常也会陪伴着越大的质量影响。那么大家就想着降低临界区,让程序发生一同的地点尽量减弱,能够在自然水准上减小品质消耗。一种性子修正的艺术如下:

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            simulateRandomActivity();
            instance = new Singleton();
        }
    }
    logger.info("created singleton: " + instance);
    return instance;
}

本条代码片段只同步关键代码,并不是合营整个艺术,裁减了临界区。大家美好中是想着
getInstance
方法只供给在第壹次调用的时候要求线程同步,那样能够一点都不小程度优良于一块整个艺术。可是能够是美好的,现实是骨感的!这段代码和未有同台从前存在同样的线程安全难题,聪明的你势必供给看出关键点正是在异常if 推断。当有四个线程同期进到 if
块中依旧得创立七个单例,难题如故没解决啊!

要小心种类化

万一单例完成了种类化,它将在面前碰着另多个压迫。因而需求将具有字段评释为transient(那样它就不会被体系化)并提供三个自定义的readResolve(卡塔尔方法再次来到独一实例INSTANCE的援用。

重新加锁检查

public static Singleton getInstance() {
    if (instance == null) {
        synchronized (Singleton.class) {
            if (instance == null) {
                simulateRandomActivity();
                instance = new Singleton();
            }
        }
    }
    logger.info("created singleton: " + instance);
    return instance;
}

当时就是有多个线程恰好同有的时候候进到 if
块中,由于联合块的存在,线程得排队去实施临界区的代码,那么在率先个线程施行完实例化的口舌后,instance便不为空,待线程调治到第二个线程实践时她会先进行判断,开采instance
不为空直接过去了,有限扶植了只实例化二回!这种技能正是红得发紫的双检锁本领

至此线程因素的假造获得康健化解。

枚举

此处用枚举作为单例INSTANCE的器皿:

public enum FooEnumSingleton {
    INSTANCE;
    public static FooEnumSingleton getInstance() { return INSTANCE; }
    public void bar() { }
}

依据Java语言专门的职业8.9,“Enum的final克隆方法保障枚举长久比比较小概被克隆,其别具肺肠的种类化机制保险不能反连串化获得拷贝的指标。同期,还禁用反射对枚举进行实例化。保障了那多少个地点,在枚举常量之外,就不会有其余同类的枚举实例存在。”

与上述同类,大家就像很简短地就幸免了种类化、克隆和反光的大张讨伐。第叁次探访这段话,笔者那时想要表明它是错的。如下代码所示,绕过这个保养是很容易的:

 Constructor con = FooEnumSingleton.class.getDeclaredConstructors()[0];
 Method[] methods = con.getClass().getDeclaredMethods();
 for (Method method : methods) {
     if (method.getName().equals("acquireConstructorAccessor")) {
         method.setAccessible(true);
         method.invoke(con, new Object[0]);
     }
  }
  Field[] fields = con.getClass().getDeclaredFields();
  Object ca = null;
  for (Field field : fields) {
      if (field.getName().equals("constructorAccessor")) {
          field.setAccessible(true);
          ca = field.get(con);
      }
  }
  Method method = ca.getClass().getMethod("newInstance", new Class[]{Object[].class});
  method.setAccessible(true);
  FooEnumSingleton spuriousEnum = (FooEnumSingleton) method.invoke(ca, new Object[]{new Object[]{"SPURIOUS_INSTANCE", 1}});
  printInfo(FooEnumSingleton.INSTANCE);
  printInfo(spuriousEnum);
}
private static void printInfo(FooEnumSingleton e) {
    System.out.println(e.getClass() + ":" + e.name() + ":" + e.ordinal());
}

实施这段代码,获得结果:

class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:INSTANCE:0
class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:SPURIOUS_INSTANCE:1

枚举的劣点是它无法从另三个基类世襲,因为它曾经三番两次自java.lang.Enum。如若想要模拟这种持续,能够参见小编另一篇随笔中介绍的混入形式(mixin
pattern)。

枚举的一个优点是,假设您之前期望有“二例(dualton)”或“三例(tringleton)”,只必要扩大新的枚举实例就可以。比如,有了一个单例的缓存之后,你恐怕还想给缓存引进七个档次。

三行代码给你个单例情势!

三行代码给出的一种实现单例的技巧,俗称饿汉式!城会玩!

public final class HungerSingleton {
    public final static HungerSingleton INSTANCE = new HungerSingleton();
    private HungerSingleton() {
    }
}

三行代码的单例想想也挺极端的,把所有的变化都写死了!来看看更折中一点的实现:

public final class HungerSingleton {
    private static HungerSingleton INSTANCE = new HungerSingleton();

    private HungerSingleton() {
    }

    public static HungerSingleton getInstance() {
        return INSTANCE;
    }
}

因而 getInstance
方法重临单例,能够通过更换那么些措施来充实一些可变因素,灵活了累累。

这种饿汉式轻巧、飞速,而且经过 classloader
机制也制止了二十四线程的一道难点。然而,instance
在类装载时就实例化,即使造成类装载的来头有众多,在单例形式中山大学多是调用
getInstance 方法引致的(不消弭有任王琴态方法),那个时候初步化 instance
就从不了上边这种懒汉式单例的延迟加载(lazy loading)的魔法。这种方法只要
HungerSingleton 类棉被服装载,那么 instance 就能被实例化,试想倘使实例化
instance 很成本财富,笔者想让她延迟加载,或然说笔者不想在 HungerSingleton
类Nokia载的时候就实例化 instance,因为我不能作保 HungerSingleton
类还大概在其余地点被主动选用进而被加载,这时候实例化 instance
分明是不成立的。来看一种接受静态内部类本事实现的折中方案:

public final class HungerSingleton {
    private static class SingletonHolder {
        private static final HungerSingleton INSTANCE = new HungerSingleton();
    }

    private HungerSingleton() {
    }

    public static final HungerSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

那时候纵然 HungerSingleton 类被加载了,INSTANCE 也不必然被实例化,因为
SingletonHolder 类没有被主动利用,独有突显通过调用 getInstance
方法时才会显得装载 SingletonHolder 类,进而实例化 INSTANCE,达到 lazy
loading 效果。

结论

纵然绕过单例的那几个维护并不便于,但的确并未有一种百下百全的方案。要是你有越来越好的方案,请多都赐教!

枚举是兑现单例方式的简约而又火速的办法。要是想要有一连或懒汉式加载,延迟最先化方案是不错的选取。

祝你的单例好运!

多 classloader 对单例格局的熏陶

对此自由二个类,都急需由它的类加载器和那个类本人一起确立其在 Java
设想机中的独一性,而使用八个类加载器是很广阔的,所以不管您在落到实处单例类时多多当心你都最后能够收获八个单例类的实例。来看一个自定义类加载器加载类的示范:

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {

        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        // 由系统应用程序类加载器加载
        Singleton instance = Singleton.getInstance();
        // 自定义的类加载器加载
        Class<?> clazz = myLoader.loadClass("com.saga.patterns.singleton.Singleton");
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object obj = constructor.newInstance();

        System.out.println(obj.getClass());
        // false
        System.out.println(obj == instance);
    }
}

澳门新浦京娱乐游戏 6

从输出结果中看出,第一个 Singleton 是有调用 getInstance
方法由系统类加载器加载实例化出来的靶子,而第二个 Singleton
实例是由我们和好完结的类加载器加载的,做靶子相等推断的时候结果是
false。所以就算它们都出自同三个 Class
文件,但在程序中仍是五个单身的类。

一扫而空由此引发的单例难点,能够利用加载单例类基类的特别类加载器:

private static Class<?> getClass(String classname) throws ClassNotFoundException {
    ClassLoader loader = Thread.currentThread().getContextClassLoader();
    if (loader == null) {
        loader = Singleton.class.getClassLoader();
    }
    return (loader.loadClass(classname));
}

反射破坏单例原则

在单例方式中,大家只对外提供工厂方法(获取单例),而私有化结构函数,来严防外部多余的创始。对于平日的表面调用来说,私有结构函数已经很安全了。可是一些特权客户能够通过反射来访谈私有布局函数,把寻访权限展开setAccessible(true),就足以访问私有结构函数了,那样损坏了单例的私房结构函数珍贵,实例化了二个新的实例。假使要守护那样的反射侵入,能够改革构造函数,加上第贰次实例化的检查,当爆发成立第贰个单例的号令时会抛出十三分。

private static int cntInstance = 0;

private Singleton() throws Exception {
    if (cntInstance++ > 1) {
        throw new Exception("can't create another singleton instance.");
    }
}

系列化的圈套

如若类别化二个单例类,然后三回重构它,那么你会获得单例类的八个实例,除非完毕了
readResolve(卡塔尔(قطر‎ 方法。如下达成二个足以系列化的单例类:

public final class SerializableSingleton implements Serializable {

    private static final long serialVersionUID = 1L;
    private static SerializableSingleton instance = new SerializableSingleton();

    private SerializableSingleton() {

    }

    public static SerializableSingleton getInstance() {
        return instance;
    }

    private Object readResolve() {
        return instance;
    }
}

为地点的可种类化单例类写个测验案例,检查被重构的单例类实例是还是不是同八个指标:

public class SerializableSingletonTest extends TestCase {
    private SerializableSingleton sone = null, stwo = null;
    private static final Logger logger = Logger.getRootLogger();

    public SerializableSingletonTest(String name) {
        super(name);
    }

    @Override
    protected void setUp() throws Exception {
        sone = SerializableSingleton.getInstance();
        stwo = SerializableSingleton.getInstance();
    }

    public void testSerialize() {
        logger.info("testing singleton serialization...");
        writeSingleton();
        SerializableSingleton s1 = readSingleton();
        SerializableSingleton s2 = readSingleton();
        Assert.assertEquals(true, s1 == s2);
    }

    private void writeSingleton() {
        try {
            FileOutputStream fos = new FileOutputStream("serializedSingleton");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            SerializableSingleton s = SerializableSingleton.getInstance();

            oos.writeObject(SerializableSingleton.getInstance());
            oos.flush();
        } catch (NotSerializableException se) {
            logger.fatal("Not Serializable Exception: " + se.getMessage());
        } catch (IOException iox) {
            logger.fatal("IO Exception: " + iox.getMessage());
        }
    }

    private SerializableSingleton readSingleton() {
        SerializableSingleton s = null;

        try {
            FileInputStream fis = new FileInputStream("serializedSingleton");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s = (SerializableSingleton) ois.readObject();
        } catch (ClassNotFoundException cnf) {
            logger.fatal("Class Not Found Exception: " + cnf.getMessage());
        } catch (NotSerializableException se) {
            logger.fatal("Not Serializable Exception: " + se.getMessage());
        } catch (IOException iox) {
            logger.fatal("IO Exception: " + iox.getMessage());
        }
        return s;
    }

    public void testUnique() {
        logger.info("checking singletons for equality");
        Assert.assertEquals(true, sone == stwo);
    }
}

澳门新浦京娱乐游戏 7

重磅推出:Effective Java 推荐使用枚举实现单例

关于单例格局,以上探讨了饿汉式、懒汉式、依据内部类、双检锁等都足以兑现,这么些落成能够保证线程安全,可是在好几特殊境况下并不可以看到确认保障险单独唯有二个单例,像连串化、反射攻击、五个类加载器等往往能够生成新的实例对象,对此
Effective Java 书中引入应用枚举单例。

public enum EnumSingleton {
    INSTANCE {
        @Override
        protected void read() {
            System.out.println("read...");
        }

        @Override
        protected void write() {
            System.out.println("write...");
        }
    };
    protected abstract void read();

    protected abstract void write();
}

如上是叁个枚举单例的例证,通过 EnumSingleton.INSTANCE
获取实例,此种情势能够保障单例线程安全、防反射攻击、幸免体系化生成新实例。知其然,亦要知其所以然!JVM为我们达成了许多东西,来会见上边代码反编写翻译的结果:

public abstract class EnumSingleton extends Enum
{

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

    protected abstract void read();

    protected abstract void write();

    public static EnumSingleton[] values()
    {
        EnumSingleton asingleton[];
        int i;
        EnumSingleton asingleton1[];
        System.arraycopy(asingleton = ENUM$VALUES, 0, asingleton1 = new EnumSingleton[i = asingleton.length], 0, i);
        return asingleton1;
    }

    public static EnumSingleton valueOf(String s)
    {
        return (EnumSingleton)Enum.valueOf(singleton/EnumSingleton, s);
    }

    EnumSingleton(String s, int i, EnumSingleton singleton)
    {
        this(s, i);
    }

    public static final EnumSingleton INSTANCE;
    private static final EnumSingleton ENUM$VALUES[];

    static 
    {
        INSTANCE = new EnumSingleton("INSTANCE", 0) {

            protected void read()
            {
                System.out.println("read");
            }

            protected void write()
            {
                System.out.println("write");
            }

        };
        ENUM$VALUES = (new EnumSingleton[] {
            INSTANCE
        });
    }
}
  • 至于防反射攻击,注意看 EnumSingleton 类的修饰
    abstract,所以无法实例化,反射也无从
  • 有关线程安全,枚举单例其实是由此类加运载飞机制来保管,注意看 INSTANCE
    的实例化机缘,是在 static 块中
  • 至于幸免体系化生成新的实例,Java 对 Enum
    类型的指标类别化和别的种类的指标类别化有所分化,在类别化的时候 Java
    仅仅是将枚举对象的 name 属性输出到结果中,反类别化的时候则是因此java.lang.Enum 的 valueOf
    方法来依据名字查找枚举对象。同时,编写翻译器是分歧意其他对这种系列化学工业机械制的定制的,由此禁止使用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等办法。

对单例方式垃圾回笼难题的研究

Java1.2 未来单例是不会被回收的!详见

续:使用 ThreadLocal 为各样线程生成二个莫衷一是的单例别本

 

单例方式大致却轻松令人吸引,极度是对于Java的开拓者来讲,笔者认为它不轻易!您感觉吧?

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图