设计模式之单例模式
单例模式,是一种比较简单的设计模式,也是属于创建型模式(提供一种创建对象的模式或者方式)。
要点:
**1.**涉及一个单一的类,这个类来创建自己的对象(不能在其他地方重写创建方法,初始化类的时候创建或者提供私有的方法进行访问或者创建,必须确保只有单的对象被创建)。
**2.**单例模式不一定是线程不安全的。
**3.**单例模式可以分为两种:懒汉模式(在第一次使用类的时候才创建,可以理解为类加载的时候特别懒,要用的时候才去获取,要是没有就创建,由于是单例,所以只有第一次使用的时候没有,创建后就可以一直用同一个对象),饿汉模式(在类加载的时候就已经创建,可以理解为饿汉已经饿得饥渴难耐,肯定先把资源紧紧拽在自己手中,所以在类加载的时候就会先创建实例)
关键字:
- 单例:singleton
- 实例:instance
- 同步: synchronized
饿汉模式(2种):
第一种single是public,可以直接通过Singleton类名来访问
public class Singleton { //私有化构造方法,以防止外界使用该构造方法创建新的实例 private Singleton(){ } //默认是public,访问可以直接通过Singleton.instance来访问 static Singleton instance = new Singleton();}
,第二种是用private修饰singleton,那么就需要提供static 方法来访问。
public class Singleton { private Singleton(){ } //使用private修饰,那么就需要提供get方法供外界访问 private static Singleton instance = new Singleton(); //static将方法归类所有,直接通过类名来访问 public static Singleton getInstance(){ return instance;. }}
饿汉模式,这样的写法是没有问题的,不会有线程安全问题,但是是有缺点的,因为instance的初始化是在类加载的时候就在进行的,所以类加载是由ClassLoader来实现的,那么初始化得比较早好处是后来直接可以用,坏处也就是浪费了资源,要是只是个别类使用这样的方法,依赖的数据量比较少,那么这样的方法也是一种比较好的单例方法。
在单例模式中一般是调用getInsta()方法来触发类装载,以上的两种饿汉模式显然没有实现lazyload(个人理解是用的时候才触发类加载)
所以下面有一种饿汉模式的改进版,利用内部类实现懒加载
这种方式Singleton类被加载了,但是instance也不一定被初始化,要等到SingletonHolder被主动使用的时候,也就是显式调用getInstance方法的时候,才会显式的装载SingletonHolder类,从而实例化instance。这种方法使用类装载器保证了只有一个线程能够初始化instance,那么也就保证了单例,并且实现了懒加载。静态内部类虽然保证了单例在多线程并发下的线程安全性,但是在遇到序列化对象时,默认的方式运行得到的结果就是多例的。
public class Singleton { private Singleton(){ } //内部类 private static class SingletonHolder{ private static final Singleton instance = new Singleton(); } //对外提供的不允许重写的获取方法 public static final Singleton getInstance(){ return SingletonHolder.instance; }}
懒汉模式(2种):
最基础的代码**(线程不安全)**:
public class Singleton { private static Singleton instance = null; private Singleton(){ } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}
这种写法,是在每次获取实例instance的时候进行判断,如果没有那么就会new一个出来,否则就直接返回之前已经存在的instance。但是这样的写法不是线程安全的,当有多个线程都执行getInstance()方法的时候,都判断都是否等于null的时候,就会各自创建新的实例,这样就不能保证单例了。所以我们就会想到同步锁,使用synchronized关键字:
加同步锁的代码**(线程安全,效率不高)**
public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } return instance; }}
这样的话,getInstance()方法就会被锁上,当有两个线程同时访问这个方法的时候,总会有一个线程先获得了同步锁,那么这个线程就可以执行下去,而另一个线程就必须等待,等待第一个线程执行完getInstance()方法之后,才可以执行。这段代码是线程安全的,但是效率不高,因为假如有很多线程,那么就必须让所有的都等待正在访问的线程,这样就会大大降低了效率。那么我们有一种思路就是,将锁出现的概率再降低,也就是我们所说的双重检查。
public class Singleton { private static Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null){ synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } } return instance; }}
**1.**第一个if判断,是为了降低锁的出现概率,前一段代码,只要执行到同一个方法都会触发锁,而这里只有singleton为空的时候才会触发,第一个进入的线程会创建对象,等其他线程再进入时对象已创建就不会继续创建,如果对整个方法同步,所有获取单例的线程都要排队,效率就会降低。
**2.**第二个if判断是和之前的代码起一样的作用。
上面的代码看起来已经像是没有问题了,事实上,还有有很小的概率出现问题,那么我们先来了解:原子操作,指令重排。
1.原子操作
- 原子操作,可以理解为不可分割的操作,就是它已经小到不可以再切分为多个操作进行,那么在计算机中要么它完全执行了,要么它完全没有执行,它不会存在执行到中间状态,可以理解为没有中间状态。比如:赋值语句就是一个原子操作:
n = 1; //这是一个原子操作
假设n的值以前是0,那么这个操作的背后就是要么执行成功n等于1,要么没有执行成功n等于0,不会存在中间状态,就算是并发的过程中也是一样的。
下面看一句不是原子操作的代码:
int n =1;//不是原子操作
**原因:**这个语句中可以拆分为两个操作,1.声明变量n,2.给变量赋值为1,从中我们可以看出有一种状态是n被声明后但是没有来得及赋值的状态,这样的情况,在并发中,如果多个线程同时使用n,那么就会可能导致不稳定的结果。
2.指令重排
所谓指令重排,就是计算机会对我们代码进行优化,优化的过程中会在不影响最后结果的前提下,调整原子操作的顺序。比如下面的代码:
int a ; // 语句1 a = 1 ; // 语句2int b = 2 ; // 语句3int c = a + b ; // 语句4
正常的情况,执行顺序应该是1234,但是实际有可能是3124,或者1324,这是因为语句3和4都没有原子性问题,那么就有可能被拆分成原子操作,然后重排.
原子操作以及指令重排的基本了解到这里结束,看回我们的代码:
主要是instance = new Singleton(),根据我们所说的,这个语句不是原子操作,那么就会被拆分,事实上JVM(java虚拟机)对这个语句做的操作:
1.给instance分配了内存
2.调用Singleton的构造函数初始化了一个成员变量,产生了实例,放在另一处内存空间中
3.将instance对象指向分配的内存空间,执行完这一步才算真的完成了,instance才不是null。
在一个线程里面是没有问题的,那么在多个线程中,JVM做了指令重排的优化就有可能导致问题,因为第二步和第三步的顺序是不能够保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回instance,然后使用,就会报空指针。
从更上一层来说,有一个线程是instance已经不为null但是仍没有完成初始化中间状态,这个时候有一个线程刚刚好执行到第一个if(instance==null),这里得到的instance已经不是null,然后他直接拿来用了,就会出现错误。
对于这个问题,我们使用的方案是加上volatile关键字。
public class Singleton { private static volatile Singleton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null){ synchronized(Singleton.class){ if (instance == null) instance = new Singleton(); } } return instance; }}
volatile的作用:禁止指令重排,把instance声明为volatile之后,这样,在它的赋值完成之前,就不用会调用读操作。也就是在一个线程没有彻底完成instance = new Singleton();之前,其他线程不能够去调用读操作。
- 上面的方法实现单例都是基于没有复杂序列化和反射的时候,否则还是有可能有问题的,还有最后一种方法是使用枚举来实现单例,这个可以说的比较理想化的单例模式:
public enum Singleton { INSTANCE; public void doSomething() { }}
此文章仅代表自己(本菜鸟)学习积累记录,或者学习笔记,如有侵权,请联系作者删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有错误之处,还望指出,感激不尽~
技术之路不在一时,山高水长,纵使缓慢,驰而不息。
公众号:秦怀杂货店

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
白话运维监控系统-1.1 运维监控系统概述
写在前面 笔者从Open-Falcon开源到现在,从事运维监控领域相关工作差不多7年,在做Open-Falcon和Nightingale的社区答疑过程中发现,有大量的小白问题,很多是因为对这个领域缺乏基础认识,所以,想写一个针对入门级用户的系列文章,做一下知识的普及。 另外,监控这个事情,其实也是研发人员走到某个段位之后必须要了解的。因为监控是稳定性体系建设中最重要的一环,普通研发人员往架构师转变,需要了解更多横向的知识,比如持续集成、服务治理、稳定性保障等等,所以了解一下监控,也很有必要。 这是一个很公益的事情,希望大家一起参与讨论,分享经验,为小白领路,功德无量~ 监控的价值 监控是保障业务稳定性的重要手段,那怎么提升稳定性呢?简单来说,就是减少故障,一个是减少故障的数量,一个是减少单一故障的影响时长,即出现故障后快速止损。减少故障这个方面,更多的要诉诸于鲁棒的业务系统架构和稳定的基础设施,监控在这个方面没有办法提供太多助力。对于减少单一故障的影响时长,监控是非常有价值的。 在出现故障时,监控系统可以及时感知,及时发告警通知相关人员,让值班的人快速响应,处理故障。处理故障的第一步就...
- 下一篇
hive-19994 bug java.sql.BatchUpdateException
事件背景: 大数据应用跑业务时候,涉及对hive表删除操作。删除过程中,会引起hive metastore报错:java.sql.BatchUpdateException: Cannot delete or update a parent row: a foreign key constraint fails ("hivemetastore_emtig3vtq7qp1tiooo07sb70ud"."COLUMNS_V2", CONSTRAINT "COLUMNS_V2_FK1" FOREIGN KEY ("CD_ID") REFERENCES "CDS" ("CD_ID"))。 问题分析: 此为hive 的一个bug,详细信息如 https://issues.apache.org/jira/browse/HIVE-19994 hive的表元数据存储在mysql中,mysql中两表之间有外键约束(关于mysql外键约束详见下文),导致无法删除。 mysql 外键约束介绍: l RESTRICT: 拒绝删除或者更新父表。指定RESTRICT(或者NO ACTION)和忽略ON DELETE...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Hadoop3单机部署,实现最简伪集群
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS8编译安装MySQL8.0.19
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16