定义
保证一个类仅有一个实例,并提供一个全局访问点
类型
创建型
适用场景
想保证任何情况下都绝对只有一个实例。(数据库连接池。。。。。)
优点
1、在内存里只有一个实例,减少了内存开销。
2、可以避免对资源的多重占用
3、设置全局访问点,严格控制访问。
缺点
1、没有接口,扩展困难
重点
- 私有构造器
- 线程安全
- 延迟加载
- 序列化与反序列化安全
- 反射
懒汉模式
public class LazySingleton {
private static LazySingleton lazySingleton=null;
private LazySingleton(){
}
public static LazySingleton getInstance(){
//线程不安全
if(lazySingleton==null){
lazySingleton=new LazySingleton();
}
return lazySingleton;
}
}
为什么说线程不安全呢?我们来测试一下
public class T implements Runnable{
@Override
public void run() {
LazySingleton lazySingleton=LazySingleton.getInstance();
System.out.println(Thread.currentThread().getName()+" "+lazySingleton);
}
}
public class Test {
public static void main(String[] args) {
Thread t1=new Thread(new T());
Thread t2=new Thread(new T());
t1.start();
t2.start();
System.out.println("ok");
}
}
我们分别在LazySingleton类的if(lazySingleton==null)处、T类的system处、Test的System处打上断点并设置为Thread
(如何操作左键打好断点、在断点上右键)
首先我们直接运行
表面上看,我们得到的是一个对象,但是真相是怎样的?
现在我们用debug的方式去执行。
首先我们选择Thread-0单步执行
可以看到它首先去判断lazySingleton是否为null,这个时候他是null,我们继续单步执行。
可以看到达17行是还为null因为他的赋值还没有完成。
这时,我们切换到Thread-1上,单步。
这个时候还为空,这是因为Thread-0还没赋值完成,单步进入17行。
然后我们切汇Thread-0单步。
这时已经完成为503。
我们切回Thread-1。
这个时候他还没有执行但是已经是504了,我们单步执行。
执行完成后编号为505。
这就说明在多线程的情况下这种懒汉模式会生成不止一个实例。违背了初衷。
我们切回Thread-0。
这时lazySinleton已经被Thread-1重新赋值了为505。
我们让线程执行完。
从控制台看到,两个线程拿到的还是同一个对象。但是通过刚才的debug,可以知道其实是不同实例,Threa-1进行重新赋值,然后才进行return的。
怎么输出不同的实例呢?
选择Thread-0单步执行到17行。
再切换到Thread-1单步执行到17行。
我们让Thread直接执行。
在切回Thread-0。
Thread-0这时是504继续执行完。
最后就可以获得两个不同的实例对象。
所以就算是控制台输出相同的对象,但是在具体过程中也可能会产生不同的对象。
所以获得相同对象、和获得不同对象具有随机性。这与我们使用单例模式的初衷相违背。
改进
在getInstance上加上synchronized,让这个方法成为同步方法。
如果锁加在静态方法上,锁的就是这个类的class文件,如果这个方法不是静态方法,则锁的是堆内存上的对象。
public static LazySingleton getInstance(){
synchronized(LazySingleton.class){
if(lazySingleton==null){
lazySingleton=new LazySingleton();
}
return lazySingleton;
}
}
public synchronized static LazySingleton getInstance(){
if(lazySingleton==null){
lazySingleton=new LazySingleton();
}
return lazySingleton;
}
这两种方法是等价的。
这时候我们debug当Thread-0进入getInstance方法Thread-1是不能进入的。这就保证我们始终生成的是一个实例。
Double Check(双重从检查锁)
单线程情况下2,3步骤是可以颠倒的。但是在多线程情况先就会出现问题(重排序问题)
当出现重排序问题,线程0分配内存空间,并设置instance指向内存空间,但是此时并没有初始化对象。而此次线程1来了,它首先判断instance是否为null,而此次instance已经指向内存空间,所以不为null,但是当线程1初次访问对象就会出现问题。
处理方法:
1、不允许线程0出现重排序(使用volatile修饰变量)
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton=null;
private LazyDoubleCheckSingleton(){
}
public static LazyDoubleCheckSingleton getInstance(){
if(lazyDoubleCheckSingleton==null){
synchronized (LazyDoubleCheckSingleton.class){
if(lazyDoubleCheckSingleton==null){
lazyDoubleCheckSingleton=new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
2、允许线程0出现2,3步骤重排序,但是不允许线程1看到这个重排序(静态内部类)
public class StaticInnerClassSingleton {
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton=new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
private StaticInnerClassSingleton(){}
}
线程1不会看到重排序。
当线程0和线程1试图获取Class对象的初始化锁的时候(只有一个线程可以获得这个锁),假设线程0获得这个锁,线程0执行静态内部类的初始化,即使2,3之间存在重排序,线程1也是无法看到的。
类(包括接口)被立刻初始化的五种情况(假设这个类为A):
- 有一个A类型的实例被创建
- A类中的静态方法被调用
- A类中的静态成员被赋予值
- A类中的静态成员被使用(不是常量成员)
- A类是一个顶级类,并且这个类中有断言语句
饿汉模式
package com.design.pattern.creational.singleton.hungry;
import java.io.Serializable;
/**
* 单例 饿汉式
* @ClassName HungrySingleton
* @Author chenchen
* @Date 2019/8/25 21:23
* @Version 1.0
**/
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton =new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
反射破坏单例模式
我们写一个测试类
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
HungrySingleton instance=HungrySingleton.getInstance();
ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(instance);
File file=new File("singleton");
ObjectInputStream ois=new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance=(HungrySingleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance==newInstance);
}
}
可以看到在经过序列化、反序列化之后不再是一个对象。这就违背了单例模式的初衷。
在原有代码下加入下面代码就会拿到相同对象(此方法名称必须为readResolve)
private Object readResolve(){
return hungrySingleton;
}
为什么加入此方法后就能拿到相同对象?
我们看一下ObjectInputStream.readObject()方法。
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
//重点
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
我们重点看一下readObject0()
在readObject0()方法中我们属于OBJECT类型,别的不关注。
这里将readOrdinaryObject方法的返回值作为checkResolve方法的参数。
先看readOridinaryObject
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 {
//如果desc.isInstantiable返回true就会返回一个新的对象否则就会返回null
//重点1
obj = desc.isInstantiable() ? desc.newInstance() : null;
} 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);
//重点2
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
//重点3
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
重点1
obj = desc.isInstantiable() ? desc.newInstance() : null;
如果desc.isInstantiable返回true就会返回一个新的对象否则就会返回null。
/**
* Returns true if represented class is serializable/externalizable and can
* be instantiated by the serialization runtime--i.e., if it is
* externalizable and defines a public no-arg constructor, or if it is
* non-externalizable and its first non-serializable superclass defines an
* accessible no-arg constructor. Otherwise, returns false.
*/
boolean isInstantiable() {
return (cons != null);
}
cons是构造器类型Constructor
根据此方法的注释可以看出, 如果表示的类是可序列化/可外部化的并且可以由序列化运行时实例化则返回true。根据我们的情况此方法返回true。这里返回true上一步则会newInstance。通过反射创建对象,肯定和之前的对象不是同一个对象。
重点2
desc.hasReadResolveMethod()
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
return (readResolveMethod != null);
}
根据注释可以知道,如果这个类是可以序列化的且有readResolve方法返回true
重点3
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
if (readResolveMethod != null) {
try {
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
根据反射得到这个方法(readResolve)。
到现在为止我们还没有找到对readResolve的定义。
在此类我们ctrl+f找一下readResolve可以找到对readResolve的赋值。
反射攻击
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射攻击
Class obj=HungrySingleton.class;
Constructor constructor=obj.getDeclaredConstructor();
HungrySingleton instance=HungrySingleton.getInstance();
HungrySingleton object=(HungrySingleton)constructor.newInstance();
System.out.println(instance);
System.out.println(object);
System.out.println(instance==object);
}
因为他的构造方法是私有的,所以报错了。
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//反射攻击
Class obj=HungrySingleton.class;
Constructor constructor=obj.getDeclaredConstructor();
//修改权限
constructor.setAccessible(true);
HungrySingleton instance=HungrySingleton.getInstance();
HungrySingleton object=(HungrySingleton)constructor.newInstance();
System.out.println(instance);
System.out.println(object);
System.out.println(instance==object);
}
通过修改构造方法的权限可以得到实例。
饿汉模式,是在类加载的时候就获得了实例。为了解决这个问题,我们在私有构造方法中添加了判断。
public class HungrySingleton implements Serializable {
private final static HungrySingleton hungrySingleton;
static{
hungrySingleton =new HungrySingleton();
}
private HungrySingleton(){
if(hungrySingleton!=null){
throw new RuntimeException("单例构造器禁止反射调用");
}
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
private Object readResolve(){
return hungrySingleton;
}
}
这种方法对类加载时就把类创建好这种类是可以的。
对于不是在类加载时初始化好对象的类是不行的。