深入分析单例模式

单例模式,是设计模式中最常见的一种设计模式,也是面试官最喜欢考察的模式。那什么是单例模式,这里我就不在赘述了,百度一大堆。

那么如何写一个好的单例模式呢?网上最经典的就是那七种单例模式。但是基本都是浅显地解释为什么要那样写单例,如何保证线程安全。但是对于单例的编写,我们不仅仅需要知道可以那样写,还应该知道为什么可以那样写,还应该知道怎么想出可以那样写的。这样我相信才会对童鞋们帮助更大。

第一层就是最简单的懒汉模式

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

这种写法是普遍都知道的入门级。这种写法的目的是延迟加载,当instance没有初始化的时候才进行初始化。但是毫无疑问,无法保证线程安全的。为什么? 当然是因为现在假如有两个线程A,B。线程A执行代码1的同时,线程B执行代码2。这会出现什么问题?

实例的创建,主要分三步:

  1. 分配对象的内存空间
  2. 初始化对象,进行一下赋值操作
  3. 将实例的引用指向刚才分配的内存地址

JMM在进行实例创建的时候,只会保证intra-thread-semantics,即只保证在单线程中,不改变程序的执行结果(as-if-serial)。我们知道,为了进一步优化程序的执行,编译器或者处理器会进行指令的重排。也就说在上面的三个步骤中,2,3两个步骤可能会发生重排,可能具体执行的顺序为3,2。intra-thread-semantics只保证单线程执行正确,所以会允许这样的重排。

那么这样就带来了问题。假如进行了重排,先3再2,那么当线程B在执行了2,先给instance赋值后,然后执行3的时候,此时线程A看到instance已经被赋值了,那么就会认为对象已经创建完成,然后使用该instance。但是实际上,该instance还没完成初始化。这就造成了问题。

因为指令的重排,这种写法无法保证线程安全。那么我们从指令重排的角度来考虑怎么解决。既然是因为2,3步骤重排,那么可以用两个方法解决:1.即使发生了重排,那我们只要保证在执行期间,其他线程看不到这个重排。2.既然是因为重排导致的问题,那就禁止2,3重排好了。

解决思路一:禁止重排

重排不可见,解决方法很多。

法一

直接在方法层上加锁。

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

这样就保证了线程安全。但是,假如getInstance的调用很频繁,每次都会去竞争锁。那就很尴尬了。因为锁的竞争是需要开销的。虽然说synchronized经过优化,在java8里开销已经比较小了,在高并发场景下吞吐相对于lock也小了。但是仍然是会有开销的。

因此机智的人们就想到了很经典的双重检查锁,来减少锁的竞争。
于是就有了这样的代码:

public class Singleton {  
    private  static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}  

ok,这样的话,只有在还没有创建实例之前需要竞争锁去创建。但,这样的修改,线程仍然是安全的吗?大家看看先代码想一下。好吧,我知道大部分人不会想,那就直接说。

虽然上面的代码减少了锁的竞争,但却也把原来的线程安全又给搞得不安全了。仍然是实例还没有初始化结束,另一个线程就能读到。原因和上面讲的一样。不过有的童鞋会问:“你不是已经用了synchronized (Singleton.class) 吗?那这个synchronized还存在的意义是啥”?

因为(需要待求证)

那怎么办?

法二

那我们的解决方法,就是在instance上加volatile。需要在JDK5+上运行。代码如下

完整代码

public class Singleton {  
    private  volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }  
            }  
        }  
        return singleton;  
    }  
}  

这样的话,2,3的重排序就会被禁止。

解决思路二:重排对其他线程不可见

这个思路,我们需要利用JVM的类的初始化的特性。我们知道,在初始化一个类的时候,为了防止多个线程同时对一个类做初始化,JVM会利用锁。在执行类的初始化之前呢,先获取锁,然后去初始化。那么我们做延迟加载,在类需要的时候才去初始化,就可以利用这个特性了。

不过类的初始化,是在某些场景下才会调用初始化的。比如说创建一个类的instance的时候,比如类中的静态方法被调用了,比如类中的静态方法被赋值了等等。那这里呢,我们采用静态类的形式来做。代码如下

public class Singleton {  
    private static class InstanceHolder{
        public static Instance instance = new Instance();
    }
    public static Singleton getSingleton() {  
         return InstanceHolder.instance;  
    }  
}  

上面的代码中,当getSingleton的时候,会调用静态成员变量,这就会触发类的初始化,转而去new 一个Instance。利用JVM初始化会加锁,就可以达到即使内部重排,也不会影响锁外面的情况。使得其他线程不会看到非法的情况。

这样,就通过两个角度,解决了单例中的线程不安全的问题了。那这两种方案其实都可以,不过基于volatile的方法更加灵活,除了可以对静态变量做延迟加载,也可以对实例对象做延迟加载。但是一般来讲,采用延迟加载可能比正常加载效率更低一些,会增加访问延迟初始化的字段的开销。因此需要谨慎使用。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Elasticsearch是一个开源的分布式搜索和分析引擎,用于处理大规模数据集的实时搜索和分析。它基于Lucene库构建而成,提供了强大的全文搜索、分布式性能和可扩展性。 在简单运用Elasticsearch之前,你需要先安装和配置Elasticsearch。安装完成后,你可以使用Elasticsearch提供的RESTful API进行索引数据、搜索和聚合操作。 下面是一个简单的例子,演示如何使用Elasticsearch进行索引和搜索操作: 1. 导入Elasticsearch客户端库(例如elasticsearch-py): ```python from elasticsearch import Elasticsearch ``` 2. 创建Elasticsearch客户端实例: ```python es = Elasticsearch('http://localhost:9200') ``` 3. 创建索引: ```python index_name = 'my_index' mapping = { "properties": { "title": {"type": "text"}, "content": {"type": "text"} } } es.indices.create(index=index_name, body={"mappings": mapping}) ``` 4. 索引文档: ```python document = { "title": "Elasticsearch Introduction", "content": "Elasticsearch is a distributed search engine." } es.index(index=index_name, body=document) ``` 5. 搜索文档: ```python query = { "query": { "match": {"content": "distributed"} } } result = es.search(index=index_name, body=query) print(result['hits']['hits']) ``` 以上是一个简单的示例,演示了如何使用Elasticsearch进行索引和搜索操作。你可以根据自己的需求进一步深入学习和了解Elasticsearch的其他功能和用法。 关于枚举实现单例模式的问题,你可以使用Python中的枚举(Enum)来实现单例模式。以下是一个示例: ```python from enum import Enum class Singleton(Enum): INSTANCE = 1 # 使用方式 instance = Singleton.INSTANCE ``` 在这个示例中,我们定义了一个枚举Singleton,它只有一个枚举成员INSTANCE。通过访问INSTANCE成员,我们可以获取到单例对象的实例。 需要注意的是,Python的枚举并不是传统意义上的单例模式,它只是提供了一种简洁明了的方式来实现单例。如果你需要更复杂的单例模式实现,可以考虑使用装饰器、元或其他方式来实现。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值