研二學妹面試字節,竟倒在了ThreadLocal上,這是不要應屆生還是不要女生啊?

一、寫在開頭

? ? 今天和一個之前研二的學妹聊天,聊及她上周面試字節的情況,著實感受到了Java后端現在找工作的壓力啊,記得在18,19年的時候,研究生計算機專業的學生,背背八股文找個Java開發工作毫無問題,但現在即便你是應屆生,問的考題也非常的深入和細節了,只會背八股,沒有一定的代碼量和項目積累,根本找不到像樣的工作,具體聊天內容如下:

在這里插入圖片描述
既然大廠的面試都拷問到ThreadLocal了,那今天build哥就花點時間也來溫習一下這個知識點吧,盡可能整理的細致一點!🤓🤓

二、ThreadLocal簡介

2.1 ThreadLocal的作用

處理并發編程的時候,其核心問題是當多個線程去訪問共享變量時,因為順序、資源分配等原因帶來了數據的不準確,我們叫這種情況為線程不安全,為了解決線程安全問題,在Java中可以采用Lock、 synchronzed關鍵字等方式,但這種方式對于沒有持有鎖的線程來說會阻塞,這樣以來在時間性能上就有所損失。

為了解決這個問題,Java的lang包中誕生出了一個類,名為 ThreadLocal,見名知意,它被視為線程的“本地變量”,主要用來存儲各線程的私有數據,當多個線程訪問同一個ThreadLocal變量時,實際上它們訪問的是各自線程本地存儲的副本,而不是共享變量本身。因此,每個線程都可以獨立地修改自己的副本,而不會影響到其他線程。這種以空間換時間的方式,可以大大的提升處理時間。

2.2 ThreadLocal的使用案例

上面了解了它的特性后,我們來寫一個小demo感受一下ThreadLocal的使用。

public class TestService implements Runnable{// SimpleDateFormat 不是線程安全的,所以每個線程都要有自己獨立的副本//共享變量private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));public static void main(String[] args) throws InterruptedException {TestService obj = new TestService();//循環創建5個線程for(int i=0 ; i<5; i++){Thread t = new Thread(obj, ""+i);Thread.sleep(new Random().nextInt(1000));t.start();}}@Overridepublic void run() {System.out.println("Thread:"+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());try {Thread.sleep(new Random().nextInt(1000));} catch (InterruptedException e) {e.printStackTrace();}//formatter pattern is changed here by thread, but it won't reflect to other threads//設置副本的值formatter.set(new SimpleDateFormat());System.out.println("Thread:"+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());}
}

輸出:

Thread:0 default Formatter = yyyyMMdd
Thread:1 default Formatter = yyyyMMdd
Thread:2 default Formatter = yyyyMMdd
Thread:1 formatter = yy-M-d ah:mm
Thread:0 formatter = yy-M-d ah:mm
Thread:3 default Formatter = yyyyMMdd
Thread:2 formatter = yy-M-d ah:mm
Thread:3 formatter = yy-M-d ah:mm
Thread:4 default Formatter = yyyyMMdd
Thread:4 formatter = yy-M-d ah:mm

從輸出中可以看出,雖然 Thread-0 已經改變了 formatter 的值,但 Thread-1 默認格式化值與初始化值相同并沒有被修改,其他線程也一樣,這說明每個線程獲取ThreadLocal變量值的時候,確訪問的時線程本地的副本值。

三、ThreadLocal的實現原理

我們從Thread源碼入手,一步步的跟進,去探索ThreadLocal的實現原理。首先,在Thread的源碼中,我們看到了這樣的兩句定義語句:

public class Thread implements Runnable {//......//與此線程有關的ThreadLocal值。由ThreadLocal類維護ThreadLocal.ThreadLocalMap threadLocals = null;//與此線程有關的InheritableThreadLocal值。由InheritableThreadLocal類維護ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;//......
}

threadLocals 、inheritableThreadLocals 都是ThreadLocalMap變量,而這個Map我們可以看作是ThreadLocal的定制化HashMap,用來存儲線程本地變量的容器,是一個靜態內部類,而這兩個變量的值初始為null,只有當前線程調用 ThreadLocal 類的 set或get方法時才創建它們,那我們繼續去看set/get方法。

【set方法解析】

public void set(T value) {//1. 獲取當前線程實例對象Thread t = Thread.currentThread();//2. 通過當前線程實例獲取到ThreadLocalMap對象ThreadLocalMap map = getMap(t);if (map != null)//3. 如果Map不為null,則以當前ThreadLocal實例為key,值為value進行存入map.set(this, value);else//4.map為null,則新建ThreadLocalMap并存入valuecreateMap(t, value);
}

