一.什么是单例模式
1.什么是单例模式
单例模式就是一个类对外只有一个实例
2.场景
- 实例化类需要消耗很多的资源
- 该类只能实例化一次,对外是唯一的对象
3.表现形式:
- 构造方法是私有的,即外部是无法实例化该类的
- 有一个保存当前实例的私有静态变量
- 有一个对外开放的访问该类对象的方法
4.单例的线程安全性检测
通过多线程调用单例的获取实例的方法,检查每次的实例是否相同
二.单例模式的实现方式
1.饿汉式(线程安全)
(1)特点:
- 在类加载的时候就实例化类(感觉很饿一样,饭一到就直接吃)
(2)实例:
/**
* 饿汉式单例模式
*/
public class HungrySingleton {
/**
* 在类加载时就实例化
*/
private static final HungrySingleton INSTANCE = new HungrySingleton();
/**
* 构造方法外对私有
*/
private HungrySingleton() {
}
/**
* 对外开放的访问实例对象的方法
*/
public static HungrySingleton getInstance() {
return INSTANCE;
}
}
测试代码
class HungrySingletonRunnable implements Runnable {
@Override
public void run() {
System.out.println( HungrySingleton.getInstance());
}
}
/**
* 饿汉式测试
* @author shixinke
*/
public class HungrySingletonTest {
public static void main(String[] args) {
new Thread(new HungrySingletonRunnable()).start();
new Thread(new HungrySingletonRunnable()).start();
new Thread(new HungrySingletonRunnable()).start();
}
}
打印结果:
com.shixinke.practise.design.pattern.creation.singleton.HungrySingleton@5eacd0a4
com.shixinke.practise.design.pattern.creation.singleton.HungrySingleton@5eacd0a4
com.shixinke.practise.design.pattern.creation.singleton.HungrySingleton@5eacd0a4
(3)优点
- 是线程安全的
注:JVM保证一个类只会被加载一次,因为它在类加载时就被实例化,因此可以保证它只被实例化一次
(4)缺点
- 加载类时就初始化,会立即占用内存空间,如果不使用它,会造成资源浪费,对整个程序加载速度有一定影响
2.懒汉式
(1)特点:
在使用时,才实例化,就延迟加载的感觉(感觉它比较懒,不用它,它就不干活)
(2)实例
/**
* 懒汉式
* @author shixinke
*/
public class LazySingleton {
/**
* 先不初始化
*/
private static LazySingleton INSTANCE;
private LazySingleton() {
}
/**
* 在使用时初始化
* @return
*/
public static LazySingleton getInstance() {
/**
* 如果当前实例不存在,则new一个
*/
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}
测试代码:
class LazyRunnable implements Runnable {
@Override
public void run() {
System.out.println(LazySingleton.getInstance());
}
}
/**
* 懒汉式测试
* @author shixinke
*/
public class LazySingletonTest {
/**
* 单线程测试
*/
public static void singleThread() {
LazySingleton ls1 = LazySingleton.getInstance();
LazySingleton ls2 = LazySingleton.getInstance();
System.out.println(ls1);
System.out.println(ls2);
}
public static void main(String[] args) {
//singleThread();
new Thread(new LazyRunnable()).start();
new Thread(new LazyRunnable()).start();
new Thread(new LazyRunnable()).start();
}
}
测试结果:
com.shixinke.practise.design.pattern.creation.singleton.LazySingleton@48df2951
com.shixinke.practise.design.pattern.creation.singleton.LazySingleton@6fb104b6
com.shixinke.practise.design.pattern.creation.singleton.LazySingleton@1467bfc6
(3)优点:
- 懒加载:在类加载后,并不进行实例化,而是在使用时才实例化,在一定程度上能降低资源占用
(4)缺点:
- 不是线程安全的,无法保证所有对外的方法生成的是同一个对象
补充:我们通过单步调试来分析这个结果:
注:为什么它不是线程安全的呢?
- 因为编译器为了进行程序优化,进行指令重排序,因此无法保证if判断在前,初始化在后
- 因为CPU缓存问题,多线程对同一个共享变量的操作对于另外的线程有可见性问题,因此无法保证创建同一个对象
3.改进懒汉式
通过加锁的方式来保证线程安全
/**
* 懒汉式(加锁)
* @author shixinke
*/
public class SynchronizedLazySingleton {
private static SynchronizedLazySingleton INSTANCE;
private SynchronizedLazySingleton() {
}
public static synchronized SynchronizedLazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new SynchronizedLazySingleton();
}
return INSTANCE;
}
}
测试代码:
class SynchronizedLazyRunnable implements Runnable {
public void run() {
System.out.println( SynchronizedLazySingleton.getInstance());
}
}
/**
* 懒汉式测试
* @author shixinke
*/
public class SynchronizedLazySingletonTest {
public static void singleThread() {
SynchronizedLazySingleton ls1 = SynchronizedLazySingleton.getInstance();
SynchronizedLazySingleton ls2 = SynchronizedLazySingleton.getInstance();
System.out.println(ls1);
System.out.println(ls2);
}
public static void main(String[] args) {
//singleThread();
new Thread(new SynchronizedLazyRunnable()).start();
new Thread(new SynchronizedLazyRunnable()).start();
new Thread(new SynchronizedLazyRunnable()).start();
}
}
注:在getInstance前面添加synchronized关键词,表示添加一把内置锁
- 优点:可保证线程安全,即多个线程也可以获取到同一对象
- 缺点:因为加锁(synchronized是重量级锁)对性能有很大损耗,而且因为这里的锁是对整个类加锁,所以使用getInstance时,只能有一个线程访问资源
4.双重检测+懒汉式
目的:改进上面synchronized加锁的性能
/**
* 懒汉式
* @author shixinke
*/
public class DoubleCheckLazySingleton {
private volatile static DoubleCheckLazySingleton INSTANCE;
private DoubleCheckLazySingleton() {
}
public static DoubleCheckLazySingleton getInstance() {
if (INSTANCE == null) {
synchronized(DoubleCheckLazySingleton.class) {
if (INSTANCE == null) {
INSTANCE = new DoubleCheckLazySingleton();
}
return INSTANCE;
}
}
return INSTANCE;
}
}
测试代码:
class DoubleCheckLazyRunnable implements Runnable {
public void run() {
System.out.println( DoubleCheckLazySingleton.getInstance());
}
}
/**
* 双重检测测试
* @author
*/
public class DoubleCheckSingletonTest {
public static void main(String[] args) {
new Thread(new DoubleCheckLazyRunnable()).start();
new Thread(new DoubleCheckLazyRunnable()).start();
new Thread(new DoubleCheckLazyRunnable()).start();
}
}
分析:
- 只有当INSTANCE为null时,才加锁进入下一步操作
- 在锁内再次进行判断,是检测在加锁进入后这一对象是否被其他线程初始化
- 使用volatile关键词,可以保证多线程之间的有序性和可见性,即对if判断和后面的初始化禁用重排序,另外,其他线程对INSTANCE的操作,对当前线程也是可见的
5.静态内部类
通过在静态内部类中初始化当前类的实例,也是通过JVM来保证该类只会被实例化一次
/**
* 内部类的方式
* @author shixinke
*/
public class InnerClassSingleton {
private InnerClassSingleton() {
}
public static InnerClassSingleton getInstance() {
return Inner.INSTANCE;
}
private static class Inner {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
}
测试代码:
class InnerClassRunnable implements Runnable {
public void run() {
System.out.println(InnerClassSingleton.getInstance());
}
}
/**
* 内部类测试
* @author shixinke
*/
public class InnerClassSingletonTest {
public static void singleThread() {
InnerClassSingleton ls1 = InnerClassSingleton.getInstance();
InnerClassSingleton ls2 = InnerClassSingleton.getInstance();
System.out.println(ls1);
System.out.println(ls2);
}
public static void main(String[] args) {
//singleThread();
new Thread(new InnerClassRunnable()).start();
new Thread(new InnerClassRunnable()).start();
new Thread(new InnerClassRunnable()).start();
}
}
6.枚举类
这是最推荐使用的单例设计模式实现方式
public enum EnumInstance {
INSTANCE;
EnumInstance() {
}
public static EnumInstance getInstance() {
return INSTANCE;
}
}
测试代码:
class EnumSingletonRunnable implements Runnable {
public void run() {
System.out.println( EnumInstance.getInstance());
}
}
/**
* 枚举测试
* @author
*/
public class EnumInstanceTest {
public static void main(String[] args) {
new Thread(new EnumSingletonRunnable()).start();
new Thread(new EnumSingletonRunnable()).start();
new Thread(new EnumSingletonRunnable()).start();
}
}
补充:
Q:为什么枚举类单例是线程安全的?
A:这里借助反编译工具jad来查看反编译编译后的enum源码
- jad官方网站: https://varaneckas.com/jad/
- 下载安装: 直接下载解压即可(根据自己的操作系统选择对应的版本),把jad所在目录放入到环境变量PATH中
- 反编译.class文件
cd /data/program/github/degisn-pattern-practise/target/classes/com/shixinke/practise/design/pattern/content/creation/singleton
jad EnumInstance.class
- 查看生成的反编译后的文件(与类名同名的文件,并以.jad为后缀)
// Decompiled by Jad v1.5.8e. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.geocities.com/kpdus/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumInstance.java
package com.shixinke.practise.design.pattern.content.creation.singleton;
//它是一个final的class,因此它不能被继承
public final class EnumInstance extends Enum
{
public static EnumInstance[] values()
{
return (EnumInstance[])$VALUES.clone();
}
public static EnumInstance valueOf(String name)
{
return (EnumInstance)Enum.valueOf(com/shixinke/practise/design/pattern/content/creation/singleton/EnumInstance, name);
}
private EnumInstance(String s, int i)
{
super(s, i);
}
public static EnumInstance getInstance()
{
return INSTANCE;
}
//INSTANCE是final的,因此也无法改变它
public static final EnumInstance INSTANCE;
private static final EnumInstance $VALUES[];
//在static中实例化(JVM保证了它只会被实例化一次)
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
7.容器类
- 一个容器,来保存所有的实例
- 容器对外有两个方法:
- 注册方法:将实例对象注册到容器中(一般在初始化时注册)
- 获取实例方法:从容器中取出实例
/**
* 容器类型的单例
* @author shixinke
*/
public class ContainerSingleton {
/**
* 保存实例的容器
*/
private static Map<String, Object> INSTANCE_MAP = new ConcurrentHashMap<String, Object>(10);
private ContainerSingleton() {
}
/**
* 添加instance到容器
* @param key
* @param instance
*/
public static void register(String key, Object instance) {
if (key != null && instance != null) {
if (!INSTANCE_MAP.containsKey(key)) {
INSTANCE_MAP.put(key, instance);
}
}
}
/**
* 取instance
* @param key
* @return
*/
public static Object getInstance(String key) {
return INSTANCE_MAP.get(key);
}
}
测试代码:
package com.shixinke.practise.design.pattern.content.creation.singleton;
class RegisterSingletonRunnable implements Runnable {
private String key;
private Object value;
public RegisterSingletonRunnable(String key, Object value) {
this.key = key;
this.value = value;
}
public void run() {
ContainerSingleton.register(this.key, this.value);
}
}
class GetSingletonRunnable implements Runnable {
public void run() {
System.out.println( ContainerSingleton.getInstance("object"));
}
}
/**
* 容器单例测试类
* @author shixinke
*/
public class ContainerSingletonTest {
public static void main(String[] args) {
new Thread(new RegisterSingletonRunnable("object", new Object())).start();
new Thread(new RegisterSingletonRunnable("object", new Object())).start();
new Thread(new RegisterSingletonRunnable("object2", new Object())).start();
new Thread(new GetSingletonRunnable()).start();
new Thread(new GetSingletonRunnable()).start();
new Thread(new GetSingletonRunnable()).start();
}
}
8总结
单例模式实现方式有多种,推荐顺序如下:
枚举类单例模式 > 静态内部类单例模式 > 饿汉式单例模式
- 枚举类单例模式不用考虑序列化和反射来破坏单例模式的情况
- 其他实现方式需要考虑序列化和反射破坏单例模式的情况
三.源码中单例模式的使用
1.JDK中单例模式的使用
(1)Runtime类
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
}
- Runtime类使用的是懒汉式的单例模式
2.Mybatis中单例模式的使用
注:ErrorContext 只保证在线程内部,实例对象是唯一的(因为使用了ThreadLocal,这个是每个线程都有一个)
public class ErrorContext {
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal();
private ErrorContext() {
}
//对外提供的获取实例的方法
public static ErrorContext instance() {
ErrorContext context = (ErrorContext)LOCAL.get();
if (context == null) {
context = new ErrorContext();
LOCAL.set(context);
}
return context;
}
}