java面试之单例模式的七种实现方式

我相信很多java程序员,Android程序员在面试路上,肯定都被问到过单例模式的问题?

单例模式之所以被频繁当做面试题,毋庸置疑,肯定是相当重要的,不能不会,这里我们系统的介绍一下各个单例方式的实现方法以及优缺点;

在说单例模式实现之前,我们首先来看看,啥叫单例模式?什么情况需要使用单例模式?我们一个一个来:

1.啥叫单例模式?

普遍解释:浓缩一下,属于创建型模式,全局只创建一个实例对象,并且频繁使用。

我们在了解一个概念之前,最好的方法就是先了解它的特征,比如你从出生到现在都没见过舔狗,那么别人给你介绍一堆说什么,脊索动物门、脊椎动物亚门、哺乳纲、真兽亚纲、食肉目、裂脚亚目、犬科动物。中文亦称“犬”,狗分布于世界各地;介绍完你知道啥叫舔狗???肯定是说了等于没说,但是如果告诉你,舔狗就是,四条腿,2只耳朵,2只眼睛,全身长满毛,并且还会汪汪叫,惹急了还会咬人,见人要么咬,要么舔,那你肯定有个大概的样子了吧。

   好了,咱们说回单例模式,扯得有点远,我们先看卡单例模式的特点:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。
  • 4、主要解决一个类被频繁创建,销毁,导致资源浪费的问题;

先举个代码栗子:代码简单看下就好,下面会具体说:

public class InstanceTest {

	private static InstanceTest instance = null;

	// 构造方法私有化
	private InstanceTest() {

	}

	public static InstanceTest getInstance() {
		if (instance == null) {
			instance = new InstanceTest();
		}
		return instance;
	}

}

2.什么情况需要使用单例模式?

         在看完单例模式的特点,这个其实就不难理解了,单例模式说到底就是为了解决资源浪费的问题,就是一个类里面的方法,变量啥的被频繁使用,用完系统还要回收,如果没有单例,用一次创建一个,过于频繁的使用,肯定会导致创建一堆的实例对象, 我们都知道,创建实例对象是要花内存的, 就是要花资源的,但是系统资源有限,能不浪费当然是不浪费了,所以,单例模式就来了,假如一个类,注定魅力太大,那就让它一直保持一个,把它变成单例模式,无论你要用几次,我就一个,全天候都在;

3.单例模式的实现方式

(1)饿汉式


public class InstanceTest {

	private static InstanceTest instance = new InstanceTest();

	// 构造方法私有化
	private InstanceTest() {

	}
     // 对外访问方法,外部类想要获取InstanceTest类的实例对象,只能通过这个方法了。
	public static InstanceTest getInstance() {
		
		return instance; // 因此初始化就创建了,这里就可以直接返回了
	}

}

说明:听名字就知道,这种单例实现 方法有个特点就是急,饿了就急,急就急在,类一创建就创建了实例对象,不管你用不用,我先创建再说,饥渴难耐有没有??

  优点:对外访问方法,性能高,不需要直接返回已经创建好的实例对象,相应的也是线程安全的,我们看第二种实现方法就知道了。

   缺点:类加载时候就初始化,急的后果就是浪费,我还没说要,你就弄好了,万一我没要,就浪费了;

(2)懒汉式

public class InstanceTest {

	private static InstanceTest instance = null;// 注意这里,是没有初始化的

	// 构造方法私有化
	private InstanceTest() {

	}
     // 对外访问方法,外部类想要获取InstanceTest类的实例对象,只能通过这个方法了。
	public static InstanceTest getInstance() {
		if(instance==null) { //先判断一下,这个实例对象有没有创建,没有的话,要先创建 
			instance = new InstanceTest();
		}
		return instance; // 因此初始化就创建了,这里就可以直接返回了
	}
}

 说明:懒汉式,就是懒得表现,不到用的时候,创建是不可能创建的,这辈子都不可能主动创建的;用的时候,先看看之前有没有创建,没有才给你创建,有也是不可能给你创建的。这就是懒得真谛。

