您的位置:首页 > 编程语言 > Java开发

Java单例模式

2016-06-23 13:53 351 查看

1、 什么是单例

单例,就是在应用中,只有一个实例(对象)。

在什么情况下,需要用到单例呢?比如工具类、线程池、缓存,数据库,账户登录系统、配置文件等程序中可能只允许我们创建一个对象,一方面如果创建多个对象可能引起程序的错误,另一方面创建多个对象也造成资源的浪费。

2、单例的设计思想

单例的关键,就是保证应用只有一个实例,那么如果保证呢?通过以下3点,就可以保证:

1、不允许其他程序新建(new)对象;

2、在该类中创建一个对象;

3、对外提供一个可以获取该对象的方法。

以上3点,转换成代码描述,即:

1、该类的构造函数为private;

2、在该类中new一个本来对象;

3、提供static方法,获取new的对象。

3、单例的写法

单例的写法有5种:
1、恶汉式;
2、懒汉式;
3、双重校验锁;
4、静态内部类;
5、枚举。


3.1恶汉式 (线程安全,可用)

恶汉式的写法如下所示:

public class Singleton1 {
private static Singleton1 instance = new Singleton1();

private Singleton1() {}

public static Singleton1 getInstance() {
return instance;
}
}


或者使用静态初始化:

public class Singleton1 {
private static Singleton1 instance = null;
static {
instance = new Singleton1();
}

private Singleton1() {}

public static Singleton1 getInstance() {
return instance;
}
}


优点:这种方式在加载类的时候,就完成了实例化,避免线程同步问题。

缺点:由于在类加载的时候就实例化了,所以没有达到Lazy Loading(懒加载)的效果,也就是说可能我没有用到这个实例,但是它也会加载,会造成内存的浪费(但是这个浪费可以忽略,所以这种方式也是推荐使用的)。

3.2 懒汉式(线程不安全,不可用)

懒汉式的写法,如下:

public class Singleton2 {
private static Singleton2 instance;

private Singleton2() {}

public static Singleton2 getInstance() {
if(instance == null)
instance = new Singleton2();

return instance;
}
}


这种写法,在调用getInstance方法时,才会创建对象,所以称为懒汉式。

这种写法很常见,但这种写法线程不安全。

比如有2个线程同时调用这个单例,第一个线程运行到“if(instance == null)”这条语句后阻塞了,这时还没有运行“instance = new Singleton2()”,所以instance还是为null。

第二个线程也调用getInstance方法获取单例,因为instance为null,所以会新建一个对象。

回到第一个线程,是执行了“if(instance == null)”语句阻塞的,接下来执行“instance = new Singleton2()”,又新建了一个对象,把单例对象instance改变了。

所以,这种写法是线程不安全的。

既然线程不安全,那加一个同步就可以了。这样就线程安全了。写法如下:

public class Singleton2 {
private static Singleton2 instance;

private Singleton2() {}

public static synchronized Singleton2 getInstance() {
if(instance == null)
instance = new Singleton2();

return instance;
}
}


这种写法虽然是线程安全的,但是每一次调用getInstance方法,都会进行同步。而真正需要同步的,只是第一次新建instance对象时。之后调用,都是直接返回instance对象就可以了。

所以这种写法的效率是很低的,不推荐使用。

既然需要同步的是新建instance对象,那么把同步放在新建instance对象上就好了。于是得到了下面这种写法:

public class Singleton2 {
private static Singleton2 instance;

private Singleton2() {}

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

return instance;
}
}


这种写法,和懒汉式中的第一种写法一样,也是线程不安全,原因也是一样。

如果有2个线程调用getInstance方法。2个线程都执行完了“if(instance == null)”这条语句,即使对新建instance对象进行了同步,2个线程还是都会新建instance对象。

如果对这种写法,稍微改造一下。就得到了线程安全的“双重校验锁”写法。

3.3 双重校验锁(线程安全,可用)

双重校验锁写法如下:

public class Singleton2 {
private volatile static Singleton2 instance;

private Singleton2() {}

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

return instance;
}
}


这种写法是线程安全的,比如:

2个线程都执行完了“if(instance == null)”语句,第1个线程先进入同步代码,新建instance对象,这个时候,第2个线程会阻塞。

当第1个线程新建完了instance对象后,第2个线程进入同步代码,再一次进行“if(instance == null)”的判断,这个时候,instance已经被第1个线程创建,所以不为null,不用再次新建。

为什么instance要加volatile修饰,请参考:Java并发编程:volatile关键字解析

3.4 静态内部类(线程安全,可用)

静态内部类的写法如下:

public class Singleton3 {
private Singleton3() {}

private static class SingletonHolder {
private static Singleton3 instance = new Singleton3();
}

public static Singleton3 getInstance() {
return SingletonHolder.instance;
}
}


这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonHolder类,从而完成Singleton的实例化。类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。

优点:避免了线程不安全,延迟加载,效率高。

3.5 枚举(线程安全,可用)

枚举的写法如下:

public enum Singleton4 {
instance;

private Singleton4() {}

public void func() {}
}


访问方式如下:

Singleton4.instance.func();


可以看到枚举的书写非常简单,访问也很简单在这里SingletonEnum.instance这里的instance即为SingletonEnum类型的引用所以得到它就可以调用枚举中的方法了。借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过,这种方式也是最好的一种方式,如果在开发中JDK满足要求的情况下建议使用这种方式。

总结

在真正的项目开发中一般采用3.1、3.3、3.4、3.5看你最喜欢哪种写法了,我们经常用的Android-Universal-Image-Loader这个开源项目也是采用的3.3这种写法,其实最安全的写法是3.5即枚举,它的实现非常简单而且最安全可谓很完美,但是可能是因为只支持JDK1.5吧又或者是因为枚举大家不熟悉所以目前使用的人并不多,但是大家可以尝试下。另外当我们使用反射机制时可能不能保证实例的唯一性,但是枚举始终可以保证唯一性,具体请参考次博客:http://blog.csdn.net/java2000_net/article/details/3983958但是一般情况下很少遇到这种情况。

最后,就是谢谢前辈们。

这篇文章,其实相当于照抄

http://blog.csdn.net/dmk877/article/details/50311791

之所要照着写一遍,就是想动手写一写,这样比单独看的效果要好。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息