/ java  

java设计模式之singleton

最近在看2年前买的effective java,说起来也是挺惭愧的,希望这次能静下心来

什么是singleton

singleton即我们经常说的单例模式,所谓单例就是只有一个实例,指仅仅被实例化一次的类。
通常被用来代表那些本质上唯一的系统组件,比如窗口管理器或者文件系统。最近在学redis,就把产生jedis的pool
写成一个单例模式。

package utils;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
 * Created by Kevin on 2015/1/7.
 */
public class JedisPool {
    private static JedisPool pool = null;
    private JedisPool() {
    }
    public static JedisPool getJedisPool() {
        if (pool == null) {
            return new JedisPool(new JedisPoolConfig(), "127.0.0.1");
        }
        return pool;
    }
}

当你觉得某一个对象在整个系统里只需要生成一个就好了,不需要每次都实例化一个新的,就可以用到单例模式了,它在jvm里面是唯一的。
当然上面写的单例模式是非线程安全的,如果多个线程同时调JedisPool.getJedisPool()方法时可能会创建多个JedisPool对象,完善写法如下:
package utils;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
 * Created by Kevin on 2015/1/7.
 */
public class JedisPool {
    private volatile static JedisPool pool = null; //volatile禁用指令重排序,确保A线程创建的JedisPool在完全初始化后才能给B线程使用
    private JedisPool() {
    }
    public static JedisPool getJedisPool() {
        if (pool == null) {
            synchronized (JedisPool.class) { //确保JedisPool只创建一个
                if (pool == null) {
                    return new JedisPool(new JedisPoolConfig(), "127.0.0.1");
                }
            }
        }
        return pool;
    }
}

如何实现单例模式

私有构造器,公有静态成员

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

私有构造器仅被调用一次,用来实例化INSTANCE。由于缺少公有的构造方法,所以外部每次想用Elivs的对象只能用Elvis.INSTANCE这一个对象。
我们可以通过输出他们的来简单判断2个对象是否一样,Object的toString方法后面的hashCode是和其在内存中的引用地址有关系的。

图例1

有一点需要注意的是,这种方式是可以通过反射来调用私有构造方法来生成不同的实例化对象,
书上提到了这个问题,于是我测试了一下确实是有问题的。

package com.pattern.singleton;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * Created by linuxv on 15-1-22.
 */
public class ReflectionTest {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class<?> elvisClass = Elvis.class;
        Constructor<?> cc = elvisClass.getDeclaredConstructor(null);
        cc.setAccessible(true);
        Elvis elvis = (Elvis)cc.newInstance(null);
        Elvis elvis1 = (Elvis)cc.newInstance(null);
        System.out.println(elvis);
        System.out.println(elvis1);
    }
}

运行结果
com.pattern.singleton.Elvis@d6132c4 com.pattern.singleton.Elvis@684be8b8
给出的解决方法是修改一下构造器,要求它在创建第二个实例时抛出异常。我给一个比较垃圾比较直接的实现办法,用count值判断

public static Elvis INSTANCE = null;
    public static int count = 0;
    static {
        try {
            INSTANCE = new Elvis();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private Elvis() throws Exception {
        count ++;
        if (count >= 2) {
            throw new Exception("what is the fuck");
        }
    }

由于构造器跑异常了,上面的静态域new Elivs处理不了,需要写成静态块的形式去try catch异常。。


私有构造器,私有静态成员,公有静态工厂方法

public class Elvis {
    private static final Elvis INSTACNE = new Elvis();
    private Elvis() {}
    public static Elvis getInstance() {return INSTANCE;}
}

对于静态方法getInstance的所有调用都会返回同一个对象引用,具体测试方式可以参照上图。

公有域方法(第一种)的主要好处在很清楚地用static final表名该域总是包含相同对象的引用。而工厂方法(第二种)的优势在于,他比较灵活,可以通过改变
一小部分代码就可以实现从singleton模式转变成非singleton模式,我们只需要把return INSTANCE改成new Elvis()即可

要注意的一点是如过以上的Elvis是支持序列化的,那么序列化和反序列化后得到的对象将不一致
下面看测试例子:

package com.pattern.singleton;

import java.io.*;
/**
 * Created by Kevin on 2015/1/21.
 * 经过序列化和反序列化的单例结果和原来的不同
 */
public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Elvis2 elvis = Elvis2.getInstance();
        System.out.println("original obj:" + elvis.toString());
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream os = new ObjectOutputStream(bos);
        os.writeObject(elvis);
        os.flush();
        os.close();
        byte[] bytes = bos.toByteArray();
        bos.close();
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bis);
        elvis = (Elvis2)ois.readObject();
        ois.close();
        bis.close();
        System.out.println("final result:" + elvis);
    }
}

测试结果:
例子2

我们可以看到这序列化前后对象是不一样的

这时候如果我们想解决这个问题就需要用到一个readResolve方法

具体在Elvis类里面加上

private Object readRolve() {
    return INSTANCE;
}

大家可以自己测试一下输出结果,加了之后就是一样的了。


包含单个元素的枚举类型

public enum Elvis {
    INSTANCE;
}

书上结尾处提到这种方法在功能上和方法1,方法2相近,但是它更简洁,同时提供了序列化机制,绝对防止对此实例化,即使在面临上面的反射和序列化问题时。
所以单元素的枚举类型是实现singleton的最佳方法

写一个序列化测试结果吧,另一个暴力反射的就不写了,有人测试过参考网址

例3