重学Java设计模式-结构型模式-享元模式

Posted Zhangj_9

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重学Java设计模式-结构型模式-享元模式相关的知识,希望对你有一定的参考价值。

重学Java设计模式-结构型模式-享元模式

内容摘自:https://bugstack.cn/md/develop/design-pattern/2020-06-14-重学 Java 设计模式《实战享元模式》.html#重学-java-设计模式-实战享元模式「基于redis秒杀-提供活动与库存信息查询场景」

享元模式介绍

享元模式,主要在于共享通用对象,减少内存的使用,提升系统的访问效率。而这部分共享对象通常比较耗费内存或者需要查询大量接口或者使用数据库资源,因此统一抽离作为共享对象使用。

另外享元模式可以分为在服务端和客户端,一般互联网H5和Web场景下大部分数据都需要服务端进行处理,比如数据库连接池的使用、多线程线程池的使用,除了这些功能外,还有些需要服务端进行包装后的处理下发给客户端,因为服务端需要做享元处理。但在一些游戏场景下,很多都是客户端需要进行渲染地图效果,比如;树木、花草、鱼虫,通过设置不同元素描述使用享元公用对象,减少内存的占用,让客户端的游戏更加流畅。

在享元模型的实现中需要使用到享元工厂来进行管理这部分独立的对象和共享的对象,避免出现线程安全的问题。

案例场景模拟

在这个案例中我们模拟在商品秒杀场景下使用享元模式查询优化

你是否经历过一个商品下单的项目从最初的日均十几单到一个月后每个时段秒杀量破十万的项目。一般在最初如果没有经验的情况下可能会使用数据库行级锁的方式下保证商品库存的扣减操作,但是随着业务的快速发展秒杀的用户越来越多,这个时候数据库已经扛不住了,一般都会使用redis的分布式锁来控制商品库存。

同时在查询的时候也不需要每一次对不同的活动查询都从库中获取,因为这里除了库存以外其他的活动商品信息都是固定不变的,以此这里一般大家会缓存到内存中。

这里我们模拟使用享元模式工厂结构,提供活动商品的查询。活动商品相当于不变的信息,而库存部分属于变化的信息。

享元模式重构代码

接下来使用享元模式来进行代码优化,也算是一次很小的重构。

享元模式一般情况下使用此结构在平时的开发中并不太多,除了一些线程池、数据库连接池外,再就是游戏场景下的场景渲染。另外这个设计的模式思想是减少内存的使用提升效率,与我们之前使用的原型模式通过克隆对象的方式生成复杂对象,减少rpc的调用,都是此类思想。

1. 工程结构

itstack-demo-design-11-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── util
    │           │	└── RedisUtils.java	
    │           ├── Activity.java
    │           ├── ActivityController.java
    │           ├── ActivityFactory.java
    │           └── Stock.java
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

享元模式模型结构

  • 以上是我们模拟查询活动场景的类图结构,左侧构建的是享元工厂,提供固定活动数据的查询,右侧是Redis存放的库存数据。
  • 最终交给活动控制类来处理查询操作,并提供活动的所有信息和库存。因为库存是变化的,所以我们模拟的RedisUtils中设置了定时任务使用库存。

2. 代码实现

2.1 活动信息

/**
 * 博客:https://bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!
 * 公众号:bugstack虫洞栈
 * Create by 小傅哥(fustack) @2020
 */
public class Activity 

    private Long id;        // 活动ID
    private String name;    // 活动名称
    private String desc;    // 活动描述
    private Date startTime; // 开始时间
    private Date stopTime;  // 结束时间
    private Stock stock;    // 活动库存
    
    // ...get/set

  • 这里的对象类比较简单,只是一个活动的基础信息;id、名称、描述、时间和库存。

2.2 库存信息

public class Stock 

    private int total; // 库存总量
    private int used;  // 库存已用
    
    // ...get/set

  • 这里是库存数据我们单独提供了一个类进行保存数据。

2.3 享元工厂

/**
 * 博客:https://bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!
 * 公众号:bugstack虫洞栈
 * Create by 小傅哥(fustack) @2020
 */
public class ActivityFactory 

    static Map<Long, Activity> activityMap = new HashMap<Long, Activity>();

    public static Activity getActivity(Long id) 
        Activity activity = activityMap.get(id);
        if (null == activity) 
            // 模拟从实际业务应用从接口中获取活动信息
            activity = new Activity();
            activity.setId(10001L);
            activity.setName("图书嗨乐");
            activity.setDesc("图书优惠券分享激励分享活动第二期");
            activity.setStartTime(new Date());
            activity.setStopTime(new Date());
            activityMap.put(id, activity);
        
        return activity;
    


  • 这里提供的是一个享元工厂🏭,通过map结构存放已经从库表或者接口中查询到的数据,存放到内存中,用于下次可以直接获取。
  • 这样的结构一般在我们的编程开发中还是比较常见的,当然也有些时候为了分布式的获取,会把数据存放到redis中,可以按需选择。

