前言
设计模式对于自我提升很有帮助,能够提升自己对于代码的掌控,这都是前辈经过无数验证总结出的经验,都是精华,应该认真学习。在一些开源框架中几乎随处可见,掌握好设计模式,对于我们阅读源码亦有莫大帮助。
从单例模式开始
那么多设计模式中,得益于Spring的影响,最熟悉的要属单例模式了,所以先从单例开始整起来。
定义
一个类只有一个实例,并且该类可以自行创建这个实例的一种模式。
优点
- 减少内存资源
- 保证数据内容一致性
缺点
- 单例模式一般没有接口,扩展困难,如果要扩展需要修改原来的代码,违反开闭原则
- 并发测试,单例不利于代码调试。
应用场景
- 需要频繁创建的一些类,可以考虑使用单例
- 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- 频繁访问数据库或文件的对象。
- 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
如何手写一个单例
一个单例要注意三个要素
1.构造器私有化
2. 自行创建,用静态变量保存
3. 提供获取方法(静态方法)
饿汉式
代码
/**
* 饿汉式
* 优点:效率高,线程安全
* 缺点:占用内存
*/
package com.yang.singleton;
public class Singleton01 {
//自行创建,用静态变量保存
private static Singleton01 instance = new Singleton01();
//构造器私有化
private Singleton01() {
}
//向外提供获取方法
public static Singleton01 getInstance() {
return instance;
}
}
测试代码
package com.yang.singleton;
class Test {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": " + Singleton01.getInstance());
}, "线程1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": " + Singleton01.getInstance());
}, "线程2").start();
}
}
打印结果
线程1: com.yang.singleton.Singleton01@6218fb2e
线程2: com.yang.singleton.Singleton01@6218fb2e
测试结果没问题,类加载的时候就已经初始化创建实例 ,所以不存在访问线程安全问题,但是有一个缺点,如果这个类没有被使用,就会一直占用内存资源,造成内存浪费, 可进一步优化
进一步优化
懒汉式
代码
/**
* 懒汉式
* 优点:调用时才创建实例
* 缺点:线程不安全
*/
package com.yang.singleton;
public class Singleton02 {
private static Singleton02 instance = null;
private Singleton02() {
}
//修改成需要使用时,创建对象,节省内存空间
public static Singleton02 getInstance() {
if (instance == null) {
instance = new Singleton02();
}
return instance;
}
}
打印测试结果
线程1: com.yang.singleton.Singleton02@6dd2a02f
线程2: com.yang.singleton.Singleton02@4920b564
结果创建了两个对象。
思考后认识到,修改为调用方法的时候创建对象,但是这个时候如果两个线程同时调用getInstance()
方法,此时对于两个线程来说instance == null
都为true,两个线程都会创建一个新的对象,就会有线程安全问题。
知道问题所在,进步修改
/**
* 懒汉式
* 优点:线程安全
* 缺点:性能差
*/
package com.yang.singleton;
public class Singleton02 {
private static Singleton02 instance = null;
private Singleton02() {
}
public static synchronized Singleton02 getInstance() {
if (instance == null) {
instance = new Singleton02();
}
return instance;
}
}
打印测试结果
线程1: com.yang.singleton.Singleton02@6dd2a02f
线程2: com.yang.singleton.Singleton02@6dd2a02f
在getInstance()方法上加 synchronized关键字(为方法加锁),好像可以了。但是只有在第一次实例化的时候才有必要加锁保证单例,感觉没有必要每次调用
getInstance()
方法都让它排队等待,安全性保证了,但是影响了程序的性能。
将锁的粒度调整一下
DCL(Double Check Lock)
代码
/**
* DCL(Double Check Lock)
* 优点:线程安全,调用时创建实例
* 缺点:复杂,可读性差
*/
package com.yang.singleton;
public class Singleton02 {
private volatile static Singleton02 instance = null;
private Singleton02() {
}
public static Singleton02 getInstance() {
//判断是否需要阻塞
if (instance == null) {
//线程1,线程2。。。在这里等待。。
synchronized (Singleton02.class) {
//判断是否别的线程已经创建了实例
if (instance == null) {
instance = new Singleton02();
}
}
}
return instance;
}
}
打印测试结果
线程1: com.yang.singleton.Singleton02@6dd2a02f
线程2: com.yang.singleton.Singleton02@6dd2a02f
我们真正想要加锁的其实只是创建实例这一步,在多个线程进入方法时,先判断
instance == null
,如果为true
将接下来的动作加锁,否则直接返回,极大的提高了效率,第二个instance == null
判断是否其它线程已经创建了实例,有则直接返回。
- 第一个 instance == null判断是否需要阻塞
- 第二个 instance == null判断是否别的线程已经创建了实例
细心的人会发现不仅仅是加了两层判空,在变量上还加了volatile关键字
volatile作用:
- 保证变量可见性
- 禁止指令重排序
有锁就会带来性能问题
有没有不加锁的方法?
重新整理一下思路。
以饿汉式为始,后面衍生出来的一系列问题都是由于想要将创建实例的动作滞后,达到节省内存的目的,那么有没有别的方法可以让我们在调用的时候创建。
可以利用静态内部类
调用的时候加载
静态内部类
代码
/**
* 静态内部类
* 优点:线程安全,调用时创建实例
*/
package com.yang.singleton;
public class Singleton03 {
private Singleton03() {
}
public static Singleton03 getInstance() {
return InnerSingleton.INSTANCE;
}
private static class InnerSingleton {
private static final Singleton03 INSTANCE = new Singleton03();
}
}
打印测试结果
线程2: com.yang.singleton.User@4920b564
线程1: com.yang.singleton.User@4920b564
至此,单例可以告一段落
下面补充几种网上搜集来的单例写法
枚举
/**
* 枚举
* 优点:线程安全,防止反序列化
*/
package com.yang.singleton;
public class User {
//私有化构造函数
private User() {
}
//定义一个静态枚举类
static enum SingletonEnum {
//创建一个枚举对象,该对象天生为单例
INSTANCE;
private User user;
//私有化枚举的构造函数
private SingletonEnum() {
user = new User();
}
public User getInstnce() {
return user;
}
}
//对外暴露一个获取User对象的静态方法
public static User getInstance() {
return SingletonEnum.INSTANCE.getInstnce();
}
}
容器式单例
代码
/**
* 容器式单例
* 实现思路:用一个Map保存对象,当需要使用的时候去map里拿,如果有,则直接取出来,没有则利用返回创建一个,放到map里,然后再返回出去。(相当于一个简单的Spring管理容器)
*/
package com.yang.singleton;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ContainerSingleton {
private ContainerSingleton() {
}
private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}
测试用例
package com.yang.singleton;
class Test {
public static void main(String[] args) {
Object instance1 = ContainerSingleton.getInstance("com.yang.singleton.TestPojo");
Object instance2 = ContainerSingleton.getInstance("com.yang.singleton.TestPojo");
System.out.println("instance1: " + instance1);
System.out.println("instance2: " + instance2);
}
}
打印测试结果
instance1: com.yang.singleton.TestPojo@61bbe9ba
instance2: com.yang.singleton.TestPojo@61bbe9ba
但是上述示例存在线程安全问题
看看在多线程环境下测试结果
package com.yang.singleton;
class Test {
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": " + ContainerSingleton.getInstance("com.yang.singleton.TestPojo"));
}, "线程1").start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + ": " + ContainerSingleton.getInstance("com.yang.singleton.TestPojo"));
}, "线程2").start();
}
}
打印测试结果
线程1: com.yang.singleton.TestPojo@6dd2a02f
线程2: com.yang.singleton.TestPojo@4920b564
简单修改一下
package com.yang.singleton;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ContainerSingleton {
private ContainerSingleton() {
}
private volatile static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
public static Object getInstance(String className) {
Object instance = null;
//如果容器里没有,判断是否需要加锁
if (!ioc.containsKey(className)) {
synchronized (ContainerSingleton.class) {
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
} else {
return ioc.get(className);
}
}
}
打印测试结果
线程1: com.yang.singleton.TestPojo@43d83830
线程2: com.yang.singleton.TestPojo@43d83830
CAS单例
/**
* CAS单例
* 思路:用AtomicReference包装保证原子性,然后利用无锁机制,循环去判断是否有现成的对象,有就返回,没有就创建,但后替INSTANCE
*/
package com.yang.singleton;
import java.util.concurrent.atomic.AtomicReference;
public class CASSingleton {
private static final AtomicReference<CASSingleton> INSTANCE = new AtomicReference<CASSingleton>();
private CASSingleton() {
}
public static CASSingleton getInstance() {
for (; ; ) {
CASSingleton singleton = INSTANCE.get();
if (singleton != null) {
return singleton;
}
singleton = new CASSingleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
扩展 volatile 作用
这不是本篇文章的重点
简单解释一下,想要详细了解可自行查阅相关文档。
可见性:就如字面意思,当一个线程改变了变量的值,其它持有该变量的线程能够立即感知到,避免了脏读的现象。
指令重排:Java代码最终会被转化为字节码,JVM通过解释字节码将其翻译成一条条对应的机器指令。
创建对象的过程大致分为以下几步:
- 申请分配内存空间
- 初始化对象
- 设置对象(instance )指向刚刚分配的内存地址
正常执行顺序应该是1–>2–>3,但是当2初始化对象的时间过长时,编译器会交换2和3的顺序,导致执行最终执行的顺序为1–>3–>2,当执行到3的时候另外的线程判断instance == null
不成立(此时instance
不为空,已经指向内存地址),直接返回没有初始化的instance
,造成NPE
加上volatile可以禁止2和3交换顺序,按顺序执行,避免NPE,最终得到了一个线程安全,省内存的单例。
如果这篇文章对您有帮助,非常欢迎您点赞支持!这不仅是对我工作的认可,也能鼓励我继续创作更多有价值的内容。感谢您的支持和反馈,希望未来还能为您提供更多有益的信息和灵感。