【设计模式(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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352