优点: 提高资源利用率, 用的时候再创建,不浪费资源。

缺点:非线程安全的,意思就是说,当多线程高并发的时候,有可能,instance = new InstanceTest();这个方法会同时被几个线程同时调用,而导致出现多个对象,这样一来,单例的意义就失去了。

注意一下:虽然懒汉式单例模式,是非线程安全的,但是在实际开发中,你可能还是会普遍看到这种创建单例的方式,也就是说,饿汉式单例模式仍然被广泛使用,原因很简单,我们在实际开发中,很多时候并没有多线程高并发的风险,又考虑到资源利用率的问题,自然懒汉式成为了很多时候的首选。

(3)懒汉式+Synchronized

public class InstanceTest {

	private static InstanceTest instance = null;// 注意这里,是没有初始化的

	// 构造方法私有化
	private InstanceTest() {

	}
     // 对外访问方法,外部类想要获取InstanceTest类的实例对象,只能通过这个方法了。
	// synchronized 给getInstance()方法加了同步锁
	public static synchronized InstanceTest getInstance() {
		if(instance==null) { //先判断一下,这个实例对象有没有创建,没有的话,要先创建 
			instance = new InstanceTest();
		}
		return instance; // 因此初始化就创建了,这里就可以直接返回了
	}
}

说明:这个方式和懒汉式的区别就是加了一个关键字,synchronized这个关键字的作用很明显就是为了解决懒汉式线程不安全的问题,synchronized就是当getInstance()有线程在访问的时候,告诉其他想要访问getInstance()方法的线程,现在有人占坑了,你们该干嘛干嘛去,等它用完,才能轮到你们。这个就防止了,instance = new InstanceTest();被多个线程调用的问题;

优点:懒汉式的优点加上线程安全

缺点:并发访问的时候,性能较低,原因很简单,加了同步锁,意味着要等,排队,性能自然相对会低一些;

(4)懒汉式+双重校验锁法

public class InstanceTest {

	private static InstanceTest instance = null;// 注意这里,是没有初始化的

	// 构造方法私有化
	private InstanceTest() {

	}
     // 对外访问方法,外部类想要获取InstanceTest类的实例对象,只能通过这个方法了。
	// synchronized 给getInstance()方法加了同步锁
	public static  InstanceTest getInstance() {
		if(instance==null) {//先判断一下,这个实例对象有没有创建,没有的话,要先创建 
			// 加入多个线程并发访问到了这里,然后由于synchronized锁,导致排队在等,但是这个时候
			// 这多个线程都认为instance是null空的,因此,下面还需要再判断一次,
			// 位置1
               synchronized (InstanceTest.class) {			
				if(instance==null) { //这里也需要判断一下,双重校验  // 位置2
                  
					instance = new InstanceTest(); // 位置3
				}
			}
		}
	
		return instance; // 因此初始化就创建了,这里就可以直接返回了
	}
}

说明:这种方式也是在懒汉式的基础上改进的,并且把synchronized加在了创建实例的方法上instance = new InstanceTest();

并且加了双重校验,原因如注释说的;

优点:性能比第三种方式更好,并且继承了 懒汉式的优点

缺点:通常线程安全,低概率不安全

这个原因我们具体说一下:我们先看下实例化对象过程都发生了什么:

(1)为对象分配内存空间

(2)初始化默认值

(3)执行构造方法

(4)将对象指向刚分配的内存空间

