资讯专栏INFORMATION COLUMN

一起学设计模式 - 单例模式

Keven / 2052人阅读

摘要:懒汉非线程安全,需要用一定的风骚操作控制,装逼失败有可能导致看一周的海绵宝宝饿汉天生线程安全,的时候就已经实例化好,该操作过于风骚会造成资源浪费单例注册表初始化的时候,默认单例用的就是该方式特点私有构造方法,只能有一个实例。

单例设计模式(Singleton Pattern)是最简单且常见的设计模式之一,主要作用是提供一个全局访问且只实例化一次的对象,避免多实例对象的情况下引起逻辑性错误(实例化数量可控)...

概述

Java中,单例模式主要分三种:懒汉式单例、饿汉式单例、登记式单例三种。

懒汉:非线程安全,需要用一定的风骚操作控制,装逼失败有可能导致看一周的海绵宝宝

饿汉:天生线程安全,ClassLoad的时候就已经实例化好,该操作过于风骚会造成资源浪费

单例注册表:Spring初始化Bean的时候,默认单例用的就是该方式

特点

私有构造方法,只能有一个实例。

私有静态引用指向自己实例,必须是自己在内部创建的唯一实例。

单例类给其它对象提供的都是自己创建的唯一实例

案例

在计算机系统中,内存、线程、CPU等使用情况都可以再任务管理器中看到,但始终只能打开一个任务管理器,它在Windows操作系统中是具备唯一性的,因为弹多个框多次采集数据浪费性能不说,采集数据存在误差那就有点逗比了不是么...

每台电脑只有一个打印机后台处理程序

线程池的设计一般也是采用单例模式,方便对池中的线程进行控制

注意事项

实现方式种类较多,有的非线程安全方式的创建需要特别注意,且在使用的时候尽量根据场景选取较优的,线程安全了还需要去考虑性能问题。

不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。

没有抽象层,扩展有困难。

职责过重,在一定程度上违背了单一职责原则

使用时不能用反射模式创建单例,否则会实例化一个新的对象

解锁姿势

第一种:单一检查(懒汉)非线程安全

public class LazyLoadBalancer {

    private static LazyLoadBalancer loadBalancer;
    private List servers = null;

    private LazyLoadBalancer() {
        servers = new ArrayList<>();
    }

    public void addServer(String server) {
        servers.add(server);
    }

    public String getServer() {
        Random random = new Random();
        int i = random.nextInt(servers.size());
        return servers.get(i);
    }

    public static LazyLoadBalancer getInstance() {
        // 第一步:假设T1,T2两个线程同时进来且满足 loadBalancer == null
        if (loadBalancer == null) {
            // 第二步:那么 loadBalancer 即会被实例化2次
            loadBalancer = new LazyLoadBalancer();
        }
        return loadBalancer;
    }

    public static void main(String[] args) {
        LazyLoadBalancer balancer1 = LazyLoadBalancer.getInstance();
        LazyLoadBalancer balancer2 = LazyLoadBalancer.getInstance();
        System.out.println("hashCode:"+balancer1.hashCode());
        System.out.println("hashCode:"+balancer2.hashCode());
        balancer1.addServer("Server 1");
        balancer2.addServer("Server 2");
        IntStream.range(0, 5).forEach(i -> System.out.println("转发至:" + balancer1.getServer()));
    }
}

日志

hashCode:460141958
hashCode:460141958
转发至:Server 2
转发至:Server 2
转发至:Server 2
转发至:Server 1
转发至:Server 2

分析: 在单线程环境一切正常,balancer1balancer2两个对象的hashCode一模一样,由此可以判断出堆栈中只有一份内容,不过该代码块中存在线程安全隐患,因为缺乏竞争条件,多线程环境资源竞争的时候就显得不太乐观了,请看上文代码注释内容

第二种:无脑上锁(懒汉)线程安全,性能较差,第一种升级版

public synchronized static LazyLoadBalancer getInstance() {
    if (loadBalancer == null) {
        loadBalancer = new LazyLoadBalancer();
    }
    return loadBalancer;
}

分析: 毫无疑问,知道synchronized关键字的都知道,同步方法在锁没释放之前,其它线程都在排队候着呢,想不安全都不行啊,但在安全的同时,性能方面就显得短板了,我就初始化一次,你丫的每次来都上个锁,不累的吗(没关系,它是为了第三种做铺垫的)..

第三种:双重检查锁(DCL),完全就是前两种的结合体啊,有木有,只是将同步方法升级成了同步代码块

//划重点了 **volatile**
private volatile static LazyLoadBalancer loadBalancer;

public static LazyLoadBalancer getInstance() {
    if (loadBalancer == null) {
        synchronized (LazyLoadBalancer.class) {
            if (loadBalancer == null) {
                loadBalancer = new LazyLoadBalancer();
            }
        }
    }
    return loadBalancer;
}

1.假设new LazyLoadBalancer()加载内容过多
2.因重排而导致loadBalancer提前不为空
3.正好被其它线程观察到对象非空直接返回使用

