单例模式
两件事:保证一个类只有单个实例,为该实例提供全局访问节点。
1、饿汉式:
在类加载期间初始化静态实例,保证instance实例的创建是线程安全的(实例在类加载期间实例化,有JVM保证线程安全)
特点:不支持延迟加载实例,这种类加载的方式比较慢,但是获取实例对象比较快。
问题:该对象足够大的话,没有使用到,就会造成内存的浪费。
public class Singleton_01 {
//1. 私有构造方法
private Singleton_01(){
}
//2. 在本类中创建私有静态的全局对象
private static Singleton_01 instance = new Singleton_01();
//3. 提供一个全局访问点,供外部获取单例对象
public static Singleton_01 getInstance(){
return instance;
}
}
为什么是线程安全的?实例对象在哪?【JVM 装载时,方法区】
类加载机制是类的字节码文件的数据读入内存中,同时生成数据的访问入口的特殊机制,即类加载的最终产物是数据访问入口。
类加载机制的加载的流程
通俗来讲,虚拟机将class文件加载到内存,并对数据进行解析和初始化,形成虚拟机可以直接使用的Java类型。
分为5个过程:装载、链接、初始化、使用和卸载。
装载:
查找和导入class文件。通过类的全限定名获取定义此类的二进制字节流,将该字节流所代表的静态存储结构转化为方法区的运行时数据结构,在Java堆中生成一个代表类的java.long.Class对象,作为方法区的数据访问入口。
结果:运行时的数据区的方法区和堆中有了信息,方法区:类信息、静态变量、常量,堆:代表被加载类的java.long.Class对象。
链接:验证、准备、解析
这里只讨论准备。为类的静态变量分配内存,并将其初始化为默认值。instance 对象为引用类型,此时值为null。
初始化:执行类构造器方法的过程。
通俗些,在准备阶段,类变量已经赋值过系统要求的默认值,而在初始化阶段,则根据程序员的程序制定的主观计划将初始化变量和其他资源,如赋值。此时,instance对象调用私有构造方法。getInstance()就可以拿到单例对象。
为什么说没有使用到会浪费空间?不是有GC垃圾回收机制吗?
JVM内存模型如图所示
在上一个问题中,我们可以得知单例对象在非堆区域,即方法区。垃圾回收是作用于堆区域的!
2、懒汉式(线程不安全)
此种方式的单例实现了懒加载,只有调用getInstance方法时 才创建对象.但是如果是多线程情况,会出现线程安全问题。
public class Singleton_03 {
//1. 私有构造方法
private Singleton_03(){
}
//2. 在本类中创建私有静态的全局对象
private static Singleton_03 instance;
//3. 通过添加synchronize,保证多线程模式下的单例对象的唯一性
public static synchronized Singleton_03 getInstance(){
if(instance == null){
instance = new Singleton_03();
}
return instance;
}
}
什么情况下不能保证单例?
A线程运行getInstance(),执行到创建对象的前一帧;B线程执行getInstance(),发现此时对象是null,也会创建对象。那么这样就不能保证对象唯一了。
3、懒汉式(线程安全)
原理:使用同步锁 synchronized
锁住 创建单例的方法 ,防止多个线程同时调用,从而避免造成单例被多次创建。
getInstance()方法只在一个线程中运行,另外线程试图运行该块代码,则会阻塞而一直等待。
线程安全的方法中,实现单例的创建,保证多线程模式下单例对象的唯一性。
缺点:频繁使用时,频繁加锁、释放锁、并发度低,导致性能瓶颈。
public class Singleton_03 {
//1. 私有构造方法
private Singleton_03(){
}
//2. 在本类中创建私有静态的全局对象
private static Singleton_03 instance;
//3. 通过添加synchronize,保证多线程模式下的单例对象的唯一性
public static synchronized Singleton_03 getInstance(){
if(instance == null){
instance = new Singleton_03();
}
return instance;
}
}
Synchronized 互斥锁
要明确,Synchronized锁是基于对象实现的!那么,我们来简单的看下synchronized是如何实现基于对象的互斥锁。
首先,对象在内存中是如何存储的?
无锁状态、匿名偏向状态:没有线程拿锁。
偏向锁状态:没有线程的竞争,只有一个线程再获取锁资源。 线程竞争锁资源时,发现当前synchronized没有线程占用锁资源,并且锁是偏向锁,使用CAS的方式,设置o的线程ID为当前线程,获取到锁资源,下次当前线程再次获取时,只需要判断是偏向锁,并且线程ID是当前线程ID即可,直接获得到锁资源。
轻量级锁:偏向锁出现竞争时,会升级到轻量级锁(触发偏向锁撤销)。 轻量级锁的状态下,线程会基于CAS的方式,尝试获取锁资源,CAS的次数是基于自适应自旋锁实现的,JVM会自动的基于上一次获取锁是否成功,来决定这次获取锁资源要CAS多少次。
重量级锁:轻量级锁CAS一段次数后,没有拿到锁资源,升级为重量级锁(其实CAS操作是在重量级锁时执行的)。 重量级锁就是线程拿不到锁,就挂起。
4、双重校验
饿汉式不支持延迟加载,懒汉式有性能问题,不支持高并发。那我们再来看一种既支持延迟加载、又支持高并发的单例实现方式,也就是双重检测实现方式。
/**
* 单例模式-双重校验
**/
public class Singleton_04 {
//使用 volatile保证变量的可见性
private volatile static Singleton_04 instance = null;
private Singleton_04(){
}
//对外提供静态方法获取对象
public static Singleton_04 getInstance(){
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null){
synchronized (Singleton_04.class){
//抢到锁之后再次进行判断是否为null
if(instance == null){
instance = new Singleton_04();
}
}
}
return instance;
}
}
volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。
第一:使用volatile关键字会强制将修改的值立即写入主存;
第二:使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
第三:由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。
在Java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。
这里是因为 singleton = new Singleton() ,它并非是一个原子操作,事实上,在 JVM 中上述语句至少做了以下这 3 件事:
第一步是给 singleton 分配内存空间;
第二步开始调用 Singleton 的构造函数等,来初始化 singleton;
第三步,将 singleton 对象指向分配的内存空间(执行完这步 singleton 就不是 null 了)。
这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
如果是 1-3-2,那么在第 3 步执行完以后,singleton 就不是 null 了,可是这时第 2 步并没有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。假设此时线程 2 进入 getInstance 方法,由于 singleton 已经不是 null 了,所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错.
5、静态内部类
利用静态内部类的特性,同时解决按需加载、线程安全的问题,代码简洁。
public class Singleton_05 {
private static class SingletonHandler{
private static Singleton_05 instance = new Singleton_05();
}
private Singleton_05(){}
public static Singleton_05 getInstance(){
return SingletonHandler.instance;
}
}
内部静态类不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法的时候才会加载静态内部类。
反射对于单例的破坏
反射的概念: Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。
反射技术过于强大,它可以通过setAccessible()
来修改构造器,字段,方法的可见性。单例模式的构造方法是私有的,如果将其可见性设为public
,那么将无法控制对象的创建。
解决方法之一: 在单例类的构造方法中 添加判断 instance != null
时,直接抛出异常
序列化对单例的破坏
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性
。
问题是出在ObjectInputputStream 的readObject 方法上, 我们来看一下ObjectInputStream的readObject的调用时,运行如下(本质上也是反射引起的):
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//此处省略部分代码
Object obj;
try {
//通过反射创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。
//isInstantiable:如果一个serializable的类可以在运行时被实例化,那么该方法就返回true
//desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
return obj;
}
/**
* 解决方案:只要在Singleton类中定义readResolve就可以解决该问题
* 程序会判断是否有readResolve方法,如果存在就在执行该方法,如果不存在--就创建一个对象
*/
private Object readResolve() {
return singleton;
}
实现原理:
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
6、枚举(推荐)
public enum Singleton_06{
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static Singleton_06 getInstance(){
return INSTANCE;
}
}
总结:
1 ) 单例的定义 单例设计模式保证某个类在运行期间,只有一个实例对外提供服务,而这个类被称为单例类。
2 ) 单例的实现
饿汉式
饿汉式的实现方式,在类加载的期间,就已经将 instance 静态实例初始化好了,所以,instance 实例的创建是线程安全的。不过,这样的实现方式不支持延迟加载实例。
懒汉式
相对于饿汉式的优势是支持延迟加载。这种实现方式会导致频繁加锁、释放锁,以及并发度低等问题,频繁的调用会产生性能瓶颈。
双重检测
双重检测实现方式既支持延迟加载、又支持高并发的单例实现方式。只要 instance 被创建之后,再调用 getInstance() 函数都不会进入到加锁逻辑中。所以,这种实现方式解决了懒汉式并发度低的问题。
静态内部类
利用 Java 的静态内部类来实现单例。这种实现方式,既支持延迟加载,也支持高并发,实现起来也比双重检测简单。
枚举方式
最简单的实现方式,基于枚举类型的单例实现。这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性(同时阻止了反射和序列化对单例的破坏)。