回顾
在上一节,介绍了单例模式中的懒汉式、饿汉式、静态内部类模式下的单例,这次从注册式单例模式开始
注册式单例
将每一个实例都缓存到统一的容器中,使用唯一标识获取实例
枚举式单例
/**
* 注册式单例之枚举单例
*/
public enum RegisterEnum {
INSTANCE;
public static RegisterEnum getInstance() {
return INSTANCE;
}
}
public class RegisterEnumTest {
/**
* 使用序列化形式来创建单例对象
* @param args
*/
public static void main(String[] args) {
RegisterEnum r1;
RegisterEnum r2 = RegisterEnum.getInstance();
FileOutputStream fileOutputStream;
try {
fileOutputStream = new FileOutputStream("1.obj");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(r2);
objectOutputStream.flush();
objectOutputStream.close();
FileInputStream fileInputStream = new FileInputStream("1.obj");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
r1 = (RegisterEnum) objectInputStream.readObject();
System.out.println(r1);
System.out.println(r2);
System.out.println(r1==r2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出的是:true
将序列化后的类反编译后如下(mac下的jad工具只能反编译,无法反编译.obj的文件,在window下试一下),可以看出这时反编译后的枚举类是个饿汉式的单例模式,而饿汉式的单例本身就是线程安全的。
思考一下,枚举类中没有重写readResolve(),如果序列化创建枚举对象,枚举是怎么避免序列化破坏单例的呢?还是一样看readObject()的源码,对于枚举的类型的判断,如下图所示
由源码可得,从jdk层面就为枚举不被序列化破坏而起到了根本的保证,那么我们再试试用反射的机制看能不能创建两个对象,检查枚举单例的正确性
/**
* 使用反射形式来创建单例对象
*
* @param args
*/
public static void main(String[] args) {
try {
Class<RegisterEnum> enumClass = RegisterEnum.class;
Constructor<RegisterEnum> constructor = enumClass.getDeclaredConstructor();
//关闭权限检查
constructor.setAccessible(true);
//调用构造方法,进行创建对象
RegisterEnum registerEnum = constructor.newInstance();
System.out.println(registerEnum);
} catch (Exception e) {
e.printStackTrace();
}
}
结果抛出以下异常
通过反编译后的代码可知,枚举类中只有一个有参的构造器,而newInstance(),是默认调用无参构造器,所以无法初始化类,修改代码如下,传入一个String类型和int类型的参数
/**
* 使用类名.class来进行反射
*
* @param args
*/
public static void main(String[] args) {
try {
Class<RegisterEnum> enumClass = RegisterEnum.class;
Constructor<RegisterEnum> constructor = enumClass.getDeclaredConstructor(String.class,int.class);
//关闭权限检查
constructor.setAccessible(true);
//调用构造方法,进行创建对象
RegisterEnum registerEnum = constructor.newInstance("wcj",25);
System.out.println(registerEnum);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 使用Class.forName来进行反射
*
* @param args
*/
public static void main(String[] args) {
try {
Constructor<?> declaredConstructor = Class.forName("top.wangcj.chapter02单例模式.register.RegisterEnum").getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true);
RegisterEnum registerEnum = (RegisterEnum) declaredConstructor.newInstance("wcj", 25);
System.out.println(registerEnum);
} catch (Exception e) {
e.printStackTrace();
}
}
运行,但是又报出以下错误
查询newInstance()的源码如下
由源码可知,jdk内部就进行的判断,当反射想创建枚举对象时,直接抛出的异常,这属于jdk内部的机制,直接从根本就扼杀了反射创建枚举对象的可能性,自然也保证了枚举单例模式的安全性
延伸:Class.forName()和类名.class有何区别
public class Tom {
static int num = 10;
static {
System.out.println("被执行了:"+num);
}
}
@Test
public void testTom() {
try {
//不会初始化静态成员,无输出,除非进行tomClass.newInstance()操作;
Class<Tom> tomClass = Tom.class;
//会初始化静态成员,有输出
Class<?> aClass = Class.forName("top.wangcj.chapter02单例模式.register.Tom");
} catch (Exception e) {
e.printStackTrace();
由上述代码可知,Class.forName显示加载类时候会调用代码中静态块。而直接用类.class则不加载静态代码块,只有在类名.class.newInstance()时候才会初始化这些静态块
容器式单例
Spring框架采用的就是容器式单例模式
/**
* 容器式单例
*
* @author wcj
* @description
* @date 2019/8/18 19:11
*/
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> singleton = new ConcurrentHashMap<>();
public static Object getInstance(String className) {
if (!singleton.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
singleton.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
return singleton.get(className);
}
}
/**
* 发令枪,线程并发
*/
public class ConcurrentExecutor {
/**
* @param runHandler
* @param executeCount 发起请求总数
* @param concurrentCount 同时并发执行的线程数
* @throws Exception
*/
public static void execute(final RunHandler runHandler, int executeCount, int concurrentCount) throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
//控制信号量,此处用于控制并发的线程数
final Semaphore semaphore = new Semaphore(concurrentCount);
//闭锁,可实现计数量递减
final CountDownLatch countDownLatch = new CountDownLatch(executeCount);
for (int i = 0; i < executeCount; i++) {
executorService.execute(new Runnable() {
public void run() {
try {
//执行此方法用于获取执行许可,当总计未释放的许可数不超过executeCount时,
//则允许同性,否则线程阻塞等待,知道获取到许可
semaphore.acquire();
runHandler.handler();
//释放许可
semaphore.release();
} catch (Exception e) {
e.printStackTrace();
}
countDownLatch.countDown();
}
});
}
countDownLatch.await();//线程阻塞,知道闭锁值为0时,阻塞才释放,继续往下执行
executorService.shutdown();
}
public interface RunHandler {
void handler();
}
}
/**
* 测试容器式单例线程
*
* @author wcj
* @description
* @date 2019/8/19 10:02
*/
public class ContainerSingletonTest {
public static void main(String[] args) {
try {
ConcurrentExecutor.execute(new ConcurrentExecutor.RunHandler() {
public void handler() {
Object obj = ContainerSingleton.getInstance("top.wangcj.chapter02单例模式.container.User");
;
System.out.println(System.currentTimeMillis() + ": " + obj);
}
}, 10, 6);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出的结果是:
1566184262618: top.wangcj.chapter02单例模式.container.User@2d74268a
1566184262618: top.wangcj.chapter02单例模式.container.User@1fe9b76c
1566184262618: top.wangcj.chapter02单例模式.container.User@11f3ef5e
1566184262618: top.wangcj.chapter02单例模式.container.User@1dbdc081
1566184262619: top.wangcj.chapter02单例模式.container.User@1d6bc575
1566184262618: top.wangcj.chapter02单例模式.container.User@b23d21c
1566184262618: top.wangcj.chapter02单例模式.container.User@1d6bc575
1566184262619: top.wangcj.chapter02单例模式.container.User@1d6bc575
1566184262619: top.wangcj.chapter02单例模式.container.User@1d6bc575
1566184262619: top.wangcj.chapter02单例模式.container.User@1d6bc575
由输出结果可知,并不是单例的情况,那么可以针对懒加载的单例模式的改造方案,添加synchronized关键字,但是不要加在静态方法上,因为锁住的是类,可以在方法内部加锁,修改如下
public static Object getInstance(String className) {
//方法内部加锁
synchronized (singleton){
if (!singleton.containsKey(className)) {
Object obj = null;
try {
obj = Class.forName(className).newInstance();
singleton.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
return singleton.get(className);
}
}
注意:虽然ConcurrentHashMap类是个线程安全的,它只是控制了自己内部的方法是线程安全的,但是getInstance并不是线程安全的,所以对此方法进行处理,使其安全。
容器式单例的优点
- 方便对象管理,也是属于是懒加载的方式
ThreadLocal单例模式(也是容器式单例)
/**
* ThreadLocal单例模式
* 线程间的线程安全,伪线程安全的
* @author wcj
* @description
* @date 2019/8/19 11:45
*/
public class ThreadLocalSingleton {
private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal<ThreadLocalSingleton>() {
//重写initialValue方法
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};
private ThreadLocalSingleton() {
}
public static ThreadLocalSingleton getInstance() {
return threadLocal.get();
}
}
/**
* ThreadLocal单例模式测试类
* @author wcj
* @description
* @date 2019/8/19 14:26
*/
public class ThreadLocalSingletonTest {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
System.out.println(Thread.currentThread().getName()+":"+ThreadLocalSingleton.getInstance());
Thread t1 = new Thread(new ExecuteThread());
Thread t2 = new Thread(new ExecuteThread());
t1.start();
t2.start();
}
}
输出结果如下:
main:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@66d3c617
main:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@66d3c617
main:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@66d3c617
Thread-0:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@69e15105
Thread-0:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@69e15105
Thread-0:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@69e15105
Thread-1:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@c8a4608
Thread-1:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@c8a4608
Thread-1:top.wangcj.chapter02单例模式.ThreadLocal.ThreadLocalSingleton@c8a4608
由输出结果可看出,虽然产生了并发,但是同一个线程,得到的对象还是相同的,虽然是属于线程级别的线程安全,伪线程安全。该种单例模式保证线程内部的全局唯一,且天生线程安全,为什么天生线程安全呢,查看JDK源码如下
由源码可以看出,JDK内部的ThreadLocal类中的静态类ThreadLocalMap类是用当前线程作为key,value就是我们需要的值,就如同上述代码中的其实存在着ThreadLocalMap对象,内部存储的key就是当前线程,value就是ThreadLocalSingleton对象。
使用ThreadLocal可以实现多数据源动态切换功能,后续会有
单例模式优点
- 在内存中只有一个实例,减少了内存开销
- 可以避免对资源的多重占用
- 设置全局访问点,严格控制访问
单例模式缺点
- 没有接口,扩展困难
- 如果要扩展单例对象,只有修改代码,没有其他途径
单例模式总结
- 构造器需要私有化
- 要确保其线程安全,线程不安全,就没有单例一说了
- 延迟加载
- 防止序列化和反序列化破坏单例
- 防止反射攻击单例
总结每种单例写法的优、缺点。
- 饿汉式单例:类一加载就创建,线程安全,但是不管用没用到都实例化,如果这样的类有很多,那么会造成很大资源占用
- 懒汉式单例:用到才创建,节省了资环
- 不加锁:线程不安全
- 方法锁:锁权重太大,降低性能
- 双重检查锁:降低锁权重,相对方法锁提高了效率
- 静态内部类式单例:内部类虽然是饿汉式的,但是用到时才加载内部类,实例化单例。并且没用到锁,提高了性能
- 序列化式单例:只是在简单的懒汉式上实现了序列化了,但是如果不实现readReslove()方法,无法保证单例
- 注册式单例
- 枚举式单例:JDK内部保障自由序列化,反射无法破坏,且线程安全,推荐使用
- 容器缓存单例:方便管理大量单例,但存在线程安全问题,spring内部使用的是这种
- ThreadLocal式单例:单线程内实例唯一,多线程内线程不安全,多数据源的场景会用到
破坏单例模式的方式有哪些?
- 反射破坏单例,解决方案:在构造方法中做处理,发现已有实例即抛异常;
- 序列化、反序列化破坏单例,解决方案:在单例类中,声明readReslove()方法,返回单例。
- 深克隆破坏单例,深克隆使用序列化与反序列化实现克隆,所有可以如上面方法解决。