mem = allocate();                  //LazyLoadBalancer 分配内存
instance = mem;                     //注意当前实例已经不为空了                      
initByLoadBalancer(instance);      //但是还有其它实例未初始化

存在问题: 首先我们一定要清楚,DCL是不能保证线程安全的,稍微了解过JVM的就清楚,对比C/C++它始终缺少一个正式的内存模型,所以为了提升性能,它还会做一次指令重排操作,这个时候就会导致loadBalancer提前不为空,正好被其它线程观察到对象非空直接返回使用(但实际还有部分内容没加载完成)

解决方案:volatile修饰loadBalancer,因为volatile修饰的成员变量可以确保多个线程都能够顺序处理,它会屏蔽JVM指令重排带来的性能优化

volatile详解:http://blog.battcn.com/2017/10/18/java/thread/thread-volatile/

第四种:Demand Holder (懒汉)线程安全,推荐使用

private LazyLoadBalancer() {}

private static class LoadBalancerHolder {
    //在JVM中 final 对象只会被实例化一次,无法修改
    private final static LazyLoadBalancer INSTANCE = new LazyLoadBalancer();
}

public static LazyLoadBalancer getInstance() {
    return LoadBalancerHolder.INSTANCE;
}

分析:Demand Holder中,我们在LazyLoadBalancer里增加一个静态(static)内部类,在该内部类中创建单例对象,再将
该单例对象通过getInstance()方法返回给外部使用,由于静态单例对象没有作为LazyLoadBalancer的成员变量直接实例化,类加载时并不会实例化LoadBalancerHolder,因此既可以实现延迟加载,又可以保证线程安全,不影响系统性能(居家旅行必备良药啊)

第五种:枚举特性(懒汉)线程安全

enum Lazy {
    INSTANCE;
    private LazyLoadBalancer loadBalancer;

    //枚举的特性,在JVM中只会被实例化一次
    Lazy() {
        loadBalancer = new LazyLoadBalancer();
    }

    public LazyLoadBalancer getInstance() {
        return loadBalancer;
    }
}

分析: 相比上一种,该方式同样是用到了JAVA特性:枚举类保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)

第六种:饿汉单例(天生线程安全),

public class EagerLoadBalancer {
    private final static EagerLoadBalancer INSTANCE = new EagerLoadBalancer();

    private EagerLoadBalancer() {}

    public static EagerLoadBalancer getInstance() {
        return INSTANCE;
    }
}

分析: 利用ClassLoad机制,在加载时进行实例化,同时静态方法只在编译期间执行一次初始化,也就只有一个对象。使用的时候已被初始化完毕可以直接调用,但是相比懒汉模式,它在使用的时候速度最快,但这玩意就像自己挖的坑哭着也得跳,你不用也得初始化一份在内存中占个坑...

- 说点什么

全文代码:https://gitee.com/battcn/design-pattern/tree/master/Chapter2/battcn-singleton

个人QQ:1837307557

battcn开源群(适合新手):391619659

微信公众号:battcn(欢迎调戏)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67876.html

相关文章

  • java并发编程习之Volatile

    摘要:但是的语义不足以确保递增操作的原子性,在多线程的情况下,线程不一定是安全的。检查某个状态标记,以判断是否退出循环某个方法这边和用普通的变量的区别是,在多线程的情况下,取到后,的值被改变了,判断会不正确。 多线程为什么是不安全的 这边简单的讲述一下,参考java并发编程学习之synchronize(一) 当线程A和线程B同时进入num = num + value; 线程A会把num的值...

    thekingisalwaysluc 评论0 收藏0
  • 一起设计模式 - 享元模式

    摘要:享元模式属于结构型模式的一种,又称轻量级模式,通过共享技术有效地实现了大量细粒度对象的复用概述两种结构状态内部状态享元对象内部不随外界环境改变而改变的共享部分。 享元模式(Flyweight Pattern)属于结构型模式的一种,又称轻量级模式,通过共享技术有效地实现了大量细粒度对象的复用... 概述 两种结构状态 内部状态:享元对象内部不随外界环境改变而改变的共享部分。 外部状态...

    Jason 评论0 收藏0
  • python之单例模式和工厂模式

    摘要:在工厂方法模式中,我们会遇到一个问题,当产品非常多时,继续使用工厂方法模式会产生非常多的工厂类。从简单工厂模式到抽象工厂模式,我们都是在用后一种模式解决前一种模式的缺陷,都是在最大程度降低代码的耦合性。 单例模式 所谓单例模式,也就是说不管什么时候我们要确保只有一个对象实例存在。很多情况下,整个系统中只需要存在一个对象,所有的信息都从这个对象获取,比如系统的配置对象,或者是线程池。这些...

    jayce 评论0 收藏0
  • 一张图带你设计模式-单例模式

    showImg(https://segmentfault.com/img/bVbkt56?w=1078&h=1760);

    zengdongbao 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<