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

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

概述

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

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

特点

  • 私有构造方法,只能有一个实例。
  • 私有静态引用指向自己实例,必须是自己在内部创建的唯一实例。
  • 单例类给其它对象提供的都是自己创建的唯一实例

案例

  • 在计算机系统中,内存、线程、CPU等使用情况都可以再任务管理器中看到,但始终只能打开一个任务管理器,它在Windows操作系统中是具备唯一性的,因为弹多个框多次采集数据浪费性能不说,采集数据存在误差那就有点逗比了不是么…
  • 每台电脑只有一个打印机后台处理程序
  • 线程池的设计一般也是采用单例模式,方便对池中的线程进行控制

注意事项

  • 实现方式种类较多,有的非线程安全方式的创建需要特别注意,且在使用的时候尽量根据场景选取较优的,线程安全了还需要去考虑性能问题。
  • 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
  • 没有抽象层,扩展有困难。
  • 职责过重,在一定程度上违背了单一职责原则
  • 使用时不能用反射模式创建单例,否则会实例化一个新的对象

解锁姿势

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

public class LazyLoadBalancer {

    private static LazyLoadBalancer loadBalancer;
    private List<String> 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(欢迎调戏)

Lazy-Balancer 项目起源于好哥们需要一个 7 层负载均衡器,无奈商业负载均衡器成本高昂,操作复杂。又没有特别喜欢(好看,好用)的开源产品,作为一名大 Ops 怎么能没有办法?正好最近在看 Django 框架,尝试自己给 Nginx 画皮,项目诞生!非专业开发,代码凑合看吧。 项目基于 Django   AdminLTE 构建,在 Ubuntu 14.04 上测试通过;为了保证良好的兼容性,请使用 Chrome 浏览器。 因为增加了 iptables 自动控制,所以暂不支持 docker 方部署;需要本地测试的同请使用 vagrant 方 为了后续扩展方便,请大家使用 Tengine 替代 Nginx 服务 项目地址 GITHUB - https://github.com/v55448330/lazy-balancer 码云 - http://git.oschina.net/v55448330/lazy-balancer OSCHINA - http://www.oschina.net/p/nginx-balancer 更新 将 Nginx 更换为 Tengine 以提供更灵活的功能支持以及性能提升 新增 HTTP 状态码方检测后端服务器,默认 TCP 方 新增 HTTP 状态码方支持查看后端服务器状态 修复因前方有防火墙导致无法获取后端服务器状态 修复因主机头导致后端服务器探测失败 新增自定义管理员用户 新增配置通过文件备份和还原 新增实查看访问日志和错误日志 新增实请求统计 更新 Vagrantfile 修复其他 Bug 功能 Nginx 可视化配置 Nginx 负载均衡(反向代理)配置 Nginx 证书支持 系统状态监测 自动维护防火墙规则(白名) 支持 TCP 被动后端节点宕机检测 支持 HTTP 主动后端节点宕机检测 运行 克隆代码 mkdir -p /app git clone https://github.com/v55448330/lazy-balancer.git /app/lazy_balancer cd /app/lazy_balancer 卸载 nginx apt-get -y purge nginx-* nginx* apt-get -y autoremove 安装 tengine git submodule update --init --recursive cd resource/nginx/tengine apt-get install -y build-essential libssl-dev libpcre3 libpcre3-dev zlib1g-dev ./configure --user=www-data --group=www-data --prefix=/etc/nginx --sbin-path=/usr/sbin --error-log-path=/var/log/nginx/error.log --conf-path=/etc/nginx/nginx.conf --pid-path=/run/nginx.pid make make install mkdir -p /etc/nginx/conf.d echo "daemon off;" >> /etc/nginx/nginx.conf 安装 supervisor apt-get install supervisor update-rc.d supervisor enable 配置 supervisor cp -rf service/* /etc/supervisor/ 安装依赖 apt-get install -y python-dev python-pip iptables libcurl4-openssl-dev pip install -r requirements.txt 初始化数据库 python manage.py makemigrations python manage.py migrate 启动服务 service supervisor restart
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值