@babydragon
2016-04-19T20:57:16.000000Z
字数 5130
阅读 1881
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 time
I'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 == 1
p.DoSomething(); // state == 2,但是不影响p1
p1.DoSomething();
}
}
注意,这里Product
和之前的基本相同,但是需要多实现Serializable
接口。上述代码的输出为:
I'm doing something for the 1 time
I'm doing something for the 2 time
I'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 main
I'm doing something for the 1 time in Thread-1
I'm doing something for the 2 time in Thread-1
I'm doing something for the 3 time in Thread-1
I'm doing something for the 1 time in Thread-0
I'm doing something for the 2 time in Thread-0
I'm doing something for the 3 time in Thread-0
可以看见,每个线程都创建了自己线程作用域内的ThreadProduct
对象。
单例模式使用范围非常广泛,使用Java实现一个基本的单例对象也比较方便。除了基本的静态成员变量之外,使用枚举来实现单例也是一个不错的选择。当然,使用单例对象也有一些在特定业务场景下需要关注的点,如作用域、序列化、线程安全等。