Java开发篇——设计模式单例模式你真的了解吗?

Posted weixin_43802541

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java开发篇——设计模式单例模式你真的了解吗?相关的知识,希望对你有一定的参考价值。

单例模式几乎快成了面试官张口就来的一个问题了,特别是面试一些初级岗位的java开发者大部分都会被问到过单例模式,为什么呢?虽然单例模式虽然看起来简单但是它实现的过程中包含了线程安全、类加载机制、内存模型等一些比较核心的知识,通过对单例模式的介绍面试官就可以看出面试者的基本功是否扎实。而往往技术底子不扎实的同学往往回答的时候就零零散散的简单说下就以为可以了,殊不知你已经踩到了面试官给你挖的一个坑前。

下面我们就来介绍下单例模式,看看什么样的介绍都可以让面试官夸赞不已。

一.单例模式介绍

单例模式指的是在程序的整个运行时域,一个类只有一个实例对象对外部提供调用访问。

通俗的理解那就是在古代,整个王朝就一个皇帝。如何确保一个皇帝?这就是单例模式了。

单例设计模式的特点:

  1. 单例类只能有一个实例对象;

  2. 单例类外部无法创建对象只能由本类创建唯一实例对象;

  3. 单例类要提供对外访问的公共方式;

单例设计模式的好处和不足:

好处:因为单例只为类创建一个对象资源消耗较少,避免了类频繁的创建对象导致的内存飙升、耗费时间等问题,提高了对象的访问速度,降低了系统内存的使用频率,减轻了GC(java中的垃圾回收器)压力;

例如:我们程序访问数据库的操作,创建连接数据库对象是一个非常耗时耗资源的过程,如果我们把这个对象设计为单例模式,那么我们就创建一次并重复使用这个对象即可,特别在高并发访问下大大提高了效率。

并且程序中也会出现某些特定场合一个类只能创建使用一个对象的情况,例如:证券系统在整个系统中只能有一个证券交易类负责交易等相关操作;

不足:单例类没有抽象类不能扩展,不适用于变化的对象,并且根据单例实现方式的不同可能存在多线程访问下安全和效率问题;

那么在Java中,怎么去实现单例模式呢?

二.单例的实现

单例模式因其创建对象时机的不同,它的实现主要分为懒汉式和饿汉式两种类型;

饿汉式:在类加载的时候就创建对象

懒汉式:类加载的时候不创建对象,在外部第一次调用的时候才创建对象

下面我们先看下最基本的饿汉式和懒汉式的实现。

  1. 饿汉式

public class SingleTon

//初始化singleTon实例对象

private static SingleTon singleTon = new SingleTon();

private SingleTon() //构造器私有化

//外部调用的公共方法

public static SingleTon getInstance()

return singleTon;

分析:饿汉式中SingleTon 类的构造方法使用了private修饰,那么其他的类没办法通过new来创建singleton对象实例,其他类只能通过调用静态的方法getInstance来访问,这样就可以保证singleTon的实例对象只有一个singleTon类中创建的。

特点: 饿汉式是典型的牺牲空间换取时间的方式,类一加载就创建了实例对象,也不管我们在程序中是否使用;所以会占用更多的内存,但是访问对象的速度比较快;并且在多线程访问情况下也没有线程安全的问题。

  1. 懒汉式(简单实现)

public class SingleTon

private static SingleTon singleTon;

private SingleTon()

public static SingleTon getInstance()

if (singleTon == null)

singleTon = new SingleTon();

return singleTon;

分析:懒汉式跟饿汉式不同的地方是懒汉式在getInstance方法中创建的对象,即在使用对象的时候才去创建对象,这种加载对象的方式也被称为懒加载。

特点:

(1)懒汉式因为是在外部使用的时候才调用,所以要更加节省内存一些,但是第一次访问的时候因为需要创建对象所以要比饿汉式慢;

(2)线程不安全

多线程访问下,如果出现两个以上线程在没有new SingleTon()的时候就进行了singleTon == null的判断都会返回true,那么就会出现创建了多个实例的情况,这样就违反了单例模式的设计思想。

那么怎么才能使其线程安全呢?有的人就说了,这还不简单?加上 synchronized就好了,确实,这样可以解析线程安全的问题。

懒汉式—synchronized同步锁

public class SingleTon

private static SingleTon singleTon;

private SingleTon()

public static synchronized SingleTon getInstance()

if (singleTon == null)

singleTon = new SingleTon();

return singleTon;

分析:在getInstance添加了synchronized 确实保证了线程安全,但是因为 synchronized加到方法上,一次性把整个方法给加上了锁,锁的粒度有点大,这样意味 着在多线程访问情况下如果有一个线程访问了方法getInstance获取了锁,其他线程 就要处于等待状态,这样就大大降低了访问效率;所以实际开发中这种实现方式是不可 取的。那有没有效率更高的实现方式呢?

懒汉式—DCL双重校验锁(推荐)

Synchronized同步方法的实现效率低是因为锁的粒度太大,那能不能通过减小锁的粒度来提高效率呢?这时候可以使用DCL(Double Check Lock)双重校验锁的方式来实现。

代码如下:

public class SingleTon

private static SingleTon singleTon;

private SingleTon()

public static SingleTon getInstance()

if (singleTon == null) //代码1

synchronized (SingleTon2.class) //代码2

if (singleTon == null) //代码3

singleTon = new SingleTon();//代码4

return singleTon;

分析:DCL的实现加锁的粒度变小,在多线程访问getInstance方法的时候不需要竞争获取锁,都可以进入getInstance方法。此时执行代码1进行第一次判空,如果对象实例还没创建那么开始竞争获取锁,竞争到锁的线程A就进行创建singleTon 对象,如果当线程A刚获取锁的同时另外一个线程B也正好符合代码1的判空,那么线程A创建了singleTon 对象之后线程B也要获取锁并创建对象,为了解决这个问题就加入了代码3进行了第二次判空处理。

DCL的实现锁粒度小允许多线程访问getInstance方法,所以效率比同步方法的实现要高。

但是上面的代码真的完美吗?如果是老的程序员看到这个代码就会眉头一皱,心里不禁的会想到。

上面到底问题出在了哪里呢?这里就要说了JVM给出的happens-before通用原则,这里就不详细介绍happens-before原则了,它主要规定了jvm多线程原子性、可见性和有序性的一些原则。而上面DCL代码实现中singleTon = new SingleTon();在指令操作中不是一步完成的不属于原子性操作,它的指令操作分为下面三步:

(1)memory =allocate(); 先为singleTon 分配内存空间

(2)ctorInstance(memory); 然后初始化singleton对象

(3)instance = memory;最后将singleton指向分配好的内存空间

在真正执行时,JVM虚拟机为了提高执行效率,在保证结果的情况下可能会进行指令重排,比如JVM认为指令按照 1->3->2执行效率会更高,如果按照这个顺序,假设线程A刚好执行到第三步指令的时候那么此时singleTon 还未初始化依旧是null,此时线程B执行到了代码1,判断singleTon ==null返回的却是false认为已经创建了singleTon ,那么此时就出现了一个严重的问题,线程B直接return返回了null,出现了线程不安全的情况。

(未完待续)

以上是关于Java开发篇——设计模式单例模式你真的了解吗?的主要内容,如果未能解决你的问题,请参考以下文章

你真的了解单例模式吗?

设计模式系列-你真的了解单例模式吗??

java的单例

在java中写出完美的单例模式

在java中写出完美的单例模式

在java中写出完美的单例模式