在ThreadLocal的set方法中通過getMap()方法去獲取當前線程的ThreadLocalMap對象,并對獲取到的map進行判斷,我們跟如到getMap方法中去,發現其實里面返回的是初始化定義的threadLocals變量。

ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}

在threadLocals沒有被調用初始化方法重新賦值的時候,它為null(不為null時,直接set進行賦值,當前ThreadLocal實例為key,值為value),set方法中會去調用createMap(t,value)進行處理,我們繼續跟入這個方法的源碼去看看:

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);
}

我們可以看到,在這個方法內部,會去新構造一個ThreadLocalMap的實例,并將value值初始化進去,并賦給threadLocals。

看完了set方法的底層實現我們知道:

  1. 最終變量存儲的位置在ThreadLocalMap里,ThreadLocal可以視為這個Map的封裝;
  2. 無論如何最終threadLocals存儲的數據都是以線程為key,對應的局部變量為值得映射表;
  3. 因為映射表的原因,確保了每個線程的局部變量都時獨立的。

【get方法解析】

看完了set的源碼,我們繼續來看看get方法的底層實現吧,既然有存(set)就有取(get),get 方法提供的就是獲取當前線程中 ThreadLocal 的變量值的功能!

public T get() {//1. 獲取當前線程的實例對象Thread t = Thread.currentThread();//2. 獲取當前線程的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {//3. 獲取map中當前ThreadLocal實例為key的值的entryThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")//4. 當前entitiy不為null的話,就返回相應的值valueT result = (T)e.value;return result;}}//5. 若map為null或者entry為null的話通過該方法初始化,并返回該方法返回的valuereturn setInitialValue();
}

我們上面提到了線程的變量值是和線程的ThreadLocal有映射關系的,所以這里將當前線程的ThreadLocal作為key去map中獲取值,若map為null或者entry為null的話通過該方法初始化,并返回該方法返回的value,我們去看看setInitialValue的實現:

private T setInitialValue() {T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);return value;
}
protected T initialValue() {return null;
}

這個方法里的實現和set幾乎一模一樣,這里調用了一個protected訪問修飾符的方法initialValue(),這個方法可以被子類重寫。

我們在2.2使用案例中寫道的ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));這是在Java8中的寫法,等價于:

private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){@Overrideprotected SimpleDateFormat initialValue(){return new SimpleDateFormat("yyyyMMdd");}
};

setInitialValue 方法的目的是確保每個線程在第一次嘗試訪問其 ThreadLocal 變量時都有一個合適的值。

3.1 ThreadLocalMap

上面我們也說了,ThreadLocalMap是ThreadLocal的靜態內部類,而每個線程獨立的變量副本存儲也是在這個Map中,它是一個定制的哈希表,底層維護了一個Entry 類型的數組類型的數組 table,它的內部提供了set、remove、getEntry等方法。

Entry靜態內部類
這個Entry又是ThreadLocalMap的一個靜態內部類,我們看一下它的源碼:

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

Entry 繼承了弱引用 WeakReference<ThreadLocal<?>>,它的 value 字段用于存儲與特定 ThreadLocal 對象關聯的值,key 為弱引用,意味著當 ThreadLocal 外部強引用被置為 null(ThreadLocalInstance=null)時,根據可達性分析,ThreadLocal 實例此時沒有任何一條鏈路引用它,所以系統 GC 的時候 ThreadLocal 會被回收。這種操作看似利用垃圾回收器節省了內存空間,實則存在一個風險,也就是我們下面要說的內存泄露問題!

只具有弱引用的對象,擁有更為短暫的生命周期,在GC線程掃描到它所在的內存區域的時候,一旦發現了只有弱引用的對象的時候,不管內存夠不夠用都會將其回收掉

四、ThreadLocal內存泄漏問題

4.1 內存泄漏的原因

如果非要問ThreadLocal有什么缺點的話,那就是使用不當的時候,會帶來內存泄漏問題。

內存泄漏 是指程序中已動態分配的堆內存由于某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重后果。

根據3.1中的分析,我們知道ThreadLocalMap中的使用的key是ThreadLocal的弱引用,Value為強引用,如果ThreadLocal沒有被強引用的話,key會被GC掉,而value依舊存在,若我們采用任何措施的前提下,線程一直運行,那這些value值就會一直存在,過多的占用內存,導致內存泄漏!

4.2 如何解決內存泄漏

如何解決內存泄漏呢,只需要記得在使用完 ThreadLocal 中存儲的內容后將它 remove 掉就可以了。

