线程安全的单例模式

转载自:http://devbbs.doit.com.cn/thread-30213-1-1.html


实际上使用什么样的单例实现取决于不同的生产环境,懒汉式也就是我在上面举得那个例子,这种方式适合于单线程程序,多线程情况下需要保护getInstance()方法,否则可能会产生多个Singleton对象的实例。

在此基础上确保getInstance()方法一次只能被一个线程调用就需要在getInstance()方法之前加上 synchronized 关键字,锁定整个方法,

Java代码 
public class Singleton{    
    private static Singleton instance=null;    
    private Singleton(){}    
    public static synchronized Singleton getInstance(){    
        if(instance==null){    

            instance=new Singleton();    
        }    
        return instance;    
    }    
}   

public class Singleton{ 
        private static Singleton instance=null; 
        private Singleton(){} 
        public static synchronized Singleton getInstance(){ 
                if(instance==null){ 
                        instance=new Singleton(); 
                } 
                return instance; 
        } 
}   

但很多时候我们通常会认为锁定整个方法的是比较耗费资源的,代码中实际会产生多线程访问问题的只有 instance = new Singleton(); 这一句,
为了降低 synchronized 块性能方面的影响,只锁定instance = new Singleton(); 这一句,“weishuang”回帖中使用的就是这种方式:


Java代码 
public class Singleton{    
    private static Singleton instance=null;    
    private Singleton(){}    
    public static Singleton getInstance(){    
        if(instance==null){    
            synchronized(Singleton.class){    
                instance=new Singleton();    
            }    
        }    
        return instance;    
    }    
}   

public class Singleton{ 
        private static Singleton instance=null; 
        private Singleton(){} 
        public static Singleton getInstance(){ 
                if(instance==null){ 
                        synchronized(Singleton.class){ 
                                instance=new Singleton(); 
                        } 
                } 
                return instance; 
        } 
}   

分析这种实现方式,两个线程可以并发地进入第一次判断instance是否为空的if 语句内部,第一个线程执行new操作,第二个线程阻断,当第一个线程执行完毕之后,第二个线程没有进行判断就直接进行new操作,所以这样做也并不是安全的。

为了避免第二次进入synchronized块没有进行非空判断的情况发生,添加第二次条件判断,就像“tomorrow009”在帖子中回复的示例一样

Java代码 
public static Singleton getInstance(){      
    if(instance == null){      
        synchronize{      
           if(instance == null){      
              instance =  new Singleton();       
           }      
        }      
    }      
    return instance;   
}    

public static Singleton getInstance(){   
    if(instance == null){   
        synchronize{   
           if(instance == null){   
              instance =  new Singleton();    
           }   
        }   
    }   
    return instance;
}  


这样就产生了二次检查,但是二次检查自身会存在比较隐蔽的问题,查了Peter Haggar在DeveloperWorks上的一篇文章,对二次检查的解释非常的详细:
“双重检查锁定背后的理论是完美的。不幸地是,现实完全不同。双重检查锁定的问题是:并不能保证它会在单处理器或多处理器计算机上顺利运行。双重检查锁定失败的问题并不归咎于 JVM 中的实现 bug,而是归咎于 Java 平台内存模型。内存模型允许所谓的“无序写入”,这也是这些习语失败的一个主要原因。”

其实找到这篇文章之后,我的问题基本上就已经可以解决了,但是看到回帖的同学们也有一些和我一样的问题,还想把这个问题继续梳理一遍。

使用二次检查的方法也不是完全安全的,原因是 java 平台内存模型中允许所谓的“无序写入”会导致二次检查失败,所以使用二次检查的想法也行不通了。

Peter Haggar在最后提出这样的观点:“无论以何种形式,都不应使用双重检查锁定,因为您不能保证它在任何 JVM 实现上都能顺利运行。”