2.4 模拟Redis类

/**
 * 博客:https://bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!
 * 公众号:bugstack虫洞栈
 * Create by 小傅哥(fustack) @2020
 */
public class RedisUtils 

    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

    private AtomicInteger stock = new AtomicInteger(0);

    public RedisUtils() 
        scheduledExecutorService.scheduleAtFixedRate(() -> 
            // 模拟库存消耗
            stock.addAndGet(1);
        , 0, 100000, TimeUnit.MICROSECONDS);

    

    public int getStockUsed() 
        return stock.get();
    


  • 这里处理模拟redis的操作工具类外,还提供了一个定时任务用于模拟库存的使用,这样方面我们在测试的时候可以观察到库存的变化。

2.4 活动控制类

/**
 * 博客:https://bugstack.cn - 沉淀、分享、成长,让自己和他人都能有所收获!
 * 公众号:bugstack虫洞栈
 * Create by 小傅哥(fustack) @2020
 */
public class ActivityController 

    private RedisUtils redisUtils = new RedisUtils();

    public Activity queryActivityInfo(Long id) 
        Activity activity = ActivityFactory.getActivity(id);
        // 模拟从Redis中获取库存变化信息
        Stock stock = new Stock(1000, redisUtils.getStockUsed());
        activity.setStock(stock);
        return activity;
    


  • 在活动控制类中使用了享元工厂获取活动信息,查询后将库存信息在补充上。因为库存信息是变化的,而活动信息是固定不变的。
  • 最终通过统一的控制类就可以把完整包装后的活动信息返回给调用方。

3. 测试验证

3.1 编写测试类

public class ApiTest 

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    private ActivityController activityController = new ActivityController();

    @Test
    public void test_queryActivityInfo() throws InterruptedException 
        for (int idx = 0; idx < 10; idx++) 
            Long req = 10001L;
            Activity activity = activityController.queryActivityInfo(req);
            logger.info("测试结果: ", req, JSON.toJSONString(activity));
            Thread.sleep(1200);
        
    


  • 这里我们通过活动查询控制类,在for循环的操作下查询了十次活动信息,同时为了保证库存定时任务的变化,加了睡眠操作,实际的开发中不会有这样的睡眠。

3.2 测试结果

22:35:20.285 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":1,"stopTime":1592130919931
22:35:21.634 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":18,"stopTime":1592130919931
22:35:22.838 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":30,"stopTime":1592130919931
22:35:24.042 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":42,"stopTime":1592130919931
22:35:25.246 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":54,"stopTime":1592130919931
22:35:26.452 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":66,"stopTime":1592130919931
22:35:27.655 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":78,"stopTime":1592130919931
22:35:28.859 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":90,"stopTime":1592130919931
22:35:30.063 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":102,"stopTime":1592130919931
22:35:31.268 [main] INFO  org.i..t.ApiTest - 测试结果:10001 "desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1592130919931,"stock":"total":1000,"used":114,"stopTime":1592130919931

Process finished with exit code 0
  • 可以仔细看下stock部分的库存是一直在变化的,其他部分是活动信息,是固定的,所以我们使用享元模式来将这样的结构进行拆分。

重学设计模式(三设计模式-享元模式)

1、享元模式

    首先我们思考一个问题?一个属性被赋值后不能被修改就称这个属性不可变,一个对象在被创建后,它的状态不可被改变就称这个对象不可变。

    为什么java的设计者将String对象设置为不可变类?

    String类在java中被大量的运用,String被设计成不可变的主要目的还是为了安全和高效

    其安全体现在:String类被设置为final类,不能被继承,也其内部结构是稳定的;String对象的值value是个 private final char value[]被final修饰的私有数组,没有暴露任何设置此value的方法。不会被调用者不经意间的修改而影响到其他人。

    其高效体现在:字符串在常量池的共享可以节省空间,提升效率,如果复制一个字符串变量,原始字符串与复制字符串共享相同的字符,不会为其开辟新的内存空间。

    在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象的问题,如果能通过共享已经存在的对象来大幅度减少需要创建的对象数量,从而减少开销高系统资源的利用率,从而降低系统的压力是非常有用的。我们的享元模式就是用来做这个事情的。

1.1、什么是享元模式

  • 定义

    享元模式:“享元”,顾名思义就是被共享的单元,是一种结构型设计模式。以共享的方式高效的支持大量细粒度的对象的重用。

    享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。

    内部状态:可以共享,不会随环境变化而改变。

    外部状态:不可以共享,会随环境变化而改变。

    比如,连接池中的连接对象,保存在连接对象中的用户名、密码、URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态,而不需要把这些数据保留在每个对象中。而当每个连接要被回收利用时,我们需要将它标记为不可用状态,这些为外部状态。

    享元模式的意图在于:通过共享来缓存对象,降低内存消耗,前提是享元的对象是不可变对象。

    享元模式的结构:

    抽象享元(Flyweight)角色:是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。

    非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。

    具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。

    享元工厂(Flyweight Factory)角色:负责创建并管理享元对象,享元池一般设计成键值对形式。

