本文根据自身的学习和使用,总结出了5种实现Singleton的方法,并对他们的优缺点进行了简单的总结。
第一种:最常用型,代码如下:
public class Singleton1 { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub } private static Singleton1 instance=null; private Singleton1() { } public static synchronized Singleton1 getInstance() { if(instance==null) { instance=new Singleton1(); System.out.println("Thread : "+Thread.currentThread().getName()+" is creating instance!"); } System.out.println("Thread : "+Thread.currentThread().getName()+" getting instance!"); return instance; } }
这种实现,直接对getInstance函数增加了同步关键字,因此可以保证Instance任何情况下,只被创建一次,但是有个问题,就是每次对getInstance的调用都会去试图获取该同步锁,这就增加了程序运行开销,而且,如果第一次调用已经创建了instance之后,则后续的调用实质上已经不需要再去获取同步锁,而只需要判断instance是否为Null即可。因此,这种方法,虽然实现了同步控制,但同步的粒度太大,影响了程序运行的性能。
第二种:对第一种的改造,降低同步粒度
public class Singleton2 { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub } <span style="color:#333333;">private static Integer flag = 0; </span> private static Singleton2 instance=null; private Singleton2() { } public static Singleton2 getInstance() { if(instance==null) { <span style="color:#333333;">synchronized(flag) </span> { if(instance==null) { instance=new Singleton2(); } } } return instance; } }
可以看出,上面这种实现,直接在getInstance函数里面增加同步块,这样的话,当第一次获取到同步锁后,instance被创建,后面再对getInstance的调用,首先判断instance是否为null,如果非null,则无需再去获取同步锁,这样的话,就大大降低了等待获取同步锁的开销,另外,即便是有多个线程同时进入getInstance函数,并且同时判断instance为null,那么第一个获得同步锁的线程创建了instance后,并释放同步锁,第二个线程获得了同步锁,发现instance非null,则不做任何事,释放同步锁。也比第一种效率要高些,这里在网上看到有人直接用synchronized(instance)来实现同步控制是不正确的,因为synchronized()中不能对空对象进行操作,否则会抛出 NullPointerException异常。
第三种:通过静态类成员类实现
这种实现方式的理论基础是,instance的创建在类的加载中完成,而JVM中类的加载是线程互斥的,因此,可以自动保证instance只创建一次。代码如下:
public class Singleton3 { public static void main(String[] args) { // TODO Auto-generated method stub Thread t1 = new Thread(new TestThread()); Thread t2 = new Thread(new TestThread()); t1.start(); t2.start(); } private static Singleton3 instance= new Singleton3(); private Singleton3() { System.out.println("Thread : "+Thread.currentThread().getName()+" creating instance!"); } public static Singleton3 getInstance() { System.out.println("Thread : "+Thread.currentThread().getName()+" getting instance!"); return instance; } } public class TestThread implements Runnable { public void run() { Singleton3.getInstance(); } } 输出: Thread : Thread-0 creating instance! Thread : Thread-0 getting instance! Thread : Thread-1 getting instance!
从输出结果可以看出,在Singleton3类的加载过程中,其static成员instance已经被创建,而且由于类的加载是线程互斥的,所以,可以保证instance只能被创建一次,这种实现的好处是,无需由用户自己去增加同步控制来实现instance的创建,但是,缺点是,如果这里的instance对象如果是个比较大的object,则在类加载时就要对其
创建,而不是到了使用的时候才创建,会增加程序运行的性能开销。针对这种问题,我们可以采用静态内部类来实现延迟创建,这就是第四种实现方式。
第四种:静态内部类实现
public class Singleton4 { public static void main(String[] args) { // TODO Auto-generated method stub Thread t1 = new Thread(new TestThread()); Thread t2 = new Thread(new TestThread()); t1.start(); t2.start(); } private static class SingletonFactory { private static Singleton4 instance = new Singleton4(); } private Singleton4() { System.out.println("Thread : "+Thread.currentThread().getName()+" creating instance!"); } public static Singleton4 getInstance() { System.out.println("Thread : "+Thread.currentThread().getName()+" getting instance!"); return SingletonFactory.instance; } } 输出: Thread : Thread-0 getting instance! Thread : Thread-1 getting instance! Thread : Thread-0 creating instance!
从输出结果可以看出,Singleton4中的SingletonFactory中的instance,并不是在类加载过程中就创建了,而是在调用getInstance中的SingletonFactory.instance才真正创建instance,这就达到了用时创建的延迟创建的目的,即,程序运行过程中,如果不用instance,则就不去创建,降低了运行开销。第五种:对第二种的改进
public class Singleton5 { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub } private Singleton5() { } private static Singleton5 instance=null; private static synchronized void createInstance() { if(instance==null) { instance = new Singleton5(); } } public static Singleton5 getInstance() { if(instance == null) { createInstance(); } return instance; } }
即,将第二种方法中的同步块提出来作为一个单独的函数,之所以这样做,原因如下,试想如下情形:在第二种实现中,线程A进入了同步块,判断instance为null,执行instance=new Singleton2(),JVM为instance分配空间,并把找个空间赋值给instance,稍后某个时候才对分配的内存进行初始化,A执行完同步块,这时,线程B在等待同步块,并进入同步块,判断instance非null,然后退出并返回instance的引用,这时由于instance的内存空间还没有被JVM进行初始化,因此后面的调用就可能出现异常。而,把同步块单独提出来作为一个单独的函数,则在某种意义上就会避免这种情形的发生,因为在createInstance()函数退出时,JVM一定是已经对Instance的空间进行了初始化。这个原因是我个人的理解,稍后打算花一定时间去研究JVM对象的初始化,因为,我个人感觉JVM的对象初始化机制和代码所在块有关,第二种的同步块是一段代码,第五种实现中是个同步函数,这二者的差异可能会影响到JVM对对象初始化的时间。
以上是本人对singleton的理解和总结,有不当之处,还需要牛人们多多指正。