单例模式
为了解决资源浪费、资源共享问题,只需要初始化一次,其他人都能复用
所有单例都需要解决的问题
- 将构造函数私有化
- 通过静态方法获取一个唯一实例
- 保证线程安全
- 防止反序列化造成的新实例等。
单例模式实现方式
- 饿汉式
- 懒汉式
- 注册登记式(容器式、序列化、枚举式都属于注册登记式的一种)
- 枚举式
饿汉式单例
特点
在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快
优点
使用前初始化,避免了线程安全问题,快,但浪费内存,不管用没用到这个类都占用内存,用户体验+
实现
package com.vnsun.one.hungry;
/**
* 饿汉式单例
* @author vnsun
*/
public class Hungry {
private static final Hungry hungry = new Hungry();
// 私有构造
private Hungry() {
}
public static Hungry newInstance() {
return hungry;
}
}
懒汉式单例
特点
在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢
优点
一定程度上避免了内存浪费,提高了JVM加载效率。但相对于饿汉式单例效率不高,因为存在线程安全问题,需要某些手段来保证其线程安全,
传统单例实现
package com.vnsun.one.lazy;
/**
* 懒汉式单例
* @author vnsun
*/
public class LazyOne {
private volatile static LazyOne lazyone;
private LazyOne() {
}
/**
* 改造二
* double lock锁机制 虽然优化了效率,但依然很慢
* 此时线程安全
* @return
*/
public static final LazyOne newInstance() {
if (lazyone == null) {
synchronized (LazyOne.class) {
if (lazyone == null) {
lazyone = new LazyOne();
}
}
}
return lazyone;
}
/**
* 改造一
* synchronized 加锁形式严重影响效率
* 此时线程安全
* @return
*/
/*public synchronized final static LazyOne newInstance() {
if (lazyone == null) {
lazyone = new LazyOne();
}
return lazyone;
}*/
/**
* 多线程情况下有线程安全问题
* @return
*/
/*public final static LazyOne newInstance() {
if (lazyone == null) {
lazyone = new LazyOne();
}
return lazyone;
}*/
}
上述代码虽已经进行了改造但效率依然不高,加入synchronized关键字后,造成不必要的同步开销。不建议使用,虽然JDK1.6及后续版本对改关键字进行了大量优化
double lock在高并发情况下也会存在一定缺陷,但是几率是极小的
double lock DCL失效
假设线程A执行LazyOne one = LazyOne.newInstance();线程大概做了三件事
- 给实例分配内存
- 调用构造函数,初始化成员字段
- 将instance 对象指向分配的内存空间(此时sInstance不是null)
如果执行顺序是1-3-2,那多线程下,A线程先执行3,2还没执行的时候,此时instance!=null 这时候,B线程直接取走instance ,使用会出错,难以追踪。JDK1.5及之后的volatile 解决了DCL失效问题(双重锁定失效)
测试单例安全
/**
* 测试单例并发
* @author vnsun
*/
public class ThredsalfTest {
public static void main(String[] args) {
int count = 1000;
// 发令枪,模拟多线程
CountDownLatch latch = new CountDownLatch(count);
long startTime = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
new Thread() {
@Override
public void run() {
try {
latch.await();
LazyOne lazyOne = LazyOne.newInstance();
//Hungry hungry = Hungry.newInstance();
System.out.println(System.currentTimeMillis() + ":" + lazyOne);
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
latch.countDown();
}
long endTime = System.currentTimeMillis();
System.out.println("总耗时:"+ (endTime - startTime));
}
}
image.png
静态内部类
优点:线程安全、保证单例对象唯一性,同时也延迟了单例的实例化,避免了反射入侵
缺点:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是 Class 对象还是会被创建,而且是属于永久代的对象。(综合以为,这种方式是最好的单例模式)
package com.vnsun.one.lazy;
/**
* 单例模式二
* @author
*/
public class LazyTwo {
/**
* 存在问题:通过反射暴力提升访问权限,修改掉此值(一般是闲得蛋疼)
* */
private static boolean initialized = false;
/**
* 防止暴力初始化
*/
private LazyTwo() {
synchronized (LazyTwo.class) {
if (!initialized) {
initialized = true;
} else {
throw new RuntimeException("单例已被破坏");
}
}
}
public static final LazyTwo newInstance() {
return LazyInside.LAZY;
}
private static class LazyInside {
private static final LazyTwo LAZY = new LazyTwo();
}
}
暴力测试单例侵犯
/**
* 暴力测试单例侵犯
*/
public class LazyTwoTest {
public static void main(String[] args) {
Class<LazyTwo> clazz = LazyTwo.class;
try {
// 拿到私有构造方法
Constructor c = clazz.getDeclaredConstructor(null);
// 访问权限提升
c.setAccessible(true);
Object o = c.newInstance();
System.out.println(o);
Object o1 = c.newInstance();
System.out.println(01);
System.out.println(0 == 01);
} catch (Exception e) {
e.printStackTrace();
}
}
}
image.png
枚举单例
枚举反序列化不会生成新的实例
优点:线程安全
缺点:枚举耗内存,能不用枚举就不用
class Resource{
}
public enum SomeThing {
INSTANCE;
private Resource instance;
SomeThing() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
容器单例
利用了Map的key唯一性来保证单例
/**
* 注册登记式
* spring中就是使用这种做法
* @author vnsun
*/
public class BeamFactory {
private BeamFactory() {}
private static Map<String,Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className) {
if (ioc.containsKey(className)) {
return ioc.get(className);
} else {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
}
容器测试
public static void main(String[] args) {
int count = 100;
// 发令枪,模拟多线程
CountDownLatch latch = new CountDownLatch(count);
long startTime = System.currentTimeMillis();
for (int i = 0; i < count; i++) {
new Thread() {
@Override
public void run() {
try {
latch.await();
Object bean = BeamFactory.getBean("com.vnsun.one.lazy.OneFactory");
System.out.println(System.currentTimeMillis() + ":" + bean);
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
latch.countDown();
}
long endTime = System.currentTimeMillis();
System.out.println("总耗时:"+ (endTime - startTime));
}
image.png
此时可看到容器单例也会有线程安全问题,目前只有+syncharized才能保证线程安全
所以容器单例 适合在程序初始化的时候,把多个单例对象放到map里边统一管理
序列化单例
public class Seriable implements Serializable {
public final static Seriable INSTACE = new Seriable();
private Seriable(){}
public static Seriable getInstance() {
return INSTACE;
}
/**
* 不加此方法,反序列化时,单例会是一个新实例
* @return
*/
/*private Object readResolve() {
return INSTACE;
}*/
}
测试序列化与反序列化单例
/**
* 序列化与反序列化 测试单例问题
*/
public class SeriableTest {
public static void main(String[] args) {
Seriable s1 = null;
Seriable s2 = Seriable.getInstance();
try(FileOutputStream fos = new FileOutputStream("Seriable.obj")) {
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("Seriable.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (Seriable)ois.readObject();
ois.close();
fis.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
image.png
解释一下为何反序列化后破坏了单例:
这是因为反序列化中的IO操作会重新从磁盘上读取此实例内容,而在操作的过程中会重新生产一个新的对象。
原因在于ObjectInputStream的readObject的调用栈:
readObject--->readObject0--->readOrdinaryObject--->checkResolve
readOrdinaryObject代码片段
Object obj;
try {
// desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(desc.forClass().getName(),"unable to create instance").initCause(ex);
}
防止序列化破坏单例模式
把上述代码中注释掉的readResolve()方法放开后可以防止反序列化破坏单例
总结
比较推荐的用法 double lock 、静态内部类(更为推荐)
单例优点
- 只有一个对象,内存开支少、性能好(当一个对象的产生需要比较多的资源,如读取配置、产生其他依赖对象时,可以通过应用启动时直接产生一个单例对象,让其永驻内存的方式解决)
- 避免对资源的多重占用(一个写文件操作,只有一个实例存在内存中,避免对同一个资源文件同时写操作)
- 在系统设置全局访问点,优化和共享资源访问(如:设计一个单例类,负责所有数据表的映射处理)
单例缺点
- 一般没有接口,扩展难
网友评论