@babydragon
2016-04-19T12:57:16.000000Z
字数 5130
阅读 2166
java
和其他面向对象语言类似,Java语言实现单例模式比较直接,通常的实现方式如下:
public class Product{private Product() { }public static Product Instance() {return instance;}private static Product instance = new Product();public void DoSomething() {state++;System.out.println("I'm doing something for the " + state + " time");}private int state = 0;}public class Main{public static void main(String... args) {Product.Instance().DoSomething();Product.Instance().DoSomething();Product.Instance().DoSomething();}}
这个是一个最常规的单例创建方式,既:
针对Java语言的特性,上面示例中的单例创建方式,需要考虑以下一些问题。
根据Java虚拟机对于私有静态成员变量初始化的方式,示例中的instance成员变量在Product类初始化的时候已经完成初始化。由于Java类加载机制的特殊性,Product类的单例作用域事实上仅仅是加载了Product的类加载器。
也就是说,如果Product类通过不同的类加载器加载,每个类加载器会初始化一个实例,例如:
public class Test {public static void main(String [] args) throws InterruptedException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {invoke();invoke();}private static void invoke() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {MyClassLoader c = new MyClassLoader();Class<?> productClass = c.loadClass("Product");Method instance = productClass.getMethod("Instance");Method doSomthing = productClass.getMethod("DoSomething");Object obj = instance.invoke(null);doSomthing.invoke(obj);}}
其中MyClassLoader是一个自定义的类加载器,上述代码会有以下输出:
I'm doing something for the 1 timeI'm doing something for the 1 time
虽然将单例对象序列化在一般应用场景中非常少见,但是如果单例类实现了Serializable接口,就需要考虑该类在反序列化后的场景。下面是一个简单的Java序列化/反序列化的示例:
public class TestSerialization {public static void main(String [] args) throws IOException, ClassNotFoundException {Product p = Product.Instance();p.DoSomething();ByteArrayOutputStream bout = new ByteArrayOutputStream();ObjectOutputStream out = new ObjectOutputStream(bout);out.writeObject(p);ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());ObjectInputStream in = new ObjectInputStream(bin);Product p1 = (Product)in.readObject();// 新对象,state == 1p.DoSomething(); // state == 2,但是不影响p1p1.DoSomething();}}
注意,这里Product和之前的基本相同,但是需要多实现Serializable接口。上述代码的输出为:
I'm doing something for the 1 timeI'm doing something for the 2 timeI'm doing something for the 2 time
既反序列化之后的对象和原对象为不同的对象,但是保留了序列化当时的状态(这和“序列化”的概念相同)。因此,如果需要反序列化之后Product的实例仍然只有一个,就需要将反序列化后对象的状态和当前单例对象进行合并。
Java 1.5引入了枚举对象,枚举对象的每个枚举值都符合单例模式的描述,因此可以用枚举来实现Java的单例模式。
public enum EnumProduct {INSTANCE;private int state = 0;public void DoSomething() {++state;System.out.println("I did something for the " + state + " time");}}
使用枚举来实现单例有以下好处:
对于序列化,使用和前面序列化类似的例子:
public class TestEnumSerialization {public static void main(String [] args) throws IOException, ClassNotFoundException {EnumProduct p = EnumProduct.INSTANCE;p.DoSomething();ByteArrayOutputStream bout = new ByteArrayOutputStream();ObjectOutputStream out = new ObjectOutputStream(bout);out.writeObject(p);ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());ObjectInputStream in = new ObjectInputStream(bin);EnumProduct p1 = (EnumProduct)in.readObject();p.DoSomething();assert p == p1;}}
这里的p1对象虽然由ObjectInputStream反序列化而来,但是p和p1仍然是同一个对象,这和自己编写的单例对象有很大的区别。
另外,由于枚举的特殊性,使用枚举作为单例实现可能会遇到初始化问题和继承问题,当然这和使用场景有很大关系。
在编写单例对象的时候,还有一个需要特别注意的地方,就是线程安全。由于JVM是一个多线程运行环境,单例对象的方法可能会并行的在多个线程中同时调用,因而产生多线程问题。
public class ThreadsafeProduct{private ThreadsafeProduct() { }public static ThreadsafeProduct Instance() {return instance;}private static ThreadsafeProduct instance = new ThreadsafeProduct();public void DoSomething() {lock.lock();try {state++;System.out.println("I'm doing something for the " + state + " time");}finally {lock.unlock();}}private int state = 0;private Lock lock = new ReentrantLock();}
如上面示例那样,如果单例对象中存在状态,需要在访问时使用锁、原子变量等方式确保线程安全。
前面示例中的单例范围都是全局的(类加载器级别),但是Java语言针对线程还提供了ThreadLocal变量,使得我们能实现作用范围为线程的“单例模式”。如以下示例:
public class ThreadProduct {private static ThreadLocal<ThreadProduct> threadLocal = new ThreadLocal<ThreadProduct>();public static ThreadProduct Instance() {ThreadProduct threadProject = threadLocal.get();if(threadProject == null) {threadProject = new ThreadProduct();threadLocal.set(threadProject);}return threadProject;}public void DoSomething() {state++;System.out.println("I'm doing something for the " + state + " time in " + Thread.currentThread().getName());}private int state = 0;}
示例中的ThreadProject类主要功能和前面的Product功能相同,但是静态方法在初始化对象的时候,既不是直接返回静态的对象,也不是直接实例化对象,而是尝试在ThreadLocal对象中获取。如果已经获取到了,说明针对当前线程的ThreadProject对象已经存在,直接返回;如果获取到的是null,说明针对当前线程的对象还不存在,则新创建一个对象(注意,这里没有线程安全)。我们通过以下代码来进行测试:
public class TestThread {public static void main(String [] args) throws InterruptedException {Thread t1 = new Thread(new TestRunner());Thread t2 = new Thread(new TestRunner());ThreadProduct.Instance().DoSomething();t1.start();t2.start();t1.join();t2.join();}static class TestRunner implements Runnable {public void run() {IntStream.range(0, 3).forEach(i -> ThreadProduct.Instance().DoSomething());}}}
这里我们创建三个线程(包括主线程),每个线程都获取自己线程对应的ThreadProduct实例,然后执行DoSomething()方法。最终的输出为:
I'm doing something for the 1 time in mainI'm doing something for the 1 time in Thread-1I'm doing something for the 2 time in Thread-1I'm doing something for the 3 time in Thread-1I'm doing something for the 1 time in Thread-0I'm doing something for the 2 time in Thread-0I'm doing something for the 3 time in Thread-0
可以看见,每个线程都创建了自己线程作用域内的ThreadProduct对象。
单例模式使用范围非常广泛,使用Java实现一个基本的单例对象也比较方便。除了基本的静态成员变量之外,使用枚举来实现单例也是一个不错的选择。当然,使用单例对象也有一些在特定业务场景下需要关注的点,如作用域、序列化、线程安全等。