大家好,我是老田,今天我給大家分享設計模式中的
享元模式
。用貼切的生活故事,以及真實項目場景來講設計模式,最後用一句話來總結這個設計模式。
下面是本文目錄:
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAjM2EzLcd3LcJzLcJzdllmVldWYtl2Pn5GcugDZ0IDZ4MmZzYjMlFjNkBzM1YzYjNmN5MGM0EzNxMTYvwVN0kzM0EDNtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.png)
關于設計模式系列,前面我們已經分享過:
三國演義:責任鍊模式
韓信拜将:委派模式
3年工作必備 裝飾器模式
工作五年了,居然還不懂 門面模式
點外賣,讓我想起了 政策模式
初級必備:單例模式的7個問題
快速掌握 模闆方法 模式
五分鐘 掌握 原型模式
背景
享元模式(
Flyweight Pattern
)又叫作輕量級模式,是對象池的一種實作。
類似線程池,線程池可以避免不停地建立和銷毀多個對象,消耗性能。
享元模式
提供了
減少對象數量
進而改善應用所需的對象結構的方式。
英文解釋:
Use sharing to support large numbers of fine-grained objects efficiently.
享元模式(
Flyweight Pattern
)其宗旨是
共享細粒度對象
,将多個對同一對象的通路集中起來,不必為每個通路者都建立一個單獨的對象,
主要用于減少建立對象的數量,以減少記憶體占用和提高性能
。
屬于結構性設計模式,其中結構性設計模式有:代理、門面、裝飾器、享元、橋接、擴充卡、組合。
注意:
享元模式把一個對象的狀态分成内部狀态和外部狀态,内部狀态是不變的,外部狀态是變化的;然後通過共享不變的部分,達到減少對象數量并節約記憶體的目的。
生活案例
房屋中介
隻要是個城市,就少不了房屋中介,房屋中介存有大量的出租房屋資訊,并且一家房屋中介往往會有多個門店,但是所有門店都共享這些房屋資訊(
共享的是出租房屋的資訊
)。
個人身份證資訊
每個中國公民都有一張身份證,并且這張身份證資訊在公安系統中是共享的,全國各警察局派出所都會共享你的身份證資訊(
共享的是個人身份資訊
)。
聯考志願填報
每所大學在每個省都有明确的招收名額,這些名額對于該省的所有聯考生而言都是共享的(
共享的是招收名額
)。
圖書館
圖書館裡的可借書籍,對多有讀者是共享的,大家都可以查詢此書是否已經被借出去,還剩基本可借(
共享的是圖書
)。
....
簡單代碼實作
下面我們通過一個案例來示範享元模式(圖書館為例)。
public interface Book {
void borrow();
}
/**
* @author java後端技術全棧
*/
public class ConcreteBook implements Book {
//被借出去的書名
private String name;
public ConcreteBook(String name) {
this.name = name;
}
@Override
public void borrow() {
System.out.println("圖書館借出去一本書,書名:"+this.name);
}
}
import java.util.HashMap;
import java.util.Map;
/** 圖書館
* @author java後端技術全棧
*/
public class Llibrary {
private Map<String, Book> bookMap = new HashMap<>();
private Llibrary() {
}
//隻能有一個圖書館
public static Llibrary getInstance() {
return LazyHolder.LAZY_STATIC_SINGLETON;
}
//通過書名name來借書
public Book libToBorrow(String name) {
Book book;
//如果圖書館有,直接把書借走
if (bookMap.containsKey(name)) {
book = bookMap.get(name);
} else {//圖書館沒有,則錄入一本書,然後把書借走
book = new ConcreteBook(name);
bookMap.put(name, book);
}
return book;
}
//傳回還有多少本書
public int bookSize() {
return bookMap.size();
}
private static class LazyHolder {
private static final Llibrary LAZY_STATIC_SINGLETON = new Llibrary();
}
}
import java.util.ArrayList;
import java.util.List;
public class Student {
private static List<Book> bookList = new ArrayList<>();
private static BookFactory bookFactory;
public static void main(String[] args) {
bookFactory = BookFactory.getInstance();
studenBorrow("java 從入門到精通");
studenBorrow("java 從入門到放棄");
studenBorrow("JVM java虛拟機");
studenBorrow("java程式設計思想");
//還了後,再借一次
studenBorrow("java 從入門到精通");
studenBorrow("java 從入門到放棄");
studenBorrow("JVM java虛拟機");
studenBorrow("java程式設計思想");
//還了後,再借一次
studenBorrow("java 從入門到精通");
studenBorrow("java 從入門到放棄");
studenBorrow("JVM java虛拟機");
studenBorrow("java程式設計思想");
//把每一本書借出去
for (Book book:bookList){
book.borrow();
}
System.out.println("學生一共借了 "+bookList.size()+"本書");
System.out.println("學生一共借了 "+ bookFactory.bookSize()+"本書");
}
private static void studenBorrow(String name) {
bookList.add(bookFactory.libToBorrow(name));
}
}
複制
運作結果
圖書館借出去一本書,書名:java 從入門到精通
圖書館借出去一本書,書名:java 從入門到放棄
圖書館借出去一本書,書名:JVM java虛拟機
圖書館借出去一本書,書名:java程式設計思想
圖書館借出去一本書,書名:java 從入門到精通
圖書館借出去一本書,書名:java 從入門到放棄
圖書館借出去一本書,書名:JVM java虛拟機
圖書館借出去一本書,書名:java程式設計思想
圖書館借出去一本書,書名:java 從入門到精通
圖書館借出去一本書,書名:java 從入門到放棄
圖書館借出去一本書,書名:JVM java虛拟機
圖書館借出去一本書,書名:java程式設計思想
學生一共借了 12本書
學生一共借了 4本書
複制
其實,圖書館隻有四本書,但是多個人借,A借來看完了,B再去借,B還了C再去借。
這些書籍就被大家共享了。
享元模式的UML類圖如下:
1622982477555
由上圖可以看到,享元模式主要包含3個角色。
- 抽象享元角色(
):享元對象抽象基類或者接口,同時定義出對象的外部狀态和内部狀态的接口或實作。Book
- 具體享元角色(
):實作抽象角色定義的業務。該角色的内部狀态處理應該與環境無關,不會出現一個操作改變内部狀态、同時修改了外部狀态的情況。ConcreteBook
- 享元工廠(
):負責管理享元對象池和建立享元對象。BookFactory
也許這個例子你還是不太明白,下面我們就用工作中常見的場景來解釋一通。
大佬們是怎樣使用的
關于享元模式,在JDK中大量的使用,比如:String、Integer、Long等類中,都有使用到。
Integer中的享元模式
下面這段代碼輸出什麼?
/**
* 歡迎關注公衆号:java後端技術全棧
*
* @author 田維常
* @date 2021/06/02 19:30
*/
public class IntegerDemo {
public static void main(String[] args) {
Integer a = 100;
Integer b = Integer.valueOf(100);
System.out.println(a == b);
Integer c = new Integer(1000);
Integer d = Integer.valueOf(1000);
System.out.println(c == d);
}
}
複制
很多人可能會認為輸出
true
true
複制
其實,非也,這裡最終輸出的是:
true
false
複制
為什麼呢?100就可以比較,1000就不能比較了?
其實,在Integer裡就用到了享元模式,它就是把-128到127這個範圍的資料緩存起來(放在Integer類型的數組中)。
static final int low = -128;
public static Integer valueOf(int i) {
//high預設是127
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
複制
下面進行一個簡要的分析:
關于Integer的緩存,推薦看這篇文章:
這裡Integer裡的
IntegerCache
裡就用到了
享元模式
。
關于Integer 推薦:面試官:說說Integer緩存範圍
String中的享元模式
Java中講String類定義為final不能繼承,并且将屬性value也定義為final便是不可變,JVM中字元串一般儲存在字元串常量池中,Java會確定一個字元串在常量池中隻會有一份拷貝,這個字元串常量池在JDK1.6中位于方法區(永久代)中,而JDK1.7以後,JVM講其從方法區移動到了堆heap中。
下面這段代碼輸出什麼?
/**
* 歡迎關注公衆号:java後端技術全棧
*
* @author 田維常
* @date 2021/06/03
*/
public class StringDemo {
public static void main(String[] args) throws Exception {
String s1 = "abcd";
String s2 = "abcd";
String s3 = "ab" + "cd";
String s4 = "ab" + new String("cd");
String s5 = new String("abcd");
String s6 = s5.intern();
String s7 = "a";
String s8 = "bcd";
String s9 = s7 + s8;
System.out.println("s1 == s2 " + (s1 == s2));
System.out.println("s1 == s3 " + (s1 == s3));
System.out.println("s1 == s4 " + (s1 == s4));
System.out.println("s1 == s6 " + (s1 == s6));
System.out.println("s1 == s9 " + (s1 == s9));
System.out.println("s4 == s5 " + (s4 == s5));
}
}
複制
String
類中的
value
是final修飾的,以字面量的形式建立String變量時,JVM會在編譯期間就把該字面量“abcd”放到字元串常量池彙總,有
Java
程式啟動的時候就已經加載到記憶體中了。這個字元串常量的特點就是有且僅有一份相同的字面量,如果其他相同字面量,JVM則傳回這個字面量的引用,如果沒有相同的字面量,則再字元串常量池中建立這個字面量并傳回它的引用。
由于
s2
指向字面量
"abcd"
在常量池中已經存在了(s1先于s2),于是JVM就傳回這個字面量綁定的引用,是以
s1==s2
。
s3中字面量的拼接其實在JVM層已經做了優化,在JVM編譯期間就對s3的拼接做了優化,是以s1、s2、s3都可以了解為是同一個,即
s1==s3
。
s4中的
new String("cd")
,此時生成了兩個對象,
"cd"
和
new String("cd")
,"cd"存在于字元串常量池中,new
String("cd")
存在于堆heap中,
String s4="ab"+ new String("cd");
實質上是兩個對象的相加,編譯器不會對其進行優化,相加的結果存在于堆heap中,而s2存在于字元串常量池中,當然不相等,即
s1!=s4
。
s4
和
s5
最終的結果都是在堆中,是以此時
s4!=s5
s5.intern()
方法能是一個次元對總的字元串在運作期間動态地加入到字元串常量池中(字元串常量池的内容是程式啟動的時候就以及酒精加載好了,如果字元串常量池中存在該對象對應的字面量,則傳回該字面量在字元串常量池中的引用,否則,建立複制一份該字面量到字元串常量池中并發那會它的引用),是以
s1==s6
。
s9是s7和s8拼接而成,但是jvm并沒有對其進行優化,是以
s1!=s9
最後,上面這段代碼輸出:
s1 == s2 true
s1 == s3 true
s1 == s4 false
s1 == s6 true
s1 == s9 false
s4 == s5 false
複制
JVM中的常量池也是享元模式的經典實作之一。
關于String延伸内容:
美團面試題:String s = new String("111")會建立幾個對象?
Long中的享元模式
Long中和Integer中類似,也是最
-128到127
的數進行了緩存,請看Long中的valueOf()方法源碼部分:
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) { // will cache
return LongCache.cache[(int)l + offset];
}
return new Long(l);
}
複制
這個就沒必要進行示範了,和Integer一樣,都是使用了緩存,也就是享元模式。
在Apache Commons Pool中的享元模式
對象池化的基本思路是:将用過的對象儲存起來,等下一次需要這種對象的時候,再拿出來重複使用,進而在一定程度上減少頻繁建立對象造成的消耗。用于充當儲存對象的“容器”的對象,被稱為對象池(Object Pool,簡稱Pool)。
Apache Pool實作了對象池的功能,定義了對象的生成、銷毀、激活、鈍化等操作及其狀态轉換,并提供幾個預設的對象池實作,
有如下幾個重要的角色:
- Pooled Object(池化對象):用于封裝對象(例如,線程、資料庫連接配接和TCP連接配接),将其包裹成可被對象池管理的對象。
- Pooled Object Factory(池化對象工廠):定義了操作Pooled Object執行個體生命周期的一些方法,Pooled Object Factory必須實作線程安全。
- Object Pool(對象池):Object Pool負責管理Pooled Object,例如,借出對象、傳回對象、校驗對象、有多少激活對象和有多少空閑對象。
在
ObjectPool
類的子類
org.apache.commons.pool2.impl.GenericObjectPool
種有個屬性:
private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects;
複制
這個Map就是用來緩存對象的,是以這裡也是享元模式的實作。
享元模式的擴充
享元模式中的狀态
享元模式的定義提出了兩個要求:
細粒度
和
共享對象
。
因為要求細粒度,是以不可避免地會使對象數量多且性質相近,此時我們就将這些對象的資訊分為兩個部分:内部狀态和外部狀态。
内部狀态
指對象共享出來的資訊,存儲在享元對象内部,并且不會随環境的改變而改變;
外部狀态
指對象得以依賴的一個标記,随環境的改變而改變,不可共享。
比如:連接配接池中的連接配接對象,儲存在連接配接對象中的使用者名、密碼、連接配接URL等資訊,在建立對象的時候就設定好了,不會随環境的改變而改變,這些為内部狀态。而當每個連接配接要被回收利用時,我們需要将它标記為可用狀态,這些為外部狀态。
優缺點
優點
- 減少對象的建立,降低記憶體中對象的數量,降低系統的記憶體,提高效率。
- 減少記憶體之外的其他資源占用。
缺點
- 關注内、外部狀态,關注線程安全問題。
- 使系統、程式的邏輯複雜化。
總結
享元模式,單從概念來講估計很多人不是很了解,但是從Integer、String已經生活中的場景結合起來了解,就能輕松了解享元模式,享元模式的實作基本上都伴随着一個集合用來存這些對象。
一句話總結:
優化資源配置,減少資源浪費
參考:Tom的設計模式課程
好了,今天的分享就到此結束,希望大家能明白什麼是享元模式,享元模式的思想我們在開發中是否能借鑒,面試的時候就不要再說你不會設計模式了。