“
Hello,大家好!我是老丑,祝大家新年快乐呀!别人在玩,那么我们就学习呗。今天给大家带来的是——单例模式
单例模式是什么?
单例,顾名思义:便是唯一实例。这个实例大家可以理解为创建出来的对象,也就是new出来的对象。
模式,大家可以理解为一种方式、方法、思想。
因此单例模式,就是让该类的对象单一化的一种方式。
单例模式的实现方案
单例模式的实现方案有5种,且听我慢慢道来。
饿汉式模式
饿汉式:假设你饿了很多天,突然有一个天使给你送来食物,想必你脑海中已经有画面了。
OK,其实饿汉式代表的是迫切的意思。
因此,饿汉式模式来实现单例模式,会很迫切的创建一个对象,并保证对象唯一。
实现代码
package cn.laochou.singleton.hungry;
public class Singleton {
// 我们在本类中创建一个对象,除此之外没有提供方法进行创建对象。
private static final Singleton singleton = new Singleton();
// 私有化构造函数,只有本类内可以访问
private Singleton() {}
// 返回我们所创建的唯一函数
public static Singleton getInstance() {
return singleton;
}
}
OK,好像确实可以满足我们的单例需求。
但是,大家仔细想想,真的能满足吗?
大家在这里,一定得记得一个知识点就是反射。如果不了解反射的,可以通过Java基础之反射篇 这篇文章来做一个初步认识,但是我在反射这篇文章中,并没有说明反射可以通过私有构造函数来创建对象。在这里做一个补充,小伙伴们看好了。
package cn.laochou.singleton.hungry;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class Crack {
public static void main(String[] args) {
Class<?> clazz = Singleton.class;
try {
Constructor<?> constructor = clazz.getDeclaredConstructor();
Singleton singleton = (Singleton) constructor.newInstance();
System.out.println(singleton);
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
大家如果看完反射的那篇文章的时候,就知道上面一段代码的含义了。大家运行的时候,其实会出现一个Exception。我们来看下这个异常
java.lang.IllegalAccessException: Class cn.laochou.singleton.hungry.Crack can not access a member of class cn.laochou.singleton.hungry.Singleton with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
at cn.laochou.singleton.hungry.Crack.main(Crack.java:12)
大家可以看到这一段话:Class cn.laochou.singleton.hungry.Crack can not access a member of class cn.laochou.singleton.hungry.Singleton with modifiers “private”
翻译过来就是无法访问,对于私有构造方法,我们需要让它变得可以访问。需要加入下面的一行代码。
constructor.setAccessible(true);
public class Crack {
public static void main(String[] args) {
Class<?> clazz = Singleton.class;
try {
Constructor<?> constructor = clazz.getDeclaredConstructor();
// 设置允许访问
constructor.setAccessible(true);
Singleton singleton = (Singleton) constructor.newInstance();
System.out.println(singleton);
} catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
cn.laochou.singleton.hungry.Singleton@1b6d3586
OK,对象创建出来了。因此我们是有方法来破解我们的饿汉式单例模式。
总结: 饿汉式单例模式并不能保证绝对的单例,毕竟可以通过反射来破解。但是饿汉式单例模式没有线程安全这个问题,但是饿汉式模式没有延迟实例化带来的节约资源的好处。
懒汉式模式
懒汉式模式就是与我们的饿汉式模式相反。懒汉式模式就是不到使用的时候就不创建。
实现代码
package cn.laochou.singleton.lazy;
public class Singleton {
public static Singleton singleton = null;
// 私有化构造函数
private Singleton() {
}
public static Singleton getInstance() {
if(singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
如果大家仔细阅读了饿汉式模式,其实也能知道我们懒汉式模式也不能保证绝对的单例(可以通过反射来破解)。除此之外,我们的懒汉式模式还有线程安全问题。
这里给大家假设一个场景:
有两个线程,分别为线程A,线程B。
线程A 运行到 if(singleton == null) 的时候,条件满足,进入if结构之后。由于程序调度,这个时候并没有执行 singleton = new Singleton(); 这个时候程序调度给了线程B,那么线程B运行到 if(singleton == null)的时候,条件也满足,进入if结构之后。那么现在其实很清楚了,两个线程都会运行 singleton = new Singleton(); 这条语句。因此Singleton是会被new两次的,也就是创建两个对象。这里就不满足我们的唯一单例了。
如何解决:可以通过锁进行解决,大家如果不了解锁。可以阅读这两篇 Java中的并发会有什么问题呢?如何解决呢? 和 干货满满的synchronized详解!!! 进行一个初步了解。后续持续更新
其实还有一个问题:
singleton = new Singleton(); 这条语句,在Java里面看似是一条语句。这条语句其实是分为三步执行的:
-
为singleton 分配内存空间 -
初始化 singleton -
将 singleton 指向分配的内存地址
一般来讲执行的顺序是 1->2->3。但是由于JVM具有指令重排的特性,执行顺序有可能变为1->3->2。在这里大家如果不知道指令重排,可以自行百度了解,我在这里简单的介绍下功效。因为你看上去的一条Java语句,但是对于汇编而言,是可能被翻译为多条语句,而且这些语句所执行的组件都是不同的,为了增加运行效率,采用了流水线的执行方式,但是在流水线执行的过程中,可能由于一些语句导致前后无法衔接,我们称之为中断,因此指令重排主要是最大程度上来减少这种中断。指令重排是建立在happens-before原则之上的。(大家可以自行了解,后续的推文可能会详解)。我们接着上面来讲,1->3->2的执行顺序,对于单线程环境是不存在任何问题的,但是呢对于多线程的环境下,也是存在问题的。
这里又给大家模拟一遍。
有两个线程,线程A,线程B。
假设线程A执行了1->3,但是没有进行初始化,但是此时线程B调用了getInstance()方法来获取实例,因为此时singleton对象已经不为空了,但是此时我们的singleton还没有初始化,从而出现问题。
如何解决 :我们可以通过使用volatile关键字来禁止JVM的指令重排,保证在多线程的环境下也能正常运行。
总结: 懒汉式模式的优势主要在于延迟实例化、节约资源、用时加载。但是简单的懒汉式模式存在线程安全问题。
懒汉式模式-线程安全
想让懒汉式模式变得线程安全的方式有很多种,在这里我们列举一个比较常用的方式——Double Check(双重校验)
实现代码
package cn.laochou.singleton.check;
public class Singleton {
private static volatile Singleton singleton = null;
// 私有化构造函数
private Singleton() {}
public static Singleton getInstance() {
// double check
if(singleton == null) {
// 上锁
synchronized (Singleton.class) {
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
TIP:不了解synchronized的,可以阅读 干货满满的synchronized详解!!!
如果大家仔细阅读了饿汉式模式,其实也能知道我们懒汉式模式-线程安全也不能保证绝对的单例(可以通过反射来破解)。
不知道小伙伴会不会有这样的一个疑问——“第一个if不是判断了是不是为null,然后就上锁了,为什么上锁之后还需要进行一个判断呢”
Answer(答):这里还是给大家假设一个场景,两个线程,线程A,线程B。假设线程A执行完了 if(singleton == null) 条件满足,进入了if结构,但是这个时候由于线程调度的原因,线程A没有继续执行后面的语句,而是执行权交给了线程B,这个时候线程B执行 if(singleton == null) 很明显条件满足,因此也进入了if结构。所以如果synchronized结构里面没有进行判断,还是会创建两个实例对象。只是先后问题,因为synchronized可以保证该结构在同一时刻只能被某一个线程执行。因此是需要进行双重检测的。
那么小伙伴可能又会提问了——“我直接方法上锁不香吗?”
Answer(答):香啊,超级香。但是你有没有考虑性能的问题呢,方法上锁,锁的粒度很大,但是有的时候反而还需要进行锁粗化。这个是要根据场景来定的,根据不同的场景选择不同的锁粒度,该细化就细化,该粗化就粗化。但是对于我们上面的代码,很明显锁粒度越小越好,因为在方法中只有一个上锁代码块。
如果细心的小伙伴,可能很快发现,我们在 singleton的静态变量用volatile进行修饰了。这样就可以避免JVM 的指令重排了。也就是解决了上面关于懒汉式模式后面的一个问题。
总结: 懒汉式模式-线程安全主要是解决了线程安全问题和指令重排问题
静态内部类
直接上代码
实现代码
package cn.laochou.singleton.inner;
public class Singleton {
// 私有化构造函数
private Singleton() {
}
// 静态内部类
private static class InnerClass{
private static final Singleton singleton = new Singleton();
}
public static Singleton getInstance() {
return InnerClass.singleton;
}
}
同样,静态内部类的方式也是无法保证唯一单例(可以通过反射来进行破解),但是静态内部类是线程安全的。实例化一次是通过JVM来保证的,因为是在InnerClass类加载的时候才会进行创建singleton实例,而且静态内部类实现的单例模式也是即用即加载,这里其实还是有一个学问的,就是InnerClass是什么时候进行加载的。
我们写个demo简单的看下
package cn.laochou.singleton.inner.demo;
public class InnerClassLoader {
private static class InnerClass {
static {
System.out.println("InnerClass is loading");
}
public static void hello() {
System.out.println("hello");
}
}
public static void show() {
InnerClass.hello();
}
static {
System.out.println("InnerClassLoader is loading");
}
public static void main(String[] args) {
show();
}
}
运行效果
InnerClassLoader is loading
InnerClass is loading
hello
从结果上来,加载InnerClassLoader类的时候,并没有加载 InnerClass。而是在调用InnerClass.hello()方法的时候,进行加载的。所以静态内部类是即用即加载。因此使用静态内部类实现单例模式的实例是即用即创建。
具体的类加载会在JVM系列细讲,在这里就简单的一笔带过,大家留个印象。
枚举
大Boss,也是 《Effective java》 极力推荐实现单例模式的方法。
实现代码
package cn.laochou.singleton.enums;
public enum Singleton {
INSTANCE;
public void hello() {
System.out.println("hello");
}
}
上面就已经实现了单例模式的思想。
通过枚举实现单例模式,可以防止反射破解。而其他的实现方式都是可以通过反射来进行获取私有构造函数,并设置运行访问,即可创建多个对象。枚举方式是通过JVM来进行保证唯一实例,而且这种方式还是线程安全的。
单例模式的运用
看到这里,想必小伙伴们已经了解了单例模式的实现方案。我们学习单例模式,那么它具体用在哪里呢?
且听我细细道来
场景:
-
资源共享的情况下,避免由于资源操作时导致性能问题或者损耗。比如ID构造器。 -
控制资源的情况下,方便资源之间的管理。比如连接池,线程池。
单例模式实战
我们写一个简单的ID构造器吧,就是用这个组件来生成ID。
如果大家觉得ID构造器,没有作用的话,我就举个列子,假设你的项目业务很多,每个请求都会有一个唯一ID,这个ID不能重复,因为一旦很多重复的,一旦业务请求出现问题,完全无法排查。孰轻孰重,大家现在应该知道了吧。
实现代码
package cn.laochou.singleton.id;
import java.util.concurrent.atomic.AtomicInteger;
public enum IDGenerator {
INSTANCE;
private AtomicInteger atomicInteger;
private IDGenerator() {
this.atomicInteger = new AtomicInteger();
}
public int getId() {
return atomicInteger.getAndIncrement();
}
}
以上就是一个简单的ID生成器。大家可以看看。
我也把我的测试代码贴在这里
public static void main(String[] args) {
// 在这里,我之前使用lambda表达式来写的,但是考虑lambda的推文还没完成,大家敬请期待。
Runnable r = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println(IDGenerator.INSTANCE.getId() + " " + Thread.currentThread().getName());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
Thread thread3 = new Thread(r);
Thread thread4 = new Thread(r);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
个人测试是没有问题的哈,打印的数字的顺序肯定是不一致的。(大家看下ID是否唯一)
最后
OK,整个推文到这里就基本结束了。
希望小伙伴们看完推文,能够有所收获。
学习路上有FingerDance,不孤单。欢迎加群——进入公众号,菜单栏【关于我们】->【加入我们】!
觉得推文写得还不错的小伙伴,还请点赞、关注、转发支持下。

我是老丑,一位又老又丑的少年!我们下期见。
往期推荐:
我们是FingerDance,欢迎加入我们!
原文始发于微信公众号(FingerDance):超凡回归带大家全面认识单例模式
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之家整理,本文链接:https://www.bmabk.com/index.php/post/26708.html