//ThreadLocal提供的清理方法
public void remove() {//1. 獲取當前線程的ThreadLocalMapThreadLocalMap m = getMap(Thread.currentThread());if (m != null)//2. 從map中刪除以當前ThreadLocal實例為key的鍵值對m.remove(this);
}
/*** ThreadLocalMap中的remove方法*/
private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.ThreadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {//將entry的key置為nulle.clear();//將該entry的value也置為nullexpungeStaleEntry(i);return;}}
}

除此之外,我們還可以使用Java 8引入的InheritableThreadLocal來替代ThreadLocal,它可以在子線程中自動繼承父線程的線程局部變量值,從而避免在創建新線程時重復設置值的問題。但是同樣需要注意及時清理資源以避免內存泄漏。

五、線程間局部變量傳值問題

上面我們提到的Java8中引入的InheritableThreadLocal類,這是實現父子線程間局部變量傳值的關鍵!
InheritableThreadLocal存在于java.lang包中是ThreadLocal的擴展,它有一個特性,那就是當創建一個新的線程時,如果父線程中有一個 InheritableThreadLocal 變量,那么子線程將會繼承這個變量的值。這意味著子線程可以訪問其父線程為此類變量設置的值。我們寫一個小demo感受一下!

public class TestService{// 創建一個 InheritableThreadLocal 變量private static final InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();public static void main(String[] args) {// 在主線程中設置值inheritableThreadLocal.set("這是父線程的值");System.out.println("父線程中的值: " + inheritableThreadLocal.get());// 創建一個子線程Thread childThread = new Thread(() -> {// 在子線程中嘗試獲取值,由于使用了 InheritableThreadLocal,這里會獲取到父線程中設置的值System.out.println("子線程中的值: " + inheritableThreadLocal.get());});// 啟動子線程childThread.start();// 等待子線程執行完成try {childThread.join();} catch (InterruptedException e) {e.printStackTrace();}// 主線程結束時清除值,防止潛在的內存泄漏inheritableThreadLocal.remove();}
}

輸出:

父線程中的值: 這是父線程的值
子線程中的值: 這是父線程的值

輸出不出所料,在子線程中獲取的其實是父線程設置的inheritableThreadLocal值。

5.1 父子線程局部變量傳值的實現原理

我們看到上面的輸出后,應該思考這樣的一個問題:子線程是怎么拿到父線程的inheritableThreadLocal值得呢?其實要從子線程的初始化開始說起,在線程Thread的內部,有著這樣的一個初始化方法:

private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,// 該參數一般默認是 trueboolean inheritThreadLocals) {// 省略大部分代碼Thread parent = currentThread();// 復制父線程的 inheritableThreadLocals 屬性,實現父子線程局部變量共享if (inheritThreadLocals && parent.inheritableThreadLocals != null) {this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); }// 省略部分代碼
}

在這里將父線程的inheritableThreadLocals賦值了進來,我們跟入createInheritedMap方法中繼續解析:

// 返回一個ThreadLocalMap,傳值為父線程的
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {return new ThreadLocalMap(parentMap);
}
//ThreadLoaclMap構建的過程中會調用該構造方法
private ThreadLocalMap(ThreadLocalMap parentMap) {Entry[] parentTable = parentMap.table;int len = parentTable.length;setThreshold(len);table = new Entry[len];// 一個個復制父線程 ThreadLocalMap 中的數據for (int j = 0; j < len; j++) {Entry e = parentTable[j];if (e != null) {@SuppressWarnings("unchecked")ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();if (key != null) {// childValue 方法調用的是 InheritableThreadLocal#childValue(T parentValue)Object value = key.childValue(e.value);Entry c = new Entry(key, value);int h = key.threadLocalHashCode & (len - 1);while (table[h] != null)h = nextIndex(h, len);table[h] = c;size++;}}}
}

在這個構造方法中,我們終于看到了InheritableThreadLocal的身影,childValue()方法就是其中的一個方法,用來給子線程賦父線程的inheritableThreadLocals值;其實InheritableThreadLocal的源碼非常非常的簡單,大部分的實現都取自父類ThreadLocal。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {protected T childValue(T parentValue) {return parentValue;}ThreadLocalMap getMap(Thread t) {return t.inheritableThreadLocals;}void createMap(Thread t, T firstValue) {t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);}
}

六、總結