然后仔细看上面的代码,我标记了3个位置,位置1、位置2和位置3,假如线程1,执行到了位置2,线程2恰好执行到了位置1,这个时候线程1已经把位置2那块的代码锁定了,线程2在位置1等着,线程1在位置2处判断,并顺利进入位置3,但由于 Java 平台内存模型,为了性能等原因内存模型允许所谓的“无序写入”,有可能第(3)步和第(4)步会出现对调的情况,也就是说,先把对象给你,我稍后就执行构造方法,实际上给的是null,这个原理在实际生活中也常用,比如你朋友要去大宝剑,但是没带钱,问你借钱,你就说,你先去,我待会把钱发你微信,实际上,你朋友在大宝剑的时候,他是没钱付的,但是,等他大宝剑完了,实际是可以付的起的, 因为这个时候,你肯定已经发给他钱了。就是这个道理,话糙理不糙。如果第3和第4步换了位置,那线程1在初始化实例对象的时候,这个时候同步锁已经打开,线程2进入加锁代码块,执行到位置2的时候,判断instance 是为非空的,然后线程2就返回了一个假的实例对象,没有执行构造函数的实例,等线程1执行完实例化方法的时候,线程1就正常返回执行过构造器方法的实例,也就是说,线程1返回的是正常的,线程2有可能返回的是个假的实例,未初始化完成;

(5)懒汉式+双重校验锁+volatile


public class InstanceTest {

	private static volatile  InstanceTest instance = null;// 注意这里,是没有初始化的
	// 并且加了volatile关键字

	// 构造方法私有化
	private InstanceTest() {

	}
     // 对外访问方法,外部类想要获取InstanceTest类的实例对象,只能通过这个方法了。
	// synchronized 给getInstance()方法加了同步锁
	public static  InstanceTest getInstance() {
		if(instance==null) {//先判断一下,这个实例对象有没有创建,没有的话,要先创建 
			// 加入多个线程并发访问到了这里,然后由于synchronized锁,导致排队在等,但是这个时候
			// 这多个线程都认为instance是null空的,因此,下面还需要再判断一次,
			synchronized (InstanceTest.class) {			
				if(instance==null) { //这里也需要判断一下,双重校验
					instance = new InstanceTest();
				}
			}
		}
	
		return instance; // 因此初始化就创建了,这里就可以直接返回了
	}
}

说明:这种方式和第四种方法几乎一样,唯一不同的就是在实例对象声明的时候加了一个volatile关键字,这个关键字的作用也很明显就是为了弥补第四种方式的不足,   所以到这里我们不难发现,第3种,第4种,第5种方式其实就是第2种方式的升级版,理论上是同一种方式,只不过3,4,5种方式都在打补丁,那里出问题就补哪里,补了3次而已;

优点:线程安全,性能得到优化;

缺点:个人觉得写起来有点麻烦;

(6)静态类内部加载

public class InstanceTest {

	private InstanceTest() {

	}

//静态修饰符static修饰,并且在InstanceTest内部创建的类,静态内部类
	private static class InstanceTestHolder {
		private static InstanceTest instance = new InstanceTest();
	}
    
	public static InstanceTest getInstance() {
		return InstanceTestHolder.instance;
	}
}

说明:这种方式,代码不复杂,单例InstanceTest类的构造方法私有化了,并且在类里面新创建了一个内部类,InstanceTestHolder被static静态修饰符修饰;静态内部类不会在单例加载时就加载,而是在调用getInstance()方法时才进行加载,达到了类似懒汉模式的效果,而这种方法又是线程安全的;

优点:性能高,资源利用率高,线程安全

缺点:继续往后看

(7)枚举方法


public enum InstanceEm {
	INSTANCE;

	// 其他方法,这里举个栗子,add()方法
	public void add() {
		// 此处省略具体实现代码
	}
}

/**
 * 枚举方法调用
 * @author xyc
 *
 */
public class TestInstance  {
	public static void main(String[] args) {
      InstanceEm.INSTANCE.add();// 调用add()方法
	}
}

说明:第一段代码是创建枚举类,第二段代码是使用;这种写法,是一本书叫《Effective Java》的作者Josh Bloch 提倡的方式,

大佬的写法很飘逸,非常简单,推荐装逼佬使用,但是,完美的东西是不存在的,这种写法唯一的缺点就是无法继承, 枚举类无法被其他类继承,其他都不错;可以拿去装逼;

奥,还有一个问题,前6种方式都把构造方法私有化了,但是并不是绝对的外面类访问不了,可以使用java反射来强行调用私有构造器,第7种方式,还解决了这个问题。

优点:代码飘逸,线程安全,达到单例目的

缺点:无法被继承

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值