这篇是入职之后的第二篇了,上一篇我简单介绍了一下LOCK里面的类的方法,感兴趣的话可以去了解一下,以后坚持每周至少会更新一篇关于多线程方面的文章,希望博友们可以一起加油成长。
这篇主要的内容是单例模式在多线程环境下的设计,这篇算是比较重要的内容,我会进行文字和代码的共同说明来讲解记录
1、立即加载(饿汉模式)
说到标题,有人会说什么是立即加载呢?立即加载就是使用类的时候已经将对象创建完毕了,比如说直接new实例化对象。也就是在调用方法之前,实例已经被创建了
public class MyObject { private static MyObject myObject = new MyObject(); private MyObject(){ } public static MyObject getInstance(){ return myObject; } }
看这段代码,他的缺点是不能有其他实例变量。外部的使用者需要使用MyObject实例的时候,只能通过getInstance方法,另外假如没有用到这个实例的时候,他已经创建了出现,会有资源浪费的情况出现的。还有因为getInstance方法没有同步,所以有可能出现非线程安全的问题。
2、延迟加载(懒汉模式)
延迟加载就是在调用要使用的那个方法(假如MyObject方法)的时候实例才会被创建,实现方法就是在MyObject方法里面进行new实例化。
public class MyObject { private static MyObject myObject; private MyObject(){ } public static MyObject getInstance(){ if(myObject==null){ myObject = new MyObject(); } return myObject; } }
此代码虽然取得了一个对象,没毛病。单例!但是如果在多线程情况下,就会取出多个实例的情况,这个是与单例模式的初衷背道而驰的。
public class MyObject { private static MyObject myObject; private MyObject(){ } public static MyObject getInstance(){ if(myObject==null){ try { Thread.sleep(3000); myObject = new MyObject(); }catch(Exception e){ e.printStackTrace(); } } return myObject; } }
可以试着自己多建立几个线程,运行一下这段代码,发现在多线程环境在建立出来了多个实例(可以打印对象的hashcode值进行比较)
那我们应该怎么去解决这个问题呢?
3、延迟加载(懒汉模式)——synchronized
public class MyObject { private static MyObject myObject; private MyObject(){ } synchronized public static MyObject getInstance(){ if(myObject==null){ try { Thread.sleep(3000); myObject = new MyObject(); }catch(Exception e){ e.printStackTrace(); } } return myObject; } }
这样OK?看上去是的,是解决了得到了相同的实例,但是见到了synchronized这个东西,你不得犹豫一下么?加在了整个方法上啊,如果这个方法设计到比如说很多的过程或者运算,下一个线程想要取得对象,不是要等到程序员找到女朋友才行么。也就是要等到上一个线程释放锁之后,才可以继续进行。
有人说,我不加全部,我加部分不行么?
public static MyObject getInstance(){ synchronized (MyObject.class) { if (myObject == null) { try { Thread.sleep(3000); myObject = new MyObject(); } catch (Exception e) { e.printStackTrace(); } } } return myObject; } }
你仔细看,有啥大的变化么,并没有吧。因为效率还是一样的低低低低。每次我调用getIstance的时候是不是还要同步啊,所以太大变化啦
然后机智的我想出了这样的方法
public class MyObject { private static MyObject myObject; private MyObject(){ } public static MyObject getInstance(){ if (myObject == null) { try { Thread.sleep(3000); synchronized (MyObject.class) { myObject = new MyObject(); } } catch (Exception e) { e.printStackTrace(); } } return myObject; } }
我只加载了需要创建对象的那个关键地方,看到了么,这样效率大大滴提升了。但是,重点来了,我靠,打印出来了两个不同对象,打印出来的对象hashcode值不一样了啊,不是一个对象了,因为两个线程都进入了if语句内,之后没有在进行判断,所以创建了两个对象。我单例什么呢?所以这个方法也pass
那我到底该怎么办呢?别着急,DCL双重检查所机制,不废话直接看代码
public class MyObject { private static MyObject myObject; private MyObject(){ } public static MyObject getInstance(){ if (myObject == null) { try { Thread.sleep(3000); synchronized (MyObject.class) { if(myObject == null) { myObject = new MyObject(); } } } catch (Exception e) { e.printStackTrace(); } } return myObject; } }
哇,终于有方法可以实现单例模式在多线程环境下的正常工作了,哈哈哈哈哈,但是但是那你们就打错特错了。看如下分析
从JVM的角度来说,怎么创建一个对象呢?第一是申请一块内存,调用构造方法进行初始化操作,第二是分配一个指针指向这块内存。这两个操作谁在前面,谁在后面JVM并不会管它。那么就存在这么一种情况,JVM是先开辟出一块内存,然后把指针指向这块内存,最后调用构造方法进行初始化。
线程A开始创建MyObject的实例,此时线程B调用了getInstance()方法,首先判断MyObject是否为null。假设A已经把MyObject指向了那块内存,只是还没有调用构造方法,因此B检测到MyObject不为null,于是直接把MyObject返回了——问题出现了,尽管MyObject不为null,但它并没有构造完成结束。此时,如果B在A将MyObject构造完成之前就是用了这个实例,程序就会出现错误了!(其实在private static MyObject myObject; 改为 private volatile static MyObject myObject; 就不会发生这样的结果了。被volatile修饰的写变量不能和之前的读写代码调整,这里我们当做这个关键字不存在,以后会有专门的篇幅去详细讲解这个关键字的,这个关键字的坑有许多,我们慢慢踩)
那我们到底的咋整啊?
public class MyObject { private static MyObject myObject; private MyObject(){ } public static MyObject getInstance(){ if (myObject == null) { MyObject my; synchronized (MyObject.class) { my = myObject; if (my == null) { synchronized (MyObject.class) { if (my == null) { my = new MyObject(); } } myObject = my; } } } return myObject; } }
我们在第一个同步块里面创建一个临时变量,然后使用这个临时变量进行对象的创建,并且在最后把myObject指针临时变量的内存空间。写出这种代码基于以下思想,即synchronized会起到一个代码屏蔽的作用,同步块里面的代码和外部的代码没有联系。因此,在外部的同步块里面对临时变量my进行操作并不影响myObject,所以外部类在myObject=my;之前检测myObject的时候,结果myObject依然是null。
由于同步块的释放保证在此之前——也就是同步块里面——的操作必须完成,但是并不保证同步块之后的操作不能因编译器优化而调换到同步块结束之前进行。因此,编译器完全可以把myObject=my;这句移到内部同步块里面执行。又错了。
3、内部类实现方式
public class MyObject_inner { private static class MyObjectHandler{ private static MyObject_inner myObject_inner = new MyObject_inner(); } private MyObject_inner(){} public static MyObject_inner getInstance(){ return MyObjectHandler.myObject_inner; } }
在这一版本的单例模式实现代码中,我们使用了Java的静态内部类。这一技术是被JVM明确说明了的,因此不存在任何二义性。在这段代码中,因为Myobject_inner没有static的属性,因此并不会被初始化。直到调用getInstance()的时候,会首先加载MyObjectHandler类,这个类有一个static的MyObject_inne实例,因此需要调用MyObject_inne的构造方法,然后getInstance()将把这个内部类的myobject_inner返回给使用者。由于这个myobject_inner是static的,因此并不会构造多次。
由于MyObjectHandler是私有静态内部类,所以不会被其他类知道,同样,static语义也要求不会有多个实例存在。并且,JSL规范定义,类的构造必须是原子性的,非并发的,因此不需要加同步块。同样,由于这个构造是并发的,所以getInstance()也并不需要加同步。
但是这种情况完全是对的么?假如遇到序列化的对象呢?会是什么样的结果?
静态内部类可以达到线程安全的问题,但是如果遇到序列化对象的时候,使用默认的方式运行得到的结果还是多例的。
具体是为什么在序列化的时候不是单例的,本人我掌握的不太好,后续会把其中涉及到的知识补充之后,在完善此篇文章。
序列化会通过反射调用无参数的构造方法创建一个新的对象。解决的方式就是在implements Serializable这个方法里面加上这段代码
import java.io.Serializable; public class MyObject_inner implements Serializable{ private static final long seriaVersionUID = 8899L; private static class MyObjectHandler{ private static MyObject_inner myObject_inner = new MyObject_inner(); } private MyObject_inner(){} public static MyObject_inner getInstance(){ return MyObjectHandler.myObject_inner; } private Object readResolve(){ return MyObjectHandler.myObject_inner; } }
5、静态代码块实现单例模式
public class MyObject_inner { private static MyObject_inner instance = null; private MyObject_inner(){} static { instance = new MyObject_inner(); } public static MyObject_inner getInstance(){ return instance; } }
6、枚举方法实现单例模式
public class MyObject_enum { public enum EnumSingleton{ Instance; private MyObject_enum instance; EnumSingleton(){ instance = new MyObject_enum(); } public MyObject_enum getInstance(){ return instance; } } }
获取资源的方式很简单,只要 EnumSingleton.INSTANCE.getInstance() 即可获得所要实例。下面我们来看看单例是如何被保证的:
首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。枚举也提供了序列化机制。所以单元素的枚举类型已经成为了实现单例模式的最佳方法
这篇文章到此暂时先告一段落,里面有一点设计的序列化与反序列化实现单例模式我以后会在继续的更新补充进去的。 欢迎各位博友批评指正