【设计模式(11)】结构型模式之享元模式

个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充


前言

对于后端开发者而言,池化技术相当常见,比如线程池、数据库连接池、缓冲池,以及最最常见的Bean

对于Bean池,在系统初始化的时候初始化装载Bean对象,系统中需要使用的时候直接调用Bean池中的对象即可,而无需每次都去初始化一遍,以节省资源消耗


针对String的性能优化,Java引用了缓存池的概念,即创建String类型数据的时候,会检查缓存池中是否有相同内容的String类型对象,如果有会被直接引用,没有则会创建一个存入缓存池,从而避免重复创建String对象的无谓资源消耗

Integer型数据默认会有-128~127的缓存池,以规避这些高频率出现的数据的重复创建过程,节省资源


享元(Flyweight)模式运用共享技术,通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。


1.介绍

使用目的:运用共享技术来复用需要重复使用的资源

使用时机:系统中需要使用大量对象,且系统不依赖这些对象的身份,且这些对象可以分组,每组都可以用一个对象来代替

解决问题:系统中存在大量相同的对象,造成无谓的资源消耗,甚至造成内存溢出

实现方法:用 HashMap 存储这些对象,并使用唯一标识标记,对于不存在的对象创建并存入,已存在的则直接取出使用

应用实例:

  • Java中的String缓存
  • 池化技术,如线程池、连接池、bean池等

优点:减少了系统的资源消耗,降低了系统压力,提高执行效率

缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。

注意事项

  • 注意划分外部状态和内部状态,否则容易引起线程安全问题
  • 这些对象最好由工厂控制

2.结构

享元模式的主要角色有如下。

  • 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口。
  • 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
  • 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

image-20201027145103678

  • 客户端(client)调用享元工厂(Flyweight Factory)角色中的方法
  • 享元工厂通过HashMap结构持有具体享元(Concrete Flyweight)角色对象
  • 具体享元角色则需要实现接口Flyweight

此图中未包括复杂数据的存取,请按照实际需求调整,实际上的数据肯定更加复杂


3.实现

  1. 定义抽象享元角色(Flyweight)

    interface Flyweight {
        void operation();
    }
    
  2. 定义具体享元(Concrete Flyweight)角色,实现接口Flyweight

    class ConcreteFlyweight implements Flyweight {
        private String key;
    
        ConcreteFlyweight(String key) {
            this.key = key;
            System.out.println("具体享元" + key + "被创建!");
        }
    
        public void operation() {
            System.out.println("具体享元" + key + "被调用,");
        }
    }
    
  3. 定义享元工厂(Flyweight Factory)角色,管理Concrete Flyweight

    class FlyweightFactory {
        private HashMap<String, Flyweight> flyweights = new HashMap<>();
    
        public Flyweight getFlyweight(String key) {
            Flyweight flyweight = (Flyweight) flyweights.get(key);
            if (flyweight != null) {
    //            System.out.println("具体享元" + key + "已经存在,被成功获取!");
            } else {
                flyweight = new ConcreteFlyweight(key);
                flyweights.put(key, flyweight);
            }
            return flyweight;
        }
    }
    

完整代码

package com.company.test.flyweight;

import java.util.HashMap;

interface Flyweight {
    void operation();
}

class ConcreteFlyweight implements Flyweight {
    private String key;

    ConcreteFlyweight(String key) {
        this.key = key;
        System.out.println("具体享元" + key + "被创建!");
    }

    public void operation() {
        System.out.println("具体享元" + key + "被调用,");
    }
}

class FlyweightFactory {
    private HashMap<String, Flyweight> flyweights = new HashMap<>();

    public Flyweight getFlyweight(String key) {
        Flyweight flyweight = (Flyweight) flyweights.get(key);
        if (flyweight != null) {
//            System.out.println("具体享元" + key + "已经存在,被成功获取!");
        } else {
            flyweight = new ConcreteFlyweight(key);
            flyweights.put(key, flyweight);
        }
        return flyweight;
    }
}

public class FlyweightTest {
    public static void main(String[] args) {
        FlyweightFactory factory = new FlyweightFactory();
        System.out.println("------------------------------------------ 分别引用享元a,b ------------------------------------------");
        factory.getFlyweight("a").operation();
        factory.getFlyweight("b").operation();

        System.out.println("------------------------------------------ 分别引用享元a,b,c ------------------------------------------");
        factory.getFlyweight("a").operation();
        factory.getFlyweight("b").operation();
        factory.getFlyweight("c").operation();
    }
}

运行结果

image-20201027150024246

  • 第一轮调用享元a,b时,均需要创建对象再引用
  • 第二轮调用享元a,b时不再创建,而是直接引用,但对于c依然需要创建后引用
  • 同理,之后如果再次调用享元a,b,c也不需要创建,而是直接引用即可

4.模拟示例

为了更加贴近我们实际使用的情况,模拟一个下载管理器

临时手写的,肯定不够完整,仅写出其中主要逻辑,其中下载逻辑未补全

package com.company.test.flyweight;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 任务接口
 */
interface DownloadTask {
    void startDownload();

    void stopDownload() throws InterruptedException;
}

/**
 * 公共任务虚拟类
 */