1.2、享元模式的优缺点

  • 优点

通过共享来缓存对象,减少了系统中细粒度对象给内存带来的压力。

  • 缺点

需要将一些不能共享的状态外部化,这将增加程序的复杂性,以时间换空间。

1.3、创建方式

我们以一片森林为例,森林中的树木分布在各个方位,

1)创建享元类

//享元类
public abstract class TreeFlyWeight 

	String name; //树的名字可以共享,可能一大片都是同种树
    Color color; //树的颜色可以共享,大多数的树叶都是绿色
    
    abstract void draw(Graphics g,UnsharableFlyweight zb); //描绘树

	public TreeFlyWeight(String name, Color color) 
		super();
		this.name = name;
		this.color = color;
	

2)非享元类

//非享元类
public class UnsharableFlyweight 
	private int x,y; //外部状态坐标,树的坐标是不同的

	public UnsharableFlyweight(int x, int y) 
		super();
		this.x = x;
		this.y = y;
	

	public int getX() 
		return x;
	

	public void setX(int x) 
		this.x = x;
	

	public int getY() 
		return y;
	

	public void setY(int y) 
		this.y = y;
	
	

3)具体享元

public class ConcreteTree extends TreeFlyWeight

	public ConcreteTree(String name, Color color) 
		super(name, color);
	

	@Override
	void draw(Graphics g, UnsharableFlyweight zb) 
		g.setColor(Color.BLACK); //树干的颜色
		g.fillRect(zb.getX() - 1, zb.getY(), 3, 5); //(矩形填充)树干是黑色
        g.setColor(color);  //设置树叶的颜色
        g.fillOval(zb.getX() - 5, zb.getY() - 10, 10, 10); //绘制树叶的形状(多边形),
	
	

4)享元工厂

//享元工厂
public class TreeFlyWeightFactory 
	// 享元池
	static Map<String, TreeFlyWeight> treeTypes = new HashMap<String, TreeFlyWeight>();

	public static TreeFlyWeight getTreeType(String name, Color color) 
		TreeFlyWeight result = treeTypes.get(name);
		if (result == null) 
			result = new ConcreteTree(name, color);
			treeTypes.put(name, result);
		
		return result;
	

5)客户端

public class Client extends JFrame
	
	public static void main(String[] args) 
		testDemo1(); //同一种树的名称和颜色共享
		
		//创建一片森林,1000棵树(包含两种类别的树)
		new Client();
		
	
	
	public Client()
		this.setSize(500,500); //森林画布大小
		this.setVisible(true);
	
	
	static void testDemo1()
		TreeFlyWeight treeTypes1 = TreeFlyWeightFactory.getTreeType("樟树", Color.GREEN);
		TreeFlyWeight treeTypes2 = TreeFlyWeightFactory.getTreeType("樟树", Color.GREEN);
		System.out.println(treeTypes1==treeTypes2);//true;
	
	
	@Override
    public void paint(Graphics graphics) 
		for (int i = 0; i < 500; i++)  //每种树500棵
			TreeFlyWeight treeTypes1 = TreeFlyWeightFactory.getTreeType("樟树", Color.GREEN); //这种在for循环中并不会大量创建
			TreeFlyWeight treeTypes2 = TreeFlyWeightFactory.getTreeType("枫树", Color.YELLOW);
			treeTypes1.draw(graphics, new UnsharableFlyweight(random(0,500),random(0,500)));
			treeTypes2.draw(graphics, new UnsharableFlyweight(random(0,500),random(0,500)));
		
    
	
	private static int random(int min, int max) 
        return min + (int) (Math.random() * ((max - min) + 1));
    
  • 案例效果

1.4、总结及建议

    享元模式可以使你共享地访问那些大量出现的细粒度的对象,有人会觉得这不是和单例模式很像吗?只不过是享元模式有多个对象共享。但是他们的设计意图两个不同的出发点,享元模式是为了对象复用,节省内存;而单例模式则是为了限制对象的个数,享元对象不可变,而单例对象是可变的。

应用场景:

    1)应用程序需要产生大量类似的对象,包含可以在多个对象之间提取和共享的重复状态时可以使用享元模式。

JDK中享元模式的应用:

    java.lang.String

    java.lang.Integer#valueOf(int)

以上是关于重学Java设计模式-结构型模式-享元模式的主要内容,如果未能解决你的问题,请参考以下文章

从零开始学习Java设计模式 | 结构型模式篇:享元模式

从零开始学习Java设计模式 | 结构型模式篇:享元模式

Java进阶篇设计模式之七 ----- 享元模式和代理模式

Java设计模式-享元模式

常用设计模式系列之——享元模式

JAVA SCRIPT设计模式--结构型--设计模式之FlyWeight享元模式(11)