在设计项目的过程中,我们经常会遇到创建大量相同或相似对象实例的问题。大量创建对象会造成系统资源的浪费,最终造成系统运行效率低下。如果能将对象中相同的部分提取出来并共享,将大大减小系统资源。我们可以使用享元模式来解决这个问题。
享元模式
享元模式(Flyweight):享元即Flyweight。Flyweight是轻量级的意思。享元模式指当需要某个实例时,不总通过系统关键字new进行实例化,而是尽量使用已经存在的实例,实现实例的共享。享元模式主要共享通用对象,这个通用对象通常是不可改变的或对内存资源占用较高以及需要大量使用数据库资源的对象,因此可进行统一抽离封装作为共享对象使用。
享元模式的结构
- 抽象享元(Flyweight):抽象享元是所有具体享元的父类,为具体享元提供公共接口。
- 具体享元(ConcreteFlyweight):实现抽象享元类中的接口。
- 享元工厂(FlyweightFactory):负责创建和管理享元,在工厂中生成的享元可以实现实例的共享。
注:在GoF的书中,还有UnsharedConcreteFlyweight角色,即表示非享元对象。非享元即那些不需要进行缓存的对象,在本例中没有出现。
享元模式的实际应用场景
- Java中的包装类型(例如:Integer、Byte等):在Java标准库中,包装类型例如Integer和Byte都是不变类,无需反复创建。所以在多次调用如Integer.valueOf()这样的静态工厂方法时,会自动使用Integer共享的实例。
- 项目中需要高并发、大量数据库操作的地方:例如秒杀场景,一个商品秒杀时往往会存在大量并发。在这样的情况下,如果反复创建数据库连接和数据库行级锁会耗费大量系统资源。这时应该使用缓存,通常会使用redis的分布式锁来控制,使用享元模式将不变的信息进行缓存,节省系统资源。
示例
在前后端分离的用户登录中,我们通常会缓存用户信息,以便于前端请求时直接从缓存读取并返回。本例我们来使用过享元模式实现一个对用户信息的缓存。
在实际项目中,我们会将信息存储在Redis中,再进行读取返回。本例为了演示方便,具体Redis的逻辑没有体现,仅讲解原理,大家可自行扩展。
首先我们定义一个用户的享元类,即实体类。因为这里用户信息不会经常变动,我们将其进行缓存。
User.java
package com.yeliheng.flyweight;
/**
* 用户享元
*/
public class User {
//用户Id
private Long userId;
//用户名
private String username;
//密码
private String password;
//昵称
private String nickname;
//手机号
private String phone;
//电子邮箱
private String email;
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
用户享元中包含用户id、用户名、密码、昵称、手机号、电子邮箱信息。
注1:因为本例只存在一个享元类,故没有使用到抽象享元的角色。
接着,我们创建一个享元工厂。这里采用静态工厂模式实现。
UserFlyweightFactory.java
package com.yeliheng.flyweight;
import java.util.HashMap;
import java.util.Map;
/**
* 用户享元工厂
*/
public class UserFlyweightFactory {
private static final Map<String, User> cache = new HashMap<>();
public static void getUser(long userId) {
String key = String.valueOf(userId);
User user = cache.get(key);
if(user == null) {
System.out.println("找不到缓存的用户数据,自动生成用户信息!");
user = new User();
user.setUserId(userId);
user.setUsername("yeliheng");
user.setPassword("12356");
user.setNickname("Yeliheng");
user.setEmail("yeliheng00@163.com");
cache.put(key,user);
} else {
System.out.println("缓存中存在用户数据:" + "用户名 " + user.getUsername());
}
}
}
在用户享元工厂中,我们使用一个Map对象来模拟用户信息在Redis中的存取。
我们定义一个静态方法getUser(),getUser()方法接收一个userId来获取缓存的对象。接着我们进行判断,如果缓存中存在用户数据,则直接使用缓存的数据。否则,系统将创建一个用户对象,并写入到缓存中。实际项目中,用户的信息从前端传递过来的Body体中进行获取。
最后,我们来调用两次静态工厂获取用户信息,看看效果。
Main.java
package com.yeliheng.flyweight;
public class Main {
public static void main(String[] args) {
UserFlyweightFactory.getUser(1L); //第一次调用
UserFlyweightFactory.getUser(1L); //第二次调用
}
}
最终运行结果如下:
可以看到,第一次调用,缓存中并没有我们想要的信息,系统自动创建了对象,并将对象写入缓存,供第二次调用使用。
享元模式的优缺点
优点
- 缓存可以重复使用的对象,节省系统的内存开销。
缺点
- 为了共享对象,需要将某些对象导出到外部,即让外部能够调用。过度使用可能会适得其反,导致开销增加。
总结
当一个对象不会变化或不需要经常变化,就可以考虑享元模式来缓存对象,节省系统资源。
本文通过举例用户数据缓存来详细讲解了享元模式的应用场景。
本文示例的完整代码参见: Github