abstract class CommonTask implements DownloadTask {
    protected String fileName;
    protected int progress;
    protected boolean isAlive;
    protected Thread thread;

    @Override
    public void stopDownload() {
        //todo 仅做暂停任务,如果要删除任务,请先暂停,不然可能导致线程问题
        isAlive = false;
        System.out.println(new Date().toString() + ": " + fileName + " " + "downLoad stop!");
    }

    @Override
    public void startDownload() {
        //已下载完成,不再继续下载
        if (progress >= 100) {
            //todo 这里仅做提示,实际应用中可以添加其他逻辑
            System.out.println(new Date().toString() + ": " + fileName + " " + "has been downloaded!");
            return;
        }
        System.out.println(new Date().toString() + ": " + fileName + " " + "downLoad start!");
        //正在下载中,不需要重建线程
        if (!isAlive) {
            isAlive = true;
            thread = createThread();
            thread.start();
        }
    }

    protected Thread createThread() {
        return new Thread(() -> {
            while (isAlive && progress < 100) {
                //todo 这里只做模拟进度,实际进度请自行扩展,不做赘述
                progress += 20;
                System.out.println(new Date().toString() + ": " + fileName + " " + progress + "%");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (progress >= 100) {
                isAlive = false;
                System.out.println(new Date().toString() + ": " + fileName + " " + "has bean downLoaded successfully!");
            }
        });
    }

}

class VideoTask extends CommonTask {
    //todo video类型的特有逻辑

    public VideoTask(String fileName) {
        this.fileName = fileName;
        this.progress = 0;
        this.isAlive = false;
    }
}

class PicTask extends CommonTask {
    //todo picture类型的特有逻辑

    public PicTask(String fileName) {
        this.fileName = fileName;
        this.progress = 0;
        this.isAlive = false;
    }
}

/**
 * 下载管理器
 */
class DownloadManager {
    /**
     * 任务池
     */
    Map<String, DownloadTask> tasks = new HashMap<>();

    /**
     * 获取任务,或者创建任务
     */
    public DownloadTask getTask(String fileName) {
        DownloadTask task = tasks.get(fileName);
        if (task == null) {
            if (fileName.endsWith(".mp3")) {
                task = new VideoTask(fileName);
            } else {
                task = new PicTask(fileName);
            }
            tasks.put(fileName, task);
        }
        return task;
    }
}

public class DownloadTest {
    public static void main(String[] args) throws InterruptedException {
        DownloadManager downloadManager = new DownloadManager();

        System.out.println("------------------------------------------ 第一轮测试 ------------------------------------------");
        //获取任务,并开始下载
        DownloadTask task1 = downloadManager.getTask("你的酒馆对我打了烊.mp3");
        task1.startDownload();
        //三秒后暂停下载
        Thread.sleep(3000);
        task1.stopDownload();

        System.out.println("------------------------------------------ 第二轮测试 ------------------------------------------");
        //三秒再次获取任务并下载
        Thread.sleep(3000);
        DownloadTask task2 = downloadManager.getTask("你的酒馆对我打了烊.mp3");
        task2.startDownload();
        //获取新任务并下载
        DownloadTask task3 = downloadManager.getTask("土拨鼠尖叫.jpg");
        task3.startDownload();

        System.out.println("------------------------------------------ 第三轮测试 ------------------------------------------");
        //三秒后重新开始两项任务
        Thread.sleep(3000);
        DownloadTask task4 = downloadManager.getTask("你的酒馆对我打了烊.mp3");
        task4.startDownload();
        DownloadTask task5 = downloadManager.getTask("土拨鼠尖叫.jpg");
        task5.startDownload();

        //维持主线程
        while (true) {
        }
    }
}

运行结果

image-20201027173439398

  • 第一轮测试中新建mp3下载,并在三秒后终止,结束时下载进度为60%
  • 第二轮再次启动mp3的下载,并新建jpg下载
  • 第三轮先等待三秒再启动下载
    • mp3已下载完成,提示已下载
    • jpg仍在下载中,则继续下载,直至下载完成

仅供参考池化逻辑,实际应用中的下载管理器比这要复杂多了,但依然需要池化管理


5.其他问题

5.1.为什么需要池化管理

节省系统资源,降低内存消耗,加快执行速度

比如上面示例中,不使用池化管理,则每次获取任务都需要重建,造成不必要的消耗,且会丢失已下载进度


作者:Echo_Ye

WX:Echo_YeZ

Email :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

5.2.如何共享

在合适的地方初始化享元工厂即可,如果想在多个类中公用,也可以将其丢进bean池,或者单例模式轻松解决,Android也可以使用服务


5.3.池化一定需要享元工厂吗?是否可以将池放在客户端里?

当然可以,实际上很多地方也是这么做的,但是封装性不够好,且不便于与其他客户端共享

通常如果客户端里定义享元工厂,会将类封装起来,不将池暴露出去,达到类似的效果


别的想起来再写吧。。。


后记

实际开发中经常使用到池化管理,算是常用的实战技巧了,享元模式只是更加规范化的整理出来而已


作者:Echo_Ye

WX:Echo_YeZ

Email :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

原文地址:https://www.cnblogs.com/silent-bug/p/13886445.html