享元模式:Flyweight Pattern,又叫作轻量级模式,是对象池的一种实现。类似线程池,线程池可以避免不停地创建和销毁多个对象,消耗性能。享元模式提供了减少对象数量从而改善应用所需的对象结构的方式

  • 享元模式将一个对象的状态分为内部状态和外部状态,内部状态是不变的,外部状态是变化的;通过共享不变的部分,达到减少对象数量并节约内存的目的
    • 内部状态指对象共享出来的信息,存储在享元对象内部,并且不会随环境的改变而改变
    • 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享
  • 享元模式的本质是共享细粒度对象,将多个对同一对象的访问集中起来,不必为每个访问者都创建一个单独的对象,以此来降低内存的消耗

主要应用场景:

  • 系统底层的开发,解决性能问题
  • 系统中有大量相似对象,需要缓冲池的场景

享元模式通用模板

享元模式的 UML 图:
image.png
享元模式的角色:

  • 抽象享元角色:Flyweight,享元对象抽象基类或者接口,同时定义出对象的外部状态和内部状态的接口或实现
  • 具体享元角色:ConcreteFlyweight,实现抽象角色定义的业务。该角色的内部状态处理应该与环境无关,不会出现一个操作改变内部状态、同时修改了外部状态的情况
  • 享元工厂:FlyweightFactory,负责管理享元对象池和创建享元对象

    1. public class FlyweightDemo {
    2. public static void main(String[] args) {
    3. Flyweight flyweight = FlyweightFactory.getFlyweight("世界");
    4. flyweight.operation("你好");
    5. }
    6. interface Flyweight {
    7. void operation(String extrinsicState);
    8. }
    9. // 具体享元角色
    10. static class ConcreteFlyweight implements Flyweight {
    11. // 内部状态,不变的对象
    12. private String intrinsicState;
    13. public ConcreteFlyweight(String intrinsicState) {
    14. this.intrinsicState = intrinsicState;
    15. }
    16. @Override
    17. public void operation(String extrinsicState) {
    18. System.out.println("内部不变对象:"+extrinsicState);
    19. }
    20. }
    21. // 享元工厂
    22. static class FlyweightFactory {
    23. private static Map<String, Flyweight> pool = new HashMap<>();
    24. public static Flyweight getFlyweight(String intrinsicState) {
    25. // 由于内部状态具有不变性,所以作为缓存的键
    26. if (!pool.containsKey(intrinsicState)) {
    27. Flyweight flyweight = new ConcreteFlyweight(intrinsicState);
    28. pool.put(intrinsicState, flyweight);
    29. }
    30. return pool.get(intrinsicState);
    31. }
    32. }
    33. }

享元模式实现连接池

利用享元模式实现一个连接池:

  1. public class DataSourcePool {
  2. private List<Connection> pool;
  3. private String url = "jdbc:mysql://localhost:3306/test";
  4. private String username = "root";
  5. private String password = "1234";
  6. private String driverClassName = "com.mysql.cj.jdbc.Driver";
  7. private final int poolSize = 20;
  8. public DataSourcePool() {
  9. pool = new LinkedList<Connection>();
  10. try {
  11. Class.forName(driverClassName);
  12. for (int i = 0; i < poolSize; i++) {
  13. Connection connection = DriverManager.getConnection(url, username, password);
  14. pool.add(connection);
  15. }
  16. } catch (Exception e) {
  17. e.printStackTrace();
  18. }
  19. }
  20. public synchronized Connection getConnection(){
  21. if (pool.size() > 0) {
  22. Connection conn = pool.get(0);
  23. pool.remove(conn);
  24. return conn;
  25. }
  26. return null;
  27. }
  28. public synchronized void release(Connection connection) {
  29. pool.add(connection);
  30. }
  31. }

享元模式在 jdk 中的应用

String:由 final 修饰,属于不可变对象,一般保存在字符常量池中,java 会确保一个串在字符常量池中只有一个复制

  • 字符串常量池的位置:
    • jdk 1.7 之前:位于常量池中,属于永久代
    • jdk 1.8 之后:放在堆中
  • 字符常量池的特点:

    • 字符串常量池中的字面量是 JVM 在编译期间就已经创建好,并在 java 启动时就已经加载到内存中
    • 在通过字面量创建 String 对象时,如果常量池存在相同的字面量,则返回这个字面量的引用;如果不存在相同的字面量,则在字符串常量池中创建这个字面量并返回它的引用
    • 有且只有一份相同的字面量

      1. public static void main(String[] args) {
      2. // JVM在编译期间就将"Hello"字面量放到字符串常量池
      3. // 在java启动的时候就已经加载到内存中,所以s1执行的是常量池中的引用
      4. String s1 = "Hello";
      5. // s2指向的字面量在常量池中已经存在,返回字面量的引用
      6. String s2 = "Hello";
      7. // 字面量的拼接,JVM在编译期间会进行优化,所以
      8. String s3 = "He" + "llo";
      9. // new String("lo") 生成了两个对象:字符串常量池中的字面量"lo" 和 new String("lo") 存在于堆中
      10. // String s4 = "hel" + new String("lo") 实质上是两个对象的相加,编译器不会进行优化,相加的结果存在于堆中,而s1存在于字符串常量池中,当然不相等
      11. String s4 = "hel" + new String("lo");
      12. // 由于字面量 Hello 已经存在字符串常量池,因此new String("Hello")只在堆中创建了一个对象
      13. String s5 = new String("Hello");
      14. // s5.intern()一个位于堆中的字符串在运行期间动态地加入字符串常量池(字符串常量池的内容是在程序启动的时候就已经加载好了的)
      15. // 如果字符串常量池中有该对象对应的字面量,则返回该字面量在字符串常量池中的引用;否则,复制一份该字面量到字符串常量池并返回它的引用
      16. String s6 = s5.intern();
      17. String s7 = "H";
      18. String s8 = "ello";
      19. // 是指是两个对象的相加,编译器不会优化,相加的结果位于堆中
      20. String s9 = s7 + s8;
      21. System.out.println(s1 == s2); // true
      22. System.out.println(s1 == s3); // true
      23. System.out.println(s1 == s4); // false
      24. System.out.println(s1 == s6); // true
      25. System.out.println(s1 == s9); // false
      26. }

享元模式在 Apache Pool 源码中的应用

对象池化的基本思路是:将用过的对象保存起来,等下一次需要这种对象的时候,再拿出来重复使用,从而在一定程度上减少频繁创建对象造成的消耗。用于充当保存对象的“容器”的对象,被称为对象池(Object Pool,简称Pool)

Apache Pool实现了对象池的功能,定义了对象的生成、销毁、激活、钝化等操作及其状态转换,并提供几个默认的对象池实现,有如下几个重要的角色。

  • Pooled Object(池化对象):用于封装对象(例如,线程、数据库连接和TCP连接),将其包裹成可被对象池管理的对象
  • Pooled Object Factory(池化对象工厂):定义了操作PooledObject实例生命周期的一些方法,Pooled Object Factory必须实现线程安全
  • Object Pool(对象池):Object Pool负责管理PooledObject,例如,借出对象、返回对象、校验对象、有多少激活对象和有多少空闲对象