600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > 设计模式——单例模式(懒汉式与饿汉式)详解

设计模式——单例模式(懒汉式与饿汉式)详解

时间:2021-03-17 19:10:18

相关推荐

设计模式——单例模式(懒汉式与饿汉式)详解

一、什么是单例?

单例模式(Singleon),是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。

二、单例的种类有哪些?之间有什么区别?

懒汉式:指全局的单例实例在第一次被使用时构建。 饿汉式:全局的单例实例在类装载(ClassLoader)时构建。(饿汉式单例性能优于懒汉式单例)

1、懒汉式与饿汉式区别:

1.1懒汉式默认不会实例化,外部什么时候调用什么时候new。饿汉式在类加载的时候就实例化,并且创建单例对象。

1.2、懒汉式是延时加载,在需要的时候才创建对象,而饿汉式是在虚拟机启动的时候就会创建。

1.3、懒汉式在多线程中是线程不安全的,而饿汉式是不存在多线程安全问题的。

2、懒汉模式:

2.1、创建一个最简单的懒汉式单例【方法1】

//最简单的一种懒汉式单例模式public class SingleTest {//定义一个私有类变量来存放单例,私有的目的是指外部无法直接获取这个变量,而要使用提供的公共方法来获取。private static SingleTest instance; public SingleTest () {} //定义一个公共的公开的方法来返回该类的实例。public static SingleTest getInstance() {//第一次访问去创建实例if (instance == null) {instance = new SingleTest ();}//否则直接返回实例return instance;}}

注意:

上面是最简单的一种懒汉式单例,但是这样写会有一些问题需要改进:

1、构造器为public,这样外部可以调用,要改为私有的private,防止外部调用。

2、这种方式在多线程下是不安全的。 额。。 好吧!那么我们接着改进完善,让它越来越完美。

2.2、创建一个线程安全的懒汉式单例【方法2】

// 把构造函数设置为私有,并使用synchronized修饰词来修饰方法,保证线程同步从而达到线程安全。public class SingleTest {private static SingleTest instance;//定义私有构造器,表示只在类内部使用,只能在内部创建。private SingleTest () {}//使用synchronized修饰达到线程同步从而保证线程安全。public static synchronized SingleTest getInstance() {if (instance == null) {instance = new SingleTest ();}return instance;}}

注意:

以上方法修改之后解决了第一次创建遗留的两个问题:

1、修改构造函数为private,避免外部调用。

2、使用synchronized修饰方法(也就是获取对象的锁),此时就避免多线程同时访问同一个对象,保证线程同步,从而达到线程安全。

额。。看上去已经好了,但是还有问题

举个例子吧:

现在有A、B、C三个线程来访问此对象创建单例。由于线程执行顺序是由CPU心情决定的,所以不能保证谁先先访问到。

假设:

1、A线程先创建单例,getInstance()使用synchronized同步锁,这个时候A线程就获得了getInstance()的锁。(synchronized同步锁不了解的同学可以先学习下多线程中的线程同步)。

2、此时B、C线程也被CPU调度来创建单例。但是这个时候A线程已经获取了getInstance()的锁,那么B、C将无法调用getInstance()方法。B、C线程就会一直等待,直到A线程执行完毕才可以访问。

3、这样的话就造成了线程阻塞,影响性能。

2.3、让我们改变同步锁的位置试一试。。【方法3】

// 改变了synchronizaed同步锁的位置,双重检查判断public class SingleTest {private static SingleTest instance;private SingleTest () {}public static SingleTest getInstance() {//先进行实例判断,只有当不存在的时候才去创建实例。//这样就解决了99%的已经获取实例但是还要去获取同步锁的问题。if (instance == null) {//用synchronized 同步代码块//注意:此处不能用this,因为this不能和static同用。synchronized (SingleTest.class) {if (instance == null) {instance = new SingleTest();}}}//如果已经获取实例,那么直接返回就可以,不必要去获取同步锁,也就不会影响其他线程,造成线程阻塞问题。return instance;}}

感觉是不是已经不错了,也解决了上面写法的效率问题,为什么说解决了效率问题呢?不是解决是提升效率哈哈。。。那么我们来分析一下吧:

1、首先我们单例的定义和含义:“单例对象的类必须保证只有一个实例存在”。那么我们用单例其实就是为了限制一个类不能多个实例,也就是说只能被创建一次,明白这点之后我们来看【方法2】的代码。。。

2、【方法2】A、B、C三个线程来创建实例,只有第一个访问的线程才会走:if(instance)下面的代码 instance = new SingleTest();来创建实例。否则都会直接返回这个实例 return instance;。100次调用,1次new,99次直接return。创建实例的概率为1%,获取实例的概率为99%。

