[设计模式]单例模式

开始写设计模式系列,希望自己可以坚持下来.
第一篇:单例模式

单例模式是大家最为熟悉的设计模式也是大多数程序员接触的第一种设计模式,但是要真的去全面了解一波单例模式还是要点时间.

什么是单例:

确保某一个类在全局只有一个实例,最为常见的的场景就是全局变量以及全局的工具类,重复创建带来资源与性能的浪费.那么创建单例模式需要注意什么呢?

  1. 重写构造函数并声明为私有(private);
  2. 通过静态方法或者枚举返回单例对象;
  3. 确保全局至多只有一个对象,尤其在多线程高并发的情况下;
  4. 确保单例对象在反序列化的时候也不会重新创建新的对象;

从单例的实现,前人又将其分为多种类型,我们来分析一波他们的优劣:

饿汉模式:

我们知道在一个国家里面国王只有一个,大臣可以有多个,我们就拿这个来举例说明:

package top.huyuxin.singleton;
/**
 * 饿汉模式
 */
public class King extends Person {
    private static final King king=new King();
    private King() {
        super();
    }
    public static King getInstance() {
        return king;
    }
    @Override
    public void work() {
        System.out.println("just say hello");
    }
}
package top.huyuxin.singleton;
public class Minister extends Person {
    @Override
    public void work() {
        System.out.println("do everything");
    }
}
package top.huyuxin.singleton;

public class Main {

    public static void main(String[] args) {
        King king = King.getInstance();
        King king2  = King.getInstance();

        Minister minister=new Minister();
        Minister minister2=new Minister();

        System.out.println("king:"+king);
        System.out.println("king2:"+king2);
        System.out.println("minister:"+minister);
        System.out.println("minister2:"+minister2);
    }

}

我们可以看下因为King的构造器被声明为private,我们只能通过静态方法getInstance来获取King的对象,而Minister则可以通过构造来new获得.通过内存地址的打印,我们可以判断出他们是否是同一个对象,结果显而易见:

king:top.huyuxin.singleton.King@770848b9
king2:top.huyuxin.singleton.King@770848b9
minister:top.huyuxin.singleton.Minister@40dea6bc
minister2:top.huyuxin.singleton.Minister@5994a1e9

懒汉模式

在分析饿汉模式之前我们先来看下懒汉模式,再来分析这是两个最为简单实现:

package top.huyuxin.singleton;
/**
 * 懒汉模式
 */
public class King extends Person {

    private static King king;
    private King() {
        super();
    }
    public static King getInstance() {
        if(king==null){
            king=new King();
        }
        return king;
    }
    @Override
    public void work() {
        System.out.println("just say hello");
    }
}

就像名字一样饿汉模式一上来就直接创建一个对象,不管你要不要,懒汉模式则是你需要才会创建对象.这样在初始化内容比较少时我们可以考虑饿汉模式,这样可以在初始化时便可得到,在后面使用方便,如果初始化内容比较多那么可以放在后面需要的时候初始化,以免初始化后面却没用到,浪费资源.
有的同学说,当两个线程同时来获取单例对象上面的都不能保证获取到的是同一个,getInstance()方法不是一个原子操作.
懒汉模式的改进:

package top.huyuxin.singleton;
/**
 * 改良懒汉模式
 */
public class King extends Person {

    private static King king;
    private King() {
        super();
    }
    public static synchronized King getInstance() {
        if(king==null){
            king=new King();
        }
        return king;
    }
    @Override
    public void work() {
        System.out.println("just say hello");
    }
}

将getInstance()用声明为synchronized同步操作,这样能一定程度的避免多线程同时获取单例对象的问题,但是却还不够并且每次执行getInstance()方法都是一个同步操作,这是不能忍受的.并且在高并发下这种方式也是形同虚设.我们下面会分析.

Double Check Lock(DCL)模式

package top.huyuxin.singleton;
/**
 - DCL模式
 */
public class King extends Person {

    private static King king;
    private King() {
        super();
    }
    public static King getInstance() {
        if(king==null){
            synchronized (King.class) {
                if(king==null){
                    king=new King();
                }
            }
        }
        return king;
    }
    @Override
    public void work() {
        System.out.println("just say hello");
    }
}

通过两层的判空,从而使得当单例对象不为空时不用被加锁直接返回,里面那层判空则是为了确保king在为空才会new,很多人会说,这不是多此一举吗?外层不是已经判空了?这主要是 king=new King();操作不是原子操作,这一句代码在编译成汇编指令时大致分为三步:
- 给单例的实例分配内存
- 通过调用单例的构造,初始化成员字段
- 将单例对象指向分配的内存空间(这里单例对象已经不为空)
但是由于编译器允许处理器乱序执行,以及java1.5之前JVM(java内存模型)中对于二三步写入的顺序是无法保证的,所以还是有可能导致DCL失效,这是让人十分难以接受的,于是我们想到了一个关键字volatile,声明单例对象的时候,将其用volatile修饰,如 private volatile static King king;那么我们通过修改得到是相对于完善的DCL模式.

