淺談Java中的分布式鎖
帶入到場(chǎng)景討論分布式鎖的意義
上圖可以看到,變量A存在JVM1、JVM2、JVM3三個(gè)JVM內(nèi)存中(這個(gè)變量A主要體現(xiàn)是在一個(gè)類(lèi)中的一個(gè)成員變量,是一個(gè)有狀態(tài)的對(duì)象
例如:UserController控制器中的一個(gè)整形類(lèi)型的成員變量),如果不加任何控制的話,變量A同時(shí)都會(huì)在JVM分配一塊內(nèi)存,三個(gè)請(qǐng)求發(fā)過(guò)來(lái)同時(shí)對(duì)這個(gè)變量操作,顯然結(jié)果是不對(duì)的!
即使不是同時(shí)發(fā)過(guò)來(lái),三個(gè)請(qǐng)求分別操作三個(gè)不同JVM內(nèi)存區(qū)域的數(shù)據(jù),變量A之間不存在共享,也不具有可見(jiàn)性,處理的結(jié)果也是不對(duì)的!
如果我們業(yè)務(wù)中確實(shí)存在這個(gè)場(chǎng)景的話,我們就需要一種方法解決這個(gè)問(wèn)題!
為了保證一個(gè)方法或?qū)傩栽诟卟l(fā)情況下的同一時(shí)間只能被同一個(gè)線程執(zhí)行,在傳統(tǒng)單體應(yīng)用單機(jī)部署的情況下,可以使用Java并發(fā)處理相關(guān)的API(如ReentrantLock或Synchronized)進(jìn)行互斥控制。
在單機(jī)環(huán)境中,Java中提供了很多并發(fā)處理相關(guān)的API。
但是,隨著業(yè)務(wù)發(fā)展的需要,原單體單機(jī)部署的系統(tǒng)被演化成分布式集群系統(tǒng)后,由于分布式系統(tǒng)多線程、多進(jìn)程并且分布在不同機(jī)器上,這將使原單機(jī)部署情況下的并發(fā)控制鎖策略失效,單純的Java API并不能提供分布式鎖的能力。
為了解決這個(gè)問(wèn)題就需要一種跨JVM的互斥機(jī)制來(lái)控制共享資源的訪問(wèn),這就是分布式鎖要解決的問(wèn)題!
分布式鎖的實(shí)現(xiàn)討論
分布式鎖一般有三種實(shí)現(xiàn)方式:
- 數(shù)據(jù)庫(kù)樂(lè)觀鎖;
- 基于ZooKeeper的分布式鎖;
- 基于Redis的分布式鎖;
Redis實(shí)現(xiàn)分布式鎖
基于Redis命令:
SET key value NX EX max-lock-time
這里補(bǔ)充下: 從2.6.12版本后, 就可以使用set來(lái)獲取鎖, Lua 腳本來(lái)釋放鎖。
setnx
是老黃歷了,set命令nx,xx等參數(shù), 是為了實(shí)現(xiàn) setnx 的功能。
1.加鎖
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 嘗試獲取分布式鎖 * @param jedis Redis客戶(hù)端 * @param lockKey 鎖 * @param requestId 請(qǐng)求標(biāo)識(shí) * @param expireTime 超期時(shí)間 * @return 是否獲取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } } jedis.set(String key, String value, String nxxx, String expx, int time)
這個(gè)set()方法一共有五個(gè)形參:
- 第一個(gè)為key,我們使用key來(lái)當(dāng)鎖,因?yàn)閗ey是唯一的。
- 第二個(gè)為value,我們傳的是requestId,很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什么還要用到value?原因就是我們?cè)谏厦嬷v到可靠性時(shí),分布式鎖要滿(mǎn)足第四個(gè)條件解鈴還須系鈴人,通過(guò)給value賦值為requestId,我們就知道這把鎖是哪個(gè)請(qǐng)求加的了,在解鎖的時(shí)候就可以有依據(jù)。requestId可以使用UUID.randomUUID().toString()方法生成。
- 第三個(gè)為nxxx,這個(gè)參數(shù)我們填的是NX,意思是SET IF NOT EXIST,即當(dāng)key不存在時(shí),我們進(jìn)行set操作;若key已經(jīng)存在,則不做任何操作;
- 第四個(gè)為expx,這個(gè)參數(shù)我們傳的是PX,意思是我們要給這個(gè)key加一個(gè)過(guò)期的設(shè)置,具體時(shí)間由第五個(gè)參數(shù)決定。
- 第五個(gè)為time,與第四個(gè)參數(shù)相呼應(yīng),代表key的過(guò)期時(shí)間。
總的來(lái)說(shuō),執(zhí)行上面的set()方法就只會(huì)導(dǎo)致兩種結(jié)果:
- 當(dāng)前沒(méi)有鎖(key不存在),那么就進(jìn)行加鎖操作,并對(duì)鎖設(shè)置個(gè)有效期,同時(shí)value表示加鎖的客戶(hù)端。
- 已有鎖存在,不做任何操作。
2.解鎖
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 釋放分布式鎖 * @param jedis Redis客戶(hù)端 * @param lockKey 鎖 * @param requestId 請(qǐng)求標(biāo)識(shí) * @return 是否釋放成功 */ public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script,Collections.singletonList(lockKey),Collections.singletonList(requestId)); if (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }
那么這段Lua代碼的功能是什么呢?其實(shí)很簡(jiǎn)單,首先獲取鎖對(duì)應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。為什么要寫(xiě)這段lua腳本呢?
因?yàn)榕袛鄈ey存不存在和刪除key的操作必須是源自的,否則就有可能發(fā)生我下一個(gè)請(qǐng)求本應(yīng)該能獲取到鎖,結(jié)果因?yàn)檫€沒(méi)有被刪除而獲取不到,因?yàn)閞edis是單線程更新緩存的,如果不適用lua腳本,獲取和刪除操作就不是一個(gè)連續(xù)的事務(wù)操作。仔細(xì)觀察上面代碼實(shí)現(xiàn)還有什么問(wèn)題呢?
直接解答好了,一般情況下我們的key都會(huì)設(shè)置過(guò)期防止死鎖,假如我們程序執(zhí)行階段占用時(shí)間過(guò)長(zhǎng),就會(huì)導(dǎo)致key過(guò)期了但是程序還沒(méi)執(zhí)行完。假如這個(gè)時(shí)候下一個(gè)請(qǐng)求進(jìn)來(lái)就有可能獲取到鎖,這個(gè)時(shí)候執(zhí)行這個(gè)方法的線程就不是安全的了。為了解決這個(gè)問(wèn)題redission引入了redlock。
關(guān)于這個(gè)問(wèn)題,目前常見(jiàn)的解決方法有兩種:
1、守護(hù)線程“續(xù)命”:額外起一個(gè)線程,定期檢查線程是否還持有鎖,如果有則延長(zhǎng)過(guò)期時(shí)間。Redisson 里面就實(shí)現(xiàn)了這個(gè)方案,使用“看門(mén)狗”定期檢查(每1/3的鎖時(shí)間檢查1次),如果線程還持有鎖,則刷新過(guò)期時(shí)間。
2、超時(shí)回滾:當(dāng)我們解鎖時(shí)發(fā)現(xiàn)鎖已經(jīng)被其他線程獲取了,說(shuō)明此時(shí)我們執(zhí)行的操作已經(jīng)是“不安全”的了,此時(shí)需要進(jìn)行回滾,并返回失敗。
同時(shí),需要進(jìn)行告警,人為介入驗(yàn)證數(shù)據(jù)的正確性,然后找出超時(shí)原因,是否需要對(duì)超時(shí)時(shí)間進(jìn)行優(yōu)化等等。
守護(hù)線程續(xù)命的方案有什么問(wèn)題嗎
Redisson 使用看門(mén)狗(守護(hù)線程)“續(xù)命”的方案在大多數(shù)場(chǎng)景下是挺不錯(cuò)的,也被廣泛應(yīng)用于生產(chǎn)環(huán)境,但是在極端情況下還是會(huì)存在問(wèn)題。
問(wèn)題例子如下: 1、線程1首先獲取鎖成功,將鍵值對(duì)寫(xiě)入 redis 的 master 節(jié)點(diǎn) 2、在 redis 將該鍵值對(duì)同步到 slave 節(jié)點(diǎn)之前,master 發(fā)生了故障 3、redis 觸發(fā)故障轉(zhuǎn)移,其中一個(gè) slave 升級(jí)為新的 master 4、此時(shí)新的 master 并不包含線程1寫(xiě)入的鍵值對(duì),因此線程2嘗試獲取鎖也可以成功拿到鎖 5、此時(shí)相當(dāng)于有兩個(gè)線程獲取到了鎖,可能會(huì)導(dǎo)致各種預(yù)期之外的情況發(fā)生,例如最常見(jiàn)的臟數(shù)據(jù)
解決方法:上述問(wèn)題的根本原因主要是由于 redis 異步復(fù)制帶來(lái)的數(shù)據(jù)不一致問(wèn)題導(dǎo)致的,因此解決的方向就是保證數(shù)據(jù)的一致。 當(dāng)前比較主流的解法和思路有兩種:
1)Redis 作者提出的 RedLock; 2)Zookeeper 實(shí)現(xiàn)的分布式鎖。
接下來(lái)介紹下這兩種方案。
RedLock
首先,該方案也是基于文章開(kāi)頭的那個(gè)方案(set加鎖、lua腳本解鎖)進(jìn)行改良的,所以 antirez 只描述了差異的地方,大致方案如下。
假設(shè)我們有 N 個(gè) Redis 主節(jié)點(diǎn),例如 N = 5,這些節(jié)點(diǎn)是完全獨(dú)立的,我們不使用復(fù)制或任何其他隱式協(xié)調(diào)系統(tǒng),為了取到鎖,客戶(hù)端應(yīng)該執(zhí)行以下操作:
1、獲取當(dāng)前時(shí)間,以毫秒為單位。
2、依次嘗試從5個(gè)實(shí)例,使用相同的 key 和隨機(jī)值(例如UUID)獲取鎖。當(dāng)向Redis 請(qǐng)求獲取鎖時(shí),客戶(hù)端應(yīng)該設(shè)置一個(gè)超時(shí)時(shí)間,這個(gè)超時(shí)時(shí)間應(yīng)該小于鎖的失效時(shí)間。例如你的鎖自動(dòng)失效時(shí)間為10秒,則超時(shí)時(shí)間應(yīng)該在 5-50 毫秒之間。這樣可以防止客戶(hù)端在試圖與一個(gè)宕機(jī)的 Redis 節(jié)點(diǎn)對(duì)話時(shí)長(zhǎng)時(shí)間處于阻塞狀態(tài)。如果一個(gè)實(shí)例不可用,客戶(hù)端應(yīng)該盡快嘗試去另外一個(gè)Redis實(shí)例請(qǐng)求獲取鎖。
3、客戶(hù)端通過(guò)當(dāng)前時(shí)間減去步驟1記錄的時(shí)間來(lái)計(jì)算獲取鎖使用的時(shí)間。當(dāng)且僅當(dāng)從大多數(shù)(N/2+1,這里是3個(gè)節(jié)點(diǎn))的Redis節(jié)點(diǎn)都取到鎖,并且獲取鎖使用的時(shí)間小于鎖失效時(shí)間時(shí),鎖才算獲取成功。
4、如果取到了鎖,其有效時(shí)間等于有效時(shí)間減去獲取鎖所使用的時(shí)間(步驟3計(jì)算的結(jié)果)。
5、如果由于某些原因未能獲得鎖(無(wú)法在至少N/2+1個(gè)Redis實(shí)例獲取鎖、或獲取鎖的時(shí)間超過(guò)了有效時(shí)間),客戶(hù)端應(yīng)該在所有的Redis實(shí)例上進(jìn)行解鎖(即便某些Redis實(shí)例根本就沒(méi)有加鎖成功,防止某些節(jié)點(diǎn)獲取到鎖但是客戶(hù)端沒(méi)有得到響應(yīng)而導(dǎo)致接下來(lái)的一段時(shí)間不能被重新獲取鎖)。
可以看出,該方案為了解決數(shù)據(jù)不一致的問(wèn)題,直接舍棄了異步復(fù)制,只使用 master 節(jié)點(diǎn),同時(shí)由于舍棄了 slave,為了保證可用性,引入了 N 個(gè)節(jié)點(diǎn),官方建議是 5。
該方案看著挺美好的,但是實(shí)際上我所了解到的在實(shí)際生產(chǎn)上應(yīng)用的不多,主要有兩個(gè)原因:
1)該方案的成本似乎有點(diǎn)高,需要使用5個(gè)實(shí)例;
2)該方案一樣存在問(wèn)題。
該方案主要存以下問(wèn)題:
1)嚴(yán)重依賴(lài)系統(tǒng)時(shí)鐘。如果線程1從3個(gè)實(shí)例獲取到了鎖,但是這3個(gè)實(shí)例中的某個(gè)實(shí)例的系統(tǒng)時(shí)間走的稍微快一點(diǎn),則它持有的鎖會(huì)提前過(guò)期被釋放,當(dāng)他釋放后,此時(shí)又有3個(gè)實(shí)例是空閑的,則線程2也可以獲取到鎖,則可能出現(xiàn)兩個(gè)線程同時(shí)持有鎖了。
2)如果線程1從3個(gè)實(shí)例獲取到了鎖,但是萬(wàn)一其中有1臺(tái)重啟了,則此時(shí)又有3個(gè)實(shí)例是空閑的,則線程2也可以獲取到鎖,此時(shí)又出現(xiàn)兩個(gè)線程同時(shí)持有鎖了。
針對(duì)以上問(wèn)題其實(shí)后續(xù)也有人給出一些相應(yīng)的解法,但是整體上來(lái)看還是不夠完美,所以目前實(shí)際應(yīng)用得不是那么多。
數(shù)據(jù)庫(kù)樂(lè)觀鎖和悲觀鎖
樂(lè)觀鎖
基本原理為:樂(lè)觀鎖一般通過(guò) version 來(lái)實(shí)現(xiàn),也就是在數(shù)據(jù)庫(kù)表創(chuàng)建一個(gè) version 字段,每次更新成功,則 version+1,讀取數(shù)據(jù)時(shí),我們將 version 字段一并讀出,每次更新時(shí)將會(huì)對(duì)版本號(hào)進(jìn)行比較,如果一致則執(zhí)行此操作,否則更新失??!
樂(lè)觀鎖的簡(jiǎn)單場(chǎng)景描述: 訂單服務(wù)有A、B兩臺(tái)服務(wù)器,使用Nginx輪詢(xún)?cè)L問(wèn)A、B。兩臺(tái)服務(wù)的數(shù)據(jù)庫(kù)數(shù)據(jù)庫(kù)都是D. 假如同時(shí)有兩個(gè)用戶(hù)U1和U2對(duì)一個(gè)商品下單,同時(shí)進(jìn)入到下單方法需要扣減庫(kù)存,那么如何保證同一時(shí)間只能有一個(gè)用戶(hù)可以下單扣減庫(kù)存成功呢?
提交訂單的時(shí)候會(huì)帶上當(dāng)前的樂(lè)觀鎖版本號(hào),在進(jìn)入到下單的方法中的時(shí)候,比對(duì)傳過(guò)來(lái)的版本號(hào)和數(shù)據(jù)庫(kù)中的版本號(hào)是否一致,如果一致則調(diào)用扣減庫(kù)存服務(wù)。否則購(gòu)買(mǎi)失敗,用戶(hù)需要重新下單。如果購(gòu)買(mǎi)成功的話需要庫(kù)存-1的同時(shí)樂(lè)觀鎖的版本號(hào)也需要+1,這樣同一時(shí)間只能有一個(gè)用戶(hù)可以下單成功扣減庫(kù)存。
數(shù)據(jù)庫(kù)更新版本號(hào)的SQL必須這樣寫(xiě)
update set NAME='XXX' ... , version = version +1 where version ='傳參:期望值,就是你一開(kāi)始查數(shù)據(jù)的時(shí)候查出的version ' and ...
缺點(diǎn): 同時(shí)只能有一個(gè)用戶(hù)下單成功,失敗了之后用戶(hù)就需要重新進(jìn)入訂單獲取新的版本號(hào)。假如說(shuō)是有1000個(gè)人同時(shí)下單,那么可能這個(gè)時(shí)間只有1個(gè)人能成功,其他人都會(huì)失敗,失敗之后需要自己去重試或者用戶(hù)重新點(diǎn)擊。
悲觀鎖
//SELECT * FROM xxxx WHERE id=31212221321123 FOR UPDATE; begin;/begin work;/start transaction; (三者選一就可以) //1.查詢(xún)出商品信息 select goods_status from goods where id=1 forupdate; //2.根據(jù)商品信息生成訂單 insert into orders (goods_id,goods_count) values (3,5); //3.修改商品status為2 update goods set status=2; //4.提交事務(wù) commit; //commit work;
悲觀鎖是可以實(shí)現(xiàn)分布式鎖的,借助上述場(chǎng)景,我們?cè)谔峤挥唵蔚臅r(shí)候,先根據(jù)商品鎖住商品對(duì)應(yīng)的庫(kù)存記錄,在扣減完庫(kù)存之后提交事務(wù)釋放鎖。這個(gè)時(shí)候其實(shí)其他請(qǐng)求是會(huì)被阻塞的,等到上一個(gè)用戶(hù)購(gòu)買(mǎi)成功之后釋放鎖,下個(gè)請(qǐng)求線程競(jìng)爭(zhēng)就會(huì)拿到鎖,進(jìn)行商品庫(kù)存的扣減。
xxljob就是使用悲觀鎖來(lái)做分布式鎖,來(lái)控制任務(wù)的觸發(fā)。
缺點(diǎn): 1.多服務(wù)必須同一個(gè)數(shù)據(jù)庫(kù),加鎖的記錄必須在同一張表中,假如說(shuō)有分表那就不能用了。
2.性能低。 3.注意扣減庫(kù)存的SQL要在同一個(gè)數(shù)據(jù)庫(kù)連接中。
Zookeeper
Zookeeper 的分布式鎖實(shí)現(xiàn)方案如下: 1、創(chuàng)建一個(gè)鎖目錄 /locks,該節(jié)點(diǎn)為持久節(jié)點(diǎn)
2、想要獲取鎖的線程都在鎖目錄下創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn)
3、獲取鎖目錄下所有子節(jié)點(diǎn),對(duì)子節(jié)點(diǎn)按節(jié)點(diǎn)自增序號(hào)從小到大排序
4、判斷本節(jié)點(diǎn)是不是第一個(gè)子節(jié)點(diǎn),如果是,則成功獲取鎖,開(kāi)始執(zhí)行業(yè)務(wù)邏輯操作;如果不是,則監(jiān)聽(tīng)自己的上一個(gè)節(jié)點(diǎn)的刪除事件
5、持有鎖的線程釋放鎖,只需刪除當(dāng)前節(jié)點(diǎn)即可。
6、當(dāng)自己監(jiān)聽(tīng)的節(jié)點(diǎn)被刪除時(shí),監(jiān)聽(tīng)事件觸發(fā),則回到第3步重新進(jìn)行判斷,直到獲取到鎖。
由于 Zookeeper 保證了數(shù)據(jù)的強(qiáng)一致性,因此不會(huì)存在之前 Redis 方案中的問(wèn)題,整體上來(lái)看還是比較不錯(cuò)的。
Zookeeper 方案的主要問(wèn)題在于性能不如 Redis 那么好,當(dāng)申請(qǐng)鎖和釋放鎖的頻率較高時(shí),會(huì)對(duì)集群造成壓力,此時(shí)集群的穩(wěn)定性可用性能可能又會(huì)遭受挑戰(zhàn)。
總結(jié)
通過(guò)以上的實(shí)例可以得出以下結(jié)論: 通過(guò)數(shù)據(jù)庫(kù)實(shí)現(xiàn)分布式鎖是最不可靠的一種方式,對(duì)數(shù)據(jù)庫(kù)依賴(lài)較大,性能較低,不利于處理高并發(fā)的場(chǎng)景。
通過(guò) Redis 的 Redlock 和 ZooKeeper 來(lái)加鎖,性能有了比較大的提升。
針對(duì) Redlock,曾經(jīng)有位大神對(duì)其實(shí)現(xiàn)的分布式鎖提出了質(zhì)疑,但是 Redis 官方卻不認(rèn)可其說(shuō)法,所謂公說(shuō)公有理婆說(shuō)婆有理,對(duì)于分布式鎖的解決方案,沒(méi)有最好,只有最適合的,根據(jù)不同的項(xiàng)目采取不同方案才是最合理的。
下面是從各個(gè)方面進(jìn)行三種實(shí)現(xiàn)方式的對(duì)比
從理解的難易程度角度(從低到高)
數(shù)據(jù)庫(kù) > 緩存 > Zookeeper
從實(shí)現(xiàn)的復(fù)雜性角度(從低到高)
Zookeeper >= 緩存 > 數(shù)據(jù)庫(kù)
從性能角度(從高到低)
緩存 > Zookeeper >= 數(shù)據(jù)庫(kù)
從可靠性角度(從高到低)
Zookeeper > 緩存 > 數(shù)據(jù)庫(kù)
到此這篇關(guān)于淺談Java中的分布式鎖的文章就介紹到這了,更多相關(guān)Java分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java創(chuàng)建可執(zhí)行JAR文件的多種方式
本文主要介紹了Java創(chuàng)建可執(zhí)行JAR文件的多種方式,使用JDK的jar工具、IDE、Maven和Gradle來(lái)創(chuàng)建和配置可執(zhí)行JAR文件,具有一定的參考價(jià)值,感興趣的可以了解一下2024-07-07SpringBoot工程下Lombok的應(yīng)用教程詳解
這篇文章主要給大家介紹了關(guān)于SpringBoot工程下Lombok應(yīng)用的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11k8s部署的java服務(wù)添加idea調(diào)試參數(shù)的方法
文章介紹了如何在K8S容器中的Java服務(wù)上進(jìn)行遠(yuǎn)程調(diào)試,包括配置Deployment、Service以及本地IDEA的調(diào)試設(shè)置,感興趣的朋友跟隨小編一起看看吧2025-02-02java實(shí)現(xiàn)商品信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)商品信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-11-11Springboot結(jié)合rabbitmq實(shí)現(xiàn)的死信隊(duì)列
為了保證訂單業(yè)務(wù)的消息數(shù)據(jù)不丟失,需要使用到RabbitMQ的死信隊(duì)列機(jī)制,本文主要介紹了Springboot結(jié)合rabbitmq實(shí)現(xiàn)的死信隊(duì)列,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09Java 垃圾回收機(jī)制詳解(動(dòng)力節(jié)點(diǎn)Java學(xué)院整理)
在系統(tǒng)運(yùn)行過(guò)程中,會(huì)產(chǎn)生一些無(wú)用的對(duì)象,這些對(duì)象占據(jù)著一定的內(nèi)存,如果不對(duì)這些對(duì)象清理回收無(wú)用對(duì)象的內(nèi)存,可能會(huì)導(dǎo)致內(nèi)存的耗盡,所以垃圾回收機(jī)制回收的是內(nèi)存。下面通過(guò)本文給大家詳細(xì)介紹java垃圾回收機(jī)制,一起學(xué)習(xí)吧2017-02-02Java調(diào)用Deepseek實(shí)現(xiàn)項(xiàng)目代碼審查
這篇文章主要為大家詳細(xì)介紹了Java如何調(diào)用Deepseek實(shí)現(xiàn)項(xiàng)目代碼審查功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-02-02