1 享元模式介绍
享元模式是一种结构型设计模式,是一种对象池技术,它将多个小对象存储在一起,以节省内存。
享元模式的主要思想是共享可共享的对象,避免创建大量重复的对象,从而减少内存的使用。通过维护一个对象池,可以快速获取对象,也可以在不需要对象时及时释放对象。
📌 场景
-
对象池:如果需要频繁地创建和销毁大量对象,可以使用对象池来复用已经存在的对象,避免重复创建和销毁的性能损耗。
-
字符串池:比如 Java 中的字符串 String,其中同样字符的 String 指向同一个对象,不用字符的 String 指向不同的对象,字符拼接后相等字符的 String 也指向相同的对象。
-
资源共享:对于一些需要频繁读取的资源,可以使用享元模式来共享资源,提高性能效率。
-
连接池:数据库连接、线程池等类型的连接池都可以利用享元模式进行连接的重复使用。
📌 优缺点
- 优点:减少内存使用量、提高程序性能和效率
- 缺点:增加系统复杂性、难以维护和调试。
📌 与单例模式区别
享元工厂的实现方式与单例模式类似,有以下不同:
-
享元是对象级别的:在多个使用到这个对象的地方都只需要使用这一个对象即可满足要求。
-
单例是类级别的:这个类必须只能实例化出一个对象。
- 可以这么说:单例是享元的一种特例。单例可以看做是享元的实现方式中的一种,只不过比享元更加严格的控制了对象的唯一性。
2 享元模式实现
2.1 代码实现
以下围棋为例:
📌 1.定义棋子(抽象享元角色)
/**
* 棋子
*/
public interface Piece {
/**
* 落子
*/
public void fall();
}
📌 2.定义具体棋子(具体享元角色)
/**
* 具体棋子
*/
public class PieceImpl implements Piece{
/**
* 棋子
*/
private String color;
/**
* 构造棋子
* @param color 棋子颜色
*/
public PieceImpl(String color) {
this.color = color;
}
@Override
public void fall() {
System.out.println(this.color);
}
}
📌 3.定义棋子工厂(享元工厂)
/**
* 棋子工厂
*/
public enum PieceFactory {
/**
* 这里将前面介绍的单例模式应用起来<br>
* 单例模式的最佳实现是使用枚举类型。<br>
* 只需要编写一个包含单个元素的枚举类型即可<br>
* 简洁,且无偿提供序列化,并由 JVM 从根本上提供线程安全的保障,绝对防止多次实例化,且能够抵御反射和序列化的攻击。
*/
INSTANCE;
/**
* 棋盒
*/
private Map<String,Piece> pieceBox = new HashMap<>();
/**
* 获取棋子
* @param color 棋子颜色
* @return 棋子
*/
public Piece getPiece(String color) {
// 先从棋盒获取棋子
Piece piece = this.pieceBox.get(color);
// 如果棋盒里没有棋子
if (piece == null) {
// 创建一颗棋子
piece = new PieceImpl(color);
// 放入棋盒
this.pieceBox.put(color, piece);
}
// 得到棋子
return piece;
}
}
📌 4.定义棋子工厂(享元工厂)
// 获取棋子1
Piece piece1 = PieceFactory.INSTANCE.getPiece("白子");
// 获取棋子2
Piece piece2 = PieceFactory.INSTANCE.getPiece("白子");
// 获取棋子3
Piece piece3 = PieceFactory.INSTANCE.getPiece("黑子");
// 落子
piece1.fall();
piece2.fall();
piece3.fall();
// 比较两颗白子是否为同一对象
System.out.println(piece1 == piece2);
// 比较白子和黑子是否为同一对象
System.out.println(piece1 == piece3);
控制台输出:
白子
白子
黑子
true
false
可以发现,两颗黑子为同一对象,黑子与白子为不同对象,这样不管后面定义多少黑子与白子,都可以公用现存的两个对象,极大程度的节省了内存,这就是享元模式的优势。
2.2 线程安全问题
同单例模式一样,享元模式也存在线程安全的问题:
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Piece piece1 = PieceFactory.INSTANCE.getPiece("黑子");
Piece piece2 = PieceFactory.INSTANCE.getPiece("黑子");
System.out.println(piece1 == piece2);
}).start();
}
}
}
控制台输出:
可以看到,里面部分实例对象是不相等的,因此需要给工厂里的 getxxx 方法加锁,直接使用 synchronized 关键字即可:
public synchronized Piece getPiece(String color) {
}
控制台输出:
当然你还可以使用其他方法来保证线程安全