单例模式(Singleton Pattern)是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
上面是我从菜鸟教程摘录下关于单例的定义,单例,见名知意,即只有唯一一个实例。
下面我们从单例的多种写法开始说起。
单例的N多种写法
此次转载一篇博客文章,来自hollis的博客,hollis博客中的这篇文章是对一篇文章的转载和翻译,我们拿过来用一下。
单例的七种写法
第一种(懒汉,线程不安全):
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
第二种(懒汉,线程安全):
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。
第三种(饿汉):
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
这种方式基于classloder
机制,在深度分析Java的ClassLoader机制(源码级别)和Java类的加载、链接和初始化两个文章中有关于CLassload而机制的线程安全问题的介绍,避免了多线程的同步问题,不过,instance
在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance
方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance
显然没有达到lazy loading
的效果。
第四种(饿汉,变种):
public class Singleton {
private Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton (){}
public static Singleton getInstance() {
return this.instance;
}
}
表面上看起来差别挺大,其实跟第三种方式差不多,都是在类初始化即实例化instance。
第五种(静态内部类):
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
这种方式同样利用了classloder
的机制来保证初始化instance
时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。
第六种(枚举):
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,在深度分析Java的枚举类型—-枚举的线程安全性及序列化问题中有详细介绍枚举的线程安全问题和序列化问题,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。
第七种(双重校验锁):
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
总结
有两个问题需要注意:
1.如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。
2.如果Singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。单例与序列化的那些事儿
对第一个问题修复的办法是:
private static Class getClass(String classname) throws ClassNotFoundException {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null)
classLoader = Singleton.class.getClassLoader();
return (classLoader.loadClass(classname));
}
}
对第二个问题修复的办法是:
public class Singleton implements java.io.Serializable {
public static Singleton INSTANCE = new Singleton();
protected Singleton() {
}
private Object readResolve() {
return INSTANCE;
}
}
对我来说,我比较喜欢第三种和第五种方式,简单易懂,而且在JVM层实现了线程安全(如果不是多个类加载器环境),一般的情况下,我会使用第三种方式,只有在要明确实现lazy loading效果时才会使用第五种方式,另外,如果涉及到反序列化创建对象时我会试着使用枚举的方式来实现单例,不过,我一直会保证我的程序是线程安全的,而且我永远不会使用第一种和第二种方式,如果有其他特殊的需求,我可能会使用第七种方式,毕竟,JDK1.5已经没有双重检查锁定的问题了。
不过一般来说,第一种不算单例,第四种和第三种就是一种,如果算的话,第五种也可以分开写了。所以说,一般单例都是五种写法。懒汉,恶汉,双重校验锁,枚举和静态内部类。
以上为转载内容,关于单例,这里列了7种,其实严格说来只有5种,用起来比较顺手的可能就是第三种、第五种和第七种,关于枚举的话,如果第一次看到会完全蒙圈,这是啥写法啊,就1行就实现单例了吗,我初次见到枚举这种单例时,也是非常的迷惑,后来使用javap反编译了枚举类,才稍微看明白了一点。
单例需要添加私有的构造函数,有2个目的
- 强调类的单例模式
- 通过类的私有构造函数来强调类的不可实例化
通过私有构造函数来强调这是个单例类
序列化对单例的破坏
在上一节单例写法介绍中,介绍了如何保证单例的线程安全,这一节来介绍一下如何通过序列化和反序列化破坏单例
先引用一下hollis的文章
单例与序列化的那些事
先上结论:
序列化会通过反射调用无参的构造方法创建一个新的对象,从而破坏单例。
代码示例
首先来写一个单例的类:
code 1
package com.hollis;
import java.io.Serializable;
/**
* Created by hollis on 16/2/5.
* 使用双重校验锁方式实现单例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
接下来是一个测试类:
code 2
package com.hollis;
import java.io.*;
/**
* Created by hollis on 16/2/5.
*/
public class SerializableDemo1 {
//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
//Exception直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
//判断是否是同一个对象
System.out.println(newInstance == Singleton.getSingleton());
}
}
//false
输出结构为false,说明:
通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。
这里,在介绍如何解决这个问题之前,我们先来深入分析一下,为什么会这样?在反序列化的过程中到底发生了什么。
ObjectInputStream
对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream 的readObject
方法执行情况到底是怎样的。
为了节省篇幅,这里给出ObjectInputStream的readObject
的调用栈:

这里看一下重点代码,readOrdinaryObject
方法的代码片段: code 3
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
//此处省略部分代码
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//此处省略部分代码
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);
}
}
return obj;
}
code 3 中主要贴出两部分代码。先分析第一部分:
code 3.1
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);
}
这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject
返回的对象。

isInstantiable
:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。
desc.newInstance
:该方法通过反射的方式调用无参构造方法新建一个对象。
所以。到目前为止,也就可以解释,为什么序列化可以破坏单例了?
答:序列化会通过反射调用无参数的构造方法创建一个新的对象。
那么,接下来我们再看刚开始留下的问题,如何防止序列化/反序列化破坏单例模式。
防止序列化破坏单例模式
先给出解决方案,然后再具体分析原理:
只要在Singleton类中定义readResolve
就可以解决该问题:
code 4
package com.hollis;
import java.io.Serializable;
/**
* Created by hollis on 16/2/5.
* 使用双重校验锁方式实现单例
*/
public class Singleton implements Serializable{
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
private Object readResolve() {
return singleton;
}
}
还是运行以下测试类:
package com.hollis;
import java.io.*;
/**
* Created by hollis on 16/2/5.
*/
public class SerializableDemo1 {
//为了便于理解,忽略关闭流操作及删除文件操作。真正编码时千万不要忘记
//Exception直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
//Write Obj to file
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(Singleton.getSingleton());
//Read Obj from file
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
//判断是否是同一个对象
System.out.println(newInstance == Singleton.getSingleton());
}
}
//true
本次输出结果为true。具体原理,我们回过头继续分析code 3中的第二段代码:
code 3.2
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);
}
}
hasReadResolveMethod
:如果实现了serializable 或者 externalizable接口的类中包含readResolve
则返回true
invokeReadResolve
:通过反射的方式调用要被反序列化的类的readResolve方法。
所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
总结
在涉及到序列化的场景时,要格外注意他对单例的破坏。
以上的反序列化对单例模式的破坏,文章中也说清楚了,在单例类里添加一个readResolve方法即可防止单例被破坏。
单例模式虽然定义简单,不过要考虑的内容也实在挺多,既要考虑线程安全,又要考虑不同的类加载器导致不同的实例,还要考虑反射、序列化等对单例模式的破坏,冷不丁再给你出个不使用synchronized和lock,如何实现一个线程安全的单例的需求(单例太难了)。
枚举实现单例
代码展示
在枚举实现单例那个部分,介绍了枚举实现单例,代码非常的简单
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
就这几行代码,我实在看不出哪来的单例,单例不应该先定义一个静态变量,再定义一个静态方法去获取这个静态变量吗,这枚举写的是啥玩意,后来通过反编译才有点明白
这是枚举类
/**
* 枚举类实现单例
*
* @author 18220
*/
public enum SingletonTest6 {
/**
* 实例
*/
INSTANCE;
public void whateverMethod() {
}
}
使用javap -p 进行反编译
public final class com.fc.singleton.SingletonTest6 extends java.lang.Enum<com.fc.singleton.SingletonTest6> {
public static final com.fc.singleton.SingletonTest6 INSTANCE;
private static final com.fc.singleton.SingletonTest6[] $VALUES;
public static com.fc.singleton.SingletonTest6[] values();
public static com.fc.singleton.SingletonTest6 valueOf(java.lang.String);
private com.fc.singleton.SingletonTest6();
public void whateverMethod();
static {};
}
从这个反编译的内容我们可以看出一点门道,使用关键字enum可以定义一个枚举类,经过反编译之后,我们可以看到定义的枚举类是一个final修饰的类,继承自Enum<T>,我们在枚举类中定义的INSTANCE是一个static final类型的变量,包含了一个私有构造函数,经过反编译之后,枚举的单例写法就和第一部分介绍的写法没什么区别了。
使用javap -p反编译出来的代码,虽然进行了INSTANCE实例的定义,但是没有任何赋值的语句,static{}块为空,猜测INSTANCE的初始化是在static块中做的,下面使用javap -c命令看一下static块中做了些啥
E:\javase\basis\vamei\target\classes\com\fc\singleton>javap -c SingletonTest6
: ļSingletonTest6com.fc.singleton.SingletonTest6
Compiled from "SingletonTest6.java"
public final class com.fc.singleton.SingletonTest6 extends java.lang.Enum<com.fc.singleton.SingletonTest6> {
public static final com.fc.singleton.SingletonTest6 INSTANCE;
public static com.fc.singleton.SingletonTest6[] values();
Code:
0: getstatic #1 // Field $VALUES:[Lcom/fc/singleton/SingletonTest6;
3: invokevirtual #2 // Method "[Lcom/fc/singleton/SingletonTest6;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[Lcom/fc/singleton/SingletonTest6;"
9: areturn
public static com.fc.singleton.SingletonTest6 valueOf(java.lang.String);
Code:
0: ldc #4 // class com/fc/singleton/SingletonTest6
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class com/fc/singleton/SingletonTest6
9: areturn
public void whateverMethod();
Code:
0: return
static {};
Code:
0: new #4 // class com/fc/singleton/SingletonTest6
3: dup
4: ldc #7 // String INSTANCE
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field INSTANCE:Lcom/fc/singleton/SingletonTest6;
13: iconst_1
14: anewarray #4 // class com/fc/singleton/SingletonTest6
17: dup
18: iconst_0
19: getstatic #9 // Field INSTANCE:Lcom/fc/singleton/SingletonTest6;
22: aastore
23: putstatic #1 // Field $VALUES:[Lcom/fc/singleton/SingletonTest6;
26: return
}
把最后一部分关于static的代码块摘出来
static {};
Code:
0: new #4 // class com/fc/singleton/SingletonTest6
3: dup
4: ldc #7 // String INSTANCE
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field INSTANCE:Lcom/fc/singleton/SingletonTest6;
13: iconst_1
14: anewarray #4 // class com/fc/singleton/SingletonTest6
17: dup
18: iconst_0
19: getstatic #9 // Field INSTANCE:Lcom/fc/singleton/SingletonTest6;
22: aastore
23: putstatic #1 // Field $VALUES:[Lcom/fc/singleton/SingletonTest6;
26: return
查看虚拟机字节码指令表,从Code 0~10大致解释如下:
0: new #4 //创建一个对象,将其引用值压入栈
3: dup //复制栈顶数值并将复制值压入栈顶
4: ldc #7 //将String常量值SAVING从常量池推送至栈顶
6: iconst_0 //将int型0推送至栈顶
7: invokespecial #8 //调用超类构造器
10: putstatic #9 //为指定的静态域赋值
所以最后反编译完整的代码大概是这样
public final class com.fc.singleton.SingletonTest6 extends java.lang.Enum<com.fc.singleton.SingletonTest6> {
public static final com.fc.singleton.SingletonTest6 INSTANCE;
private static final com.fc.singleton.SingletonTest6[] $VALUES;
public static com.fc.singleton.SingletonTest6[] values();
public static com.fc.singleton.SingletonTest6 valueOf(java.lang.String);
private com.fc.singleton.SingletonTest6();
public void whateverMethod();
static {
INSTANCE = new com.fc.singleton.SingletonTest6();
$VALUES = new com.fc.singleton.SingletonTest6[] {ISNTANCE};
};
}
参考文章:
枚举也就那回事
为什么墙裂推荐用枚举实现单例
枚举的线程安全性和序列化问题
上一节通过枚举的代码写法和反编译,看了一下枚举类的本质,使用枚举类实现单例是非常简洁的,不用写线程安全的代码,也没有反序列化破坏单例的风险,本节就看一下这2个问题。
枚举是如何保证线程安全性的
上一节中我们通过反编译,可以得到如下类名
public final class com.fc.singleton.SingletonTest6 extends java.lang.Enum<com.fc.singleton.SingletonTest6>
编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承,而且属性和方法都是static的,类加载和初始化过程会保证静态变量的加载是线程安全的,所以枚举类可以保证线程安全。
枚举对序列化的处理
我们知道,以前的所有的单例模式都有一个比较大的问题,就是一旦实现了Serializable接口之后,就不再是单例得了,因为,每次调用 readObject()方法返回的都是一个新创建出来的对象,有一种解决办法就是使用readResolve()方法来避免此事发生。但是,为了保证枚举类型像Java规范中所说的那样,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定。原文如下:
Enum constants are serialized differently than ordinary serializable or externalizable objects. The serialized form of an enum constant consists solely of its name; field values of the constant are not present in the form. To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. To deserialize an enum constant, ObjectInputStream reads the constant name from the stream; the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method, passing the constant’s enum type along with the received constant name as arguments. Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream. The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent.
大概意思就是说,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。 我们看一下这个valueOf方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType,String name) {
T result = enumType.enumConstantDirectory().get(name);
if (result != null) {
return result;
}
if (name == null) {
throw new NullPointerException("Name is null");
}
throw new IllegalArgumentException(
"No enum const " + enumType +"." + name);
}
从代码中可以看到,代码会尝试从调用enumType这个Class对象的enumConstantDirectory()方法返回的map中获取名字为name的枚举对象,如果不存在就会抛出异常。再进一步跟到enumConstantDirectory()方法,就会发现到最后会以反射的方式调用enumType这个类型的values()静态方法,也就是上面我们看到的编译器为我们创建的那个方法,然后用返回结果填充enumType这个Class对象中的enumConstantDirectory属性。
所以,JVM对序列化有保证。
以上关于枚举序列化的内容引用了下面这篇文章
深度分析java的枚举类型
枚举对序列化反序列化重新进行了定制,可以避免对单例的破坏,综合这几个原因考虑,可以说枚举是 实现单例的最好方式了。不过大家对枚举不熟悉,可能还是会对这种写法很陌生。
单例的线程安全性
如果考虑单例的线程安全性的话,可以使用如下三种常规写法
- 静态成员变量
- 静态内部类
- 双重校验锁
static为什么能保证线程安全性呢,从盘古开天辟地开始说起,我们从类加载开始说起。
Java的类加载机制
我们知道一个Java类要想运行,必须由jvm将其装载到内存中才能运行,装载的目的就是把Java字节代码转换成JVM中的java.lang.Class类的对象。这样Java就可以对该对象进行一系列操作,装载过程有两个比较重要的特征:层次组织结构和代理模式。层次组织结构指的是每个类加载器都有一个父类加载器,通过getParent()方法可以获取到。类加载器通过这种父亲-后代的方式组织在一起,形成树状层次结构。代理模式则指的是一个类加载器既可以自己完成Java类的定义工作,也可以代理给其它的类加载器来完成。由于代理模式的存在,启动一个类的加载过程的类加载器和最终定义这个类的类加载器可能并不是一个。ClassLoader的加载类过程主要使用loadClass方法,该方法中封装了中加载机制:双亲委派模式。
一般来说,父类优先的策略就足够好了。在某些情况下,可能需要采取相反的策略,即先尝试自己加载,找不到的时候再代理给父类加载器。这种做法在Java的Web容器中比较常见,也是Servlet规范推荐的做法。比如,Apache Tomcat为每个Web应用都提供一个独立的类加载器,使用的就是自己优先加载的策略。IBM WebSphere Application Server则允许Web应用选择类加载器使用的策略。
类加载器的一个重要用途是在JVM中为相同名称的Java类创建隔离空间。在JVM中,判断两个类是否相同,不仅是根据该类的二进制名称,还需要根据两个类的定义类加载器。只有两者完全一样,才认为两个类的是相同的。因此,即便是同样的Java字节代码,被两个不同的类加载器定义之后,所得到的Java类也是不同的。如果试图在两个类的对象之间进行赋值操作,会抛出java.lang.ClassCastException。这个特性为同样名称的Java类在JVM中共存创造了条件。在实际的应用中,可能会要求同一名称的Java类的不同版本在JVM中可以同时存在。通过类加载器就可以满足这种需求。这种技术在OSGi中得到了广泛的应用
Java类的加载过程:
1.通过类的全名产生对应类的二进制数据流。(如果没找到对应类文件,只有在类实际使用时才抛出错误。)
2.分析并将这些二进制数据流转换为方法区(JVM 的架构:方法区、堆,栈,本地方法栈,pc 寄存器)特定的数据结构(这些数据结构是实现有关的,不同 JVM 有不同实现)。这里处理了部分检验,比如类文件的魔数的验证,检查文件是否过长或者过短,确定是否有父类(除了 Obecjt 类)。
3.创建对应类的 java.lang.Class 实例(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载链链接!)。
Java类的链接
Java类的链接指的是将Java类的二进制代码合并到JVM的运行状态之中的过程。在链接之前,这个类必须被成功加载。
链接的过程比加载过程要复杂很多,这是实现java的动态性的重要一步!分为三部分:verification (检测), preparation(准备) 和 resolution(解析)
1.verification(检测):
验证是用来确保Java类的二进制表示在结构上是完全正确的。如果验证过程出现错误的话,会抛出java.lang.VerifyError错误。
linking的resolve会把类中成员方法、成员变量、类和接口的符号引用替换为直接引用,而在这之前,需要检测被引用的类型正确性和接入属性是否正确(就是public ,private的的问题)诸如,检查final class 没有被继承,检查静态变量的正确性等等。
验证是用来确保Java类的二进制表示在结构上是完全正确的。如果验证过程出现错误的话,会抛出java.lang.VerifyError错误。
2.preparation(准备):
准备过程则是创建Java类中的静态域,即static变量和staitic代码块,并将这些域的值设为默认值。准备过程并不会执行代码。在一个Java类中会包含对其它类或接口的形式引用,包括它的父类、所实现的接口、方法的形式参数和返回值的Java类等。
对类的成员变量分配空间。虽然有初始值,但这个时候不会对他们进行初始化(因为这里不会执行任何 Java 代码)。具体如下:
所有原始类型的值都为 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底层实现大多使用 int),引用类型则为 null。值得注意的是,JVM 可能会在这个时期给一些有助于程序运行效率提高的数据结构分配空间。
也就是说这一步仅仅是分配了空间,比如static int a =10;
在准备阶段只会分配一个o值,变成static int a = 0;
,而具体的赋值则会在初始化阶段进行。
3.resolution(解析):
解析的过程就是确保这些被引用的类能被正确的找到。解析的过程可能会导致其它的Java类被加载。
为类、接口、方法、成员变量的符号引用定位直接引用(如果符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。
这一步是可选的。可以在符号引用第一次被使用时完成,即所谓的延迟解析(late resolution)。但对用户而言,这一步永远是延迟解析的,即使运行时会执行 early resolution,但程序不会显示的在第一次判断出错误时抛出错误,而会在对应的类第一次主动使用的时候抛出错误!
另外,这一步与之后的类初始化是不冲突的,并非一定要所有的解析结束以后才执行类的初始化。不同的 JVM 实现不同。
看下面一段代码:
public class LinkTest {
public static void main(String[] args) {
ToBeLinked toBeLinked = null;
System.out.println("Test link.");
}
}
类 LinkTest引用了类ToBeLinked,但是并没有真正使用它,只是声明了一个变量,并没有创建该类的实例或是访问其中的静态域。如果把编译好的ToBeLinked的Java字节代码删除之后,再运行LinkTest,程序不会抛出错误。这是因为ToBeLinked类没有被真正用到。链接策略使得ToBeLinked类不会被加载,因此也不会发现ToBeLinked的Java字节代码实际上是不存在的。如果把代码改成ToBeLinked toBeLinked = new ToBeLinked();之后,再按照相同的方法运行,就会抛出异常了。因为这个时候ToBeLinked这个类被真正使用到了,会需要加载这个类。
Java类的初始化
开发 Java 时,接触最多的是对象的初始化。实际上类也是有初始化的。相比对象初始化,类的初始化机制要简单不少。
类的初始化也是延迟的,直到类第一次被主动使用(active use),JVM 才会初始化类。
当一个Java类第一次被真正使用到的时候,JVM会进行该类的初始化操作。初始化过程的主要操作是执行静态代码块和初始化静态域。在一个类被初始化之前,它的直接父类也需要被初始化。但是,一个接口的初始化,不会引起其父接口的初始化。在初始化的时候,会按照源代码中从上到下的顺序依次执行静态代码块和初始化静态域。
public class StaticTest {
public static int X = 10;
public static void main(String[] args) {
System.out.println(Y); //输出60
}
static {
X = 30;
}
public static int Y = X * 2;
}
在上面的代码中,在初始化的时候,静态域的初始化和静态代码块的执行会从上到下依次执行。因此变量X的值首先初始化成10,后来又被赋值成30;而变量Y的值则被初始化成60。
类的初始化分两步:
1.如果基类没有被初始化,初始化基类。
2.有类构造函数,则执行类构造函数。
类构造函数是由 Java 编译器完成的。它把类成员变量的初始化和 static 区间的代码提取出,放到一个<clinit>方法中。这个方法不能被一般的方法访问(注意,static final 成员变量不会在此执行初始化,它一般被编译器生成 constant 值)。同时,<clinit>中是不会显示的调用基类的<clinit>的,因为 1 中已经执行了基类的初始化。该初始化过程是由 Jvm 保证线程安全的。。
Java类和接口的初始化只有在特定的时机才会发生,这些时机包括:
创建一个Java类的实例。如
MyClass obj = new MyClass()
调用一个Java类中的静态方法。如
MyClass.sayHello()
给Java类或接口中声明的静态域赋值。如
MyClass.value = 10
访问Java类或接口中声明的静态域,并且该域不是常值变量。如
int value = MyClass.value
在顶层Java类中执行assert语句。
通过Java反射API也可能造成类和接口的初始化。需要注意的是,当访问一个Java类或接口中的静态域的时候,只有真正声明这个域的类或接口才会被初始化。考虑下面的代码:
class B {
static int value = 100;
static {
System.out.println("Class B is initialized."); //输出
}
}
class A extends B {
static {
System.out.println("Class A is initialized."); //不会输出
}
}
public class InitTest {
public static void main(String[] args) {
System.out.println(A.value); //输出100
}
}
在上述代码中,类InitTest通过A.value引用了类B中声明的静态域value。由于value是在类B中声明的,只有类B会被初始化,而类A则不会被初始化。
以上为引用内容,类加载过程分为三步
- 加载
- 链接
- 初始化
然后static会在链接的准备过程中分配地址,并在初始化的过程中进行真正的赋值
java的类加载过程是由jvm保证线程安全的。
类加载过程源码分析
为了更好的理解类的加载机制,我们来深入研究一下ClassLoader
和他的loadClass()
方法。
源码分析
public abstract class ClassLoader
ClassLoader
类是一个抽象类,sun公司是这么解释这个类的:
/**
* A class loader is an object that is responsible for loading classes. The
* class <tt>ClassLoader</tt> is an abstract class. Given the <a
* href="#name">binary name</a> of a class, a class loader should attempt to
* locate or generate data that constitutes a definition for the class. A
* typical strategy is to transform the name into a file name and then read a
* "class file" of that name from a file system.
**/
大致意思如下:
class loader是一个负责加载classes的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,class loader尝试定位或者产生一个class的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。
接下来我们看loadClass方法的实现方式:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
还是来看sun公司对该方法的解释:
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <p><ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
*/
大致内容如下:
使用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类: 调用
findLoadedClass(String)
方法检查这个类是否被加载过 使用父加载器调用loadClass(String)
方法,如果父加载器为Null
,类加载器装载虚拟机内置的加载器调用findClass(String)
方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve
参数的值为true
,那么就调用resolveClass(Class)
方法来处理类。ClassLoader
的子类最好覆盖findClass(String)
而不是这个方法。 除非被重写,这个方法默认在整个装载过程中都是同步
的(线程安全
的)
接下来,我们开始分析该方法。
protected Class loadClass(String name, boolean resolve) 该方法的访问控制符是`protected`,也就是说该方法**同包内和派生类中可用** 返回值类型`Class
,这里用到**泛型**。这里使用通配符
?作为泛型实参表示对象可以 接受任何类型(类类型)。因为该方法不知道要加载的类到底是什么类,所以就用了通用的泛型。
String name要查找的类的名字,
boolean resolve,一个标志,
true表示将调用
resolveClass(c)`处理该类
throws ClassNotFoundException 该方法会抛出找不到该类的异常,这是一个非运行时异常
synchronized (getClassLoadingLock(name)) 看到这行代码,我们能知道的是,这是一个同步代码块,那么synchronized的括号中放的应该是一个对象。我们来看getClassLoadingLock(name)
方法的作用是什么:
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
以上是getClassLoadingLock(name)
方法的实现细节,我们看到这里用到变量parallelLockMap
,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个putIfAbsent(className, newLock);
方法来给刚刚创建好的对象赋值,这个方法的作用我们一会讲。那么这个parallelLockMap
变量又是哪来的那,我们发现这个变量是ClassLoader
类的成员变量:
private final ConcurrentHashMap<String, Object> parallelLockMap;
这个变量的初始化工作在ClassLoader
的构造函数中:
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
这里我们可以看到构造函数根据一个属性ParallelLoaders
的Registered
状态的不同来给parallelLockMap
赋值。 我去,隐藏的好深,好,我们继续挖,看看这个ParallelLoaders
又是在哪赋值的呢?我们发现,在ClassLoader类中包含一个静态内部类private static class ParallelLoaders
,在ClassLoader
被加载的时候这个静态内部类就被初始化。这个静态内部类的代码我就不贴了,直接告诉大家什么意思,sun公司是这么说的:Encapsulates the set of parallel capable loader types
,意识就是说:封装了并行的可装载的类型的集合。
上面这个说的是不是有点乱,那让我们来整理一下: 首先,在ClassLoader类中有一个静态内部类
ParallelLoaders
,他会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么他就给parallelLockMap
定义,就是new
一个ConcurrentHashMap<>()
,那么这个时候,我们知道如果当前的加载器是具有并行能力的,那么parallelLockMap
就不是Null
,这个时候,我们判断parallelLockMap
是不是Null
,如果他是null,说明该加载器没有注册并行能力,那么我们没有必要给他一个加锁的对象,getClassLoadingLock
方法直接返回this
,就是当前的加载器的一个实例。如果这个parallelLockMap
不是null
,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。然后就是创建一个新的Object对象,调用parallelLockMap
的putIfAbsent(className, newLock)
方法,这个方法的作用是:首先根据传进来的className,检查该名字是否已经关联了一个value值,如果已经关联过value值,那么直接把他关联的值返回,如果没有关联过值的话,那就把我们传进来的Object对象作为value值,className作为Key值组成一个map返回。然后无论putIfAbsent方法的返回值是什么,都把它赋值给我们刚刚生成的那个Object对象。 这个时候,我们来简单说明一下getClassLoadingLock(String className)
的作用,就是: 为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。
Class c = findLoadedClass(name); 在这里,在加载类之前先调用findLoadedClass
方法检查该类是否已经被加载过,findLoadedClass会返回一个Class
类型的对象,如果该类已经被加载过,那么就可以直接返回该对象(在返回之前会根据resolve
的值来决定是否处理该对象,具体的怎么处理后面会讲)。 如果,该类没有被加载过,那么执行以下的加载过程
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
如果父加载器不为空,那么调用父加载器的loadClass
方法加载类,如果父加载器为空,那么调用虚拟机的加载器来加载类。
如果以上两个步骤都没有成功的加载到类,那么
c = findClass(name);
调用自己的findClass(name)
方法来加载类。
这个时候,我们已经得到了加载之后的类,那么就根据resolve
的值决定是否调用resolveClass
方法。resolveClass
方法的作用是:
链接指定的类。这个方法给
Classloader
用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照Java™
规范中的Execution
描述进行链接。。。
至此,ClassLoader类以及loadClass方法的源码我们已经分析完了,那么。结合源码的分析,我们来总结一下:
总结
java中的类大致分为三种:
1.系统类 2.扩展类 3.由程序员自定义的类
类装载方式,有两种:
1.隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。 2.显式装载, 通过class.forname()等方法,显式加载需要的类
类加载的动态性体现:
一个应用程序总是由n多个类组成,Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现
java类装载器
Java中的类装载器实质上也是类,功能是把类载入jvm中,值得注意的是jvm的类装载器并不是一个,而是三个,层次结构如下:

为什么要有三个类加载器,一方面是分工,各自负责各自的区块,另一方面为了实现委托模型,下面会谈到该模型
类加载器之间是如何协调工作的
前面说了,java中有三个类加载器,问题就来了,碰到一个类需要加载时,它们之间是如何协调工作的,即java是如何区分一个类该由哪个类加载器来完成呢。 在这里java采用了委托模型机制,这个机制简单来讲,就是“类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类”

下面举一个例子来说明,为了更好的理解,先弄清楚几行代码:
Public class Test{
Public static void main(String[] arg){
ClassLoader c = Test.class.getClassLoader(); //获取Test类的类加载器
System.out.println(c);
ClassLoader c1 = c.getParent(); //获取c这个类加载器的父类加载器
System.out.println(c1);
ClassLoader c2 = c1.getParent();//获取c1这个类加载器的父类加载器
System.out.println(c2);
}
}
运行结果:
。。。AppClassLoader。。。
。。。ExtClassLoader。。。
Null
可以看出Test是由AppClassLoader加载器加载的,AppClassLoader的Parent
加载器是 ExtClassLoader,但是ExtClassLoader
的Parent
为 null
是怎么回事呵,朋友们留意的话,前面有提到Bootstrap Loader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,所以在java
程序代码里试图打印出其内容时,我们就会看到输出为null
。
类装载器ClassLoader(一个抽象类)描述一下JVM加载class文件的原理机制
类装载器就是寻找类或接口字节码文件进行解析并构造JVM内部对象表示的组件,在java中类装载器把一个类装入JVM,经过以下步骤:
1、装载:查找和导入Class文件 2、链接:其中解析步骤是可以选择的 (a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用 3、初始化:对静态变量,静态代码块执行初始化工作
类装载工作由ClassLoder
和其子类负责。JVM在运行时会产生三个ClassLoader:根装载器,ExtClassLoader
(扩展类装载器)和AppClassLoader
,其中根装载器不是ClassLoader的子类,由C++编写,因此在java中看不到他,负责装载JRE的核心类库,如JRE目录下的rt.jar,charsets.jar等。ExtClassLoader
是ClassLoder
的子类,负责装载JRE扩展目录ext下的jar类包;AppClassLoader
负责装载classpath路径下的类包,这三个类装载器存在父子层级关系****,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认情况下使用AppClassLoader装载应用程序的类
Java装载类使用“全盘负责委托机制”。“全盘负责”是指当一个ClassLoder
装载一个类时,除非显示的使用另外一个ClassLoder
,该类所依赖及引用的类也由这个ClassLoder
载入;“委托机制”是指先委托父类装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全方面考虑的,试想如果一个人写了一个恶意的基础类(如java.lang.String
)并加载到JVM
将会引起严重的后果,但有了全盘负责制,java.lang.String
永远是由根装载器来装载,避免以上情况发生 除了JVM默认的三个ClassLoder
以外,第三方可以编写自己的类装载器,以实现一些特殊的需求。类文件被装载解析后,在JVM
中都有一个对应的java.lang.Class
对象,提供了类结构信息的描述。数组,枚举及基本数据类型,甚至void
都拥有对应的Class
对象。Class
类没有public
的构造方法,Class
对象是在装载类时由JVM
通过调用类装载器中的defineClass()
方法自动构造的。
以上为引用内容
这篇文章从源码分析了类加载是如何保证了线程安全性的,对类加载的三个步骤进行了介绍,引出了双亲委派模型。并介绍了常见的类加载器,自己也可以实现自己的类加载器。
单例模式的破坏
单例模式需要考虑很多问题,线程安全,反射,序列化、反序列化,不同的类加载器加载同一个类等都有可能对单例进行破坏,详细的内容上面都有介绍,如果有别的方法破坏单例欢迎补充。
不使用synchronized和lock如何实现单例
上面静态变量、静态内部类、枚举底层都是依赖锁保证单例的线程安全性的,那有没有不使用同步机制实现单例的方式呢
答案是可以使用cas,如果有别的方式也可以补充
借助CAS(AtomicReference)实现单例模式:
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。
CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。
参考文章
http://www.hollischuang.com/archives/1866
http://www.hollischuang.com/archives/199
http://www.hollischuang.com/archives/2498
http://www.hollischuang.com/archives/205
http://www.hollischuang.com/archives/1144
http://www.hollischuang.com/archives/197
https://www.cnblogs.com/summerday152/p/12347079.html
网友评论