OK,基于學妹在字節面試的考點,我們又梳理了一遍ThreadLocal,這個類大家還是要好好學一學的,畢竟在日后的工作中,我們肯定會使用到,譬如用它來保存用戶登錄信息,這樣在同一個線程中的任何地方都可以獲取到登錄信息;用于保存事務上下文,這樣在同一個線程中的任何地方都可以獲取到事務上下文等等。

七、結尾彩蛋

如果本篇博客對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯系Build哥!
在這里插入圖片描述
如果您想與Build哥的關系更近一步,還可以關注“JavaBuild888”,在這里除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!
在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/16935.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/16935.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/16935.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

本地圖形客戶端查看git提交歷史 使用 TortoiseGit

要在本地查看提交記錄和修改歷史&#xff0c;可以使用 TortoiseGit 和 Git-SCM。這兩個工具都提供了強大的功能來管理和查看 Git 倉庫中的提交記錄和歷史修改。 使用 TortoiseGit 查看提交記錄和修改歷史 查看提交記錄&#xff08;Log&#xff09;&#xff1a; 右鍵點擊項目文…

抖音里賣什么最賺錢?4個冷門的高利潤商品,還有誰不知道!

哈嘍~我的電商月月 做抖音小店的新手朋友&#xff0c;一定很想知道&#xff0c;在抖音里賣什么最賺錢&#xff1f; 很多人都會推薦&#xff0c;日常百貨&#xff0c;小風扇&#xff0c;女裝&#xff0c;寵物用品等等&#xff0c;這些商品確實很好做&#xff0c;你們可以試試 …

Euraka詳解:實現微服務架構的關鍵組件

在當今互聯網時代&#xff0c;微服務架構已經成為許多企業構建和部署應用程序的首選方法之一。而要在微服務架構中實現高可用性和靈活性&#xff0c;服務發現和注冊是至關重要的一環。Eureka作為Netflix開源的服務發現組件&#xff0c;為實現這一目標提供了高效可靠的解決方案。…

備忘錄可以統計字數嗎?備忘錄里在哪查看字數?

在這個信息爆炸的時代&#xff0c;很多人喜歡使用備忘錄app來記錄生活中的點點滴滴。備忘錄不僅可以幫助我們記事、安排日程&#xff0c;還能提醒我們完成各種任務&#xff0c;是我們日常生活中不可或缺的小助手。 然而&#xff0c;在使用備忘錄時&#xff0c;有時我們會遇到需…

不用BookStack的企業都在用什么知識庫軟件

現如今&#xff0c;越來越多的企業使用知識庫軟件對企業內部知識進行管理。BookStack作為一款功能強大的開源知識庫軟件&#xff0c;成為很多企業的首選。但是還是有一部分人群認為BookStack不適合他們的企業那么他們都是在用什么別的知識庫軟件呢&#xff1f;LookLook同學今天…

《python本機環境多版本切換》-兩種方式以及具體使用--venv/pyenv+pycharm測試

阿丹&#xff1a; source myenv/bin/activate 在開發使用rasa的時候發現自己安裝的python環境是3.12的&#xff0c;和rasa不兼容&#xff0c;所以實踐一下更換多python環境。 使用虛擬環境 在Python中使用虛擬環境來切換Python版本是一個常見的做法&#xff0c;這可以幫助你…

Minikube部署單節點Kubernetes

1.1 Minikube部署單節點K8s Minikube是由Kubernetes社區維護的單機版的Kubernetes集群&#xff0c;支持macOS, Linux, andWindows等多種操作系統平臺&#xff0c;使用最新的官方stable版本&#xff0c;并支持Kubernetes的大部分功能&#xff0c;從基礎的容器編排管理&#xff0…

實用篇| huggingface網絡不通

之前文章《Transformer原理》中介紹過,Transformers 是由 Hugging Face 開發的一個包&#xff0c;支持加載目前絕大部分的預訓練模型。隨著 BERT、GPT 等大規模語言模型的興起&#xff0c;越來越多的公司和研究者采用 Transformers 庫來構建應用。 Hugging Face是一家美國公司…

Easy IP + DNAT(服務器NAT轉換)

第一章 Easy IP 1.1 一般家庭和企業使用的地址轉換方式 直接使用出接口的地址做轉換Easy IP適用于小規模居于網中的主機訪問Internet的場景如&#xff1a;家庭、小型網吧、小型辦公室中&#xff0c;這些地方內部主機不多&#xff0c;出接口可以通過撥號方式獲取一個臨時公網I…

2.Nginx上配置圖片訪問

