你真的会单例模式吗?
博客专区 > EQShen 的博客 > 博客详情
你真的会单例模式吗?
EQShen 发表于1年前
你真的会单例模式吗?
  • 发表于 1年前
  • 阅读 2
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 十分钟定制你的第一个小程序>>>   

单例模式怎么写?大家都知道单例模式分为:懒汉式和饿汉式。

一、首先我们来一个非线程安全的例子:

public class SingletonTest
{
    public static void main(String ...args) throws Exception{
        MyThread ta=new MyThread();
        MyThread tb=new MyThread();
        ta.start();
        tb.start();
        ta.join();
        tb.join();
    }
}


class Singleton
{
    private static Singleton instance;
    private Singleton(){}
   
    public static Singleton getInstance(){
        if(instance ==null){
            instance =new Singleton();
            System.out.println("new Singleton");
        }
        return instance;
    }
}


class MyThread extends Thread
{
    public MyThread(){
    }

    public void run(){
        for(int i=0;i<3;i++){
            Singleton.getInstance();
        }
    }
}

一个很简单的例子,打印结果如图,理论上new Singleton这句话应该只被打印1次的,但运行结果是有时一次、有时是两次:

image

出现这个现象的原因很简单,在new Singleton之前,多个线程进入if(instance ==null)就会导致这个现象。

二、懒汉式,线程安全的写法

public static synchronized Singleton getInstance() {

    if (instance == null) {

        instance = new Singleton();

    }

    return instance;

}

这种是在方法上加synchronized,即默认使用当前对象作为锁,而且同一时间内只有一个线程能进入该方法内。这种方式实现了真正的线程安全,但是并发的性能不高,因为在任何时候只能有一个线程调用 getInstance() 方法。

三、双重检验锁 DCL(double checked locking)

public static Singleton getSingleton() {

    if (instance == null) {                         //Single Checked

        synchronized (Singleton.class) {

            if (instance == null) {                 //Double Checked

                instance = new Singleton();

            }

        }

    }

    return instance ;

}

为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if    (这个时候多个线程都在等锁),如果在同步块内不进行二次检验的话就会生成多个实例了。那么这种方式是线程安全的吗?答案是肯定的,但还有网上还有这个说法“

这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存

  2. 调用 Singleton 的构造函数来初始化成员变量

  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。

我们只需要将 instance 变量声明成 volatile 就可以了。即private volatile static Singleton instance;

正确与否还未验证(我用这种方法写多次运行没报错,看的张振华著的《Java并发编程从入门到精通》里面说这种方法也是没问题的)。

四、饿汉式

又称静态工厂实现方法,这种方法非常简单,因为单例的实例被声明成 static 和 final 变量了,在第一次加载类到内存中时就会初始化,所以创建实例本身是线程安全的。

public class Singleton{

    //类加载时就初始化

    private static final Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){

        return instance;

    }

}

 

这种写法如果完美的话,就没必要在啰嗦那么多双检锁的问题了。缺点是它不是一种懒加载模式(lazy initialization),单例会在加载类后一开始就被初始化,即使客户端没有调用 getInstance()方法。饿汉式的创建方式在一些场景中将无法使用:譬如 Singleton 实例的创建是依赖参数或者配置文件的,在 getInstance() 之前必须调用某个方法设置参数给它,那样这种单例写法就无法使用了。

五、静态内部类

静态内部类只有在return SingletonHolder.INSTANCE时才被调用,且只调用一次,Think in java里面推荐这种写法

public class Singleton { 

    private static class SingletonHolder { 

        private static final Singleton INSTANCE = new Singleton(); 

    } 

    private Singleton (){} 

    public static final Singleton getInstance() { 

        return SingletonHolder.INSTANCE;

    } 

}

 

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

六、枚举

一种非常简单的写法,Effective Java中推荐的。

public enum Singleton {

INSTANCE;

public void whateverMethod() { }

}

参考一些文章做的笔记,如有错误请及时指出,谢谢。

共有 人打赏支持
粉丝 4
博文 19
码字总数 15673
×
EQShen
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: