文章目录
- 案例引入
- 介绍
- 基本介绍
- 原理类图
- 外部状态和内部状态
- 登场角色
- 案例实现
- 案例1
- 类图
- 代码实现
- 案例2
- 类图
- 代码实现
- 享元模式在JDK源码中的使用
- 总结
案例引入
你的公司主要做一些小型的外包项目,之前给客户A做一个产品展示网站,客户A的朋友感觉效果不错,也希望做这样的产品展示网站,但是要求有些不同
- 有客户要求以新闻的形式发布
- 有客户要求以博客的形式发布
- 有客户要求以微信公众号的形式发布
【 传统方式】
直接复制粘贴一份代码,根据客户的不同要求进行定制修改
分析:
- 需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,造成服务器的资源浪费
解决思路:
- 使用享元模式,整合到一个网站中,共享部分代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源
- 对于代码来说,由于是一份实例,维护和扩展都更加容易
介绍
基本介绍
- 享元模式,也叫蝇量模式: 通过尽量共享实例来避免new出新的实例,让程序占用的内存更小
- 常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中如果有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个
- 享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时。不需总是创建新对象,可以从缓冲池里拿。这样可以降低系统内存,同时提高效率(如先创建好棋盘对象,用户开始下棋时,直接从缓冲池中获取这个棋盘对象返回即可)
- 享元模式
经典的应用场景就是池化技术
了,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池化技术的重要实现方式
原理类图
FlyWeight
是抽象的享元角色,他是产品的抽象类,同时定义出对象的外部状态(频繁变化的,每个对象都不一样,如下棋时棋子的位置)和内部状态(稳定的,如棋子的颜色)的接口或实现ConcreteFlyWeight
是具体的享元角色,实现抽象角色定义相关业务,不同的客户可能对应不同的ConcreteFlyWeightUnSharedConcreteFlyWeight
是不可共享的角色,一般不会出现在享元工厂,getFlyWeight方法获取不到FlyWeightFactory
是享元工厂类,用于构建一个池容器(集合),同时提供从池中获取对象的方法Client
:使用享元工厂来获取实例
外部状态和内部状态
比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色多点,所以棋子颜色就是棋子的内部状态,而各个棋子之间的差别就是位置的不同,当我们落子后落子颜色是定的,但位置是变化的,所以棋子坐标就是棋子的外部状态
- 享元模式提出了两个要求: 细粒度和共享对象。这里就涉及到内部状态和外部状态,即将对象的信息分为两个部分: 内部状态和外部状态(使用享元模式,必须将对象的内部状态和外部状态分析出来)
- 内部状态指对象共享出来的信息,不会随环境的改变而改变(可以共享,建议存储在享元角色内部)
- 外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态(外部状态相关属性建议封装享元角色外的其他类中)
围棋理论上有361个空位可以放棋子,每盘棋都有可能有两三百个棋子对象产生,因为内存空间有限,一台服务器很难支持更多的玩家玩围棋游戏,如果用享元模式来处理棋子,那么棋子对象就可以减少到只有两个实例(一个黑子、一个白子,只是位置不同),这样就很好的解决了对象的开销问题
登场角色
Flyweight(轻量级)
:表示的就是那些实例会被共享的类FlyweightFactory(轻量级工厂)
:FlyweightFactory角色是生成Flyweight角色的工厂Client(请求者)
:Client角色使用FlyweightFactory 角色来生成Flyweight角色
案例实现
案例1
类图
每个客户的网站不同,所以将Website做成抽象类
- 网站的用户不同,User是享元模式的外部状态
- 内部状态全部写到ConcreteWebSite中,外部状态使用独立的类来封装
代码实现
【抽象网站】
package com.atguigu.flyweight;
public abstract class WebSite {
/**
* 抽象方法
* @param user
*/
public abstract void use(User user);
}
【具体网站】
package com.atguigu.flyweight;
/**
* 具体网站
*/
public class ConcreteWebSite extends WebSite {
/**
* 网站发布的形式(类型)
* 共享的部分,内部状态
*/
private String type = "";
/**
* 构造器
* @param type
*/
public ConcreteWebSite(String type) {
this.type = type;
}
@Override
public void use(User user) {
System.out.println("网站的发布形式为:" + type + " 在使用中 .. 使用者是" + user.getName());
}
}
【享元工厂】
package com.atguigu.flyweight;
import java.util.HashMap;
/**
* 网站工厂类,根据需要返回一个网站
*/
public class WebSiteFactory {
/**
* 集合, 充当池的作用
*/
private HashMap<String, ConcreteWebSite> pool = new HashMap<>();
/**
* 根据网站的类型,返回一个网站, 如果没有就创建一个网站,并放入到池中,并返回
* @param type
* @return
*/
public WebSite getWebSiteCategory(String type) {
if(!pool.containsKey(type)) {
//就创建一个网站,并放入到池中
pool.put(type, new ConcreteWebSite(type));
}
return (WebSite)pool.get(type);
}
/**
* 获取网站分类的总数 (池中有多少个网站类型)
* @return
*/
public int getWebSiteCount() {
return pool.size();
}
}
【客户】
package com.atguigu.flyweight;
public class User {
private String name;
public User(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
【主类】
package com.atguigu.flyweight;
public class Client {
public static void main(String[] args) {
// 创建一个工厂类
WebSiteFactory factory = new WebSiteFactory();
// 客户要一个以新闻形式发布的网站
WebSite webSite1 = factory.getWebSiteCategory("新闻");
webSite1.use(new User("tom"));
// 客户要一个以博客形式发布的网站
WebSite webSite2 = factory.getWebSiteCategory("博客");
webSite2.use(new User("jack"));
// 客户要一个以博客形式发布的网站
WebSite webSite3 = factory.getWebSiteCategory("博客");
webSite3.use(new User("smith"));
// 客户要一个以博客形式发布的网站
WebSite webSite4 = factory.getWebSiteCategory("博客");
webSite4.use(new User("king"));
System.out.println("网站的分类共=" + factory.getWebSiteCount());
}
}
【运行】
网站的发布形式为:新闻 在使用中 .. 使用者是tom
网站的发布形式为:博客 在使用中 .. 使用者是jack
网站的发布形式为:博客 在使用中 .. 使用者是smith
网站的发布形式为:博客 在使用中 .. 使用者是king
网站的分类共=2
Process finished with exit code 0
案例2
类图
代码实现
【大型字符类】
package com.atguigu.flyweight.Sample;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
/**
* 大型字符
* 从文件中读取大型字符的字体数据,并保存到内存中
* 大型字符会消耗较多内存,需要考虑尽可能高香BigChar类的实例
*/
public class BigChar {
/**
* 字符名字
*/
private char charname;
/**
* 保存大型字符对应的字符串(由'#' '.' '\n'组成)
*/
private String fontdata;
/**
* 构造函数
* 传入数字,获取对应的大型字符,存储到fontdata中
* @param charname
*/
public BigChar(char charname) {
this.charname = charname;
try {
BufferedReader reader = new BufferedReader(
new FileReader("big" + charname + ".txt")
);
String line;
StringBuffer buf = new StringBuffer();
while ((line = reader.readLine()) != null) {
buf.append(line);
buf.append("\n");
}
reader.close();
this.fontdata = buf.toString();
} catch (IOException e) {
this.fontdata = charname + "?";
}
}
/**
* 显示大型字符
*/
public void print() {
System.out.print(fontdata);
}
}
代码中读取的txt文件,分别是一些符号形成的数字,如big1.txt,内容如下:
......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
【享元工厂】
package com.atguigu.flyweight.Sample;
import java.util.HashMap;
/**
* 共享 BigChar 实例
*/
public class BigCharFactory {
/**
* 管理已经生成的BigChar的实例
*/
private HashMap pool = new HashMap();
/**
* Singleton模式
*/
private static BigCharFactory singleton = new BigCharFactory();
/**
* 构造函数
*/
private BigCharFactory() {
}
/**
* 获取唯一的实例
* @return
*/
public static BigCharFactory getInstance() {
return singleton;
}
/**
* 生成(共享)BigChar类的实例
* @param charName
* @return
*/
public synchronized BigChar getBigChar(char charName) {
BigChar bc = (BigChar) pool.get("" + charName);
if (bc == null) {
// 生成BigChar的实例
bc = new BigChar(charName);
pool.put("" + charName, bc);
}
return bc;
}
}
使用synchronized来修饰方法有以下作用:
- 确保同一时间只有一个线程可以访问被修饰的方法。当一个线程获得了对象的锁后,其他线程必须等待该线程释放锁才能访问该方法
- 解决多线程并发访问共享资源产生的数据不一致问题。当多个线程同时访问一个共享资源时,可能会出现数据竞争的问题,导致数据的不一致。使用synchronized修饰方法可以确保同一时间只有一个线程访问共享资源,从而避免数据不一致的问题
- 需要注意的是,synchronized修饰方法时锁定的是调用该方法的对象。如果有多个对象调用同一个被synchronized修饰的方法,那么每个对象都会有一个独立的锁。因为这里使用的是单例模式,所以只有一个对象
【工厂调用类】
传入一个字符串,根据每个字符,获取其BigChar,存储到bigchars中。并提供一个print方法来显示bigchars的元素
package com.atguigu.flyweight.Sample;
public class BigString {
/**
* “大型字符”的数组
*/
private BigChar[] bigchars;
/**
* 构造函数
*
* @param string
*/
public BigString(String string) {
bigchars = new BigChar[string.length()];
BigCharFactory factory = BigCharFactory.getInstance();
for (int i = 0; i < bigchars.length; i++) {
bigchars[i] = factory.getBigChar(string.charAt(i));
}
}
/**
* 显示
*/
public void print() {
for (int i = 0; i < bigchars.length; i++) {
bigchars[i].print();
}
}
}
【主类】
package com.atguigu.flyweight.Sample;
public class Main {
public static void main(String[] args) {
String input = "1212123";
BigString bs = new BigString(input);
bs.print();
}
}
【运行】
......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
....######......
..##......##....
..........##....
......####......
....##..........
..##............
..##########....
................
......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
....######......
..##......##....
..........##....
......####......
....##..........
..##............
..##########....
................
......##........
..######........
......##........
......##........
......##........
......##........
..##########....
................
....######......
..##......##....
..........##....
......####......
....##..........
..##............
..##########....
................
....######......
..##......##....
..........##....
......####......
..........##....
..##......##....
....######......
................
Process finished with exit code 0
享元模式在JDK源码中的使用
package com.atguigu.jdk;
public class FlyWeight {
public static void main(String[] args) {
//如果 Integer.valueOf(x) x 在 -128 --- 127 直接,就是使用享元模式返回,如果不在范围类,则仍然 new
//小结:
//1. 在valueOf 方法中,先判断值是否在 IntegerCache 中,如果不在,就创建新的Integer(new), 否则,就直接从 缓存池 返回
//2. valueOf 方法,就使用到享元模式
//3. 如果使用valueOf 方法得到一个Integer 实例,范围在 -128 - 127 ,执行速度比 new 快
Integer x = Integer.valueOf(127); // 得到 x实例,类型 Integer
Integer y = new Integer(127); // 得到 y 实例,类型 Integer
Integer z = Integer.valueOf(127);//..
Integer w = new Integer(127);
System.out.println(x.equals(y)); // 大小,true
System.out.println(x == y ); // false
System.out.println(x == z ); // true
System.out.println(w == x ); // false
System.out.println(w == y ); // false
Integer x1 = Integer.valueOf(200);
Integer x2 = Integer.valueOf(200);
System.out.println("x1==x2" + (x1 == x2)); // 数据不在范围内,返回false
}
}
小技巧
如果使用valueOf 方法得到一个Integer 实例,范围在 -128 - 127 ,执行速度比 new 快
总结
- 享元模式可以这样理解,“享”就表示共享,“元”表示对象
- 系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以共用时我们就可以考虑选用享元模式
- 用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用HashMap/HashTable存储
- 享元模式大大减少了对象的创建,降低了程序内存的占用,同时可以减少
new 实例
的时间的花费,提高效率 - 享元模式提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方
- 使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制
【注意】
- 由于多处使用了共享对象,如果修改共享对象,多处会受到影响
- 被池管理的实例不会被垃圾回收,如果不用的实例,可以从池中移除