package top.huyuxin.singleton;
/**
 * DCL模式
 */
public class King extends Person {

    private volatile static King king =null;
    private King() {
        super();
    }
    public static King getInstance() {
        if(king==null){
            synchronized (King.class) {
                if(king==null){
                    king=new King();
                }
            }
        }
        return king;
    }
    @Override
    public void work() {
        System.out.println("just say hello");
    }
}

通过DCL模式我们能一定程度的解决资源消耗,多余同步,线程安全问题.但是通过volatile来修饰单例对象从而达到一种打补丁式的一定程度的修复DCL失效的问题,这是让人十分难受的.

静态内部类单例模式

package top.huyuxin.singleton;
/**
 * 静态内部类单例模式
 */
public class King extends Person {
    private King() {
        super();
    }
    public static King getInstance(){
        return KingHolder.mInstance;
    }
    private static class KingHolder{
        private static final King mInstance=new King();
    }
    @Override
    public void work() {
        System.out.println("just say hello");
    }
}

通过这种方式避免了懒汉模式那种类加载就被初始化的尴尬处境,只有在调用getInstance的时候才会获取单例对象.这种方式既能保证线程安全,也使得单例的初始化进程得以推迟,初始化时序可控.但是这种就完美了吗?no..no..no..!
在工作中将对象序列化用户网络传输与持久化是十分常见的场景,我们将King实现(implements) Serializable接口,然后将其序列化后保存到文件(持久化),再通过反序列化,将文件内的对象再读取出来,看下还是不是同一个对象

/**
*对象的序列化与反序列化
**/
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {

            File file = new File("king.txt");  
            ObjectOutputStream oout = new ObjectOutputStream(new FileOutputStream(file));  
            oout.writeObject(King.getInstance()); // 保存单例对象  
            oout.close();  

            ObjectInputStream oin = new ObjectInputStream(new FileInputStream(file));  
            Object newKing = oin.readObject();  
            oin.close();  
            System.out.println(King.getInstance()); 
            System.out.println(newKing);  

            System.out.println(King.getInstance() == newKing); 
    }

结果却是让人失望的:

top.huyuxin.singleton.King@336bc75c
top.huyuxin.singleton.King@46af2a50
false

他们不是同一个对象,这就让我想到了,把乌鸡国国王藏到井里.跳出个妖精.可怕可怕!
那能改吗?当然能,java支持自定义的序列化与反序列化,最简单的方式就是重写readResolve()方法

package top.huyuxin.singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * 静态内部类单例模式优化版
 */
public class King extends Person implements Serializable {
    private King() {
        super();
    }

    public static King getInstance(){
        return KingHolder.mInstance;
    }

    private static class KingHolder{
        private static final King mInstance=new King();
    }

     private Object readResolve() throws ObjectStreamException {  
            return KingHolder.mInstance;  
     } 

    @Override
    public void work() {
        System.out.println("just say hello");
    }

}

结果就是可以了,即使反序列化也是同一个国王.完美!完美??呸这和之前的用volatile修饰单例对象有什么区别?不过话说回来,静态内部类方式是推荐的单例实现方案.

枚举单例模式

我们在序列化对象的时候我们会发现在除了基础数据类型之外,String,数组,Enum,或者实现Serializable接口可以被序列化,从java的序列化部分实现代码就可以看出

private void writeObject0(Object obj, boolean unshared) throws IOException {  
      ...
    if (obj instanceof String) {  
        writeString((String) obj, unshared);  
    } else if (cl.isArray()) {  
        writeArray(obj, desc, unshared);  
    } else if (obj instanceof Enum) {  
        writeEnum((Enum) obj, desc, unshared);  
    } else if (obj instanceof Serializable) {  
        writeOrdinaryObject(obj, desc, unshared);  
    } else {  
        if (extendedDebugInfo) {  
            throw new NotSerializableException(cl.getName() + "\n" 
                    + debugInfoStack.toString());  
        } else {  
            throw new NotSerializableException(cl.getName());  
        }  
    }  
    ...  
} 

而我们知道Enum(枚举)这种类型比较有意思,就是他就像类一样可以有自己的字段以及方法,那么就用枚举来实现序列化不是可以防止出现反序列化new新的单例对象的问题?是的.

/**
 * 枚举单例
 */
public enum King  {
    INSTANCE;
    public static  int age;
    public  static void work(){
        System.out.println("age: "+age);
    }

}

简洁,线程安全.即使反序列化也是同一个对象,问题就是里面的字段和方法得是静态的.而且因为是单继承,Enum(枚举)类型是已经默认继承了Enum.所以不能继承其他类.日!!!!!!!!!!!!!!!!!!!!!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值