单例模式
单例模式(Singleton Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛。 例如,国家主席、公司 CEO、部门经理等。在 J2EE 标准中,ServletContext、 ServletContextConfig 等;在 Spring 框架应用中 ApplicationContext;数据库的连接 池也都是单例形式。
单例模式可以保证内存里只有一个实例,减少了内存开销;可以避免对资源的多重占用。
单例模式特点
1.单例类只能有一个实例;2.单例类必须自己创建自己的唯一实例;3.单例类必须给所有其他对象提供这一实例。
常见单例模式
单例模式的写法有很多种,这里主要提供四种:饿汉式单例、懒汉式单例、枚举式单例、容器式单例(枚举式单例和容器式单例都属于注册式单例)
饿汉式单例
在类加载的时候就立即初始化,并且创建单例对象。绝对线程安全,在线 程还没出现以前就是实例化了,不可能存在访问安全问题, Spring 中 IOC 容器 ApplicationContext 本身就是典型的饿汉式单例
- 优点:没有加任何的锁、执行效率比较高,在用户体验上来说,比懒汉式更好。
- 缺点:类加载的时候就初始化,不管用与不用都占着空间,浪费了内存。
package org.about.designpatterns.singleton.hungry;
/**
* @Package org.about.designpatterns.singleton.hungry
* @Author
* @Description 饿汉式单例 在类加载的时候就立即初始化,并且创建单例对象。
* 绝对线程安全,在线程还没出现以前就是实例化了,不可能存在线程安全问题
* @Version: V1.0
*/
public class HungrySingleton {
/**
* 定义该单例属性
*/
private static final HungrySingleton HUNGRY_SINGLETON;
static {
HUNGRY_SINGLETON = new HungrySingleton();
}
/**
* 构造方法私有化
*/
private HungrySingleton() {
// 防止反射破坏单例,下一篇会介绍
if (HUNGRY_SINGLETON != null) {
throw new RuntimeException("不允许创建多个单例");
}
}
/**
* 提供全局唯一访问点
* @return HUNGRY_SINGLETON
*/
public static HungrySingleton getInstance() {
return HUNGRY_SINGLETON;
}
// /**
// * 防止序列化破坏单例
// * 如果该单例实现了 Serializable 序列化接口,为了防止序列化破坏单例,需要重写 readResolve()方法
// * 下一篇会讲解序列化破坏单例
// * @return SINGLETON
// */
// private Object readResolve() {
// return HUNGRY_SINGLETON;
// }
}
懒汉式单例
被外部类调用的时候内部类才会加载单例对象,避免了饿汉式在类加载的时候就产生实例而产生性能消耗
双重检查锁写法
package org.about.designpatterns.singleton.lazy;
/**
* @Package org.about.designpatterns.singleton.lazy
* @Author Epocher
* @Description 懒汉模式: 双重检查锁
* <p>
* 知识点: 多线程下指令重排问题 volatile
* (this) lazy = new LazyDoubleCheckSingleton();
* cpu执行上面的时候会转换为JVM指令:1.分配内存给这个对象; 2.属性初始化; 3.引用指向对象
* 上面语句为非原子性,所以上面可能会产生指令重排问题 即:正常情况下执行顺序为 1->2->3 但是实际情况下可能就是 1->3->2
*
* (1)单线程情况下,指令重排没有影响;
* (2)但在多线程情况下,假如线程(1)执行 lazy = new LazyDoubleCheckSingleton()语句时先1 再 3,
* 但是此时系统调度线程(2),没来得及执行步骤2,但此时已有引用指向对象(即已经执行3 但是没有完成初始化2)
* 故线程2在第一次检查时不满足条件直接返回 lazy,此时 lazy 为 null
* volatile 关键字可保证 lazy = new LazyDoubleCheckSingleton();的语句执行顺序为 1 2 3
* 具体可以参考 volatile 的特性。这里就不做过多说明了
* </p>
* @Date 2020-07-28 16:35
* @Version: V1.0
*/
public class LazyDoubleCheckSingleton {
/**
* LazyDoubleCheckSingleton 实例属性
* volatile 关键字禁止指令重排, 即为第二层锁
*/
private volatile static LazyDoubleCheckSingleton lazy;
/**
* 构造方法私有化
*/
private LazyDoubleCheckSingleton() {
// 这个是为了防止 通过反射机制从而破坏了单例模式
// 下一篇会做讲解
if (lazy != null) {
throw new RuntimeException("不允许创建多个单例!");
}
}
/**
* 提供全局唯一访问点
* <p>
* synchronized 虽然在性能上已经有了优化,但是还是不可避免的产生内存的消耗
* 下面会介绍更加好的单例模式
* </p>
* @return lazy
*/
public static LazyDoubleCheckSingleton getInstance() {
// 第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入 的时候才创建单例,避免重复创建
if (lazy == null ) {
// 第一层锁,synchronized 保证只有一个线程进入
synchronized (LazyDoubleCheckSingleton.class) {
// 第二层检查,若不检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
if (lazy == null) {
// 第二层锁:利用 volatile 关键字防止指令重排
lazy = new LazyDoubleCheckSingleton();
}
}
}
return lazy;
}
// /**
// * 防止序列化破坏单例
// * 如果该单例实现了 Serializable 序列化接口,为了防止序列化破坏单例,需要重写 readResolve()方法
// * 下一篇会讲解序列化破坏单例
// * @return SINGLETON
// */
// private Object readResolve() {
// return lazy;
// }
}
静态内部类写法
利用静态内部类加载的特性:不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法的时候才会加载静态内部类。利用JVM底层特性,巧妙的避免了线程安全问题。性能相对来说很优秀
package org.about.designpatterns.singleton.lazy;
/**
* @Package org.about.designpatterns.singleton.lazy
* @Author
* @Description 静态内部类实现懒汉式单例 性能最好 全程没有用到 synchronized关键字
* @Version: V1.0
*/
public class LazyInnerClassSingleton {
/**
* 构造方法私有化
*/
private LazyInnerClassSingleton(){
// 这个是为了防止 通过反射机制从而破坏了单例模式
// 下一篇会做讲解
if (LazyHolder.LAZY != null) {
throw new RuntimeException("不允许创建多个单例!");
}
}
/**
* 全局唯一访问点
* static 是为了使单例的空间共享
* final 保证这个方法不会被重写,重载
* @return LazyHolder.LAZY
*/
public static final LazyInnerClassSingleton getInstance() {
// 在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
/**
* 静态内部类 利用了静态内部类 加载时的特性
* 内部静态类不会自动初始化,只有调用静态内部类的方法,静态域,或者构造方法的时候才会加载静态内部类。
* JVM 底层的逻辑,完美的避免了线程安全问题
*/
private static class LazyHolder {
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
// /**
// * 防止序列化破坏单例
// * 如果该单例实现了 Serializable 序列化接口,为了防止序列化破坏单例,需要重写 readResolve()方法
// * 下一篇会讲解序列化破坏单例
// * @return SINGLETON
// */
// private Object readResolve() {
// return LazyHolder.LAZY;
// }
}
饿汉模式与懒汉模式
- 时间和空间饿汉式:是典型的空间换时间,当类装载的时候就会创建类实例,不管你用不用,先创建出来,然后每次调用的时候,就不需要再判断了,节省了运行时间。懒汉式:是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。当然,如果一直没有人使用的话,那就不会创建实例,则节约内存空间
- 线程安全饿汉式:线程绝对安全,因为在类加载的时候就已经初始化了对象实例。JVM只会装载一次,在并发发生之前就已经创建了唯一实例懒汉式:存在线程安全问题。好的解决方案是 上面的双重检查锁和静态内部类写法
注册式单例
注册式单例:又称为登记式单例,就是将每一个实例都登记到某一个地方,使用唯一的标识获取实例。注册式单例有两种写法:一种为容器缓存,一种为枚举登记
枚举式单例
不仅能解决多线程同步问题,而且能防止反序列化重新创建新的对象和反射破坏单例。因为是java底层对枚举进行单独的处理。而枚举出现的也对较晚jdk1.5,所以使用的频率也不是特别高,本质上也属于饿汉式单例,但是很多书籍都推荐的一种写法
package org.about.designpatterns.singleton.register;
/**
* @Package org.about.designpatterns.singleton.register
* @Author
* @Description 注册式: 枚举式单例
* @Version: V1.0
*/
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;
}
}
容器式单例
相当于有一个容器装载所有实例,在实例产生之前先检查下容器有没有,如果有就直接取出来,如果没有就先new一个放进去,然后给后面的人用,SpringIoc容器就是一种注册登记式单例登记式单例实际上维护了一种单例类的实例,将这些实例存放在一个Map中,对于已经登记过的实例,则从Map直接返回,没有登记的,则先登记,然后返回;登记式单例内部实现其实还是用的饿汉式,因为其中的static方法块,它的的单例在类被装载时就被实例化了不加锁的话会存在线程安全问题
package org.about.designpatterns.singleton.register;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Package org.about.designpatterns.singleton.register
* @Author
* @Description 容器式单例
* @Version: V1.0
*/
public class ContainerSingleton {
/**
* 构造方法私有化
*/
private ContainerSingleton() {}
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
public static Object getBean(String className) {
// 防止线程安全问题
// 对象方便管理,其实也属于懒加载
synchronized (ioc) {
if (!ioc.containsKey(className)) {
Object obj = null;
try {
// 这里是简单工厂模式
obj = Class.forName(className).newInstance();
ioc.put(className, obj);
} catch (Exception e) {
e.printStackTrace();
}
// 不存在就创建一个,然后返回
return obj;
}
// 存在就直接返回 object
return ioc.get(className);
}
}
}
以上就是几种比较常见的单例。
最后
感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!