单例模式
# 单例模式
# 一、单例模式
原理:单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。
优缺点
- 1 优点
- 唯一实例:确保一个类只有一个实例,节省了系统资源,尤其是在需要频繁创建和销毁实例的情况下。
- 全局访问:通过全局访问点提供对唯一实例的访问,简化了对共享资源的管理。
- 延迟实例化:可以实现延迟实例化,即在首次使用时才创建实例,优化了系统资源的使用。
- 灵活性:可以通过继承和多态性来扩展单例模式,使得单例类具有更大的灵活性。
- 线程安全:通过适当的实现方法(如双重检查锁定、静态内部类)可以确保单例模式在多线程环境下的安全性。
- 2 缺点
- 全局状态:单例模式引入了全局状态,可能导致系统的难以测试和调试,增加了系统的复杂性。
- 并发问题:在多线程环境下,如果实现不当,可能会导致线程安全问题。
- 隐藏依赖关系:单例模式使得类之间的依赖关系不明显,可能导致代码的可读性和可维护性下降。
- 难以扩展:单例模式的类很难被子类化,因为构造函数是私有的,限制了扩展性。
- 生命周期不明确:单例实例的生命周期与应用程序一致,可能导致资源无法及时释放,从而引起内存泄漏等问题。
适用场景
- 配置管理:系统的配置文件读取和管理通常需要全局唯一的实例。
- 日志管理:全局共享的日志管理器确保所有模块都能记录日志。
- 连接池:数据库连接池、线程池等资源池需要全局唯一实例进行管理。
- 缓存:系统缓存通常需要全局唯一实例进行管理,以确保数据的一致性。
实现方式
- 饿汉式:类加载时就创建实例,简单但在某些情况下会浪费资源。
- 懒汉式:首次使用时才创建实例,但需要注意线程安全问题。
- 双重检查锁定:懒汉式的改进版本,通过双重检查和同步块保证线程安全。
- 静态内部类:利用类加载机制,既实现了延迟加载又保证了线程安全。
- 枚举:使用枚举来实现单例,天生保证线程安全和防止反序列化破坏单例。
# 二、单例模式实现代码
懒汉模式(线程不安全)
public class Singleton_01 { private static Singleton_01 instance; private Singleton_01() { } public static Singleton_01 getInstance(){ if (null != instance) return instance; instance = new Singleton_01(); return instance; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14懒汉模式(线程安全)
public class Singleton_02 { private static Singleton_02 instance; private Singleton_02() { } public static synchronized Singleton_02 getInstance(){ if (null != instance) return instance; instance = new Singleton_02(); return instance; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15- 该方法会导致性能开销。在高并发环境下,每次获取实例时都需要进行同步,可能会显著降低系统性能。
饿汉模式(线程安全)
public class Singleton_03 { private static Singleton_03 instance = new Singleton_03(); private Singleton_03() { } public static Singleton_03 getInstance() { return instance; } }
1
2
3
4
5
6
7
8
9
10
11
12使用内部类(线程安全)
public class Singleton_04 { private static class SingletonHolder { private static Singleton_04 instance = new Singleton_04(); } private Singleton_04() { } public static Singleton_04 getInstance() { return SingletonHolder.instance; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14- 实例在首次调用
getInstance
方法时才会创建。这是通过静态内部类实现的,因为静态内部类不会在其外部类加载时就加载,只有在被引用时才会加载和初始化。
- 实例在首次调用
双重锁校验(线程安全)
public class Singleton_05 { private static volatile Singleton_05 instance; private Singleton_05() { } public static Singleton_05 getInstance(){ if(null != instance) return instance; synchronized (Singleton_05.class){ if (null == instance){ instance = new Singleton_05(); } } return instance; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18- 为什么要两次判断
instance
:假如instance没有被实例化,两个进程通过第一个判断,由于”锁“机制,只能由一个线程进入,另一个才能进入。如果没有第二层判断,则第一个线程创建实例后,第二个线程仍然能创建线程,不符合单例。
- 为什么要两次判断
CAS「AtomicReference」(线程安全)
public class Singleton_06 {
private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>();
private Singleton_06() {
}
public static final Singleton_06 getInstance() {
for (; ; ) {
Singleton_06 instance = INSTANCE.get();
if (null != instance) return instance;
INSTANCE.compareAndSet(null, new Singleton_06());
return INSTANCE.get();
}
}
public static void main(String[] args) {
System.out.println(Singleton_06.getInstance());
System.out.println(Singleton_06.getInstance());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- 使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
- CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。
- 这种使用
AtomicReference
的方式是一种线程安全且延迟加载的单例实现,适用于希望避免显式同步并利用 CAS 提高并发性能的场景。但由于实现相对复杂,在实际应用中需要权衡使用场景和代码可维护性。对于大多数情况,使用静态内部类或双重检查锁定(DCL)会是更简单和直观的选择。
枚举单例(线程安全)
public enum Singleton { INSTANCE; public void someMethod() { // 单例类中的方法 } }
1
2
3
4
5
6
7public class Main { public static void main(String[] args) { Singleton singleton = Singleton.INSTANCE; singleton.someMethod(); } }
1
2
3
4
5
6实现分析:
线程安全:枚举类型的线程安全由 Java 虚拟机保证,JVM 确保枚举类型在使用时会被正确地加载和初始化。
防止反射攻击:反射攻击是通过反射机制绕过访问控制进行对象创建或方法调用。枚举类型在 Java 中是一个特殊的类,它的构造方法在字节码层面是
private
的,且在任何尝试通过反射调用时会抛出java.lang.NoSuchMethodException
或java.lang.IllegalArgumentException
,防止了通过反射创建实例。序列化:在 Java 的序列化机制中,枚举类型有着与普通类不同的序列化和反序列化逻辑。在反序列化过程中,JVM 会通过
Enum.valueOf
方法查找已存在的枚举实例,而不会新建对象,这样就防止了反序列化破坏单例。
# 三、优化后
- 工厂结构
itstack-demo-design-1-02
└── src
├── main
│ └── java
│ └── org.itstack.demo.design
│ ├── store
│ │ ├── impl
│ │ │ ├── CardCommodityService.java
│ │ │ ├── CouponCommodityService.java
│ │ │ └── GoodsCommodityService.java
│ │ └── ICommodity.java
│ └── StoreFactory.java
└── test
└── java
└── org.itstack.demo.design.test
└── ApiTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UML图
发奖接口
public interface ICommodity {
void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception;
}
1
2
3
4
5
2
3
4
5
实现奖品发放接口(发奖工厂)
优惠劵
public class CouponCommodityService implements ICommodity {
private Logger logger = LoggerFactory.getLogger(CouponCommodityService.class);
private CouponService couponService = new CouponService();
public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
CouponResult couponResult = couponService.sendCoupon(uId, commodityId, bizId);
logger.info("请求参数[优惠券] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
logger.info("测试结果[优惠券]:{}", JSON.toJSON(couponResult));
if (!"0000".equals(couponResult.getCode())) throw new RuntimeException(couponResult.getInfo());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
事物商品
public class GoodsCommodityService implements ICommodity {
private Logger logger = LoggerFactory.getLogger(GoodsCommodityService.class);
private GoodsService goodsService = new GoodsService();
public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
DeliverReq deliverReq = new DeliverReq();
deliverReq.setUserName(queryUserName(uId));
deliverReq.setUserPhone(queryUserPhoneNumber(uId));
deliverReq.setSku(commodityId);
deliverReq.setOrderId(bizId);
deliverReq.setConsigneeUserName(extMap.get("consigneeUserName"));
deliverReq.setConsigneeUserPhone(extMap.get("consigneeUserPhone"));
deliverReq.setConsigneeUserAddress(extMap.get("consigneeUserAddress"));
Boolean isSuccess = goodsService.deliverGoods(deliverReq);
logger.info("请求参数[优惠券] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
logger.info("测试结果[优惠券]:{}", isSuccess);
if (!isSuccess) throw new RuntimeException("实物商品发放失败");
}
private String queryUserName(String uId) {
return "花花";
}
private String queryUserPhoneNumber(String uId) {
return "15200101232";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
第三方兑换卡
public class CardCommodityService implements ICommodity {
private Logger logger = LoggerFactory.getLogger(CardCommodityService.class);
// 模拟注入
private IQiYiCardService iQiYiCardService = new IQiYiCardService();
public void sendCommodity(String uId, String commodityId, String bizId, Map<String, String> extMap) throws Exception {
String mobile = queryUserMobile(uId);
iQiYiCardService.grantToken(mobile, bizId);
logger.info("请求参数[爱奇艺兑换卡] => uId:{} commodityId:{} bizId:{} extMap:{}", uId, commodityId, bizId, JSON.toJSON(extMap));
logger.info("测试结果[爱奇艺兑换卡]:success");
}
private String queryUserMobile(String uId) {
return "15200101232";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 创建商店工厂
public class StoreFactory {
public ICommodity getCommodityService(Integer commodityType) {
if (null == commodityType) return null;
if (1 == commodityType) return new CouponCommodityService();
if (2 == commodityType) return new GoodsCommodityService();
if (3 == commodityType) return new CardCommodityService();
throw new RuntimeException("不存在的商品服务类型");
}
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 测试验证
@Test
public void test_commodity() throws Exception {
StoreFactory storeFactory = new StoreFactory();
// 1. 优惠券
ICommodity commodityService_1 = storeFactory.getCommodityService(1);
commodityService_1.sendCommodity("10001", "EGM1023938910232121323432", "791098764902132", null);
// 2. 实物商品
ICommodity commodityService_2 = storeFactory.getCommodityService(2);
Map<String,String> extMap = new HashMap<String,String>();
extMap.put("consigneeUserName", "谢飞机");
extMap.put("consigneeUserPhone", "15200292123");
extMap.put("consigneeUserAddress", "吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109");
commodityService_2.sendCommodity("10001","9820198721311","1023000020112221113", extMap);
// 3. 第三方兑换卡(爱奇艺)
ICommodity commodityService_3 = storeFactory.getCommodityService(3);
commodityService_3.sendCommodity("10001","AQY1xjkUodl8LO975GdfrYUio",null,null);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
结果:
模拟发放优惠券一张:10001,EGM1023938910232121323432,791098764902132
22:48:10.922 [main] INFO o.i.d.d.s.i.CouponCommodityService - 请求参数[优惠券] => uId:10001 commodityId:EGM1023938910232121323432 bizId:791098764902132 extMap:null
22:48:10.957 [main] INFO o.i.d.d.s.i.CouponCommodityService - 测试结果[优惠券]:{"code":"0000","info":"发放成功"}
模拟发货实物商品一个:{"consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserName":"谢飞机","consigneeUserPhone":"15200292123","orderId":"1023000020112221113","sku":"9820198721311","userName":"花花","userPhone":"15200101232"}
22:48:10.962 [main] INFO o.i.d.d.s.impl.GoodsCommodityService - 请求参数[优惠券] => uId:10001 commodityId:9820198721311 bizId:1023000020112221113 extMap:{"consigneeUserName":"谢飞机","consigneeUserAddress":"吉林省.长春市.双阳区.XX街道.檀溪苑小区.#18-2109","consigneeUserPhone":"15200292123"}
22:48:10.962 [main] INFO o.i.d.d.s.impl.GoodsCommodityService - 测试结果[优惠券]:true
模拟发放爱奇艺会员卡一张:15200101232,null
22:48:10.963 [main] INFO o.i.d.d.s.impl.CardCommodityService - 请求参数[爱奇艺兑换卡] => uId:10001 commodityId:AQY1xjkUodl8LO975GdfrYUio bizId:null extMap:null
22:48:10.963 [main] INFO o.i.d.d.s.impl.CardCommodityService - 测试结果[爱奇艺兑换卡]:success
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 四、实现方式
- 让所有产品都遵循同一接口。 该接口必须声明对所有产品都有意义的方法。
- 在创建类中添加一个空的工厂方法。 该方法的返回类型必须遵循通用的产品接口。
- 在创建者代码中找到对于产品构造函数的所有引用。 将它们依次替换为对于工厂方法的调用, 同时将创建产品的代码移入工厂方法。
- 现在, 为工厂方法中的每种产品编写一个创建者子类, 然后在子类中重写工厂方法, 并将基本方法中的相关创建代码移动到工厂方法中。
- 如果应用中的产品类型太多, 那么为每个产品创建子类并无太大必要, 这时你也可以在子类中复用基类中的控制参数。
结合案例理解
- 源代码产品是GoodsService、CouponService、IQiYiCardService(可以加个接口Service声明对这些产品都有意义的方法)
- 工厂接口ICommodity,子工厂CouponCommodityService、GoodsCommodityService、CardCommodityService
- 各个子工厂实现对各自产品的操作
- 操作工厂getCommodityService(Integer commodityType),根据commodityType获取你想要的工厂
编辑 (opens new window)