单例模式
一、什么是单例模式?
单例模式是确保一个类只能实例化出一个对象,保证全局的唯一。
二、单例模式的三个基本要素
a. 这个类只能有一个实例
b. 该类实例化出来的唯一对象,必须是由他自己所创建的
c. 它必须向整个系统提供这个实例
说到这里就很像我们在spring中所说的Bean,它向整个系统提供了这个实例
三、单例模式的使用场景
应用场景:J2EE中的ServlertContext、ServletContextConfig等、Spring框架应用中的ApplicationContext、数据库连接池等。
那么接下来我们就真正的通过代码的形式带大家了解什么事单例模式了。
四、单例模式的实现方式
在本文介绍了四种单例模式的实现方式,这里的介绍包括了他们的优缺点,以及在何种方式下更加适合我们的使用。
这里着重的介绍了四种方式:
● 饿汉单例模式
● 懒汉单例模式
● 枚举类自带单例模式
● 静态内部类的单例模式
(一)、饿汉单例模式
饿汉单例模式:指我们的程序在启动时,类加载过程中,即在jvm方法区内存初始化类之后,由于类中有一个由final修饰的静态常量,所以该Hungry对象会被加载初始化到静态常量池中,并且hungry对象是不可变的。
public class Hungry {
private byte[] bytes1=new byte[1024*1024];
private byte[] bytes2=new byte[1024*1024];
private byte[] bytes3=new byte[1024*1024];
private byte[] bytes4=new byte[1024*1024];
private final static Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
这就是饿汉单例模式的实现方式,那么对于这个单例模式的优缺点又是什么呢?
优点:即开即用, 类加载的时候就初始化。没有任何锁,执行效率高,用户体验比懒汉式单例模式更好如果需要在程序启动之初就立马使用,可以使用到饿汉单例模式。
缺点:不管使不使用这个类的实例化对象,在类加载时,就会获取到实例,不管有没有占用资源。(资源的体现就在于我代码中的几个byte属性)
建议:如果需要程序一启动就使用该类的实例化对象,那么就可以使用饿汉单例模式。
如果是一些工具类,就还是用懒汉的单例模式比较好。想用的时候在通过getInstance()方法获取。
(二)、懒汉单例模式
懒汉单例模式的使用场景还是挺多的,这里我们采用循序渐进的方式来帮助大家理解这种实现方式。
先附上最简单的实现方式:
//懒汉式单例模式
public class LazyMan {
//单例模式下,类的构造方法需要私有化
private LazyMan(){
System.out.println(Thread.currentThread().getName()+"线程的LazyMan的实例已被初始化");
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan==null){
lazyMan=new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
//这种情况在单线程下不会存在问题,但是在多线程情况下就会出现问题了,不满足单例模式的条件
//多线程下回抢占cpu的一个执行线程,都抢着创建我们的LazyMan对象
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
结果测试:
出现的问题:
这种情况在单线程下不会存在问题,但是在多线程情况下就会出现问题了,不满足单例模式的条件,
多线程下回抢占cpu的一个执行线程,都抢着创建我们的LazyMan对象,这时候就创建出了很多个实例对象,
但是也有可能会成功的创建出一个实例,不过还是不安全的。
改进:懒汉模式之DCL双重检测机制
什么是DCL双重检测机制呢?
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
既如上代码所示,
(1)、在我们创建实例前,要判断变量是否已经初始化,如果已存在,则直接返回实例。
(2) 、否则的话获得锁定。
(3)、在仔细判断变量是否已经初始化,这里的目的是(有两个或多个线程经过了条件一的判定):如果有另一个线程首先获得锁,那么在将变量初始化后,另一个线程就可以直接返回已经初始化的变量。
完整代码:
public class LazyMan {
//单例模式下,类的构造方法需要私有化
private LazyMan(){
//System.out.println("LazyMan的实例已被初始化");
System.out.println(Thread.currentThread().getName()+"线程的LazyMan的实例已被初始化");
}
private static LazyMan lazyMan;
//这时我们进行双重检查锁 dcl
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//这种情况在单线程下不会存在问题,当时在多线程情况下就会出现问题了,不满足单例模式的条件
//多线程下回抢占cpu的一个执行线程,都抢着创建我们的LazyMan对象
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
结果:
为什么这里要用到DCL双重检测机制呢?
DCL本质上也就是减少了锁粒度,如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅度的降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美。多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。
但是在多线程的模式下,可能还是会出现一定的问题,会出现什么问题呢?
lazyMan = new LazyMan();
以上这行代码是不遵循原子性的。
我们在创建这个对象的时候,在java中经过了这三个步骤:
- 分配对象的内存空间
- 初始化对象
- 设置instance指向刚分配的内存地址
实际上这3个步骤它就未必是按照顺序执行的,上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的)。
如果是按上面123的步骤进行执行那么就不会出现问题。
如果是按照132的方式执行,可能会出现问题,这是如果说A线程已经开始创建LazyMan这个对象时,并且先执行了3这个步骤,此时一个B线程进来了,进来后判断lazyMan不为空,由于A线程还未初始化对象,那么B线程获得就是一个空对象,会出现异常。
解决方案:
在知晓问题的根源后(也就是重排序问题),
不允许2和3重排序(在JDK 1.5后可以基于volatile来解决);
private volatile static LazyMan lazyMan;
//这时我们进行双重检查锁 dcl
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
不过到这里你以为我们就真的可以确保获取唯一的实例了吗?其实不然如果我们使用到反射机制呢?以这种暴力的方式你还能确保实例唯一吗?
下面就来看看
public class LazyMan {
//单例模式下,类的构造方法需要私有化
private LazyMan(){
System.out.println("LazyMan的实例已被初始化");
//System.out.println(Thread.currentThread().getName()+"线程的LazyMan的实例已被初始化");
}
private static LazyMan lazyMan;
//这时我们进行双重检查锁 dcl
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//通过暴力的方式破坏这个单例模式,那就是反射,反射:我想干嘛就干嘛,就是这么暴力
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
//这一步直接无视了我们私有的构造器
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan = getInstance();
System.out.println(lazyMan1+" "+lazyMan);
}
}
通过反射机制获取这个类的构造器,并通过这个setAccessible这个不要脸的方法,直接无视了我们的私有构造器,之后成功通过newInstance的方法创建了两个不相同的实例出来。
针对这一问题,它不就是拿到了我的构造方法吗?那我可以在构造器上做做手脚了。
private LazyMan(){
synchronized (LazyMan.class){
if(lazyMan!=null){
throw new RuntimeException("不要试图想着利用反射机制来破坏单例模式");
}
}
}
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
LazyMan lazyMan = getInstance();
//通过暴力的方式破坏这个单例模式,那就是反射,反射:我想干嘛就干嘛,就是这么暴力
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
//这一步直接无视了我们私有的构造器
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
System.out.println(lazyMan1+" "+lazyMan);
}
诶,这个时候我似乎觉得自己又行了,但是不是这样的,如果我们在已开是没有通过getInstance方法去获取我们的实例呢,而是直接通过反射机制,通过反射获得的构造器创建实例不就不抱错了吗,而且还是可以获取多个实例,因为我们只要不通过getInstance方法获取实例,lazyMan就一直为空。
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//LazyMan lazyMan = getInstance();
//通过暴力的方式破坏这个单例模式,那就是反射,反射:我想干嘛就干嘛,就是这么暴力
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
//这一步直接无视了我们私有的构造器
declaredConstructor.setAccessible(true);
LazyMan lazyMan1 = declaredConstructor.newInstance();
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(lazyMan1+" "+lazyMan2);
}
这样就有行了。所以反射还是赢了。
哪有人又会想了,我在构造方法放入一个变量,判断变量的值来防止反射机制破坏单例模式。
private static LazyMan lazyMan;
private LazyMan(){
synchronized (LazyMan.class){
if(tao==false){
tao=true;
}else{
throw new RuntimeException("不要试图想着利用反射机制来破坏单例模式");
}
}
}
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor();
Field tao = LazyMan.class.getDeclaredField("tao");
//无视构造方法和属性的私有化
declaredConstructor.setAccessible(false);
tao.setAccessible(false);
tao.set(tao,false);
LazyMan lazyMan = declaredConstructor.newInstance();
System.out.println(lazyMan);
}
这个时候反射机制一样能够巧妙破解。
那么真的就没有办法了吗,我们点击进入newInstance这个方法后,我们可以看到枚举类的构造器不能通过反射机制获取否者会报错。
那么接下来我们就会讲到枚举类自带的单例模式
(三)、枚举类自带的单例模式
public enum SingleEnum {
INSTANCE;
public static SingleEnum getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) {
SingleEnum instance = SingleEnum.getInstance();
SingleEnum instance2 = SingleEnum.getInstance();
System.out.println(instance);
System.out.println(instance2);
}
}
经过测试,枚举类确实是自带单例模式的,那么到底反射机制能不能破坏他呢?
虽然报错了,但是不是我们想要的那个错误,这里是获取SingleEnum的构造方法报错了,但是我们打开编译后的代码发现确实是没问题呀?
所以是idea骗了我们,这里我们用jad工具将class文件反编译回来,发现代码确实不一样,构造方法是有String和int参数的,那么我们再来一次
class Test{
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Constructor<SingleEnum> declaredConstructor = SingleEnum.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
SingleEnum singleEnum1 = declaredConstructor.newInstance();
SingleEnum singleEnum2 = declaredConstructor.newInstance();
System.out.println(singleEnum1);
System.out.println(singleEnum2);
}
}
这样就对了,发现反射机制确实不能破坏枚举类自带的单例模式。
(四)、静态内部类的单例模式
这个就直接粘代码啦!
//静态类的方式去获取单例模式
public class Holder {
private Holder(){
}
public static Holder getInstance(){
return InnerClass.holder;
}
public static class InnerClass{
private static final Holder holder = new Holder();
}
}