解锁享元模式:内存优化与性能提升的关键密码
系列文章目录
待后续补充~~~
文章目录
- 一、享元模式初相识
- 二、享元模式的核心概念
- 2.1 内部状态与外部状态
- 2.2 享元角色剖析
- 三、Java 代码中的享元模式
- 3.1 简单示例代码实现
- 3.2 代码解析与关键步骤
- 四、实际应用场景探秘
- 4.1 文本编辑器中的享元模式
- 4.2 游戏开发中的享元模式
- 4.3 数据库连接池与享元模式
- 五、享元模式的优势与挑战
- 5.1 显著优势
- 5.2 面临的挑战
- 六、何时使用享元模式
- 6.1 适用场景判断
- 6.2 与其他模式的协作
- 七、总结与展望
一、享元模式初相识
在软件开发的世界里,我们常常会遇到这样的情况:创建大量相似的对象,占用了大量的内存资源,导致程序性能下降。就好比你要在游戏中创建成千上万棵树,每棵树都有自己的属性,如颜色、形状、位置等。如果为每棵树都创建一个独立的对象,那内存开销可就大了去了。享元模式,就是为了解决这类问题而生的。
享元模式(Flyweight Pattern)是一种结构型设计模式,它主要用于减少创建对象的数量,以减少内存占用和提高性能。这种模式通过共享对象来避免创建过多的相似对象,从而提高系统的资源利用率。在享元模式中,我们将对象的状态分为内部状态和外部状态。内部状态是对象可共享的部分,它不随环境的改变而改变;外部状态是对象不可共享的部分,它会随环境的改变而改变。通过共享内部状态,我们可以减少对象的数量,从而达到节省内存的目的。
二、享元模式的核心概念
2.1 内部状态与外部状态
在享元模式中,对象的状态被巧妙地划分为内部状态(Intrinsic State)和外部状态(Extrinsic State)。理解这两种状态的区别,是掌握享元模式的关键。
内部状态是对象中可共享的部分,它不依赖于外部环境,始终保持不变。以游戏中的树木为例,树木的种类、形状、基本颜色等属性,就属于内部状态。无论这棵树出现在游戏地图的哪个位置,它的种类和形状都不会改变,这些属性可以被所有相同类型的树木对象共享。在 Java 代码中,我们可以将这些内部状态作为对象的成员变量,在对象创建时就进行初始化,并且在对象的生命周期内不会被修改。比如:
public class Tree {// 内部状态:树木种类private String treeType;// 内部状态:树木形状private String shape;// 内部状态:基本颜色private String color;public Tree(String treeType, String shape, String color) {this.treeType = treeType;this.shape = shape;this.color = color;}// 省略getter和setter方法
}
这样,当我们需要创建多棵相同类型的树木时,就可以共享这些内部状态,而不需要为每棵树都重复创建这些属性。
外部状态则是对象中不可共享的部分,它会随着环境的变化而改变。继续以游戏中的树木为例,树木的位置、生长状态(如是否被砍伐、是否结果等)就属于外部状态。不同位置的树木,其位置属性必然不同;而同一棵树在不同的游戏阶段,其生长状态也会发生变化。在 Java 代码中,我们通常不会将外部状态作为对象的成员变量,而是在需要使用时,通过方法参数的形式传递给对象。比如:
public class Tree {// 省略内部状态相关代码public void draw(int x, int y, String growthState) {// 根据传入的位置和生长状态绘制树木System.out.println("在位置 (" + x + ", " + y + ") 绘制 " + treeType + ",生长状态:" + growthState);}
}
这里的x和y表示树木的位置,growthState表示树木的生长状态,这些外部状态在每次调用draw方法时都可能不同。
通过区分内部状态和外部状态,享元模式能够有效地减少对象的数量,提高内存利用率。我们只需要创建少量的对象来共享内部状态,而外部状态则根据实际需求在运行时动态传入,这样就避免了为每个对象都创建大量重复的内部状态,从而节省了内存空间。
2.2 享元角色剖析
享元模式中包含了几个重要的角色,它们各自承担着不同的职责,共同协作实现了对象的共享和高效利用。
抽象享元角色(Flyweight)
抽象享元角色是所有具体享元类的抽象基类或接口,它定义了具体享元类需要实现的公共接口。这个接口通常包含了一些方法,用于操作对象的内部状态和外部状态。在游戏树木的例子中,我们可以定义一个抽象的TreeFlyweight接口:
public interface TreeFlyweight {void draw(int x, int y, String growthState);
}
这个接口定义了一个draw方法,用于绘制树木,其中x和y表示树木的位置(外部状态),growthState表示树木的生长状态(外部状态)。所有具体的树木享元类都需要实现这个接口,以提供具体的绘制逻辑。
具体享元角色(ConcreteFlyweight)
具体享元角色是抽象享元角色的具体实现类,它实现了抽象享元角色中定义的接口,并为内部状态提供存储空间。在游戏中,我们可以有具体的OakTree类和PineTree类,它们分别表示橡树和松树,是具体的享元类:
public class OakTree implements TreeFlyweight {// 内部状态:树木种类private String treeType = "橡树";// 内部状态:树木形状private String shape = "圆形树冠";// 内部状态:基本颜色private String color = "绿色";@Overridepublic void draw(int x, int y, String growthState) {System.out.println("在位置 (" + x + ", " + y + ") 绘制 " + treeType + ",形状:" + shape + ",颜色:" + color + ",生长状态:" + growthState);}
}public class PineTree implements TreeFlyweight {// 内部状态:树木种类private String treeType = "松树";// 内部状态:树木形状private String shape = "尖塔形树冠";// 内部状态:基本颜色private String color = "深绿色";@Overridepublic void draw(int x, int y, String growthState) {System.out.println("在位置 (" + x + ", " + y + ") 绘制 " + treeType + ",形状:" + shape + ",颜色:" + color + ",生长状态:" + growthState);}
}
这些具体享元类实现了draw方法,根据自身的内部状态和传入的外部状态,完成树木的绘制操作。
非共享具体享元角色(UnsharedConcreteFlyweight)
并非所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类。非共享具体享元类的对象通常是在需要时直接创建,而不会从享元池中获取。在游戏中,如果有一些特殊的树木,它们具有独特的属性,无法与其他树木共享内部状态,那么就可以将它们定义为非共享具体享元类。比如,有一棵被魔法诅咒的树木,它的外观和行为都与普通树木不同,我们可以定义一个CursedTree类:
public class CursedTree implements TreeFlyweight {// 独特的内部状态private String curseEffect;public CursedTree(String curseEffect) {this.curseEffect = curseEffect;}@Overridepublic void draw(int x, int y, String growthState) {System.out.println("在位置 (" + x + ", " + y + ") 绘制被诅咒的树木,诅咒效果:" + curseEffect + ",生长状态:" + growthState);}
}
这个CursedTree类虽然实现了TreeFlyweight接口,但它的内部状态是独特的,无法与其他树木共享,因此每次需要使用时都需要创建新的对象。
享元工厂角色(FlyweightFactory)
享元工厂角色负责创建和管理享元角色。它维护一个享元池(通常使用HashMap等集合来实现),当客户端请求一个享元对象时,享元工厂会先检查享元池中是否已经存在符合要求的享元对象,如果存在,则直接返回该对象;如果不存在,则创建一个新的享元对象,并将其加入享元池中,然后返回。以下是一个简单的享元工厂类的实现:
import java.util.HashMap;
import java.util.Map;public class TreeFlyweightFactory {private static final Map<String, TreeFlyweight> flyweightMap = new HashMap<>();public static TreeFlyweight getTreeFlyweight(String treeType) {if (flyweightMap.containsKey(treeType)) {return flyweightMap.get(treeType);} else {TreeFlyweight flyweight;if ("橡树".equals(treeType)) {flyweight = new OakTree();} else if ("松树".equals(treeType)) {flyweight = new PineTree();} else {// 其他类型的树木处理flyweight = null;}if (flyweight!= null) {flyweightMap.put(treeType, flyweight);}return flyweight;}}
}
在这个享元工厂类中,getTreeFlyweight方法根据传入的treeType来获取相应的享元对象。如果享元池中已经存在该类型的享元对象,则直接返回;否则,根据treeType创建新的享元对象,并将其加入享元池中。通过这种方式,享元工厂确保了相同类型的享元对象只会被创建一次,从而实现了对象的共享。
三、Java 代码中的享元模式
3.1 简单示例代码实现
为了更直观地理解享元模式在 Java 中的应用,我们以绘制图形为例,创建一个简单的图形绘制系统。假设我们需要绘制不同颜色的圆形,每个圆形的半径是固定的,半径可以作为共享的内部状态,而颜色作为不共享的外部状态。
首先,定义一个抽象享元角色Shape接口,它定义了绘制图形的方法,其中包含颜色这个外部状态作为参数:
public interface Shape {void draw(String color);
}
接着,创建具体享元角色Circle类,它实现了Shape接口,并保存了内部状态(这里是固定的半径),同时根据传入的颜色(外部状态)来绘制圆形:
public class Circle implements Shape {// 内部状态:半径,固定为100private int radius = 100;@Overridepublic void draw(String color) {System.out.println("绘制一个半径为 " + radius + " 颜色为 " + color + " 的圆形");}
}
然后,创建享元工厂角色ShapeFactory类,它负责创建和管理享元对象。通过HashMap来存储已经创建的圆形对象,当请求获取某个颜色的圆形时,先检查HashMap中是否已存在,若存在则直接返回,否则创建新的圆形对象:
import java.util.HashMap;
import java.util.Map;public class ShapeFactory {private static final Map<String, Shape> circleMap = new HashMap<>();public static Shape getCircle(String color) {if (circleMap.containsKey(color)) {return circleMap.get(color);} else {Shape circle = new Circle();circleMap.put(color, circle);System.out.println("创建颜色为 " + color + " 的圆形");return circle;}}
}
最后,在客户端代码中使用享元模式,获取不同颜色的圆形并绘制:
public class Client {private static final String[] colors = {"红色", "绿色", "蓝色", "白色", "黑色"};public static void main(String[] args) {for (int i = 0; i < 5; i++) {String color = colors[(int) (Math.random() * colors.length)];Shape circle = ShapeFactory.getCircle(color);circle.draw(color);}}
}
3.2 代码解析与关键步骤
在上述代码中,Shape接口定义了所有具体图形享元类需要实现的draw方法,这是抽象享元角色,它为具体享元类提供了统一的操作接口。Circle类实现了Shape接口,是具体享元角色,它将半径作为内部状态固定下来,只根据传入的颜色(外部状态)进行绘制操作。
ShapeFactory类是享元工厂角色,它的核心功能是创建和管理享元对象。circleMap这个HashMap就像是一个享元池,存储着已经创建的圆形享元对象。getCircle方法是享元工厂的关键方法,当客户端请求获取某个颜色的圆形时,它首先检查circleMap中是否已经存在该颜色的圆形享元对象。如果存在,就直接返回这个已有的对象,这体现了享元模式的共享特性,避免了重复创建相同的对象,节省了内存空间和创建对象的开销。如果不存在,就创建一个新的Circle对象,将其加入到circleMap中,并返回这个新创建的对象。
在客户端代码Client类中,通过循环随机获取不同颜色的圆形,并调用draw方法进行绘制。每次获取圆形时,都会先经过享元工厂ShapeFactory,如果是已经创建过的颜色对应的圆形,就直接从享元池中获取,不会再次创建,从而实现了对象的共享和复用。例如,第一次请求获取红色圆形时,享元工厂会创建一个红色圆形并放入享元池;当第二次再次请求获取红色圆形时,享元工厂会直接从享元池中返回之前创建的红色圆形,而不会重新创建一个新的红色圆形对象。
四、实际应用场景探秘
4.1 文本编辑器中的享元模式
在日常使用的文本编辑器中,享元模式发挥着重要的作用。当我们在文本编辑器中输入大量文本时,每个字符都可以看作是一个对象。如果为每个字符都创建一个独立的包含所有属性(如字体、字号、颜色、样式等)的对象,那么内存的占用将是巨大的。因为在一篇文档中,会有大量相同的字符,并且很多字符具有相同的样式属性。
利用享元模式,我们可以将字符的共享属性(如字体、字号、颜色等)作为内部状态,提取出来共享。而字符的位置等非共享属性作为外部状态,在需要时传递给享元对象。
例如,在一个 Java 实现的文本编辑器中,我们可以定义一个抽象的字符享元接口CharacterFlyweight:
public interface CharacterFlyweight {void display(int row, int col);
}
然后创建具体的字符享元类,比如LetterA表示字母 A 的享元类:
public class LetterA implements CharacterFlyweight {// 内部状态:字体private String font;// 内部状态:字号private int size;// 内部状态:颜色private String color;public LetterA(String font, int size, String color) {this.font = font;this.size = size;this.color = color;}@Overridepublic void display(int row, int col) {System.out.println("在(" + row + ", " + col + ")位置显示字母A,字体:" + font + ",字号:" + size + ",颜色:" + color);}
}
接着创建享元工厂类CharacterFactory来管理和创建享元对象:
import java.util.HashMap;
import java.util.Map;public class CharacterFactory {private static final Map<String, CharacterFlyweight> flyweightMap = new HashMap<>();public static CharacterFlyweight getCharacterFlyweight(String font, int size, String color, char character) {String key = font + "-" + size + "-" + color + "-" + character;if (flyweightMap.containsKey(key)) {return flyweightMap.get(key);} else {CharacterFlyweight flyweight;if (character == 'A') {flyweight = new LetterA(font, size, color);} else {// 其他字符的处理flyweight = null;}if (flyweight!= null) {flyweightMap.put(key, flyweight);}return flyweight;}}
}
在客户端代码中,模拟文本编辑器的字符显示:
public class TextEditorClient {public static void main(String[] args) {String font = "宋体";int size = 12;String color = "黑色";// 获取字母A的享元对象并显示CharacterFlyweight charA = CharacterFactory.getCharacterFlyweight(font, size, color, 'A');charA.display(1, 1);// 再次获取相同属性的字母A,从享元池中获取,不会创建新对象CharacterFlyweight anotherA = CharacterFactory.getCharacterFlyweight(font, size, color, 'A');anotherA.display(1, 2);}
}
通过这种方式,对于相同样式的字符,只需要创建一个享元对象,大大减少了内存中字符对象的数量,提高了文本编辑器的性能和内存利用率。
4.2 游戏开发中的享元模式
在游戏开发领域,享元模式有着广泛的应用。以一款角色扮演游戏为例,游戏中会存在大量的游戏角色,如战士、法师、刺客等,每个角色都有一些基本属性,如生命值、魔法值、攻击力、防御力等,同时还有一些外观属性,如服装、发型、武器模型等。此外,游戏中还有各种各样的道具,如药水、武器、装备等。
对于这些大量的相似对象,如果每个对象都独立创建并拥有所有属性,将会占用大量的内存资源。利用享元模式,我们可以将角色和道具的共享属性(如角色的基本属性、道具的基本类型属性等)作为内部状态,将那些不共享的属性(如角色在游戏场景中的位置、道具的使用次数等)作为外部状态。
比如,定义一个抽象的游戏角色享元接口GameCharacterFlyweight:
public interface GameCharacterFlyweight {void move(int x, int y);void attack();
}
创建具体的战士角色享元类WarriorFlyweight:
public class WarriorFlyweight implements GameCharacterFlyweight {// 内部状态:生命值private int health;// 内部状态:攻击力private int attackPower;// 内部状态:防御力private int defense;public WarriorFlyweight(int health, int attackPower, int defense) {this.health = health;this.attackPower = attackPower;this.defense = defense;}@Overridepublic void move(int x, int y) {System.out.println("战士移动到(" + x + ", " + y + ")位置");}@Overridepublic void attack() {System.out.println("战士发动攻击,攻击力:" + attackPower);}
}
创建享元工厂类CharacterFactory来管理游戏角色享元对象:
import java.util.HashMap;
import java.util.Map;public class CharacterFactory {private static final Map<String, GameCharacterFlyweight> flyweightMap = new HashMap<>();public static GameCharacterFlyweight getCharacterFlyweight(String characterType, int health, int attackPower, int defense) {String key = characterType + "-" + health + "-" + attackPower + "-" + defense;if (flyweightMap.containsKey(key)) {return flyweightMap.get(key);} else {GameCharacterFlyweight flyweight;if ("warrior".equals(characterType)) {flyweight = new WarriorFlyweight(health, attackPower, defense);} else {// 其他角色类型的处理flyweight = null;}if (flyweight!= null) {flyweightMap.put(key, flyweight);}return flyweight;}}
}
在游戏的客户端代码中,创建和使用战士角色:
public class GameClient {public static void main(String[] args) {int health = 100;int attackPower = 20;int defense = 10;// 获取战士角色享元对象GameCharacterFlyweight warrior = CharacterFactory.getCharacterFlyweight("warrior", health, attackPower, defense);warrior.move(10, 20);warrior.attack();}
}
通过享元模式,游戏中相同类型且具有相同基本属性的角色可以共享同一个享元对象,大大减少了内存占用,提高了游戏的运行效率。同时,对于游戏中的道具等其他大量相似对象,也可以采用类似的方式应用享元模式,从而优化游戏性能。
4.3 数据库连接池与享元模式
在数据库应用开发中,数据库连接池是一个非常重要的组件,它的实现原理与享元模式密切相关。数据库连接是一种昂贵的资源,创建和销毁数据库连接都需要消耗一定的时间和系统资源。如果每次数据库操作都创建一个新的连接,当并发访问量较大时,会导致系统资源的极大浪费,甚至可能因为连接过多而耗尽系统资源,导致系统崩溃。
数据库连接池基于享元模式的思想,将数据库连接对象作为共享对象进行管理。在连接池中,预先创建一定数量的数据库连接对象(享元对象),当应用程序需要数据库连接时,不是直接创建新的连接,而是从连接池中获取一个已经创建好的连接对象。当数据库操作完成后,将连接对象归还到连接池中,而不是销毁它,以便后续再次使用。
以 Java 的 JDBC 连接池为例,首先定义一个抽象的数据库连接享元接口DataBaseConnection:
import java.sql.Connection;public interface DataBaseConnection {Connection getConnection();
}
然后创建具体的 JDBC 数据库连接享元类JDBCConnection:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;public class JDBCConnection implements DataBaseConnection {// 内部状态:数据库驱动private String driver;// 内部状态:数据库URLprivate String url;// 内部状态:用户名private String username;// 内部状态:密码private String password;public JDBCConnection(String driver, String url, String username, String password) {this.driver = driver;this.url = url;this.username = username;this.password = password;}@Overridepublic Connection getConnection() {try {Class.forName(driver);return DriverManager.getConnection(url, username, password);} catch (Exception e) {throw new RuntimeException(e);}}
}
接着创建数据库连接池工厂类ConnectionPoolFactory来管理连接池:
// 连接池工厂类
public class ConnectionPoolFactory {private static final List<DataBaseConnection> connectionPool = new ArrayList<>();private static final int INITIAL_POOL_SIZE = 5;static {// 初始化连接池,创建一定数量的连接对象for (int i = 0; i < INITIAL_POOL_SIZE; i++) {DataBaseConnection connection = new JDBCConnection("com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/test", "root", "password");connectionPool.add(connection);}}public static Connection getConnection() {if (connectionPool.isEmpty()) {// 如果连接池为空,可以选择等待、创建新连接或者抛出异常throw new RuntimeException("连接池为空,无法获取连接");}DataBaseConnection connection = connectionPool.remove(0);return connection.getConnection();}public static void releaseConnection(Connection connection) {// 遍历连接池,找到对应的 DataBaseConnection 对象for (DataBaseConnection dbConnection : connectionPool) {if (dbConnection.getConnection() == connection) {// 如果已经在连接池中,不做处理return;}}// 找到对应的 JDBCConnection 对象for (DataBaseConnection dbConnection : new ArrayList<>(connectionPool)) {if (dbConnection.getConnection() == null) {((JDBCConnection) dbConnection).connection = connection;connectionPool.add(dbConnection);return;}}// 如果没有找到合适的对象,创建一个新的 JDBCConnection 对象并添加到连接池DataBaseConnection jdbcConnection = new JDBCConnection("com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/test", "root", "password");((JDBCConnection) jdbcConnection).connection = connection;connectionPool.add(jdbcConnection);}
}
在客户端代码中,使用数据库连接池获取和释放连接:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;public class DatabaseClient {public static void main(String[] args) {Connection connection = null;PreparedStatement statement = null;ResultSet resultSet = null;try {connection = ConnectionPoolFactory.getConnection();String sql = "SELECT * FROM users";statement = connection.prepareStatement(sql);resultSet = statement.executeQuery();while (resultSet.next()) {// 处理查询结果System.out.println(resultSet.getString("username"));}} catch (SQLException e) {e.printStackTrace();} finally {if (resultSet!= null) {try {resultSet.close();} catch (SQLException e) {e.printStackTrace();}}if (statement!= null) {try {statement.close();} catch (SQLException e) {e.printStackTrace();}}if (connection!= null) {ConnectionPoolFactory.releaseConnection(connection);}}}
}
通过这种基于享元模式的数据库连接池实现,应用程序可以复用数据库连接对象,减少了连接创建和销毁的开销,提高了数据库操作的效率和系统的性能,同时也降低了系统资源的消耗,增强了系统的稳定性和可扩展性。
五、享元模式的优势与挑战
5.1 显著优势
享元模式在软件开发中展现出了诸多令人瞩目的优势,使其成为解决特定类型问题的有力工具。
-
减少内存使用:这是享元模式最为突出的优势之一。通过共享相同或相似的对象,系统中对象的数量得以大幅减少。在文本编辑器中,大量相同样式的字符对象可以共享内部状态,如字体、字号、颜色等,避免了为每个字符都创建独立的包含所有属性的对象,从而显著降低了内存消耗。在一个包含数万字的文档中,如果每个字符都独立创建对象,内存占用将是巨大的;而使用享元模式后,相同样式的字符只需共享一个享元对象,内存占用可能会减少数倍甚至数十倍,这对于资源有限的系统,如移动设备应用程序来说,尤为重要。
-
提高性能:由于减少了对象的创建和销毁操作,系统的性能得到了明显提升。对象的创建和销毁都需要消耗一定的时间和系统资源,包括内存分配、初始化等操作。在游戏开发中,若大量使用享元模式,如对游戏场景中的大量相似道具、建筑等对象进行共享,就可以避免频繁地创建和销毁这些对象,使得游戏在运行过程中更加流畅,响应速度更快。在一款大型多人在线角色扮演游戏中,可能同时存在成千上万的玩家角色,每个角色都有一些基本的装备和技能,如果为每个角色的相同装备和技能都创建独立对象,系统的性能将会受到严重影响;而采用享元模式共享这些装备和技能对象,游戏的帧率和响应速度都会得到显著改善。
-
增强系统扩展性:享元模式为系统的扩展提供了便利。当系统需要添加新的享元对象或修改现有享元对象的共享方式时,对系统其他部分的影响较小。在一个图形绘制系统中,如果需要添加一种新类型的图形享元对象,只需要在享元工厂类中添加相应的创建逻辑,而不会影响到其他已经存在的图形享元对象以及使用它们的代码。这使得系统在面对业务需求变化时,能够更加灵活地进行调整和扩展,降低了系统维护和升级的成本。
5.2 面临的挑战
然而,享元模式并非完美无缺,在实际应用中也面临着一些挑战。
-
增加系统复杂性:享元模式需要分离对象的内部状态和外部状态,这无疑增加了系统设计和实现的复杂性。开发人员需要花费更多的时间和精力来分析对象的状态,确定哪些是可以共享的内部状态,哪些是需要外部传入的外部状态。在一个复杂的企业级应用系统中,业务对象的状态可能非常复杂,要准确地划分内部状态和外部状态并非易事,这可能导致开发过程中出现错误,增加了系统调试和维护的难度。此外,享元模式还需要引入享元工厂类来管理和创建享元对象,这也增加了系统的类结构和代码量。
-
使代码逻辑复杂化:为了实现对象的共享,享元模式可能需要引入额外的代码来管理状态。在享元工厂类中,需要编写复杂的逻辑来判断享元池中是否已经存在符合要求的享元对象,以及如何创建和添加新的享元对象。在多线程环境下,还需要考虑线程安全问题,确保多个线程同时访问享元池时不会出现数据不一致或其他错误。这使得代码的逻辑变得更加复杂,对于开发人员的编程能力和经验要求较高。如果代码逻辑处理不当,可能会导致系统出现各种难以排查的问题,影响系统的稳定性和可靠性。
-
外部状态处理不当会引发线程安全问题:在多线程环境下,共享对象的外部状态处理不当可能会导致线程安全问题。如果多个线程同时访问和修改共享对象的外部状态,可能会出现数据竞争和不一致的情况。在一个多线程的文本编辑应用中,多个线程可能同时尝试修改同一个字符享元对象的外部状态(如位置),如果没有进行适当的同步控制,就可能导致字符的位置显示错误或其他异常情况。因此,在使用享元模式时,需要特别注意对外部状态的管理和同步,确保线程安全,这无疑增加了开发的难度和工作量。
六、何时使用享元模式
6.1 适用场景判断
在软件开发过程中,准确判断何时使用享元模式至关重要。当面临以下几种情况时,享元模式往往是一个不错的选择。
-
系统存在大量相似对象:如果系统中需要创建和使用大量相似的对象,这些对象在很多属性上是相同的,只是部分属性有所差异,那么就适合使用享元模式。在一个在线地图应用中,地图上会有大量的建筑物对象,如房屋、商店、学校等。这些建筑物可能具有相同的建筑风格、基本结构等属性,而它们的位置、名称等属性不同。通过享元模式,我们可以将建筑风格、基本结构等共享属性作为内部状态,将位置、名称等非共享属性作为外部状态,这样就可以共享相同建筑风格的建筑物对象,大大减少内存中建筑物对象的数量。
-
对象创建和销毁成本较高:当创建和销毁对象的过程需要消耗大量的系统资源,如时间、内存、网络连接等时,享元模式可以有效减少这种开销。以数据库连接对象为例,建立数据库连接需要进行网络通信、验证用户身份、加载数据库驱动等操作,这些操作都需要消耗一定的时间和资源。如果每次数据库操作都创建新的连接对象,当并发访问量较大时,系统性能会受到严重影响。而使用享元模式,将数据库连接对象作为共享对象进行管理,在连接池中预先创建一定数量的连接对象,当有数据库操作需求时,从连接池中获取连接对象,操作完成后再归还到连接池,这样就避免了频繁创建和销毁连接对象,提高了系统的性能和稳定性。
-
对象状态可分离为共享和非共享部分:只有当对象的状态能够清晰地分离为共享部分(内部状态)和非共享部分(外部状态)时,享元模式才能发挥其优势。在一个图形绘制系统中,绘制圆形时,圆形的半径、颜色等属性可以作为共享的内部状态,因为对于同一种类型的圆形,这些属性可能是相同的;而圆形在画布上的位置等属性则作为非共享的外部状态,因为每个圆形的位置都可能不同。通过这种状态的分离,我们可以共享相同半径和颜色的圆形对象,根据不同的位置需求来绘制圆形,从而减少对象的创建数量,提高系统的绘制效率。
6.2 与其他模式的协作
享元模式在实际应用中,常常与其他设计模式协作,以发挥更大的作用。
-
与工厂模式的协作:享元模式通常与工厂模式紧密结合。工厂模式负责创建对象,而享元模式中的享元工厂角色(Flyweight Factory)本质上就是一个特殊的工厂,它负责创建和管理享元对象。在之前的图形绘制示例中,ShapeFactory类既是享元工厂,也是工厂模式的体现。它通过getCircle方法,根据传入的颜色来创建和管理圆形享元对象。工厂模式使得享元对象的创建和管理更加统一和高效,避免了在客户端代码中直接创建享元对象的复杂性。同时,享元模式通过共享对象,进一步优化了工厂模式创建对象的过程,减少了对象的创建数量,提高了系统的性能。
-
与单例模式的协作:在某些情况下,享元工厂本身可以采用单例模式来实现。单例模式确保一个类在整个系统中只有一个实例,并且提供一个全局访问点。将享元工厂设计为单例模式,可以避免重复创建享元工厂实例,节省系统资源。在一个大型企业级应用中,可能有多个模块需要使用享元模式来管理某些共享对象,如数据库连接池的享元工厂。如果每个模块都创建自己的享元工厂实例,不仅会浪费内存资源,还可能导致不同模块之间的享元对象管理不一致。而将享元工厂设计为单例模式,整个系统中只有一个享元工厂实例,所有模块都通过这个唯一的实例来获取和管理享元对象,保证了享元对象管理的一致性和高效性。
七、总结与展望
享元模式作为一种强大的结构型设计模式,在 Java 开发中具有不可忽视的地位。它通过巧妙地共享对象,将对象的状态分为内部状态和外部状态,有效减少了内存的占用,显著提升了系统的性能。从文本编辑器中字符对象的共享,到游戏开发里大量相似游戏元素的复用,再到数据库连接池中连接对象的管理,享元模式在各个领域都展现出了卓越的优化能力。
在实际应用中,我们要精准判断何时使用享元模式。当系统面临大量相似对象的创建,且这些对象的创建和销毁成本较高,同时对象的状态能够清晰地分离为共享和非共享部分时,享元模式便是一个理想的选择。但我们也要清楚地认识到,享元模式在带来优势的同时,也会增加系统的复杂性,需要我们精心处理内部状态和外部状态的分离,以及共享对象的管理。
展望未来,随着软件系统的规模不断扩大,对性能和资源利用的要求也越来越高。享元模式有望在更多的场景中得到应用和拓展。例如,在大数据处理、人工智能等领域,当面临海量的数据对象和复杂的计算任务时,享元模式或许能够为优化系统性能提供新的思路和方法。同时,随着技术的不断发展,享元模式也可能会与其他新兴的设计模式或技术相结合,创造出更高效、更灵活的解决方案。希望大家在今后的 Java 开发中,能够充分发挥享元模式的优势,打造出性能卓越、资源利用率高的软件系统。
相关文章:
解锁享元模式:内存优化与性能提升的关键密码
系列文章目录 待后续补充~~~ 文章目录 一、享元模式初相识二、享元模式的核心概念2.1 内部状态与外部状态2.2 享元角色剖析 三、Java 代码中的享元模式3.1 简单示例代码实现3.2 代码解析与关键步骤 四、实际应用场景探秘4.1 文本编辑器中的享元模式4.2 游戏开发中的享元模式4.3…...
负载均衡 方式
DNS 软件负载均衡 Nginx 也是 软件负载均衡 各种策略 1、轮询(默认) 2、weight(权重) 3、IP Hash (会话粘滞) 4、fair 5、UrlHash...
CAS单点登录(第7版)18.日志和审计
如有疑问,请看视频:CAS单点登录(第7版) 日志和审计 Logging 概述 Logging CAS 提供了一个日志记录工具,用于记录重要信息事件,如身份验证成功和失败;可以对其进行自定义以生成用于故障排除的其他信息。…...
Linux多版本管理工具介绍
一、update-alternatives工具 1. 简介 update-alternatives是Linux系统自带的一个用于管理多个版本命令的工具。它允许用户在不同的软件版本之间进行切换,而不需要手动修改环境变量或者链接文件。 2. 基本使用 查看已安装的alternatives 使用命令update-alterna…...
DeepSeek笔记(二):DeepSeek局域网访问
如果有多台电脑,可以通过远程访问,实现在局域网环境下多台电脑共享使用DeepSeek模型。在本笔记中,首先介绍设置局域网多台电脑访问DeepSeek-R1模型。 一、启动Ollama局域网访问 1.配置环境变量 此处本人的操作系统是Windows11,…...
摄像头畸变矫正
简单介绍 所谓畸变其实就是由摄像头引起的图片失真, 一般在广角摄像头表现明显, 原本平整的桌面通过镜头看像个球面, 直观的解释直线被拍成了曲线, 这让我想起来了一个表情包. 去畸变的办法 首先我们需要一个标准棋盘(印有特定的标定图案), 如图: 把它摊平放在桌子上, 然后用…...
EasyRTC:智能硬件适配,实现多端音视频互动新突破
一、智能硬件全面支持,轻松跨越平台障碍 EasyRTC 采用前沿的智能硬件适配技术,无缝对接 Windows、macOS、Linux、Android、iOS 等主流操作系统,并全面拥抱 WebRTC 标准。这一特性确保了“一次开发,多端运行”的便捷性,…...
机器视觉--图像的运算(乘法)
一、引言 在图像处理领域,Halcon 是一款功能强大且广泛应用的机器视觉软件库。它提供了丰富的算子和工具,能够满足各种复杂的图像处理需求。图像的乘法运算作为其中一种基础操作,虽然不像一些边缘检测、形态学处理等操作那样被频繁提及&…...
蓝桥杯 Java B 组之哈希表应用(两数之和、重复元素判断)
Day 5:哈希表应用(两数之和、重复元素判断) 一、哈希表(Hash Table)基础 1. 什么是哈希表? 哈希表(Hash Table) 是一种键值对(key-value)存储的数据结构&…...
Kafka分区管理大师指南:扩容、均衡、迁移与限流全解析
#作者:孙德新 文章目录 分区分配操作(kafka-reassign-partitions.sh)1.1 分区扩容、数据均衡、迁移(kafka-reassign-partitions.sh)1.2、修改topic分区partition的副本数(扩缩容副本)1.3、Partition Reassign场景限流1.4、节点内副本移动到不…...
vue 接口传formdata
在Vue中,如果你需要向服务器发送FormData对象,通常是为了上传文件或者需要发送表单数据。FormData是一个非常有用的工具,因为它可以直接使用表单元素的值以及文件内容,并以一种浏览器兼容的方式来发送这些数据。下面是如何在Vue中…...
图像处理篇---基本OpenMV图像处理
文章目录 前言1. 灰度化(Grayscale)2. 二值化(Thresholding)3. 掩膜(Mask)4. 腐蚀(Erosion)5. 膨胀(Dilation)6. 缩放(Scaling)7. 旋转…...
DeepSeek预测25考研分数线
25考研分数马上要出了。 目前,多所大学已经陆续给出了分数查分时间,综合往年情况来看,每年的查分时间一般集中在2月底。 等待出成绩的日子,学子们的心情是万分焦急,小编用最近爆火的“活人感”十足的DeepSeek帮大家预…...
数据融合的经典模型:早期融合、中期融合与后期融合的对比
数据融合是处理多源数据时非常重要的技术,尤其是在多模态学习、传感器网络和智能系统中。它的目标是将来自不同来源、不同模态的数据进行有效结合,从而获得更准确、更全面的信息。在数据融合的过程中,不同的融合策略能够在性能、效率和应用场…...
Linux环境Docker使用代理推拉镜像
闲扯几句 不知不觉已经2月中了,1个半月忙得没写博客,这篇其实很早就想写了(可追溯到Docker刚刚无法拉镜像的时候),由于工作和生活上的事比较多又在备考软考架构,拖了好久…… 简单记录下怎么做的…...
LabVIEW用CANopen的设备属性配置与心跳消息和PDO读取
本示例展示了如何通过SDO(服务数据对象)配置设备属性,以及如何读取从设备周期性发送的心跳消息和PDO(进程数据对象)消息。通过该示例,可以有效地进行设备配置并实现数据监控,适用于CANopen网络中…...
DeepSeek01-本地部署大模型
一、ollama简介: 什么是 Ollama? Ollama 是一个用于本地部署和管理大模型的工具。它提供了一个简单的命令行界面, 使得用户可以轻松地下载、运行和管理各种大模型。Ollama 支持多种模型格式, 并且可以与现有的深度学习框架&#x…...
python学opencv|读取图像(七十五)人脸识别:Fisherfaces算法和LBPH算法
【1】引言 前序学习进程中,已经掌握了使用Eigenfaces算法进行的人脸识别。相关文章链接为: python学opencv|读取图像(七十四)人脸识别:EigenFaces算法-CSDN博客 在此基础上,学习剩余两种人脸识别算法&am…...
UMLS数据下载及访问
UMLS数据申请 这个直接在官网上申请即可,记得把地址填全,基本都会拿到lisence。 UMLS数据访问 UMLS的数据访问分为网页访问,API访问以及数据下载后的本地访问,网页访问,API访问按照官网的指示即可,这里主…...
UE_C++ —— Container TArray
目录 一,TArray 二,Creating and Filling an Array 三,Iteration 四,Sorting 五,Queries 六,Removal 七,Operators 八,Heap 九,Slack 十,Raw Memor…...
第435场周赛:奇偶频次间的最大差值 Ⅰ、K 次修改后的最大曼哈顿距离、使数组包含目标值倍数的最少增量、奇偶频次间的最大差值 Ⅱ
Q1、奇偶频次间的最大差值 Ⅰ 1、题目描述 给你一个由小写英文字母组成的字符串 s 。请你找出字符串中两个字符的出现频次之间的 最大 差值,这两个字符需要满足: 一个字符在字符串中出现 偶数次 。另一个字符在字符串中出现 奇数次 。 返回 最大 差值…...
模拟解决哈希表冲突
目录 解决哈希表冲突原理: 模拟解决哈希表冲突代码: 负载因子: 动态扩容: 总结: HashMap和HashSet的总结: 解决哈希表冲突原理: 黑色代表一个数组,当 出现哈希冲突时࿰…...
UIView 与 CALayer 的联系和区别
今天说一下UIView 与 CALayer 一、UIView 和 CALayer 的关系 在 iOS 开发中,UIView 是用户界面的基础,它负责处理用户交互和绘制内容,而 CALayer 是 UIView 内部用于显示内容的核心图层(Layer)。每个 UIView 内部都有…...
Android 10.0 移除wifi功能及相关菜单
介绍 客户的机器没有wifi功能,所以需要删除wifi相关的菜单,主要有设置-网络和互联网-WLAN,长按桌面设置弹出的WALN快捷方式,长按桌面-微件-设置-WLAN。 修改 Android10 上直接将config_show_wifi_settings改为false,这样wifi菜单的入口就隐…...
电力与能源杂志电力与能源杂志社电力与能源编辑部2024年第6期目录
研究与探索 含电动汽车虚拟电厂的优化调度策略综述 黄灿;曹晓满;邬楠; 643-645663 含换电站的虚拟电厂优化调度策略综述 张杰;曹晓满;邬楠;杨小龙; 646-649667 考虑虚拟负荷研判的V2G储能充电桩设计研究 徐颖;张伟阳;陈豪; 650-654 基于状态估计的电能质量监测…...
简站主题:简洁、实用、SEO友好、安全性高和后期易于维护的wordpress主题
简站主题以其简洁的设计风格、实用的功能、优化的SEO性能和高安全性而受到广泛好评。 简洁:简站主题采用扁平化设计风格,界面简洁明了,提供多种布局和颜色方案,适合各种类型的网站,如个人博客和企业网站。 实用&…...
Redis(高阶篇)03章——缓存双写一致性之更新策略探讨
一、反馈回来的面试题 一图你只要用缓存,就可能会涉及到redis缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性的问题双写一致性,你先动缓存redis还是数据库mysql哪一个&#x…...
【Git】说说Git中开发测试的使用Git分支Git标签的使用场景
一、环境介绍 dev环境:开发环境,外部用户无法访问,开发人员使用,版本变动很大。test环境:测试环境,外部用户无法访问,专门给测试人员使用的,版本相对稳定。pre环境:灰度环…...
Spring Boot中使用Server-Sent Events (SSE) 实现实时数据推送教程
一、简介 Server-Sent Events (SSE) 是HTML5引入的一种轻量级的服务器向浏览器客户端单向推送实时数据的技术。在Spring Boot框架中,我们可以很容易地集成并利用SSE来实现实时通信。 二、依赖添加 在Spring Boot项目中,无需额外引入特定的依赖&#x…...
【Golang学习之旅】Go 语言微服务架构实践(gRPC、Kafka、Docker、K8s)
文章目录 1. 前言:为什么选择Go语言构建微服务架构1.1 微服务架构的兴趣与挑战1.2 为什么选择Go语言构建微服务架构 2. Go语言简介2.1 Go 语言的特点与应用2.2 Go 语言的生态系统 3. 微服务架构中的 gRPC 实践3.1 什么是 gRPC?3.2 gRPC 在 Go 语言中的实…...
数据结构:栈(Stack)及其实现
栈(Stack)是计算机科学中常用的一种数据结构,它遵循先进后出(Last In, First Out,LIFO)的原则。也就是说,栈中的元素只能从栈顶进行访问,最后放入栈中的元素最先被取出。栈在很多应用…...
DeepSeek在linux下的安装部署与应用测试
结合上一篇文章,本篇文章主要讲述在Redhat linux环境下如何部署和使用DeepSeek大模型,主要包括ollama的安装配置、大模型的加载和应用测试。关于Open WebUI在docker的安装部署,Open WebUI官网也提供了完整的docker部署说明,大家可…...
Next.js【详解】获取数据(访问接口)
Next.js 中分为 服务端组件 和 客户端组件,内置的获取数据各不相同 服务端组件 方式1 – 使用 fetch export default async function Page() {const data await fetch(https://api.vercel.app/blog)const posts await data.json()return (<ul>{posts.map((…...
pnpm, eslint, vue-router4, element-plus, pinia
利用 pnpm 创建 vue3 项目 pnpm 包管理器 - 创建项目 Eslint 配置代码风格(Eslint用于规范纠错,prettier用于美观) 在 设置 中配置保存时自动修复 提交前做代码检查 husky是一个 git hooks工具(git的钩子工具,可以在特定实际执行特…...
将jar安装到Maven本地仓库中
将jar安装到Maven本地仓库中 1. 使用 mvn install:install-file 命令模版示例 2.项目中添加依赖 将一个 .jar 文件安装到 Maven 本地仓库中是一个常见的操作,尤其是在你想要在本地测试一个尚未发布到中央仓库的库时。以下是如何将 .jar 文件安装到 Maven 本地仓库的…...
Spring 和 Spring MVC 的关系是什么?
Spring和Spring MVC的关系就像是“大家庭和家里的小书房”一样。 Spring是一个大家庭,提供了各种各样的功能和服务,比如管理Bean的生命周期、事务管理、安全性等,它是企业级应用开发的全方位解决方案。这个大家庭里有很多房间,每个…...
Ollama ModelFile(模型文件)
1. 什么是 Modelfile? Modelfile 是 Ollama 的配置文件,用于定义和自定义模型的行为。通过它,你可以: 基于现有模型(如 llama2、mistral)创建自定义版本 调整生成参数(如温度、重复惩罚&#…...
基于python深度学习遥感影像地物分类与目标识别、分割实践技术应用
我国高分辨率对地观测系统重大专项已全面启动,高空间、高光谱、高时间分辨率和宽地面覆盖于一体的全球天空地一体化立体对地观测网逐步形成,将成为保障国家安全的基础性和战略性资源。未来10年全球每天获取的观测数据将超过10PB,遥感大数据时…...
(蓝桥杯——10. 小郑做志愿者)洛斯里克城志愿者问题详解
题目背景 小郑是一名大学生,她决定通过做志愿者来增加自己的综合分。她的任务是帮助游客解决交通困难的问题。洛斯里克城是一个六朝古都,拥有 N 个区域和古老的地铁系统。地铁线路覆盖了树形结构上的某些路径,游客会询问两个区域是否可以通过某条地铁线路直达,以及有多少条…...
基于 Ollama 工具的 LLM 大语言模型如何部署,以 DeepSeek 14B 本地部署为例
简简单单 Online zuozuo :本心、输入输出、结果 文章目录 基于 Ollama 工具的 LLM 大语言模型如何部署,以 DeepSeek 14B 本地部署为例前言下载 Ollama实际部署所需的硬件要求设置 LLM 使用 GPU ,发挥 100% GPU 性能Ollama 大模型管理命令大模型的实际运行资源消耗基于 Ollam…...
大模型工具大比拼:SGLang、Ollama、VLLM、LLaMA.cpp 如何选择?
简介:在人工智能飞速发展的今天,大模型已经成为推动技术革新的核心力量。无论是智能客服、内容创作,还是科研辅助、代码生成,大模型的身影无处不在。然而,面对市场上琳琅满目的工具,如何挑选最适合自己的那…...
【05】密码学与隐私保护
5-1 零知识证明 零知识证明介绍 零知识证明的概念 设P(Prover)表示掌握某些信息,并希望证实这一事实的实体,V(Verifier)是验证这一事实的实体。 零知识证明是指P试图使V相信某一个论断是正确的,但却不向…...
Flink SQL与Doris实时数仓Join实战教程(理论+实例保姆级教程)
目录 第一章:Regular Joins 深度解析 1.1 核心原理与适用场景 1.2 电商订单 - 商品实时关联案例 1.2.1 数据流设计 1.2.2 Doris 表设计优化 1.2.3 性能调优要点 第二章:Interval Joins 实战应用 2.1 时间区间关联原理 2.2 优惠券使用有效性验证 2.2.1 业务场景说明 …...
DeepSeek 助力 Vue 开发:打造丝滑的范围选择器(Range Picker)
前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏关注哦 💕 目录 Deep…...
68页PDF | 数据安全总体解决方案:从数据管理方法论到落地实践的全方位指南(附下载)
一、前言 这份报告旨在应对数字化转型过程中数据安全面临的挑战,并提供全面的管理与技术体系建设框架。报告首先分析了数字化社会的发展背景,强调了数据安全在国家安全层面的重要性,并指出数据安全风险的来源和防护措施。接着,报…...
【Github每日推荐】-- 2024 年项目汇总
1、AI 技术 项目简述OmniParser一款基于纯视觉的 GUI 智能体,能够准确识别界面上可交互图标以及理解截图中各元素语义,实现自动化界面交互场景,如自动化测试、自动化操作等。ChatTTS一款专门为对话场景设计的语音生成模型,主要用…...
【Spring详解一】Spring整体架构和环境搭建
一、Spring整体架构和环境搭建 1.1 Spring的整体架构 Spring框架是一个分层架构,包含一系列功能要素,被分为大约20个模块 Spring核心容器:包含Core、Bean、Context、Expression Language模块 Core :其他组件的基本核心ÿ…...
Spring Boot(8)深入理解 @Autowired 注解:使用场景与实战示例
搞个引言 在 Spring 框架的开发中,依赖注入(Dependency Injection,简称 DI)是它的一个核心特性,它能够让代码更加模块化、可测试,并且易于维护。而 Autowired 注解作为 Spring 实现依赖注入的关键工具&…...
Machine Learning:Optimization
文章目录 局部最小值与鞍点 (Local Minimum & Saddle Point)临界点及其种类判断临界值种类 批量与动量(Batch & Momentum)批量大小对梯度下降的影响动量法 自适应学习率AdaGradRMSPropAdam 学习率调度优化总结 局部最小值与鞍点 (Local Minimum & Saddle Point) 我…...
wordpress get_footer();与wp_footer();的区别的关系
在WordPress中,get_footer() 和 wp_footer() 是两个不同的函数,它们在主题开发中扮演着不同的角色,但都与页面的“页脚”部分有关。以下是它们的区别和关系: 1. get_footer() get_footer() 是一个用于加载页脚模板的函数。它的主…...