我相信很多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种方式,还解决了这个问题。
优点:代码飘逸,线程安全,达到单例目的
缺点:无法被继承