"netrice"在回复中提到了使用“java5以后的volatile关键字”,用volatile关键字来声明变量,声明成 volatile 的变量被认为是顺序一致的,即,不是重新排序的。但是volatile关键字的特性并不适用于这篇帖子所讨论的问题关键。

通过上面的分析,可以看到使用懒汉式的lazy方式实现单例弯弯绕太多,在单线程编程的情况下懒汉式单例实现是没有任何问题的,如果在多线程的情况下,我们需要比较小心,对getInstances()方法加上synchronized关键字,这样虽然可能有一些性能上的牺牲,但是更加的安全。绕了这么大的一个弯,又回来了:

Java代码 
/* 安全的方式 1 */  
public class Singleton{    
    private static Singleton instance=null;    
    private Singleton(){}    
    public static synchronized Singleton getInstance(){    
        if(instance==null){    
            instance=new Singleton();    
        }    
        return instance;    
    }    
}   

/* 安全的方式 1 */
public class Singleton{ 
        private static Singleton instance=null; 
        private Singleton(){} 
        public static synchronized Singleton getInstance(){ 
                if(instance==null){ 
                        instance=new Singleton(); 
                } 
                return instance; 
        } 
}    
Peter Haggar提到的另外一种实现方式是这样的,放弃使用 synchronized 关键字,而使用 static 关键字:

Java代码 
/* 安全的方式 2 */  
public class Singleton {   
  
  private static Singleton instance = new Singleton();   
  
  private Singleton() {}   
  
  public static Singleton getInstance() {   
    return instance;   
  }   
  
}  

/* 安全的方式 2 */
public class Singleton {

  private static Singleton instance = new Singleton();

  private Singleton() {}

  public static Singleton getInstance() {
    return instance;
  }


这种方式没有使用同步,并且确保了调用static getInstance()方法时才创建Singleton的引用(static 的成员变量在一个类中只有一份)。

还有“keshin”提到的方式则更加灵巧,没有使用同步但保证了只有一个实例,还同时具有了Lazy的特性(出自Lazy Loading Singletons)

Java代码 
/* 安全的方式 3 */  
public class ResourceFactory {      
    private static class ResourceHolder {      
        public static Resource resource = new Resource();      
    }      
     
    public static Resource getResource() {      
        return ResourceFactory.ResourceHolder.resource;      
    }      
     
    static class Resource {      
    }      
}    

/* 安全的方式 3 */
public class ResourceFactory {   
    private static class ResourceHolder {   
        public static Resource resource = new Resource();   
    }   
  
    public static Resource getResource() {   
        return ResourceFactory.ResourceHolder.resource;   
    }   
  
    static class Resource {   
    }   
}     
上面的方式是值得借鉴的,在ResourceFactory中加入了一个私有静态内部类ResourceHolder ,对外提供的接口是 getResource()方法,也就是只有在ResourceFactory .getResource()的时候,Resource对象才会被创建,

这种写法的巧妙之处在于ResourceFactory 在使用的时候ResourceHolder 会被初始化,但是ResourceHolder 里面的resource并没有被创建,

这里隐含了一个是static关键字的用法,使用static关键字修饰的变量只有在第一次使用的时候才会被初始化,而且一个类里面static的成员变量只会有一份,这样就保证了无论多少个线程同时访问,所拿到的Resource对象都是同一个。


饿汉式的实现方式虽然貌似开销比较大,但是不会出现线程安全的问题,也是解决线程安全的单例实现的有效方式。

至于ThreadLocal,我认为还是应该由使用场景来决定。

在《Java与模式》中,作者提出:“饿汉式单例类可以在Java语言实现,但不易在C++内实现,因为静态初始化在C++里没有固定的顺序,因而静态的instance变量的初始化与类的加载顺序没有保证,可能会出问题。这就是为什么GoF在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java语言中单例类的例子也大多是懒汉式的。实际上,本书认为饿汉式单例类更符合Java语言本身的特点。”

由此可见在应用设计模式的同时,分析具体的使用场景来选择合适的实现方式是非常必要的。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值