目录
撰文目的:确保单例模式在多线程环境下是安全和正确的。
单例模式的三个特点:
1、构造方法私有
2、实例化的变量引用私有化
3、获取实例的方法公有
一、饿汉模式和懒汉模式实现单例
1、饿汉模式——立即加载(线程安全)
立即加载是在使用类的时候已经将对象创建完毕,立即加载有“着急”、“迫切”的含义,因此也叫饿汉模式。
测试代码:饿汉模式在调用方法前,对象已经被创建。
public class MyService {
// static 立即加载——饿汉模式
private static MyService myService = new MyService();
private MyService(){}
public static MyService getInstance(){
return myService;
}
static class MyThread extends Thread{
@Override
public void run() {
System.out.println(MyService.getInstance().hashCode());
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
测试结果:
2、懒汉模式——延迟加载(非线程安全)
延迟专加载实在调用get()方法时实例才被创建,延迟加载有“缓慢”,“不急迫”的含义,所以也称为懒汉模式。
测试代码:懒汉模式单例模式在多线程环境下存在严重的线程安全问题
public class MyService {
// 延迟加载——懒汉模式
private static MyService myService;
private MyService(){}
public static MyService getInstance(){
try {
if(myService == null){
// 延迟加载
Thread.sleep(3000);
myService = new MyService();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myService;
}
static class MyThread extends Thread{
@Override
public void run() {
System.out.println(MyService.getInstance().hashCode());
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
测试结果:
因为对象在被调用时才会实例化对象,所以如果有多个线程同时调用实例化方法,根本不能保证实例化对象的唯一性。
3、DCL双检查锁机制确保延迟加载的线程安全
DCL是大多数多线程结合单例模式使用的解决方案。
第一次检查时,主要是避免在有实例的情况下,还去执行同步代码块,提高运行效率。
第二次检查,是检测对象是否已经被创建,保证单例模式。
测试代码:Double——Checked Lock 双检查锁机制,定义单例对象最好用volatile关键字来修饰,保证可见性!!!
public class MyService {
// 延迟加载——懒汉模式
private static MyService myService;
private MyService(){}
public static MyService getInstance(){
try {
if(myService == null){// 第一次检查,使不需要同步的代码异步执行
Thread.sleep(3000);
synchronized (MyService.class) {
if(myService == null){// 第二次检查,保证单例模式
// 延迟加载
myService = new MyService();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return myService;
}
static class MyThread extends Thread{
@Override
public void run() {
System.out.println(MyService.getInstance().hashCode());
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
测试结果:
二、使用静态内部类和static代码块实现单例模式
两者在实现单例模式上都是利用static关键字的特性,即对象在使用前就已经被初始化,下面来分别看一下各自的实现形式。
1、静态内部类(线程安全)
使用静态内部类的好处——避免静态实例的加载,初始化时只加载Class文件,减小初始化的负载。
public class MyService {
private MyService(){}
// 使用私有静态内部类
private static class MyObjectHandler{
private static MyService myService = new MyService();
}
public static MyService getInstance(){
return MyObjectHandler.myService;
}
static class MyThread extends Thread{
@Override
public void run() {
System.out.println(MyService.getInstance().hashCode());
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
测试结果:
2、使用static代码块(线程安全)
静态代码块跟static关键字一样,使对象在使用的时候就已经被创建了,利用这一特性来实现单例模式,可以确保线程安全。
测试代码:
public class MyService {
private MyService(){}
private static MyService myService = null;
static {// 使用静态代码块
myService = new MyService();
}
public static MyService getInstance(){
return myService;
}
static class MyThread extends Thread{
@Override
public void run() {
System.out.println(MyService.getInstance().hashCode());
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.start();
}
}
测试结果:
三、序列化的单例模式
1、单例模式被破坏
单例模式在遇到序列化和反序列化时会遭到破坏,因为序列化前对象和反序列化后对象不是同一个对象。
原因:反序列化时会通过反射调用无参的构造方法创建一个新的对象
代码测试:序列化前后对象不一样
public class MyService implements Serializable{
private static final long serialVersionUID = 1L;
private MyService(){}
// 使用内部类方式
private static class MyServiceHandler{
private static MyService myService = new MyService();
}
public static MyService getInstance(){
return MyServiceHandler.myService;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
/**使用输出流保存对象*****************************/
MyService myService = MyService.getInstance();
// "myService.txt" - 其实这个文件具体是什么都无所谓,主要是需要有个名字在这里
FileOutputStream output = new FileOutputStream(new File("myService.txt"));
ObjectOutputStream objectOut = new ObjectOutputStream(output);
objectOut.writeObject(myService);
objectOut.close();
output.close();
System.out.println(myService.hashCode());
/**使用输入流读取对象******************************/
FileInputStream fileInput = new FileInputStream(new File("myService.txt"));
ObjectInputStream objectInput = new ObjectInputStream(fileInput);
MyService service = (MyService) objectInput.readObject();
objectInput.close();
fileInput.close();
System.out.println(service.hashCode());
}
}
测试结果:
造成这种现象的具体原因,我们可以简单的看一下反序列化的源码和实现:
public final Object readObject()
throws IOException, ClassNotFoundException
{
// 代码省略
try {
Object obj = readObject0(false);//找到这个方法
// 代码省略
} finally {
// 代码省略
}
}
private Object readObject0(boolean unshared) throws IOException {
// 代码省略
try {
switch (tc) {
// 代码省略
case TC_OBJECT:// 看到这里,如果读的是对象,将调用下边的方法
return checkResolve(readOrdinaryObject(unshared));
// 代码省略
}
} finally {
// 代码省略
}
}
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
// 代码省略
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;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
// 代码省略
return obj;
}
是的,我们清楚的看到:obj = desc.isInstantiable() ? desc.newInstance() : null;它生成了一个新的对象!
2、单例模式的实现
实现序列化的单例模式,只要在需要实现单例模式的类中定义readResolve()方法,返回跟序列化前相等的对象就可以了。
用途:readResolve()方法用来替换从流中读取的对象,唯一的用途是强制执行单例。
测试代码:向类中添加了readResolve()方法
public class MyService implements Serializable{
private static final long serialVersionUID = 1L;
private MyService(){}
// 使用内部类方式
private static class MyServiceHandler{
private static MyService myService = new MyService();
}
public static MyService getInstance(){
return MyServiceHandler.myService;
}
// 为保证单例模式添加的方法
protected Object readResolve(){
return MyServiceHandler.myService;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
/**使用输出流保存对象*****************************/
MyService myService = MyService.getInstance();
// "myService.txt" - 其实这个文件具体是什么都无所谓,主要是需要有个名字在这里
FileOutputStream output = new FileOutputStream(new File("myService.txt"));
ObjectOutputStream objectOut = new ObjectOutputStream(output);
objectOut.writeObject(myService);
objectOut.close();
output.close();
System.out.println(myService.hashCode());
/**使用输入流读取对象******************************/
FileInputStream fileInput = new FileInputStream(new File("myService.txt"));
ObjectInputStream objectInput = new ObjectInputStream(fileInput);
MyService service = (MyService) objectInput.readObject();
objectInput.close();
fileInput.close();
System.out.println(service.hashCode());
}
}
测试结果:
为了使原因更清晰,我们可以回过头来看一下readOrdinaryObject()源码的具体实现:
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
// 代码省略
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
// 如果readResolve()方法存在
{
// 通过readResolve()方法创建实例
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
// 在这里进行实例的替换,维持单例模式!!!
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
源码中,判断反序列化类中是否存在readResolve()方法,如果存在,就通过反射调用这个方法创建一个实例,在最后进行替换。
四、使用枚举类实现单例模式
1、反射破坏传统单例模式
传统的单例模式可以通过反射进行攻击!
借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。
测试代码:反射对单例模式的破坏
public class MyService implements Serializable{
private static final long serialVersionUID = 1L;
private MyService(){}
// 使用内部类方式
private static class MyServiceHandler{
private static MyService myService = new MyService();
}
public static MyService getInstance(){
return MyServiceHandler.myService;
}
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
MyService myServiceA = MyServiceHandler.myService;
MyService myServiceB = MyServiceHandler.myService;
// 获取构造器
Constructor<MyService> constructor = MyService.class.getDeclaredConstructor();
// 暴力破除私有化
constructor.setAccessible(true);
// 反射调用私有化构造方法实例化对象
MyService myService = constructor.newInstance();
System.out.println("myServiceA:"+myServiceA.hashCode());
System.out.println("myServiceB:"+myServiceB.hashCode());
System.out.println("反射创建myService:"+myService.hashCode());
}
}
测试结果:
2、枚举类的单例实现方式
使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
——《Effective java》
(1)序列化安全
枚举序列化是由JVM保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的。
在枚举类型的序列化和反序列化上,java做了特殊的规定:在序列化时,java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性。
(2)反射安全
反射不能创建枚举实例(源码分析待续...)
(3)枚举单例的实现
把需要实例化的对象设计为枚举类,枚举类中的方法跟普通类中的方法定义是一样的。只不过,需要为这个枚举类定义一个枚举变量才可以调用到这些方法,而这个枚举变量总是唯一的(也就是单例)。
测试代码:枚举单例——实际上枚举就是用来替代常量的,而我们知道常量总是固定不变的,唯一的
public enum MyObject {
MYOBJECT;
public MyObject getInstance(){
return MYOBJECT;
}
public void doSomthing(){
System.out.println(Thread.currentThread().getName()+"——获取变量Hash值:"+MyObject.MYOBJECT.getInstance().hashCode());
}
static class DemoThread extends Thread{
private MyObject myObject;
public DemoThread(MyObject myObject){
this.myObject = myObject;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"——获取变量Hash值:"+myObject.hashCode());
}
}
public static void main(String[] args) {
MyObject myObject = MyObject.MYOBJECT.getInstance();
DemoThread a = new DemoThread(myObject);
DemoThread b = new DemoThread(myObject);
DemoThread c = new DemoThread(myObject);
a.start();
b.start();
c.start();
myObject.doSomthing();
}
}
测试结果:枚举类是实现单例最简单,也是最安全的方式,它是最好的!!!