`
donlianli
  • 浏览: 336136 次
  • 性别: Icon_minigender_1
  • 来自: 北京
博客专栏
Group-logo
Elasticsearch...
浏览量:216495
社区版块
存档分类
最新评论

Java单实例模式

阅读更多

前言:
代码简洁与性能高效无法两全其美,本文章专注于大并发程序的性能,如果您追求代码简洁,本文章可能不太适合,因为本文章主要讨论如何写出在高并发下也能运行很好的代码。

 

并文章属于Java并发编程实战中例子。但结合实际场景进行了阐述。

通常,我们如果写一个单实例模式的对象,一般会这样写:

写法一:

 

public class Singleton {
	private static final Singleton instance = new Singleton();
	/**
	 * 防止其他人new对象
	 */
	private Singleton(){
		System.out.println("init");
	}
	public static Singleton getInstance(){
		return instance;
	}
}

 

 

这种方式叫饥饿式单实例,意思是说,不管你用不用这个类的方法,我都把这个类需要的一切资源都分配好。但这样写有一个问题,就是如果这类需要的资源比较多,在系统启动的时候,就会很慢。

因此要求有懒汉式单实例,于是就出现了第二中写法,

写法二:

 

public class Singleton {
	private static Singleton instance = null;
	/**
	 * 防止其他人new对象
	 */
	private Singleton(){
		System.out.println("init");
	}
	public static Singleton getInstance(){
		if(instance == null){
			instance = new Singleton();
		}
		return instance;
	}
}

 这种方式叫懒汉式单实例,即通常所说的延迟加载。这样,在系统启动的时候,不会加载类所需要的各种资源,只有真正使用的时候才去加载各种资源。

 

但这种方法马上就可以看出问题,因为在多线程情况下,可能会导致重复初始化的问题(不明白这个道理,那您需要补充一下同步及多线程知识了)。于是有了改进版,即目前网上比较流行的写法。

写法三:

 

public class Singleton {
	private static Singleton instance = null;
	/**
	 * 防止其他人new对象
	 */
	private Singleton(){
		System.out.println("init");
	}
	public static synchronized Singleton getInstance(){
		if(instance == null){
			instance = new Singleton();
		}
		return instance;
	}
}

 加上关键字synchronized,可以保证只有一个线程在执行这个方法。这个方法至此应该说是比较完美的了,但是,专家不这么认为,在高并发多线程的访问系统中,synchronized关键字会让程序的吞吐量急剧下降,因此,在高并发系统中,应该尽量避免使用synchronized锁。

但这并不能难住我们聪明的软件工程师,有人便写出了双重锁的程序。方法如下:

写法四:

public class Singleton {
	private static Singleton instance = null;
	/**
	 * 防止其他人new对象
	 */
	private Singleton(){
		System.out.println("init");
	}
	public static  Singleton getInstance(){
		if(instance == null){
			synchronized(Singleton.class){
				if(instance == null){
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

这样,通常获得单实例引用是没有锁的,只有第一次初始化时才会加锁,而且如果多个线程进入临界区区后,理论上只有第一个进入临界区的线程才会初始化对象,之后进入临界区的线程因为之前的线程已经初始化,就不会再次进行初始化。

但专家怎么说呢?这个代码有问题。首先,这个程序对同步的应用很到位,即当进入synchronied区,只有一个线程在访问Singleton类。但却忽略了变量的可见性。因为在没有同步的保护下,instance的值在多个线程中可能都是空的,因为即便第一个线程对类进行了初始化,并把类的引用赋值给了instance变量,但也不能保证instance变量的值对其他线程是可见的,因为变量instance没有采用同步的机制。

在java5之后,可以在instance前面添加volatile关键字来解决这个问题,但是这种双重锁的方式已经不建议使用。

 

那么,看看大师推荐的写法吧,见 Java Concurrency In Practice的List 16.6代码:

写法五:

public class Singleton {
	private static class SingletonHolder {
        public static Singleton resource = new Singleton();
    }
    public static Singleton getResource() {
        return  SingletonHolder.resource ;
    }
    
    private Singleton(){
    	
    }
}

 

综上各种写法,发现写法一虽然在启动时会让系统启动的慢一些,但却不失为一种简洁而高效的写法,当然,如果确实对系统启动时的速度要求高的话,则应该考虑写法五了。

另外,其实单实例方法还有好多种,在effective Java中有写到:

写法六:

public class Singleton {
	public static final Singleton INSTANCE = new Singleton();
	
	private Singleton(){}
	
	public void method(){
		//...
	}
	public static void main(String[] a){
		//调用方法。
		Singleton.INSTANCE.method();
	}
}

 写法七:

/**
 * 利用枚举巧妙创建单实例
 */
public enum Singleton {
	INSTANCE;
	public void method(){
		//...
	}
	public static void main(String[] a){
		//调用方法。
		Singleton.INSTANCE.method();
	}
}

 另外,双重锁的方式,在加上volatile关键字后,也是高效安全的写法。

写法八:

public class Singleton {
	private static volatile Singleton instance = null;
	/**
	 * 防止其他人new对象
	 */
	private Singleton(){
		System.out.println("init");
	}
	public static  Singleton getInstance(){
		if(instance == null){
			synchronized(Singleton.class){
				if(instance == null){
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

 

 

其实,在今天spring大行其道的天下,单实例需求已经不多,spring中的bean默认都是单实例。但是要做一些app程序或者开发一个产品时,这种模式还是很重要的。综上所述,我个人比较推荐写法五和写法一,写法七怎么看着也别扭。

另外感谢大家的讨论,这个话题先到这儿吧,我写本文章的主要目的是为了纠正写法三的错误。不知道你的项目中是否还存在写法三的代码呢?

 
对性能感兴趣?请查看 并发编程 系列文章,持续更新中
对这类话题感兴趣?欢迎发送邮件至donlianli@126.com
关于我:邯郸人,擅长Java,Javascript,Extjs,oracle sql。
更多我之前的文章,可以访问:http://hi.baidu.com/donlian

 

6
6
分享到:
评论
13 楼 IT民工% 2013-07-04  
线程安全全
harry775 写道
IT民工% 写道
没看懂第五种写法,求详解


内部类加载和实例创建、线程安全全是由classloader控制。

线程安全全怎么控制的?
12 楼 harry775 2013-07-04  
IT民工% 写道
没看懂第五种写法,求详解


内部类加载和实例创建、线程安全全是由classloader控制。
11 楼 harry775 2013-07-04  
呵呵,这几种单例在  java程序性能优化里面也有。在并发编程实战里面也有。可以关注下,我也在看并发编程实战,之前吧java程序性能优化看了  希望可以和楼主一起学习下、企鹅 25328291
10 楼 a5728238 2013-07-04  
单例对象如果需要持久化呢,楼主还是写下继承Serializable怎么保证对象的唯一吧。枚举是天生的单例,这个也是经常使用到的。
9 楼 sswh 2013-07-04  
BlueGuitar 写道


还是不一样的。第五种要在SingletonHolder 被加载的时候才会初始化。而SingletonHolder只有在getInstance的时候才会加载。

第一种的话,只要Singleton加载就会初始化,即使没有调用getInstance方法。

一段代码里面如果写了getInstance 但是没有执行的情况下就有区别了。



是这样的,但是差别不大。 两种写法本质相同。

第1种是Singleton类被加载时初始化,第5种是SingletonHolder类被加载时初始化。

Singleton类本身就是   只有在被使用到的情况下才加载。

第5种的唯一有用的地方就是:  加载了Singleton类,但是却不调用getInstance()方法的场合。  仔细想一下,这种场合存在吗?

所以,基本没用。
8 楼 BlueGuitar 2013-07-04  
sswh 写道


这段代码中,new Singleton()发生在Singleton 类被加载时。
所以,所谓的第1种方法会导致系统启动变慢是不靠谱的。
并不是所有的第1种写法的Singleton对象都会在系统启动时创建,要看这个类有没有被使用,有没有被加载。

第5种所谓的大师的写法,本质上就是第1种。只在使用到的时候才加载目标类,才初始化singleton对象。


还是不一样的。第五种要在SingletonHolder 被加载的时候才会初始化。而SingletonHolder只有在getInstance的时候才会加载。

第一种的话,只要Singleton加载就会初始化,即使没有调用getInstance方法。

一段代码里面如果写了getInstance 但是没有执行的情况下就有区别了。
7 楼 tjpdj1988 2013-07-04  
那个,用枚举的方式实现单例是不是更好写?
6 楼 Tyrion 2013-07-04  
直接用枚举啊
5 楼 evanzzy 2013-07-04  
系统启动的快慢,不是一个很大的问题,大型系统启动半个小时都是正常的,主要是保证运行时效率高。其实第一种写法足够了。或者……直接Spring
4 楼 sswh 2013-07-04  

对于第1种,文中是这样说的:

引用
这种方式叫饥饿式单实例,意思是说,不管你用不用这个类的方法,我都把这个类需要的一切资源都分配好。但这样写有一个问题,就是如果这类需要的资源比较多,在系统启动的时候,就会很慢。



对于第5种,文中又是这样说的:
引用
那么,看看大师推荐的写法吧,见 Java Concurrency In Practice的List 16.6代码


其实第1种和第5种压根没区别。

引用
private static final Singleton instance = new Singleton();


这段代码中,new Singleton()发生在Singleton 类被加载时。
所以,所谓的第1种方法会导致系统启动变慢是不靠谱的。
并不是所有的第1种写法的Singleton对象都会在系统启动时创建,要看这个类有没有被使用,有没有被加载。

第5种所谓的大师的写法,本质上就是第1种。只在使用到的时候才加载目标类,才初始化singleton对象。
3 楼 thc1987 2013-07-04  
楼主有时间给大家介绍下用枚举来实现单例吧
2 楼 rox 2013-07-04  
晕,double check都不建议使用了。
技术发展太快了。
1 楼 IT民工% 2013-07-03  
没看懂第五种写法,求详解

相关推荐

Global site tag (gtag.js) - Google Analytics