Java缓存浅析
拿破侖說:勝利屬于堅(jiān)持到最后的人。
而正巧,咱們今天就是要聊一個(gè),關(guān)于怎么讓系統(tǒng)在狂轟亂炸甚至泰山壓頂?shù)那闆r下,都屹立不倒并堅(jiān)持到最后的話題——緩存。
Victory belongs to the most persevering. — Napoleon Bonaparte, French military and political leader
目錄體系
下面我們先簡(jiǎn)單瀏覽一下這個(gè)分享的目錄體系。
今天我會(huì)分五個(gè)方面給大家介紹關(guān)于緩存使用的問題,包括原理、實(shí)踐、技術(shù)選型和常見問題。
這個(gè)目錄體系就是一副人體骨骼,只有把各種內(nèi)臟、器官和血肉都填充進(jìn)去,緩存之美才能躍然紙上。接下來,我就邀請(qǐng)大家跟我一起來做這件事情.
讓我們不止步于Hello World,一起來聊聊緩存。
關(guān)于緩存
What
緩存是什么?
緩存是實(shí)際工作中非常常用的一種提高性能的方法。
而在java中,所謂緩存,就是將程序或系統(tǒng)經(jīng)常要調(diào)用的對(duì)象存在內(nèi)存中,再次調(diào)用時(shí)可以快速從內(nèi)存中獲取對(duì)象,不必再去創(chuàng)建新的重復(fù)的實(shí)例。
這樣做可以減少系統(tǒng)開銷,提高系統(tǒng)效率。
目前緩存的做法分為兩種模式:
-
內(nèi)存緩存:緩存數(shù)據(jù)存放在服務(wù)器的內(nèi)存空間中。
優(yōu)點(diǎn):速度快。缺點(diǎn):資源有限。 復(fù)制代碼 -
文件緩存:緩存數(shù)據(jù)存放在服務(wù)器的硬盤空間中。
優(yōu)點(diǎn):容量大。缺點(diǎn):速度偏慢,尤其在緩存數(shù)量巨大時(shí)。 復(fù)制代碼
why
為什么要使用緩存?
對(duì)于為什么要使用緩存,我見過的最精煉的回答是:來源一個(gè)夢(mèng)想,那就是多快好省的構(gòu)建社會(huì)主義社會(huì)。
但這是一種很矛盾的說法,就好像你不是高富帥還想迎娶白富美,好像是癡人說夢(mèng)啊。
因?yàn)槎嗑筒豢赡芸?#xff0c;好就不能省,怎么做到多又快,好而且省呢?
答案就是用緩存!
下面我們就聊聊怎么用緩存實(shí)現(xiàn)這個(gè)夢(mèng)想。
首先我想先聲明一下,我什么會(huì)想到做這樣一個(gè)分享。
其實(shí),從第一次使用 Java整型的緩存,到了解CDN的代理緩存,從初次接觸 MySQL內(nèi)置的查詢緩存,到使用 Redis緩存Session,我越來越發(fā)現(xiàn)使用緩存的重要性和普遍性。
因此我覺得自己有必要把自己的所學(xué)所用梳理出來,用于工作,并造福大家,因此才有了這樣一個(gè)技術(shù)分享。
聊緩存之前我們先聊聊數(shù)據(jù)庫。
在增刪改查中,數(shù)據(jù)庫查詢占據(jù)了數(shù)據(jù)庫操作的80%以上, 非常頻繁的磁盤I/O讀取操作,會(huì)導(dǎo)致數(shù)據(jù)庫性能極度低下。
而數(shù)據(jù)庫的重要性就不言而喻了:
- 數(shù)據(jù)庫通常是企業(yè)應(yīng)用系統(tǒng)最核心的部分
- 數(shù)據(jù)庫保存的數(shù)據(jù)量通常非常龐大
- 數(shù)據(jù)庫查詢操作通常很頻繁,有時(shí)還很復(fù)雜
我們知道,對(duì)于多數(shù)Web應(yīng)用,整個(gè)系統(tǒng)的瓶頸在于數(shù)據(jù)庫。
原因很簡(jiǎn)單,Web應(yīng)用中的其他因素,例如網(wǎng)絡(luò)帶寬、負(fù)載均衡節(jié)點(diǎn)、應(yīng)用服務(wù)器(包括CPU、內(nèi)存、硬盤燈、連接數(shù)等)、緩存,都很容易通過水平的擴(kuò)展(俗稱加機(jī)器)來實(shí)現(xiàn)性能的提高。
而對(duì)于MySQL,由于數(shù)據(jù)一致性的要求,無法通過簡(jiǎn)單的增加機(jī)器來分散向數(shù)據(jù)庫 寫數(shù)據(jù) 帶來的壓力。雖然可以通過前置緩存(Redis等)、讀寫分離、分庫分表來減輕壓力,但是與系統(tǒng)其它組件的水平擴(kuò)展相比,受到了太多的限制,而切會(huì)大大增加系統(tǒng)的復(fù)雜性。
因此數(shù)據(jù)庫的連接和讀寫要十分珍惜。
可能你會(huì)想到那就直接用緩存唄,但大量的用、不分場(chǎng)景的用緩存顯然是不科學(xué)的。我們不能手里有了一把錘子,看什么都是釘子。
但緩存也不是萬能的,要慎用緩存,想要用好緩存并不容易。因此我花了點(diǎn)時(shí)間整理了一下關(guān)于緩存的實(shí)現(xiàn)以及常見的一些問題。
when
首先簡(jiǎn)單梳理一下Web請(qǐng)求的過程,以及不同節(jié)點(diǎn)緩存的作用。
how
先不講代碼,對(duì)于緩存是如何工作的,簡(jiǎn)單的緩存數(shù)據(jù)請(qǐng)求流程就如下圖。
設(shè)計(jì)緩存的時(shí)候需要考慮的最關(guān)鍵的兩個(gè)緩存策略。
-?TTL(Time To Live ) 存活期, 即從緩存中創(chuàng)建時(shí)間點(diǎn)開始直到它到期的一個(gè)時(shí)間段(不管在這個(gè)時(shí)間段內(nèi)有沒有訪問都將過期)
- TTI(Time To Idle) 空閑期, 即一個(gè)數(shù)據(jù)多久沒被訪問將從緩存中移除的時(shí)間
后面講到緩存雪崩的時(shí)候,會(huì)講到,如果緩存策略設(shè)置不當(dāng),將會(huì)造成如何的災(zāi)難性后果,以及如何避免,這里先按下不表。
自定義緩存
如何實(shí)現(xiàn)
前面介紹了關(guān)于緩存的一些概念,那么實(shí)現(xiàn)緩存,或者確切的說實(shí)現(xiàn)存儲(chǔ)的前置緩存很難嗎?
答案是:不難。
JVM本身就是一個(gè)高速的緩存存儲(chǔ)場(chǎng)所,同時(shí)Java為我們提供了線程安全的ConcurrentMap,可以非常方便的實(shí)現(xiàn)一個(gè)完全由你自定義的緩存實(shí)例。
后面你會(huì)發(fā)現(xiàn),Spring Cache的缺省實(shí)現(xiàn)SimpleCacheManager,也是這樣設(shè)計(jì)自己的緩存的。
這里放上簡(jiǎn)單的實(shí)現(xiàn)代碼,不過36行,就實(shí)現(xiàn)了對(duì)緩存的存儲(chǔ)、更新、讀取和刪除等基本操作。 再結(jié)合實(shí)際的業(yè)務(wù)代碼,就能不依賴任何三方的實(shí)現(xiàn),在JVM中輕松玩轉(zhuǎn)緩存了。
但是,我想作為有追求的技術(shù)人,各位是絕對(duì)不會(huì)止步于此的。
那么我們思考一下,我們自定義的緩存實(shí)現(xiàn),有哪些優(yōu)缺點(diǎn)呢?
同與自定義的緩存相比,就能更深刻的理解Spring Cache的原理,以及優(yōu)點(diǎn)。
這里先把Spring Cache的特性列舉出來,下面還會(huì)介紹它的原理和具體用法。
Spring Cache
Spring Cache是Spring提供的對(duì)緩存功能的抽象:即允許綁定不同的緩存解決方案(如Ehcache、Redis、Memcache、Map等等),但本身不直接提供緩存功能的實(shí)現(xiàn)。
它支持注解方式使用緩存,非常方便。
Spring Cache的實(shí)現(xiàn)本質(zhì)上依賴了Spring AOP對(duì)切面的支持。
知道了Spring Cache的原理,你會(huì)對(duì)Spring Cache的注解的使用有更深入的認(rèn)識(shí)。
Spring Cache主要用到的注解有4個(gè)。
@CacheEvict對(duì)于保證緩存一致性非常重要,后面會(huì)專門講一下這個(gè)問題。
同時(shí),Spring還支持自定義的緩存Key以及SpringEL,這里不詳細(xì)講了,感興趣的同學(xué)可以參考Spring Cache的文檔。
緩存三高音
正如寫得再好的樂譜,都需要歌唱家演唱出來才能美妙動(dòng)聽一樣。
上面講到Spring Cache是對(duì)緩存的抽象,那么常用的緩存的實(shí)現(xiàn)有哪些呢?
歌唱界有世界三大男高音,那么緩存界如果來評(píng)選一下話,三大高音會(huì)是誰呢?
Redis
redis是一個(gè)key-value存儲(chǔ)系統(tǒng),這點(diǎn)和Memcached類似。
不同的是它支持存儲(chǔ)的value類型相對(duì)更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set --有序集合)和hash(哈希類型)。這些數(shù)據(jù)類型都支持push/pop、add/remove及取交集并集和差集。
和Memcached一樣,為了保證效率,數(shù)據(jù)都是緩存在內(nèi)存中。
區(qū)別的是redis會(huì)周期性的把更新的數(shù)據(jù)寫入磁盤或者把修改操作寫入追加的記錄文件,并且在此基礎(chǔ)上實(shí)現(xiàn)了master-slave(主從)同步。 Redis支持主從同步。數(shù)據(jù)可以從主服務(wù)器向任意數(shù)量的從服務(wù)器上同步,從服務(wù)器可以是關(guān)聯(lián)其他從服務(wù)器的主服務(wù)器。這使得Redis可執(zhí)行單層樹復(fù)制。
存盤可以有意無意的對(duì)數(shù)據(jù)進(jìn)行寫操作。由于完全實(shí)現(xiàn)了發(fā)布/訂閱機(jī)制,使得從數(shù)據(jù)庫在任何地方同步樹時(shí),可訂閱一個(gè)頻道并接收主服務(wù)器完整的消息發(fā)布記錄。
同步對(duì)讀取操作的可擴(kuò)展性和數(shù)據(jù)冗余很有幫助。
Redis有哪些適合的場(chǎng)景?
缺點(diǎn):
持久化。Redis直接將數(shù)據(jù)存儲(chǔ)到內(nèi)存中,要將數(shù)據(jù)保存到磁盤上,Redis可以使用兩種方式實(shí)現(xiàn)持久化過程。
定時(shí)快照(snapshot):每隔一段時(shí)間將整個(gè)數(shù)據(jù)庫寫到磁盤上,每次均是寫全部數(shù)據(jù),代價(jià)非常高。 基于語句追加(aof):只追蹤變化的數(shù)據(jù),但是追加的log可能過大,同時(shí)所有的操作均重新執(zhí)行一遍,回復(fù)速度慢。
耗內(nèi)存,占用內(nèi)存過高。
Ehcache
Ehcache 是一個(gè)成熟的緩存框架,你可以直接使用它來管理你的緩存。
Java緩存框架 EhCache EhCache 是一個(gè)純Java的進(jìn)程內(nèi)緩存框架,具有快速、精干等特點(diǎn),是Hibernate中默認(rèn)的CacheProvider。
特性:可以配置內(nèi)存不足時(shí),啟用磁盤緩存(maxEntriesLoverflowToDiskocalDisk配置當(dāng)內(nèi)存中對(duì)象數(shù)量達(dá)到maxElementsInMemory時(shí),Ehcache將會(huì)對(duì)象寫到磁盤中)。
Memcached
Memcached 是一個(gè)高性能的分布式內(nèi)存對(duì)象緩存系統(tǒng),用于動(dòng)態(tài)Web應(yīng)用以減輕數(shù)據(jù)庫負(fù)載。它基于一個(gè)存儲(chǔ)鍵/值對(duì)的hashmap。
其守護(hù)進(jìn)程(daemon )是用C寫的,但是客戶端可以用任何語言來編寫,并通過memcached協(xié)議與守護(hù)進(jìn)程通信。
Memcached通過在內(nèi)存中緩存數(shù)據(jù)和對(duì)象來減少讀取數(shù)據(jù)庫的次數(shù),從而提高動(dòng)態(tài)、數(shù)據(jù)庫驅(qū)動(dòng)網(wǎng)站的速度。
同屬于個(gè)key-value存儲(chǔ)系統(tǒng),Memcached與Redis常常一起比:
Memcached本身并不支持分布式,因此只能在客戶端通過像一致性哈希這樣的分布式算法來實(shí)現(xiàn)Memcached的分布式存儲(chǔ)。
相較于Memcached只能采用客戶端實(shí)現(xiàn)分布式存儲(chǔ),Redis更偏向于在服務(wù)器端構(gòu)建分布式存儲(chǔ)。最新版本的Redis已經(jīng)支持了分布式存儲(chǔ)功能。
緩存三高音比較緩存進(jìn)階
緩存由于其高并發(fā)和高性能的特性,已經(jīng)在項(xiàng)目中被廣泛使用。尤其是在高并發(fā)、分布式和微服務(wù)的業(yè)務(wù)場(chǎng)景和架構(gòu)下。
無論是高并發(fā)、分布式還是微服務(wù)都依賴于高性能的服務(wù)器。而談到高性能服務(wù)器,就必談緩存。
所謂高性能主要體現(xiàn)在高可用情況下,業(yè)務(wù)處理時(shí)間短,數(shù)據(jù)正確。
數(shù)據(jù)處理及時(shí)就是個(gè)“空間換時(shí)間”的問題,利用分布式內(nèi)存或者閃存等可以快速存取的設(shè)備,來替代部署在一般服務(wù)器上的數(shù)據(jù)庫,機(jī)械硬盤上存儲(chǔ)的文件,這是緩存提升服務(wù)器性能的本質(zhì)。
高并發(fā)(High Concurrency): 是互聯(lián)網(wǎng)分布式系統(tǒng)架構(gòu)設(shè)計(jì)中必須考慮的因素之一,它通常是指,通過設(shè)計(jì)保證系統(tǒng)能夠同時(shí)并行處理很多請(qǐng)求。
分布式: 是以縮短單個(gè)任務(wù)的執(zhí)行時(shí)間來提升效率的。 比如一個(gè)任務(wù)由10個(gè)子任務(wù)組成,每個(gè)子任務(wù)單獨(dú)執(zhí)行需1小時(shí),則在一臺(tái)服務(wù)器上執(zhí)行改任務(wù)需10小時(shí)。 采用分布式方案,提供10臺(tái)服務(wù)器,每臺(tái)服務(wù)器只負(fù)責(zé)處理一個(gè)子任務(wù),不考慮子任務(wù)間的依賴關(guān)系,執(zhí)行完這個(gè)任務(wù)只需一個(gè)小時(shí)。
微服務(wù): 架構(gòu)強(qiáng)調(diào)的第一個(gè)重點(diǎn)就是業(yè)務(wù)系統(tǒng)需要徹底的組件化和服務(wù)化,原有的單個(gè)業(yè)務(wù)系統(tǒng)會(huì)拆分為多個(gè)可以獨(dú)立開發(fā),設(shè)計(jì),運(yùn)行和運(yùn)維的小應(yīng)用。這些小應(yīng)用之間通過服務(wù)完成交互和集成。
緩存一致性問題
緩存一致性是如何發(fā)生的:先寫數(shù)據(jù)庫,再淘汰緩存:
第一步寫數(shù)據(jù)庫成功,第二步淘汰緩存失敗,則會(huì)引發(fā)一次嚴(yán)重的緩存不一致問題。 復(fù)制代碼如何避免緩存不一致的問題:先淘汰緩存,再寫數(shù)據(jù)庫:
第一步淘汰緩存成功,第二步寫數(shù)據(jù)庫失敗,則只會(huì)引發(fā)一次Cache miss。 復(fù)制代碼分布式緩存一致性
我們使用zookeeper來協(xié)調(diào)各個(gè)緩存實(shí)例節(jié)點(diǎn),zookeeper是一個(gè)分布式協(xié)調(diào)服務(wù),包含一個(gè)原語集,可以通知所有watch節(jié)點(diǎn)的client端,并保證事件發(fā)生順序和client收到消息的順序一致;使用zookeeper集群可非常容易的實(shí)現(xiàn)這場(chǎng)景。
一致性Hash算法通過一個(gè)叫做一致性Hash環(huán)的數(shù)據(jù)結(jié)構(gòu),實(shí)現(xiàn)KEY到緩存服務(wù)器的Hash映射。
緩存雪崩
產(chǎn)生原因1. a. 由于Cache層承載著大量請(qǐng)求,有效的保護(hù)了Storage層(通常認(rèn)為此層抗壓能力稍弱),所以Storage的調(diào)用量實(shí)際很低,所以它很爽。 b. 但是,如果Cache層由于某些原因(宕機(jī)、cache服務(wù)掛了或者不響應(yīng)了)整體crash掉了,也就意味著所有的請(qǐng)求都會(huì)達(dá)到Storage層,所有Storage的調(diào)用量會(huì)暴增,所以它有點(diǎn)扛不住了,甚至也會(huì)掛掉
產(chǎn)生原因2. 我們?cè)O(shè)置緩存時(shí)采用了相同的過期時(shí)間,導(dǎo)致緩存在某一時(shí)刻同時(shí)失效,請(qǐng)求全部轉(zhuǎn)發(fā)到DB,DB瞬時(shí)壓力過重雪崩。
雪崩問題在國外叫做:stampeding herd(奔逃的野牛),指的的cache crash后,流量會(huì)像奔逃的野牛一樣,打向后端。
解決方案
失效時(shí)的雪崩效應(yīng)對(duì)底層系統(tǒng)的沖擊非??膳?。
大多數(shù)系統(tǒng)設(shè)計(jì)者考慮用加鎖或者隊(duì)列的方式保證緩存的單線 程(進(jìn)程)寫,從而避免失效時(shí)大量的并發(fā)請(qǐng)求落到底層存儲(chǔ)系統(tǒng)上。
加鎖排隊(duì)只是為了減輕數(shù)據(jù)庫的壓力,并沒有提高系統(tǒng)吞吐量。
假設(shè)在高并發(fā)下,緩存重建期間key是鎖著的,這是過來1000個(gè)請(qǐng)求999個(gè)都在阻塞的。同樣會(huì)導(dǎo)致用戶等待超時(shí),這是個(gè)治標(biāo)不治本的方法!
加鎖排隊(duì)的解決方式分布式環(huán)境的并發(fā)問題,有可能還要解決分布式鎖的問題;線程還會(huì)被阻塞,用戶體驗(yàn)很差!因此,在真正的高并發(fā)場(chǎng)景下很少使用!
將緩存失效時(shí)間分散開,比如我們可以在原有的失效時(shí)間基礎(chǔ)上,末尾增加一個(gè)隨機(jī)值。
當(dāng)訪問量劇增、服務(wù)出現(xiàn)問題(如響應(yīng)時(shí)間慢或不響應(yīng))或非核心服務(wù)影響到核心流程的性能時(shí),仍然需要保證服務(wù)還是可用的,即使是有損服務(wù)。
系統(tǒng)可以根據(jù)一些關(guān)鍵數(shù)據(jù)進(jìn)行自動(dòng)降級(jí),也可以配置開關(guān)實(shí)現(xiàn)人工降級(jí)。
降級(jí)的最終目的是保證核心服務(wù)可用,即使是有損的。而且有些服務(wù)是無法降級(jí)的(如加入購物車、結(jié)算)。
在進(jìn)行降級(jí)之前要對(duì)系統(tǒng)進(jìn)行梳理,看看系統(tǒng)是不是可以丟卒保帥;從而梳理出哪些必須誓死保護(hù),哪些可降級(jí)。
比如可以參考日志級(jí)別設(shè)置預(yù)案:
(1)一般:比如有些服務(wù)偶爾因?yàn)榫W(wǎng)絡(luò)抖動(dòng)或者服務(wù)正在上線而超時(shí),可以自動(dòng)降級(jí);
(2)警告:有些服務(wù)在一段時(shí)間內(nèi)成功率有波動(dòng)(如在95~100%之間),可以自動(dòng)降級(jí)或人工降級(jí),并發(fā)送告警;
(3)錯(cuò)誤:比如可用率低于90%,或者數(shù)據(jù)庫連接池被打爆了,或者訪問量突然猛增到系統(tǒng)能承受的最大閥值,此時(shí)可以根據(jù)情況自動(dòng)降級(jí)或者人工降級(jí);
(4)嚴(yán)重錯(cuò)誤:比如因?yàn)樘厥庠驍?shù)據(jù)錯(cuò)誤了,此時(shí)需要緊急人工降級(jí)。
緩存擊穿/緩存穿透
緩存穿透是指查詢一個(gè)一定不存在的數(shù)據(jù),由于緩存是不命中時(shí)被動(dòng)寫的,并且出于容錯(cuò)考慮,如果從存儲(chǔ)層查不到數(shù)據(jù)則不寫入緩存,這將導(dǎo)致這個(gè)不存在的數(shù)據(jù)每次請(qǐng)求都要到存儲(chǔ)層去查詢,失去了緩存的意義。在流量大時(shí),可能DB就掛掉了,要是有人利用不存在的key頻繁攻擊我們的應(yīng)用,這就是漏洞。
緩存穿透-解決方案1
一個(gè)簡(jiǎn)單粗暴的方法,如果一個(gè)查詢返回的數(shù)據(jù)為空(不管是數(shù) 據(jù)不存在,還是系統(tǒng)故障),我們?nèi)匀话堰@個(gè)空結(jié)果進(jìn)行緩存,
但它的過期時(shí)間會(huì)很短,最長(zhǎng)不超過五分鐘。
緩存穿透-解決方案2
最常見的則是采用布隆過濾器,將所有可能存在的數(shù)據(jù)哈希到一個(gè)足夠大的bitmap中,一個(gè)一定不存在的數(shù)據(jù)會(huì)被 這個(gè)bitmap攔截掉,從而避免了對(duì)底層存儲(chǔ)系統(tǒng)的查詢壓力。
例如,商城有100萬用戶數(shù)據(jù),將所有用戶id刷入一個(gè)Map。
當(dāng)請(qǐng)求過來以后,先判斷Map中是否包含該用戶id,不包含直接返回,包含的話先去緩存中查是否有這條數(shù)據(jù),有的話返回,沒有的話再去查數(shù)據(jù)庫。
這樣不僅減輕了數(shù)據(jù)庫的壓力,緩存系統(tǒng)的壓力也將大大降低。
寄語
古人云:紙上得來終覺淺,絕知此事要躬行。
別人的經(jīng)驗(yàn)和智慧,需要經(jīng)過你親自驗(yàn)證才知道是不是真理,要經(jīng)過親手實(shí)踐才能為我所用。
別人的知識(shí)只是一些樹枝,需要你把它們編織成一架梯子,才能助你高升。
參考鏈接
- 百度百科 - 緩存
- Spring思維導(dǎo)圖,讓Spring不再難懂(cache篇)
- importnew : Spring Cache
- 圖解分布式架構(gòu)的演進(jìn)
- EHCACHE
- ehcache官方文檔
- ehcache入門基礎(chǔ)示例
- ehcache詳細(xì)解讀
- ehcache memcache redis 三大緩存男高音
- 網(wǎng)站緩存技術(shù) ehcache memcache redis 的比較
- 緩存擊穿、失效及熱點(diǎn)key問題
- Cache 應(yīng)用中的服務(wù)過載案例研究
- Bloom Filter布隆過濾器
- 緩存在高并發(fā)場(chǎng)景下的常見問題
- 緩存雪崩問題
- 再聊緩存技術(shù)
- 緩存穿透問題
- 微服務(wù)化之緩存的設(shè)計(jì)
- 緩存與數(shù)據(jù)庫一致性保證
- 分布式之緩存擊穿
- CDN緩存小結(jié)
- ava 中整型的緩存機(jī)制
- mysql的查詢緩存
- 使用Spring Session和Redis解決分布式Session跨域共享問題
- 學(xué)習(xí)Spring-Session+Redis實(shí)現(xiàn)session共享
- 詳解 MySQL 基準(zhǔn)測(cè)試和 sysbench 工具
- 分布式之緩存擊穿
- 分布式之?dāng)?shù)據(jù)庫和緩存雙寫一致性方案解析
轉(zhuǎn)載于:https://juejin.im/post/5c8481d95188257a323f52b5
總結(jié)
- 上一篇: 计算机桌面为什么总是换,你的电脑桌面是什
- 下一篇: Java并发编程之ThreadGroup