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

【设计模式】单例模式

2016-07-31 16:07 274 查看
单例的概念

饿汉式

懒汉式

方法同步锁

双重检测锁

静态内部类

枚举类

结束语

参考资料

单例的概念

今天要讲的一个设计模式是个人觉得是最简单却又是最容易有问题的设计模式–单例模式。什么叫单例模式呢?简而言之,确保一个类只有一个实例,并提供一个全局访问点。

目前大家知道的创建单例的方式有许多,主要是下面几种:

第一,饿汉式

第二,懒汉式

第三,方法同步锁

第四,双重检测锁

第五,静态内部类

第六,枚举类

饿汉式

毫无疑问,就是不管该类用不用的到,都先创建实例化。

public class Singleton{
private final static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}

public class Main{
public static void main(String[] args){
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}


这种方法可以创建单例,但是无法实现延迟加载,使得系统开销较大。于是和它相对的方式,懒汉式出现了。

懒汉式

懒汉式,顾名思义,太懒了,只有用的到的时候才创建类,可以实现延迟加载。

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

public class Main{
public static void main(String[] args){
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1==s2);
}
}


懒汉式虽然可以实现延迟加载,也能实现单线程的单例,然而如果是多线程,就不能保证类实例化的唯一性了,因此锁机制必须引入,因此出现第一种加锁的方式。

方法同步锁

就是在getInstance上加同步锁synchronized,以保证类未创建的时候,有一个线程争取到锁之后创建。

public class Singleton{
private volatile static Singleton instance;
private Singleton(){
}
public  synchronized static  Singleton getInstance(){
if(null==instance){
instance= new Singleton();
}
return instance;
}
}
public class Threads implements Runnable{
private Singleton instance;
@Override
public void run() {
instance = Singleton.getInstance();
}
public Singleton getSingleton(){
return instance;
}
}
public class Main{
public static void main(String[] args) throws InterruptedException {
Threads t1 = new Threads();
Threads t2 = new Threads();
new Thread(t1).start();
new  Thread(t2).start();
Thread.sleep(1000);
//确保线程结束,对象都创建才能进行两个对象的比较,否则要是主线程先结束,两个子线程未创建对象,则两个对象都为null,必然是相等的。
System.out.println(t1.getSingleton()==t2.getSingleton());
}
}


在方法上加同步锁,是可以使得类实例的唯一性,但是因为每当需要调用该对象方法时都会有锁资源的争夺,如果多个同时则容易出现堵塞。因此需要把锁放入方法内部,此时就是本文的第四种创建方式:双重检测锁

双重检测锁

为了效率上的提升,将锁放入getInstance内部。

public class Singleton{
private volatile static Singleton instance;
private Singleton(){
}
public  static  Singleton getInstance(){
if(null==instance){
synchronized(Singleton.class){
if(null==instance){
instance= new Singleton();
}
}
}
return instance;
}
}
public class Threads implements Runnable{
private Singleton instance;
@Override
public void run() {
instance = Singleton.getInstance();
}
public Singleton getSingleton(){
return instance;
}
}
public class Main{
public static void main(String[] args) throws InterruptedException {
Threads t1 = new Threads();
Threads t2 = new Threads();
long tb = System.currentTimeMillis();
new Thread(t1).start();
new  Thread(t2).start();
Thread.sleep(1000);
System.out.println(t1.getSingleton()==t2.getSingleton());
System.out.println(System.currentTimeMillis()-tb);
}
}


双重检测锁的方式效率上与方法外加锁方式比较,就本文的例子来看,方法外加锁运行时间1001ms,而双重检测锁却是1000ms,大家要看到其中停顿了1000ms,也就是说双重检测锁几乎无额外开销,方法外加锁却因为额外等待使得时间消耗大。理论上也是同步一个方法可能使得效率下降100倍。

顺便提一下,双重检测锁中volatile使用的特别关键,如果是jdk1.5之前,volatile只能实现主存区和线程分区变量对象的同步(可见性),双重检测锁的方式仍然无法实现真正的单例,因为不同编译器有自己的标准和优化,使得 instance= new Singleton();这一句看似简单的步骤,却会分解成三步:申请内存空间,进行初始化,返回地址引用。因为有的编译器优化的因素,可能使得上面的步骤变成:申请内存空间,返回地址引用,进行初始化。这样new得到的对象就是一个半成品,很有可能初始化未完成。大家得到的对象只是一个半成品,有的变量都还没赋值,有的则赋值了,这虽然对象是同一个,但是对象内容却是千奇百怪,那这也并不是单例的初衷。直到jdk1.5之后,volatile增加了一个新功能就是被volatile修饰的对象防止被编译器优化,防止指令重排,也就是说new,只能是先申请内存空间,初始化完成后,再返回地址引用。

然而不管怎样,上面几种方法在jdk1.5之前是无法确保单例的实现,于是有一种方法既能实现延迟加载又能实现单例,这就是静态内部类。

静态内部类

public class Singleton{
private Singleton(){
}
public static class InnerClass{
private static Singleton instance = new Singleton();
}
public static SingleTons getInstance(){
return InnerClass.instance;
}
}
public class Main{
public static void main(String[] args) {
SingleTons s1 = SingleTons.getInstance();
SingleTons s2 = SingleTons.getInstance();
System.out.println(s1==s2);
}
}


静态内部类的方法毫无疑问因为静态类的只加载一次的性质,使得其线程安全的。但是,上面的五种方法,都无法实现真正的单例,原因有二:第一,反序列化得到的类无法保证单一性,当然如果增加额外的工作(Serializable、transient、readResolve())来实现序列化,可以解决这个问题;第二,反射强行调用私有构造器。虽然这些都有其他方法来解决,但终归有牺牲系统效率或者增加额外工作为代价。那有没有一种方式,能够真真正正实现单例呢?答案是肯定的,就是本文最后一种方式,枚举类。

枚举类

public enum Singleton{
INSTANCE("单例",1);//带构造函数
private String name;//构造函数内的参数
private int index;
private Singleton(String name,int index){//构造函数
this.name = name;
this.index = index;
}
public String getName(){
return this.name;
}
public int getIndex(){
return this.index;
}
}
public class Main{
public static void main(String[] args) {
Singleton s1 = Singleton.INSTANCE;
Singleton s2 = Singleton.INSTANCE;
System.out.println(s1.getName());
System.out.println(s2.getName());
System.out.println(s1==s2);
}
}


使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例。

然而枚举类有一个缺陷就是使用的内存空间一般来说是静态常量的两倍以上,像Android项目内就不推荐使用了,而刚好Android开发的jdk都是在1.6以上了,推荐使用双重检测锁来实现单例。

结束语

单例,之所以一个简单的模式衍生出这么多方法,主要是为了解决线程安全、延迟加载、序列化与反序列化安全三个重点问题。

参考资料

线程安全的单例模式的几种实现方法分享

你真的会写单例模式吗——Java实现
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息