GeekIBLi

设计模式之美-享元模式

2021-08-07

享元模式(Flyweight Pattern)

前言

在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。

定义

享元模式,又称为轻量级模式,运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、
避免大量相似类的开销,从而提高系统资源的利用率。它是对象池的一种实现,类似于线程池,线程池可以避免不停的创建个销毁多个对象,消耗性能(用户态和内核态);
【宗旨】:共享细粒度对象,将多个对同一对象的访问集中起来;属于结构性模式;

内部状态和外部状态

在共享对象的过程中,又两种状态:
内部状态: 内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;

就是对象所共有的信息,比如火车票,多有从北京-上海的对象可以共用一个,而不是有1000张票,创建1000个对象;

外部状态: 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享;

也是不叫好理解的,当火车票被你购买之后,就和你的身份证号(唯一)绑定了,这属于外部状态;

使用场景

1、常常应用于系统底层的开发,以便解决系统的性能问题;
2、系统有大量相似对象,需要缓冲池的地方;

举个例子🌰

享元实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Examination {
private String name;

private String phone;

private String subject;

public Examination(String subject) {
super();
this.subject = subject;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getPhone() {
return phone;
}

public void setPhone(String phone) {
this.phone = phone;
}

@Override
public String toString() {
return "Examination [name=" + name + ", phone=" + phone + ", subject=" + subject + "]";
}
}

享元工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ExaminationFactory {
// 对象池
private static Map<String, Examination> pool = new HashMap<>();

public static Examination getexExamination(String name, String phone, String subject) {
// 通过学科判断是否存在这个对象
if (pool.containsKey(subject)) {
Examination examination = pool.get(subject);
examination.setName(name);
examination.setPhone(phone);
System.out.println("在缓存池取用对象:" + examination.toString());
return examination;
} else {
Examination examination = new Examination(subject);
pool.put(subject, examination);
examination.setName(name);
examination.setPhone(phone);
System.out.println("新建对象:" + examination.toString());
return examination;
}
}

}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
Examination A = ExaminationFactory.getexExamination("A", "13812345678", "软件工程");
Examination B = ExaminationFactory.getexExamination("B", "13812341234", "软件工程");
Examination C = ExaminationFactory.getexExamination("C", "13812341328", "电子信息工程");
Examination D = ExaminationFactory.getexExamination("D", "13812345111", "桥梁工程工程");
Examination E = ExaminationFactory.getexExamination("E", "13812345444", "软件工程");

System.err.println(A.hashCode());
System.err.println(B.hashCode());
}
}

测试结果

1
2
3
4
5
6
7
新建对象:Examination [name=A, phone=13812345678, subject=软件工程]
在缓存池取用对象:Examination [name=B, phone=13812341234, subject=软件工程]
新建对象:Examination [name=C, phone=13812341328, subject=电子信息工程]
新建对象:Examination [name=D, phone=13812345111, subject=桥梁工程工程]
在缓存池取用对象:Examination [name=E, phone=13812345444, subject=软件工程]
1528902577
1528902577

实际应用

1、JDK – String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public static void main(String[] args) {
// "hello"是编译器常量, String s1是运行时,把常量地址赋值给他<br>
String s1 = "hello";
String s2 = "hello";
// "he" + "llo" 两个常量相加,会在编译期处理<br>
String s3 = "he" + "llo";
// "hel" "lo" new String 共建了3个空间,然后拼接起来是一个新的空间(why?)
String s4 = "hel" + new String("lo");
String s5 = new String("hello"); // s5存放的是堆中中间
String s6 = s5.intern(); //拿到常量中的地址,
String s7 = "h";
String s8 = "ello";
String s9 = s7 + s8; //为什么这个不一样,因为是变量相加所以编译期没有做优化

System.out.println(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
System.out.println("s1 " + System.identityHashCode(s1));
System.out.println("s2 " + System.identityHashCode(s2));
System.out.println("s3 " + System.identityHashCode(s3));
System.out.println("s4 " + System.identityHashCode(s4));
System.out.println("s5 " + System.identityHashCode(s5));
//s6为s5.intern()拿到的是常量池里的“hello”
System.out.println("s6 " + System.identityHashCode(s6));
System.out.println("s7 " + System.identityHashCode(s7));
System.out.println("s8 " + System.identityHashCode(s8));
System.out.println("s9 " + System.identityHashCode(s9));
System.out.print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
System.out.println(s1==s2);//true
System.out.println(s1==s3);//true
System.out.println(s1==s4);//false
System.out.println(s1==s9);//false
System.out.println(s4==s5);//false
System.out.println(s1==s6);//true
}

2、JDK – Integer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];

static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;

cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);

// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}

private IntegerCache() {}
}

3、线程池
4、Tomcat连接池

享元模式的优点✅

1、减少对象的创建,降低内存中对象的数量,降低系统的内存使用,提升效率;
2、减少内存之外的其他资源占用,IO,带宽等;

享元模式的缺点

1、关注内部,外部状态,关注程序线程安全问题;
2、使系统、程序的逻辑变得复杂;

参考资料

1、[设计模式] - 享元模式
2、设计模式-享元模式
3、设计模式-享元设计模式