单例模式的再一次全面解析与不断解决问题的思路

来源:互联网 发布:手机cad软件 编辑:程序博客网 时间:2024/04/28 02:56

很多地方我们要求一个对象存在一个。原因是这个对象在系统中需要保留连续的状态。从能用到好用到耐用,这种思路是我们应该学习的。

1.可用的线程不安全方法

  1. public class Singleton{
  1.     private static Singleton uniqeInstance;
  1.     //other varible
  1.     private Singleton{}
  1.     public static getInstance{
  1.           if(uniqueInstance==null){
  1.                uniqueInstance=new Singleton();
  1.            }       
  2. return uniqueInstance;
  1.     }  
  2. //other code
  1. }


这个实现在多线程调用的环境中因为没有同步,很有可能出现多次创建。线程不安全。

2.粗暴但是耐用的线程安全的方法

getInstance变为  public static sychronized getInstance(){}

这里面可以保证前程安全。

但是sychronized开销过大,会使得效率降低100倍以上。如果不是在多线程的环境中,使用第一种方法就够了。

解决这种同步的低效还有一种方法,但是会浪费空间。这也是一种空间换时间的折中吧。

3.使用早创建来代替懒创建

懒创建就是如同上面的办法,在判断为null的时候才进行创建。

public class Singleton{    private static Singleton uniqueInstance=new Singleton();     private Singleton();     public static Singleton getInstance(){           return unniqInstance;     }}

这个方法可以保证线程安全。


早创建将对象的实例化写在了类的声明中。这样在类加载到JVM中的时候就会实例化对象,这是在任何线程访问这个类之前的。只有在卸载的改类的时候该实例的空间才会被释放。



插播一段java类加载和卸载的知识:

java的类加载后且当使用阶段完成之后,java类就进入了卸载阶段,也就是所谓的释放。

使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。

一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示:

PS:关于类的卸载,在类使用完之后,如果满足下面的情况,类就会被卸载:

该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。

加载该类的ClassLoader已经被回收。

该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。



4.使用双重检查锁来代替synchronize

我们发现方法2中,因为synchronize修饰的是getInstance,所以无论是否需要创建新的实例,都需要以为该关键字而损失效率。而我们仔细分析问题会发现,只有在判断引用为空,并需要创建实例的时候才可能出现多线程不安全,实例多次创建的情况。所以仅需在判断引用为空的时候使用关键字。

  1. pulbic class Singleton{
  1.   private volatile static Singleton uniqueInstance;
  1.   private Singleton(){}
  1.   public static Singleton getInstance(){
  1.     if(uniqueInstance==null){
  1.       synchronized(Singleton.class){
  1.         if(uniqueInstance==null){
  1.           uniqueInstance=new Singleton();
  1.         }
  1.       }
  1.     }
  1.   }
  1. }

这种方法是来实现getInstance方法是最好的。可以大大减少方法的开销,同时保证线程安全。


这里面之所以进行两边uniqueInstance==null的判断,并不是冗余。

第一,实例化之前对于是否为null的判断一定是要在synchronized中才有效。所以里面要判断一下才能实例化。因为同步块以外的判断并不能用来决断接下来的状态,同步块里面的判断在同步块中保持稳定,是null就是null,而不会被其他线程悄悄改变。

第二,如果只有上面的判断就和方法2中的低效线程安全一样了。所有第一次非同步的判断是用来过滤不需要同步的情况的。因为如果不为null则压根不需要创建对象,也不需要同步。而使用中从这条判断路径为大多数,而判断为null流程指向初始化的则只有一次。大多数都不需要加锁,只有第一次才要,这就是这种方法提高效率的原因。


插播知识

1.volatile关键字 经常被用来修饰一个变量或者引用,来保证不同线程对于该引用具有最新的可见性。之所以之前的方法没有用是因为对于静态方法同步相当于类同步,类中的对象是不可以同时被不同线程访问的,所以uniqueInstance引用也不会被多线程同时访问。而本例中getInstance方法并没有同步,类中的引用uniqInstance可以同时被多个线程读取和赋值,所以这时候加上volatile关键字可以较高效的使之对于不同线程可见。

2.synchronize的除了修饰方法的其他用法。http://blog.csdn.net/yaerfeng/article/details/7254734

这里提炼出来学习的药店。

a.synchronize一定是对于某一个对象(java中基本类型的数组也是对象)加锁。只有同一个实例在不同的线程访问的时候才会进行加锁。而同一个类的不同对象在不同线程访问的时候是不影响的。

synchronize(Obj obj){

}

b.synchronize修饰方法的时候加锁的是这个方法的宿主对象。而对于静态函数的加锁等同于对于类名字面常量加锁,

即 synchronized(Singleton.class){

}这种形式。这种形式是对类的加锁,即使是同一个类的不同的对象访问,也会进行加锁同步。



0 0
原创粉丝点击