单例模式总结

来源:互联网 发布:做账软件免费版 编辑:程序博客网 时间:2024/06/05 12:45

在多线程的情况下,单例模式的getInstance()方法如果没有加上锁,还是会发生线程并发的问题。所以多线程的单例模式,为方法加锁还是不可避免的。

下面简单分析一下:

主函数:

public class VolatileWithDoubleCheckLocking {private volatile static Integer tmp = null;// 私有化默认构造器private VolatileWithDoubleCheckLocking() {throw new IllegalAccessError();};public static void main(String[] args) {for (int i = 0; i < 100; ++i) {if (((i+1) % 30) == 0)System.out.println();new Thread() {@Overridepublic void run() {Integer temp = getInstance_1();try {System.out.print(temp.toString() + "\t");} catch(NullPointerException e) {System.out.println("空指针.");System.exit(0);}}}.start();}}}

单线程的实现:

// 单线程方式public static Integer getInstance_1() {if (tmp == null) {tmp = new Integer(100);}return tmp;}
这个实现在单线程下是没有问题的。

但是在多线程下就会有问题了。

首先:多个线程同时访问方法,同时通过了非空校验,那么就会创建了多个实例,造成了资源的浪费,在并发量大的时候尤为明显。而如果每个时间段(或者其他需求条件)下创建的实例是不一样的,这样就会发生了并发的问题。

多线程的实现:

// 多线程方式public static Integer getInstance_2() {if (tmp == null) {synchronized(VolatileWithDoubleCheckLocking.class) {if (tmp == null) {tmp = new Integer(100);}}}return tmp;}

多线程的实现用了所谓的Double Checked Locking, 双重检查。

我们来分析一下:

多线程的访问方式下,比如有两个线程同时进入了方法。

1.两个线程都通过第一步: if(tmp == null)。

2.如果是null,则允许一个线程进入同步代码块,其他的线程在lock代码块外面等待。

3.第一个线程进入第二步检查 if(tmp == null), 为空,所以会执行 tmp = new Integer(100);

4. 返回结果。

5.第二个(或去他更多同时进入方法的)线程进入lock代码块, 如果没有第二重的非空校验(tmp == null),第二个(或者更多同时进入方法的线程)会再次执行tmp = new Integer(100)(这样就造成了资源的浪费了)。

6.返回结果。

结果: 返回了唯一的结果。

这样分析大家应该明白了第二重校验的作用了吧。

而为什么需要进行第一重校验呢?

像如下代码:

// 多线程方式public static Integer getInstance_2() {synchronized(VolatileWithDoubleCheckLocking.class) {if (tmp == null) {tmp = new Integer(100);}}return tmp;}
把第一步的校验去掉。

这样做关系到性能问题了。

分析一下:

多个线程同时进入了方法,如果没有第一步if(tmp == null)的话,每个线程每次对方法的访问都需要进行排队等待,要知道,lock代码块在程序中只会执行一次而已(首次访问的初始化),而有第一步的校验,在tmp初始化后,线程继续访问就不会进入lock代码块了。

这就是需要双重检查的原因。


而实例对象用了volatile 关键字声明 。

这里根据我自己的概念,大概说一下(具体详细的原理请自己查阅相关资料):

volatile 的作用是:告诉编译器,每次线程栈里的变量副本在使用前必须强制将主存的变量值更新到副本,保持变量副本一直为最新值。(有的文章说是不使用线程栈的变量副本,而直接使用主存的变量.)。用比较专业的术语来说,就是为了保证声明为volatile类型的域的写操作始终happens-before于对该域的读操作,避免了数据的脏读。

这里延伸到线程栈和主存栈:每个线程都会有一个独立的栈,栈里面维护主存变量的变量副本,不用每次都获取主存的变量,用来提高程序的性能。

为什么需要从主存栈中获取最新的变量值呢?

因为对象的创建是分3步的:

1.分配对象的内存空间。

2.初始化对象。

3.将实例指向刚分配的内存地址。

第一个线程进入lock代码块,并执行完tmp = new Integer(100), jvm为tmp分配了内存(注意,还没进行实例化,但是tmp != null) 然后退出lock代码块,在返回之前,第二个线程进入了lock代码块,发现tmp != null , 所以直接返回了(没有实例化的)结果,而在外部对返回结果使用的时候就会报空指针异常了。


静态代码块或静态内部类实现:

静态代码块:

// 用静态代码块来实现static {tmp = new Integer(100);}public static Integer getInstance_3() {return tmp;}

静态内部类:

// 用静态内部类来实现private static class InstanceHolder {static Integer tmp = new Integer(100);}private static Integer getInstance_4() {return InstanceHolder.tmp;}

使用静态内部类实现的话,单例引用是在内部类里面维护的。


小结:

使用静态代码块或静态内部类的实现, 当类被jvm加载的时候,静态的代码块和静态类以及静态方法都已经被准备好了,可以直接通过类名调用而不用先new出实例。

使用非静态实现的话,可以实现延迟加载,在单例类比较大或者程序比较庞大的情况下,可以提高性能(类似Hibernated的load和get),而相对静态实现而言稍微复杂一点。至于使用那种实现,请根据实际情况自行斟酌。


以上为本人总结,水平有限,如有错误或不足,不吝指正。


1 0
原创粉丝点击