特点
- 单例的类在整个JVM中只有一个实例
- 需要提供一个全局访问点(1.公开的静态变量,2.公开的静态方法)
- 对象在内存中只有一个,减少了内存的开销
- 类构造器私有
- 可以控制对象的创建时刻
创建单例的八种方式
饿汉式,懒汉式,枚举
饿汉式
- 类加载到内存后就实例化一个单例,JVM保证线程安全
- 提供一个全局的访问点
缺点
-
1. 饿汉式(1)
能保证单例
public class Mgr01 { // 能保证单例
private static final Mgr01 INSTANCE = new Mgr01();
private Mgr01() {
}
public static Mgr01 getInstance() {
return INSTANCE;
}
}
2. 饿汉式(2)
能保证单例
public class Mgr02 { // 能保证单例
/**
* final修饰的变量必须立即初始化,或者“代码块”初始化
* 如果是static修饰的用static代码块初始化
* 如果不是static修饰的必须用普通代码块初始化
*/
private static final Mgr02 INSTANCE;
static {
INSTANCE = new Mgr02();
}
private Mgr02() {
}
public static Mgr02 getInstance() {
return INSTANCE;
}
}
3. 懒汉式(1)
不能保证单例,线程不安全
public class Mgr03 { // 不能保证单例
private static Mgr03 INSTANCE;
private Mgr03() {
}
public static Mgr03 getInstance() {
if (INSTANCE == null) { // 多线程不安全
try {
// 模拟线程阻塞
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
}
4. 懒汉式(2)
能保证单例
public class Mgr04 { // 能保证单例
private static Mgr04 INSTANCE;
private Mgr04() {
}
/**
* 静态方法加锁是:对该类的class对象加锁。Mgr04.class (√)
* 加锁会影响性能
*/
public static synchronized Mgr04 getInstance() {
if (INSTANCE == null) {
try {
// 模拟线程阻塞
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
return INSTANCE;
}
}
5. 懒汉式(3)
不能保证单例,线程不安全
public class Mgr05 { // 不能保证单例
private static Mgr05 INSTANCE;
private Mgr05() {
}
public static Mgr05 getInstance() {
if (INSTANCE == null) { // 多线程不安全
try {
// 模拟线程阻塞
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Mgr05.class) {
INSTANCE = new Mgr05();
}
}
return INSTANCE;
}
}
6. 懒汉式(4)
能保证单例
双重校验方式
public class Mgr06 { // 能保证单例
/**
* 需要加volatile修饰,禁止指令重排
*/
private static volatile Mgr06 INSTANCE;
private Mgr06() {
}
public static Mgr06 getInstance() {
if (INSTANCE == null) { // 第一次检测,减小上锁的概率
try {
// 模拟线程阻塞
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (Mgr06.class) {
if (INSTANCE == null) { // 第二次检测,确保实例一定为空
INSTANCE = new Mgr06();
}
}
}
return INSTANCE;
}
}
为什么要加volatile呢?
创建一个对象可以分为3步!
memory = allocate(); //1.分配对象的内存空间
ctorInstance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址
其中:
2和3之间,可能会被重排序
memory = allocate(); //1.分配对象的内存空间
instance = memory; //2.设置instance指向刚分配的内存地址
ctorInstance(memory); //3.初始化对象
虽然重排序不会影响单线程的执行结果,但是由于判断的条件是instance == null,当分配了内存以后,其他线程来到判断的地方,instance不为空,所以直接将引用指向实例对象。但是实例对象还没有初始化,就会出现问题,可能会引发错误(对象还未初始化就已被使用)。
所以需要加volatile防止重排序。7. 懒汉式(5)
能保证单例
- 静态内部类方式
通过类加载过程的线程安全来保证创建单例实例的线程安全
public class Mgr07 { // 能保证单例
private Mgr07() {
}
public static class InnerMgr07 {
private static final Mgr07 INSTANCE = new Mgr07();
}
public static Mgr07 getInstance() {
return InnerMgr07.INSTANCE;
}
}
8. 枚举方式
唯一安全的单例创建方式(不能够被反序列化) ```java public enum Mgr08 { INSTANCE; }
<a name="T4VTN"></a>
#### 枚举在底层是怎么保证线程安全的?
> **解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题**
- 定义枚举时使用enum和class一样,时Java中的关键字。
- 枚举经过javac编译后,会被转成 public final class T extends Enum 的定义。
- 枚举中的各个枚举项都是通过static修饰的。
```java
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
反编译后:
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
- static 类型的属性会在类被加载之后被初始化
- 当一个Java类第一次被真正使用到的时候静态资源被初始化,Java类的加载和初始化过程都是线程安全的
- 因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全
- 所以,创建一个enum类型是线程安全的
枚举在底层是怎么避免反序列化的?
在序列化的时候,Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum
的valueOf
方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject
、readObject
等方法。
普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。
但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题
单例与序列化的爱恨情仇
序列化
- Java序列化可以保存实例属性的状态,但是不会保存方法,因为方法没有状态。
- Java序列化不会保存static变量,因为static变量是类的状态,序列化保存的是对象,而不是类。
- transient关键字修饰的变量也不会被序列化保存,反序列化时,该变量会被设置为默认值。
- 父类实现了Serializable接口,其子类会继承,从而子类不需要显示实现Serializable接口。
- 子类实现了Serializable接口,其父类如未实现该接口,子类序列化不会保存父类属性的状态。
-
单例与序列化
public class Singleton implements Serializable{
private static volatile Singleton singleton = null;
private String s;
private Singleton(){}
public static Singleton getInstance(){
if (singleton == null){
synchronized (Singleton.class){
if (singleton == null)
singleton = new Singleton();
}
}
return singleton;
}
public String getS() {
return s;
}
public void setS(String s) {
this.s = s;
}
}
以上代码是一个单例模式的实现,Singleton实现了Serializable接口,并且保证了线程安全。有一个私有属性s,通过set和get方法进行赋值取值操作。下面来看序列化与反序列化:
public class serialzableTest {
public static void serialzableSingleton(String file) {
// 1.序列化
ObjectOutputStream objectOutputStream = null;
Singleton instance1 = null;
try {
objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
instance1 = Singleton.getInstance();
instance1.setS("序列化测试!");
objectOutputStream.writeObject(instance1);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (objectOutputStream != null) {
try {
objectOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 反序列化
ObjectInputStream objectInputStream = null;
Singleton instance2 = null;
try {
objectInputStream = new ObjectInputStream(new FileInputStream(file));
instance2 = (Singleton) objectInputStream.readObject();
System.out.println(instance2.getS());
System.out.println(instance2 == instance1);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (objectInputStream != null) {
try {
objectInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
为了代码的规范,因此内容稍微有点多,但主要内容其实就是try里面的两段,最重要的就是注释1和2的两句。测试的结果为:This is a serializable test!和false。这说明序列化确实保存了属性的状态,并且反序列化后的对象singleton2和序列化之前的singleton1不是同一个对象,即单例模式被破坏。那么对ObjectInputStream的readObject方法进行跟踪,发现了以下代码:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null; //1.对象是否可以进行实例化
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj); //2. 执行readResolve方法
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep); //3. 将需要返回的obj指向rep
}
}
return obj;
}
注释1表示如果对象可以在运行时被实例化,则使用反射新建一个实例,否则返回null。因此,序列化也是使用反射新建了一个实例,破坏了单例模式。那么注释2就是避免序列化破坏单例的一种方式。我们只需要在单例的类中实现 readResolve 方法,利用它返回其实例,那么序列化得到的对象就是那个唯一的实例。
private Object readResolve(){
return singleton;
}