单例模式
- 定义:是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。
饿汉式单例模式
类加载的时候就立即初始化,并且创建单例对象,线程安全。
无锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
类加载的时候就初始化,占用空间、内存。
饿汉式适用在单例对象较少的情况。
/**
* 写法一
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
/**
* 饿汉式静态块单例
*/
class HungrySingLeton2{
private static HungrySingLeton2 hungrySingLeton2;
static {
hungrySingLeton2 = new HungrySingLeton2();
}
private HungrySingLeton2() {}
public HungrySingLeton2 getInstance(){
return hungrySingLeton2;
}
}
懒汉式单例模式
被外部需要使用的时候才进行实例化。
对内存的利用率较高。
性能较饿汉式较低,并发量较大的场景,容易造成线程阻塞,影响程序运行性能。
对内存利用率要求较高,并发较低的场景。
public class LazySimpleSingleton {
private LazySimpleSingleton() {
}
//静态块,公共内存区域
private static LazySimpleSingleton lazySimpleSingleton;
//第一种无法保证现场安全,多线程调用存在多次实例化问题,即使得到的地址值相同也可能是多次实例化后的数据,可通过debug的Thead模式进行测试
public static LazySimpleSingleton getInstance() {
if(lazySimpleSingleton==null){
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}
//第二种 加锁保证程序的原子性,synchronized修饰的代码只能串行访问,所以影响程序性能。
public static LazySimpleSingleton getInstanceSycn() {
if (lazySimpleSingleton == null) {
// 互斥锁 锁级别为类锁,多线程只能串行访问该资源
synchronized (LazySimpleSingleton.class) {
if (lazySimpleSingleton == null) {
lazySimpleSingleton = new LazySimpleSingleton();
}
}
}
return lazySimpleSingleton;
}
}
懒汉式内部类单例
从类初始化角度来考虑,可以采用静态内部类的方式;这样即兼顾饿汉式的内存浪费,又兼顾 synchronized 性能问题。
/**
* LazyInnerClassSingleton类
* 静态内部类
* 兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
* 内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题
*
* @author wangjixue
* @date 2019-05-25 16:46
*/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {}
//static:是为了使单例的空间在多个线程间共享
//final : 保证这个方法不会被重写,重载
public static final LazyInnerClassSingleton getInstance() {
//在返回结果以前,一定会先加载内部类
return LazyHodler.LAZY;
}
//默认不加载
private static class LazyHodler {
//1.分配内存地址 addr01 给LazyInnerClassSingleton对象
//2.初始化LazyInnerClassSingleton对象
//3.设置LAZY执行该刚分配的地址addr01
//因为 static:代表多个线程间访问的成员变量LAZY是同一个成员变量。
// final:代表映射的地址不变,所以每个线程T的成员变量LAZY指向的都是addr01对应的实例。
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
破坏单例
反射破坏单例
反射如何破坏单例:使用反射来调用其私有的构造方法,然后再调用 newInstance()方法就会创建不同的实例。
/**
* 通过反射方式获取懒汉式内部类单例对象
*/
class ReflectLazyInnerClassSingletonTest{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
////通过反射获取私有的构造方法
Constructor<LazyInnerClassSingleton> constructor = LazyInnerClassSingleton.class.getDeclaredConstructor();
// 强制访问
constructor.setAccessible(true);
// 初始化,此次通过私有构造方法new了两次
LazyInnerClassSingleton obj01 = constructor.newInstance();
LazyInnerClassSingleton obj02 = constructor.newInstance();
System.err.println(obj01);
System.err.println(obj02);
System.out.println("=========equal===========");
System.err.println(obj02==obj01);
}
}
懒汉式内部类单例:在其构造方法中做一些限制,一旦出现多次重复创建,则直接抛出异常。
/**
* LazyInnerClassSingleton类
* 静态内部类
* 兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
*
* @author wangjixue
* @date 2019-05-25 16:46
*/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton() {
//防止通过反射的方式获取
if(LazyHodler.LAZY!=null){
throw new RuntimeException("禁止通过单例的私有构造方法创建多个实例,因为违反了\"一个类在任何情况下都绝对只有一个实例\"的设计的初衷");
}
}
、、、
}
反序列化破坏单例
当我们将一个单例对象创建好,有时候需要将对象序列化然后写入到磁盘,下次使用时再从磁盘中读取到对象,反序列化转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。那如果序列化的目标的对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例。
/**
* SeriableLazyInnerClassSingleton类
* 可序列化静态内部类
* 兼顾饿汉式的内存浪费,也兼顾 synchronized 性能问题
* 但是反序列化会new出不同的对象实例。
*
* @author wangjixue
* @date 2019-05-25 16:46
*/
public class SeriableLazyInnerClassSingleton implements Serializable {
//序列化:将内存中的数据写入其他地方(硬盘,网络IO);
//具体来说:序列化就是说把内存中的状态转换成字节码以I/O流的形式写入硬盘等IO设备中永久的保存起来。
//反序列化:将其他地方的数据写入内存
//具体来说:将IO设备中保存的字节码通过I/O流的形式读取到内存,并转换成Java对象的过程就是反序列化。
//注意:转换成Java对象过程中会重新new对象实例;
private SeriableLazyInnerClassSingleton() {
//防止通过反射的方式获取
if(LazyHodler.LAZY!=null){
throw new RuntimeException("禁止通过单例的私有构造方法创建多个实例,因为违反了\"一个类在任何情况下都绝对只有一个实例\"的设计的初衷");
}
}
//static:是为了使单例的空间在多个线程间共享
//final : 保证这个方法不会被重写,重载
public static final SeriableLazyInnerClassSingleton getInstance() {
//在返回结果以前,一定会先加载内部类
return LazyHodler.LAZY;
}
//默认不加载
private static class LazyHodler {
//1.分配内存地址 addr01 给LazyInnerClassSingleton对象
//2.初始化LazyInnerClassSingleton对象
//3.设置LAZY执行该刚分配的地址addr01
//因为 static:代表多个线程间访问的成员变量LAZY是同一个成员变量。
// final:代表映射的地址不变,所以每个线程T的成员变量LAZY指向的都是addr01对应的实例。
private static final SeriableLazyInnerClassSingleton LAZY = new SeriableLazyInnerClassSingleton();
}
}
/**
* 通过反序列获取单例测试
*/
public class SerializableLazyInnerClassSingletonTest {
public static void main(String[] args) {
// 通过反序列化得到的实例对象
SeriableLazyInnerClassSingleton singleton01 = null;
// 内存中的实例对象
SeriableLazyInnerClassSingleton singleton02 = SeriableLazyInnerClassSingleton.getInstance();
try {
//序列化到IO设备中
FileOutputStream fos = new FileOutputStream("SeriableLazyInnerClassSingleton.class");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(singleton02);
oos.flush();
oos.close();
//反序列化为Java对象
FileInputStream fis = new FileInputStream("SeriableLazyInnerClassSingleton.class");
ObjectInputStream ois = new ObjectInputStream(fis);
singleton01 = (SeriableLazyInnerClassSingleton) ois.readObject();
ois.close();
System.err.println("singleton01 = "+singleton01);
System.err.println("singleton02 = "+singleton02);
System.out.println("======验证========");
System.err.println(singleton01==singleton02);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
只需要在单例中增加 readResolve()方法即可。
public class SeriableLazyInnerClassSingleton implements Serializable {
//序列化:将内存中的数据写入其他地方(硬盘,网络IO);
//具体来说:序列化就是说把内存中的状态转换成字节码以I/O流的形式写入硬盘等IO设备中永久的保存起来。
//反序列化:将其他地方的数据写入内存
//具体来说:将IO设备中保存的字节码通过I/O流的形式读取到内存,并转换成Java对象的过程就是反序列化。
//注意:转换成Java对象过程中会重新new对象实例;
//防止反序列化new新的实例对象
private Object readResolve(){
return getInstance();
}
、、、
}
通过查看JDK源码发现:ObjectInputStream 类的 readObject()方法首先调用ObjectStreamClass 的 isInstantiable()方法来判断构造方法是否为空,不为空返回true, (只要有无参构造方法就会实例化。)验证可以进行对象的实例化后调用hasReadResolveMethod()判断 readResolveMethod 是否为空,不为空就返回 true。那么 readResolveMethod 是在哪里赋值的呢?通过全局查找找到了赋值代码在私有方法 ObjectStreamClass()方法中给 readResolveMethod 进行赋值。
readResolveMethod = getInheritableMethod( cl, "readResolve", null, Object.class);
上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在 再 回 到 ObjectInputStream 的 readOrdinaryObject() 方 法 继 续 往 下 看 , 如 果 readResolve()存在,则调用 invokeReadResolve()方法。在invokeReadResolve内部通过反射方式调用readResolveMethod()方法。
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
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(); }
}
通过 JDK 源码分析我们可以看出,虽然增加 readResolve()方法返回实例,解决了反序列化获取单例被破坏的问题。但是,我们通过分析源码以及调试,发现实际上单例对象实例化了两 次,只不过新创建的对象没有被返回而已。如果,创建对象的动作发生频率增大,就意味着内存分配开销也就随之增大,那么如果解决多次创建的问题--注册式单例。
注册时单例
注册式单例又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记。
- 枚举单例
枚举式单例在静态代码块中就给 INSTANCE 进行了赋值,是饿汉式单例的实现。
/**
* EnumSingleton类
* 枚举注册式单例
* 《Effective Java》推荐的一种单例实现写法
*
* @author wangjixue
* @date 2019-05-26 00:17
*/
public enum EnumSingleton {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
/**
* EnumSingletonTest类
*
* @author wangjixue
* @date 2019-05-26 00:36
*/
public class EnumSingletonTest {
public static void main(String[] args) {
// 通过反序列化得到的实例对象
EnumSingleton es01 = null ;
// 内存中的实例对象
EnumSingleton es02 = EnumSingleton.getInstance();
es02.setData(new Object());
try {
//序列化到IO设备中
FileOutputStream fos = new FileOutputStream("EnumSingleton.seri");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(es02);
oos.flush();
oos.close();
//反序列化为Java对象
FileInputStream fis = new FileInputStream("EnumSingleton.seri");
ObjectInputStream ois = new ObjectInputStream(fis);
es01 = (EnumSingleton) ois.readObject();
ois.close();
System.err.println("EnumSingleton01 = "+es01.getData());
System.err.println("EnumSingleton02 = "+es02.getData());
System.out.println("======验证========");
System.err.println(es01==es02);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
通过JDK 源码,发现 readObject0()中调用了 readEnum()方法
我们发现枚举类型其实通过类名和 Class 对象类找到一个唯一的枚举对象。因此,枚举对象不可能被类加载器加载多次。
private Enum<?> readEnum(boolean unshared) throws IOException {
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false);
if (!desc.isEnum()) {
throw new InvalidClassException("non-enum class: " + desc);
}
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class) cl, name);
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
那么反射是否能破坏枚举式单例呢 ?同理通过JDK 源码分析发现进入 Constructor 的 newInstance()方法,做了强制性的判断,如果修饰符是 Modifier.ENUM 枚举类型, 直接抛出异常。
public T newInstance(Object... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException {
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs); return inst;
}
到这为止,我们分析处理枚举式单例枚举式单例也是《Effective Java》书中推荐的一种单例实现写法。在 JDK 枚举的语法特殊性,以及反射也为枚举保 驾护航,让枚举式单例成为一种比较优雅的实现。
- 容器缓存
容器式单例适用于创建实例非常多的场景,便于管理。但是是非线程安全的。
/**
* ContainerSingleton类
* 容器缓存式单例
* 优点:容器式写法适用于创建实例非常多的情况,便于管理。
* 缺点:非线程安全
*
* @author wangjixue
* @date 2019-05-26 00:19
*/
public class ContainerSingleton {
private ContainerSingleton() {}
private static Map<String ,Object> container = new ConcurrentHashMap<String ,Object>();
public Object getBean(String className){
synchronized (container){
Object instance = null;
if(!container.containsKey(className)){
try {
instance = Class.forName(className).newInstance();
container.put(className,instance);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}else{
instance= container.get(className);
}
return instance;
}
}
}
ThreadLocal 线程单例
ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,天生的线程安全。
/**
* ThreadLocalSingleton类
* ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的
* @author wangjixue
* @date 2019-05-26 00:52
*/
public class ThreadLocalSingleton {
private ThreadLocalSingleton() {}
// TODO: 2019-05-26 此处可与懒汉内部类式单例进行比较
private static final ThreadLocal<ThreadLocalSingleton> threadLocalInstance = new ThreadLocal<ThreadLocalSingleton>(){
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
public static ThreadLocalSingleton getInstance(){
return threadLocalInstance.get();
}
}
/**
* ThreadLocalSingletonTest类
* ThreadLocal 不能保证其创建的对象是全局唯一,但是能保证在单个线程中是唯一的,具体原因如下:单例模式为了达到线程安全的目的,给方法上锁,以时间换空间。ThreadLocal 将所有的对象全部放在 ThreadLocalMap 中,为每个线程都提供一个对象,实际上是以 空间换时间来实现线程间隔离的。
* @author wangjixue
* @date 2019-05-26 00:52
*/
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.err.println(ThreadLocalSingleton.getInstance());
System.err.println(ThreadLocalSingleton.getInstance());
System.err.println(ThreadLocalSingleton.getInstance());
System.err.println(ThreadLocalSingleton.getInstance()==ThreadLocalSingleton.getInstance());
System.out.println("========多线程调用=======");
Thread t1 = new Thread(new ThreadLocalExectorThread());
Thread t2 = new Thread(new ThreadLocalExectorThread());
t1.start();
t2.start();
}
}
class ThreadLocalExectorThread implements Runnable{
@Override
public void run() {
ThreadLocalSingleton lazy = ThreadLocalSingleton.getInstance();
System.err.println(Thread.currentThread().getName()+"---"+lazy);
}
}
总结
单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。
网友评论