在 Nginx 上配置圖片訪問涉及到在 Nginx 配置文件中添加相應的 location 塊來處理圖片請求。以下是一個基本的示例&#xff0c;演示如何配置 Nginx 以便在指定目錄中存儲和訪問圖片。 1.上傳圖片到服務器 首先&#xff0c;將你的圖片上傳到服務器的某個目錄&#xff0c;例如 …

視頻監控匯聚平臺LntonCVS通過GB/T28181國標協議實現視頻監控平臺的級聯方案

近年來&#xff0c;隨著網絡視頻監控應用范圍的拓展&#xff0c;越來越多的政府部門和跨區域行業單位對視頻監控的需求已經不局限于本地聯網監控。他們正在探索在原有的本地聯網監控基礎上&#xff0c;建設省級乃至全國范圍內的跨區域監控聯網&#xff0c;以全面打造數據共享平…

BUUCTF靶場[Reverse]內涵的文件、新年快樂

[reverse]內涵的文件 文件運行看一下 老規矩&#xff0c;拿到文件先用DIE查有沒有殼 沒有殼&#xff0c;且是一個32位的文件&#xff0c;用相對應的IDA打開 &#xff0c;有主函數&#xff08;mian&#xff09;&#xff0c;先點開 這里點開&#xff08;mian_0&#xff09;,發現…

Kotlin基礎之基本語法

Kotlin 簡介 Kotlin 是一種由 JetBrains 開發的靜態類型編程語言&#xff0c;設計用于與 Java 虛擬機 (JVM) 兼容&#xff0c;同時也可用于 Android、JavaScript&#xff08;通過 Kotlin/JS&#xff09;和原生&#xff08;通過 Kotlin/Native&#xff09;開發。Kotlin 旨在提供…

【詳細介紹WebKit的結構】

&#x1f3a5;博主&#xff1a;程序員不想YY啊 &#x1f4ab;CSDN優質創作者&#xff0c;CSDN實力新星&#xff0c;CSDN博客專家 &#x1f917;點贊&#x1f388;收藏?再看&#x1f4ab;養成習慣 ?希望本文對您有所裨益&#xff0c;如有不足之處&#xff0c;歡迎在評論區提出…

springboot + es7.12.3 elasticsearchRestTemplate使用記錄

private BoolQueryBuilder getQueryBuilder(QueryCollectWaterDataPageRequestVO requestVO) {BoolQueryBuilder queryBuilder QueryBuilders.boolQuery();if (!CollectionUtils.isEmpty(requestVO.getCompanyIds())) {//termsQuery 精確查找corpId字段為精確的多個值&#xf…

T-Sql 中斷正在連接的數據庫的客戶端并移除數據庫

USE master;-- 查找連接到數據庫 OGS_PlugPower 的會話ID&#xff08;SPID&#xff09; DECLARE SessionID INT;SELECT SessionID session_id FROM sys.dm_exec_sessions WHERE database_id DB_ID(OGS_PlugPower);-- 使用會話ID&#xff08;SPID&#xff09;中斷連接 IF Sess…

設計模式 15 Decorator Pattern 裝飾器模式

設計模式 15 Decorator Pattern 裝飾器模式 1.定義 Decorator Pattern 裝飾器模式是一種結構型設計模式&#xff0c;它允許在運行時給對象添加新的行為或職責&#xff0c;而無需修改對象的源代碼。這種模式通過創建一個包裝對象&#xff0c;也稱為裝飾器&#xff0c;來包裹原…

C 語言設計模式(行為型)

文章目錄 策略模式場景示例 迭代器模式場景示例 訪問者模式場景示例 觀察者模式場景示例 命令模式場景示例 模板方法模式場景示例 事件驅動模式場景示例 責任鏈模式場景示例 狀態模式場景示例 策略模式 策略模式&#xff08;Strategy Pattern&#xff09;是一種行為型設計模式…

銀行為什么要對網點開展神秘顧客檢測項目?

銀行業面臨的形勢復雜多變&#xff0c;包括技術創新、客戶行為變化、競爭加劇、監管環境變化、全球化與本地化平衡、經濟環境影響以及可持續發展和社會責任等多方面的挑戰和機遇。銀行需要通過種策略&#xff0c;積極應對這些變化&#xff0c;實現可持續發展。其中提升客戶服務…

順序表實現通訊錄項目

目錄 一.實現功能&#xff1a; 二.文件結構 三.代碼實現 1.初始化 2.通訊錄的銷毀 3.通訊錄添加數據 4.通訊錄刪除數據 5.通訊錄的修改 6.展現通訊錄數據 7.通訊錄查找 四.代碼 SeqList.h Contact.h Contact.c test(通訊錄).c 一.實現功能&#xff1a; ?少能夠存…