3、但是我们如果要像【方法2】中用synchronized同步锁来修饰getInstance()方法,那么不管是需要创建实例还是获取实例都会只能被一个线程调用,那么性能肯定会浪费很多,其实我们只关心1%创建实例。99%都是浪费性能。

4、先在判断if(instance)==null,如果是,那代表第一次来获取实例,接着我们把同步锁加在创建实例的代码块上,这样就减少了获取线程的性能消耗,只有在需要创建的时候才会加同步锁,才可能会造成线程阻塞,只有1%的情况会影响性能,而不是【方法二】100%会影响。所以性能会得到提升。否则直接返回实例 return instance。

OK,好像这样写既提升了性能,还保证了线程安全,已经很完善了,真的是这样吗?真的是安全了吗?跟着我继续看,看一看还能不能继续改进,哈哈!

2.4、让我们继续改进吧!【方法4】

//使用volatile修饰变量,具有原子性、可见性和防止指令重排的特性。public class SingleTest {private static volatile SingleTest instance;private SingleTest() {}public static SingleTest getInstance() {if (instance == null) {synchronized (SingleTest.class) {if (instance == null) {instance = new SingleTest();}}}return instance;}}

【方法4】中我们只在变量上加了一个volatile修饰词,那么为什么要这么做呢?这样做有什么好处?我们接着分析:

1、我们了解下原子操作、指令重排这两个知识点。

2、什么是原子操作呢?

简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。

小例子:

a=1;//赋值,把值1赋给a,这是原子操作。

假如m原先的值为1,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是1,而不会出现诸如m=3这种中间态——即使是在并发的线程中。

int a=1;//先声明一个变量,再把值赋给这个变量,这不是原子操作。

因为这个操作对于计算机来说是两步:

1、声明变量 int a;

2、进行赋值 a=1;

这样的话单线程情况下是没有问题的,那么在多线程下就会出现问题,因为多线程的执行顺序是不确定的。接下来大家需要了解一个新的知识点:指令重排。

3、什么是指令重排呢?

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。

4、上述案例

第一步:int a;第二步:a=1;

计算机在运行的时候不一定会按照正常的顺序1——>2步执行,它可能会是2——>1,由于计算机CPU执行存在指令重排的特性,为了提升CPU执行效率,会改变一些执行顺序,最终保证结果一直。

5、接下来我们再看我们【方法4】创建单例方法。

主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情

1、内存区域分配内存给singleton

2、 调用 Singleton 的构造函数来初始化成员变量,形成实例

3、 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null 了)

分析完之后,是不是稍微明白了点,为什么说方法【4】及时加了同步锁也不是线程安全的。

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

为了防止多线程下指令重排导致获取结果出错,所以我们通过valutile关键字修饰相关对象,禁止指令重排。

以上的话基本就是懒汉式单例模式的几种写法以及写法的深入了解。大家多多思考。

3、饿汉模式

3.1、我们来创建一个饿汉式的单例模式【方法1】

//饿汉式单例实现public class SingleTest {private static final SingleTest INSTANCE = new SingleTest();private SingleTest() {}public static SingleTest getInstance() {return INSTANCE;}}

饿汉式的写法这样就可以了,那么我们来说下饿汉式的缺点吧。

1、所以它的缺点也就只是饿汉式单例本身的缺点所在了——由于INSTANCE的初始化是在类加载时进行的,而类的加载是由ClassLoader来做的,所以开发者本来对于它初始化的时机就很难去准确把握。

2、过早的就会被实例化,可能会造成资源浪费。

3、如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好。

3.2让我们继续改进一下【方法2】

public class Singleton {//创建一个内部静态类private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();}private Singleton (){}public static final Singleton getInstance() {return SingletonHolder.INSTANCE;}}

通过内部静态类来实现对初始化的控制。

1、对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。

2、由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。

总结:它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。

4、使用枚举创建单例【最好的方式】

4.1、枚举创建单例

//枚举单例实现public enum SingleEnum {INSTANCE;public void getInstance() {System.out.print("do something");}}public static void main(String[] args) {SingleEnum.INSTANCE.getInstance();}

这样写的优点:

1、写法很简洁。

2、JVM保证线程安全。

3、防止反序列化和反射的破坏。

三、单例有哪些优点和缺点呢?

优点:

1、在内存里只有一个实例,减少了内存的开销,避免频繁的创建和销毁实例。

2、避免对资源的多重占用(比如写文件操作),提升了性能。

3、提供了对唯一实例的受控访问。

缺点:

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

2、由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。

3、从设计原则方面说,单例类的职责过重,在一定程度上违背了“单一职责原则”。

4、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

四、单例模式的使用场景:

1、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。

2、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。