Java Arraylist在多線程環(huán)境下的問題與解決方案
一、ArrayList 的線程不安全性
ArrayList 的所有方法都沒有進(jìn)行同步控制,多個(gè)線程同時(shí)添加、刪除、修改同一個(gè) ArrayList 實(shí)例時(shí),會(huì)導(dǎo)致:
- 數(shù)據(jù)不一致:例如兩個(gè)線程同時(shí)添加元素,可能導(dǎo)致 size 值錯(cuò)誤,甚至覆蓋彼此的數(shù)據(jù)。
- 數(shù)組越界異常:內(nèi)部數(shù)組擴(kuò)容時(shí),多個(gè)線程同時(shí)操作可能導(dǎo)致
ArrayIndexOutOfBoundsException。 - ConcurrentModificationException:當(dāng)一個(gè)線程正在迭代,另一個(gè)線程修改了列表結(jié)構(gòu)(如添加或刪除元素)時(shí),會(huì)快速失敗拋出該異常。
List<Integer> list = new ArrayList<>();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
executor.submit(() -> list.add(1));
}
executor.shutdown();
// 結(jié)果:可能拋出異常,或最終 size 不等于 1000
二、為什么它是線程不安全的
ArrayList 是線程不安全的,根本原因在于其內(nèi)部實(shí)現(xiàn)沒有對(duì)共享數(shù)據(jù)的并發(fā)訪問進(jìn)行任何同步控制,導(dǎo)致多線程同時(shí)修改時(shí)會(huì)出現(xiàn)數(shù)據(jù)競(jìng)爭(zhēng)。
2.1 內(nèi)部數(shù)據(jù)結(jié)構(gòu)
ArrayList 底層是一個(gè) Object 數(shù)組(elementData)和一個(gè) int 類型的 size 字段,用于記錄實(shí)際元素個(gè)數(shù):

所有修改操作如 add、remove都會(huì)直接操作這個(gè)數(shù)組和 size。
2.2 并發(fā)添加時(shí)的競(jìng)態(tài)條件
假設(shè)兩個(gè)線程同時(shí)執(zhí)行 list.add(e),該方法大致步驟如下:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 檢查是否需要擴(kuò)容
elementData[size++] = e; // 插入元素并 size 自增
return true;
}
1. 擴(kuò)容檢查
ensureCapacityInternal(size + 1)會(huì)讀取當(dāng)前 size,判斷數(shù)組是否已滿。- 如果兩個(gè)線程同時(shí)發(fā)現(xiàn)數(shù)組還有空間(比如 size=5,容量=10),它們都會(huì)認(rèn)為無(wú)需擴(kuò)容,然后繼續(xù)向下執(zhí)行。
- 但若兩個(gè)線程同時(shí)發(fā)現(xiàn)需要擴(kuò)容,它們可能各自進(jìn)行擴(kuò)容操作,最終只有一個(gè)數(shù)組被保留,另一個(gè)線程使用的數(shù)組可能被覆蓋,導(dǎo)致數(shù)據(jù)丟失。
2. 數(shù)組越界異常
更常見的情況是:兩個(gè)線程在擴(kuò)容后都準(zhǔn)備插入元素:
- 線程 A 執(zhí)行
elementData[size] = e時(shí),size 還是舊值(比如 5); - 線程 B 也執(zhí)行同樣的操作,可能線程 A 還沒更新 size,線程 B 仍然使用舊 size(5)寫入同一位置,覆蓋了線程 A 的數(shù)據(jù);
- 或者線程 B 在寫入前,線程 A 已經(jīng)更新 size 為 6,線程 B 再寫入時(shí)使用的索引 5 已經(jīng)合法,但最終 size 可能只增加了一次,導(dǎo)致少計(jì)一個(gè)元素。
- 極端情況:如果兩個(gè)線程同時(shí)寫入同一個(gè)數(shù)組下標(biāo),并且數(shù)組已滿且都觸發(fā)了擴(kuò)容,可能最終寫入時(shí)使用的數(shù)組引用不一致,導(dǎo)致
ArrayIndexOutOfBoundsException。
3. size 的非原子操作
size++ 實(shí)際上分為三步:讀取 size → 加 1 → 寫回 size。多線程環(huán)境下,這些步驟可能交錯(cuò)執(zhí)行,導(dǎo)致:
- 兩個(gè)線程都讀取到 size=5,各自加 1 后寫回 6,最終 size 為 6,但實(shí)際插入了兩個(gè)元素,丟失一次更新。
- 最終
size值小于實(shí)際元素個(gè)數(shù),或數(shù)組中有空位(null),后續(xù)操作可能出現(xiàn)問題。
三、實(shí)際測(cè)試
我們看一段如下的java代碼
/**
* 演示 ArrayList 多線程并發(fā)問題
*/
@Test
public void testArrayListConcurrencyIssue() throws InterruptedException {
List<Integer> list = new ArrayList<>();
int threadCount = 5;
CountDownLatch latch = new CountDownLatch(threadCount);
// 創(chuàng)建 5 個(gè)線程,每個(gè)線程添加 1000 個(gè)元素
for (int i = 0; i < threadCount; i++) {
final int threadId = i;
new Thread(() -> {
try {
for (int j = 0; j < 1000; j++) {
list.add(threadId * 1000 + j);
}
} catch (Exception e) {
System.err.println("線程異常:" + e);
} finally {
latch.countDown();
}
}).start();
}
latch.await();
System.out.println("預(yù)期大?。? + (threadCount * 1000));
System.out.println("實(shí)際大?。? + list.size());
System.out.println("丟失元素:" + (threadCount * 1000 - list.size()));
if (list.size() < threadCount * 1000) {
System.out.println("? 檢測(cè)到線程安全問題:數(shù)據(jù)丟失!");
}
}
其運(yùn)行結(jié)果如下:

四、解決方案
關(guān)于ArrayList多線程并發(fā)解決方案還是很多的,但是各有優(yōu)缺點(diǎn),我們來(lái)一個(gè)一個(gè)介紹。
4.1 Collections.synchronizedList 同步包裝器
它是將普通 ArrayList 轉(zhuǎn)換為線程安全版本最直接的手段。synchronizedList 的核心設(shè)計(jì)思路是 裝飾器模式。它并沒有重新實(shí)現(xiàn)一個(gè) List,而是將原有的 ArrayList 包裹起來(lái),然后在每個(gè)方法的實(shí)現(xiàn)上都加上 synchronized 代碼塊,通過同一把 互斥鎖 來(lái)保證線程安全。
// Collections類中的靜態(tài)內(nèi)部類
static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> {
final List<E> list; // 被包裝的原始ArrayList
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex); // 將mutex(鎖對(duì)象)傳給父類
this.list = list;
}
public E get(int index) {
synchronized (mutex) { // 獲取鎖
return list.get(index); // 調(diào)用原ArrayList的方法
} // 釋放鎖
}
public void add(int index, E element) {
synchronized (mutex) { // 獲取鎖
list.add(index, element); // 調(diào)用原ArrayList的方法
} // 釋放鎖
}
// ... 其他所有方法都是同樣的模式
}
優(yōu)點(diǎn)
- 使用簡(jiǎn)單:一行代碼就能把非線程安全的
ArrayList包裝成線程安全的。 - 強(qiáng)一致性:由于每次操作都加鎖,你能獲得 強(qiáng)一致性 的數(shù)據(jù)視圖(只要操作完成,其他線程立即可見)。
- 兼容性好:返回的 List 實(shí)現(xiàn)了
RandomAccess接口(如果原 List 實(shí)現(xiàn)了),所以隨機(jī)訪問的性能和ArrayList一樣好。
缺點(diǎn)
- 性能瓶頸:相當(dāng)于將并發(fā)操作強(qiáng)制變成了串行操作。在并發(fā)量高的時(shí)候,這就是一個(gè)巨大的性能瓶頸。
- 粗粒度鎖:鎖的范圍是整個(gè) List 對(duì)象,無(wú)法進(jìn)行并發(fā)讀?。ㄗx寫互斥,讀讀也互斥)。
同時(shí)它也有著大量的其他問題,例如復(fù)合操作不具備原子性。即使使用了 synchronizedList,下面這段代碼在多線程環(huán)境下仍然是錯(cuò)誤的:
List<String> list = Collections.synchronizedList(new ArrayList<>());
// ... 假設(shè)list中已經(jīng)有了一些元素
// 在多線程環(huán)境下執(zhí)行這段代碼
if (!list.contains("a")) { // 檢查操作(已加鎖)
list.add("b"); // 添加操作(已加鎖)
}
contains 和 add 雖然是兩個(gè)原子操作,但組合在一起就不是原子操作了。在 contains 檢查通過后、add 執(zhí)行之前,可能有另一個(gè)線程插進(jìn)來(lái)添加了該元素,導(dǎo)致最終重復(fù)添加。
此時(shí)你需要手動(dòng)使用同一個(gè)鎖對(duì)象來(lái)保證復(fù)合操作的原子性:
// 正確做法:使用list對(duì)象本身作為鎖,鎖住整個(gè)操作塊
synchronized (list) {
if (!list.contains("特定元素")) {
list.add("特定元素");
}
}
因?yàn)?nbsp;synchronizedList 內(nèi)部使用的是 this 作為鎖,所以外部用 synchronized (list) 可以保證與內(nèi)部方法使用的是同一把鎖。這個(gè)是多線程的知識(shí)點(diǎn),如果有不會(huì)的可以翻翻我寫的關(guān)于多線程的文章。
其次的問題就是遍歷時(shí)需要手動(dòng)加鎖。當(dāng)使用迭代器遍歷 synchronizedList 時(shí),必須在外層加鎖。
List<String> list = Collections.synchronizedList(new ArrayList<>());
// 錯(cuò)誤示例:會(huì)拋出 ConcurrentModificationException
// 因?yàn)榈鞅闅v期間,另一個(gè)線程可能修改了list
for (String item : list) {
// 處理item
}
// 正確示例
synchronized (list) {
for (String item : list) { // 在鎖的保護(hù)下遍歷
// 處理item
}
}
這是因?yàn)?nbsp;SynchronizedList 的 iterator() 方法本身并沒有加鎖,它返回的迭代器在遍歷過程中,如果有其他線程修改了 List,依然會(huì)觸發(fā)快速失敗機(jī)制。
綜上,如果在使用場(chǎng)景是讀寫比例均衡,或需要強(qiáng)一致性的場(chǎng)景,可以考慮使用Collections.synchronizedList,但是需要記住在遍歷或者任何復(fù)合操作的情況下,都需要手動(dòng)加鎖來(lái)保證原子性。
/**
* 使用 Collections.synchronizedList 解決并發(fā)問題
*/
@Test
public void testSynchronizedList() throws InterruptedException {
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
int threadCount = 10;
int opsPerThread = 50000;
CountDownLatch latch = new CountDownLatch(threadCount);
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
final int threadId = i;
new Thread(() -> {
try {
for (int j = 0; j < opsPerThread; j++) {
synchronized (list) {
list.add(threadId * opsPerThread + j);
}
}
} finally {
latch.countDown();
}
}).start();
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println("預(yù)期大小:" + (threadCount * opsPerThread));
System.out.println("實(shí)際大?。? + list.size());
System.out.println("執(zhí)行時(shí)間:" + (endTime - startTime) + "ms");
if (list.size() == threadCount * opsPerThread) {
System.out.println("? synchronizedList 保證線程安全!");
}
}

4.2 CopyOnWriteArrayList 并發(fā)集合
這是Java并發(fā)包(JUC)中專門為讀多寫極少場(chǎng)景量身定做的一個(gè)方法。它的設(shè)計(jì)思想非常巧妙,采用了不變性和寫時(shí)復(fù)制策略,徹底解決了并發(fā)沖突的問題。
CopyOnWriteArrayList 的核心思想非常簡(jiǎn)單直觀:每當(dāng)需要對(duì)列表進(jìn)行修改(增、刪、改)時(shí),不直接修改原始數(shù)組,而是先復(fù)制一份快照,在快照上修改,修改完成后再將原數(shù)組的引用指向這個(gè)新的數(shù)組。
public class CopyOnWriteArrayList<E> {
// 關(guān)鍵:使用 volatile 修飾的數(shù)組,保證修改后對(duì)其他線程的可見性
private transient volatile Object[] array;
// 添加元素的方法
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 寫操作必須加鎖,防止并發(fā)修改時(shí)復(fù)制出多個(gè)副本
try {
Object[] elements = getArray(); // 獲取當(dāng)前數(shù)組
int len = elements.length;
// 核心:復(fù)制一個(gè)新數(shù)組(長(zhǎng)度+1)
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e; // 在新數(shù)組上執(zhí)行添加操作
setArray(newElements); // 將新數(shù)組設(shè)為當(dāng)前數(shù)組
return true;
} finally {
lock.unlock();
}
}
// 讀取元素的方法(沒有加鎖)
public E get(int index) {
return get(getArray(), index); // 直接從當(dāng)前數(shù)組中獲取
}
// 返回當(dāng)前數(shù)組的快照
final Object[] getArray() {
return array;
}
}
這種設(shè)計(jì)帶來(lái)的兩個(gè)核心特性:
- 讀操作無(wú)鎖:讀線程永遠(yuǎn)不需要加鎖,因?yàn)樗鼈冊(cè)L問的是當(dāng)前時(shí)刻的數(shù)組快照。即使此時(shí)有寫線程正在復(fù)制新數(shù)組,也完全不影響讀線程訪問舊數(shù)組。
- 數(shù)據(jù)弱一致性:迭代器一旦被創(chuàng)建,它遍歷的就是創(chuàng)建時(shí)刻的那個(gè)數(shù)組快照。遍歷過程中,其他線程對(duì)列表的修改(即使已經(jīng)提交)對(duì)當(dāng)前迭代器是不可見的。這被稱為 "弱一致性" 。
優(yōu)點(diǎn)
- 極高的讀并發(fā):讀操作完全不阻塞,也不互斥。這在讀多寫少的場(chǎng)景下,性能遠(yuǎn)超
Collections.synchronizedList。 - 迭代安全:永遠(yuǎn)不會(huì)拋出
ConcurrentModificationException。因?yàn)榈鞑僮鞯氖仟?dú)立的數(shù)組快照。
缺點(diǎn)
- 內(nèi)存開銷:每次修改都要復(fù)制整個(gè)數(shù)組。如果列表很大(比如上萬(wàn)元素),頻繁復(fù)制會(huì)造成巨大的內(nèi)存壓力(老數(shù)組和正在構(gòu)建的新數(shù)組同時(shí)存在于內(nèi)存中),甚至引發(fā)頻繁的GC。
- 數(shù)據(jù)延遲:寫線程修改數(shù)據(jù)后,并不能保證讀線程立即看到最新數(shù)據(jù)。因?yàn)樽x線程訪問的可能還是舊的數(shù)組快照。不過由于
volatile變量的語(yǔ)義,這個(gè)"不可見"的時(shí)間窗口非常短(寫完成后,后續(xù)的讀操作一定能看到)。 - 不適合寫頻繁場(chǎng)景:如果寫操作較多,復(fù)制數(shù)組的開銷會(huì)急劇上升,性能可能反而不如
synchronizedList。
綜上,如果你的業(yè)務(wù)要求嚴(yán)格的實(shí)時(shí)一致性(比如支付扣款后的余額查詢),CopyOnWriteArrayList 就不適合了。
| 特性 | CopyOnWriteArrayList | Collections.synchronizedList |
|---|---|---|
| 實(shí)現(xiàn)原理 | 空間換時(shí)間:寫時(shí)復(fù)制,讀寫分離 | 時(shí)間換安全:所有操作串行化 |
| 讀鎖 | 無(wú)鎖 | 有鎖(讀讀互斥) |
| 寫鎖 | 有鎖(用ReentrantLock控制) | 有鎖 |
| 內(nèi)存占用 | 高(每次寫創(chuàng)建新數(shù)組) | 低 |
| 數(shù)據(jù)一致性 | 弱一致性(迭代器快照) | 強(qiáng)一致性(鎖保護(hù)) |
| 迭代異常 | 永不拋出 ConcurrentModificationException | 遍歷期間如有修改會(huì)拋出異常 |
| 最佳場(chǎng)景 | 讀多寫極少(配置、白名單、監(jiān)聽器列表) | 讀寫均衡,或需要強(qiáng)一致性的場(chǎng)景 |
/**
* 演示使用 CopyOnWriteArrayList 解決并發(fā)問題
*/
@Test
public void testThreadSafeList() throws InterruptedException {
List<Integer> list = new CopyOnWriteArrayList<>();
int threadCount = 10;
int opsPerThread = 50000;
CountDownLatch latch = new CountDownLatch(threadCount);
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
final int threadId = i;
new Thread(() -> {
try {
for (int j = 0; j < opsPerThread; j++) {
list.add(threadId * opsPerThread + j);
}
} finally {
latch.countDown();
}
}).start();
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println("預(yù)期大?。? + (threadCount * opsPerThread));
System.out.println("實(shí)際大?。? + list.size());
System.out.println("執(zhí)行時(shí)間:" + (endTime - startTime) + "ms");
if (list.size() == threadCount * opsPerThread) {
System.out.println("? 線程安全,數(shù)據(jù)完整!");
}else {
System.out.println("? 檢測(cè)到線程安全問題:數(shù)據(jù)不完整!");
}
}

通過上述代碼可以看到,同樣是50萬(wàn)量級(jí)的數(shù)據(jù),使用CopyOnWriteArrayList的運(yùn)行插入的時(shí)間是Collections.synchronizedList好幾百倍,所以我們一定要注意使用場(chǎng)景的問題。
我們?cè)賮?lái)用一個(gè)讀多寫少的場(chǎng)景對(duì)比一下
/**
* 讀多寫少場(chǎng)景:synchronizedList vs CopyOnWriteArrayList
*/
@Test
public void testReadHeavyScenario() throws InterruptedException {
int threadCount = 10;
int writeCount = 1000;
int readCount = 1000000;
System.out.println("=== synchronizedList 讀多寫少 ===");
long syncTime = testSynchronizedList(threadCount, writeCount, readCount);
System.out.println("\n=== CopyOnWriteArrayList 讀多寫少 ===");
long cowTime = testCopyOnWriteArrayList(threadCount, writeCount, readCount);
System.out.println("\n=== 性能對(duì)比 ===");
System.out.println("synchronizedList: " + syncTime + "ms");
System.out.println("CopyOnWriteArrayList: " + cowTime + "ms");
System.out.println("CopyOnWriteArrayList " + (syncTime > cowTime ? "更快" : "更慢") +
",提升了 " + String.format("%.2f", (double)(syncTime - cowTime) / syncTime * 100) + "%");
}
private long testSynchronizedList(int threadCount, int writeCount, int readCount) throws InterruptedException {
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
CountDownLatch latch = new CountDownLatch(threadCount);
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
final int threadId = i;
new Thread(() -> {
try {
for (int j = 0; j < writeCount; j++) {
synchronized (list) {
list.add(threadId * writeCount + j);
}
}
for (int j = 0; j < readCount; j++) {
synchronized (list) {
if (!list.isEmpty()) {
list.get(list.size() - 1);
}
}
}
} finally {
latch.countDown();
}
}).start();
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println("寫+讀執(zhí)行時(shí)間:" + (endTime - startTime) + "ms,寫大?。? + list.size());
return endTime - startTime;
}
private long testCopyOnWriteArrayList(int threadCount, int writeCount, int readCount) throws InterruptedException {
List<Integer> list = new CopyOnWriteArrayList<>();
CountDownLatch latch = new CountDownLatch(threadCount);
long startTime = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
final int threadId = i;
new Thread(() -> {
try {
for (int j = 0; j < writeCount; j++) {
list.add(threadId * writeCount + j);
}
for (int j = 0; j < readCount; j++) {
if (!list.isEmpty()) {
list.get(list.size() - 1);
}
}
} finally {
latch.countDown();
}
}).start();
}
latch.await();
long endTime = System.currentTimeMillis();
System.out.println("寫+讀執(zhí)行時(shí)間:" + (endTime - startTime) + "ms,寫大小:" + list.size());
return endTime - startTime;
}

上面的程序是十個(gè)線程,寫入1萬(wàn),讀100萬(wàn),可以看到在讀百萬(wàn)量級(jí)數(shù)據(jù)的時(shí)候,用CopyOnWriteArrayList的時(shí)間幾乎是提升了40倍。
以上就是Java Arraylist在多線程環(huán)境下的問題與解決方案的詳細(xì)內(nèi)容,更多關(guān)于Java Arraylist線程不安全性的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
從原理到實(shí)戰(zhàn)詳解Java如何實(shí)現(xiàn)數(shù)據(jù)庫(kù)讀寫分離
隨著用戶量和數(shù)據(jù)量的爆發(fā)式增長(zhǎng),數(shù)據(jù)庫(kù)往往成為系統(tǒng)中最先出現(xiàn)性能瓶頸的環(huán)節(jié),本文全面解析數(shù)據(jù)庫(kù)讀寫分離技術(shù),從核心原理到實(shí)戰(zhàn)應(yīng)用,有需要的小伙伴可以了解下2026-06-06
SpringBoot實(shí)現(xiàn)動(dòng)態(tài)配置及項(xiàng)目打包部署上線功能
本文講解的是如何使用Spring動(dòng)態(tài)配置文件,實(shí)現(xiàn)不同環(huán)境不同配置,靈活切換配置文件;以及講述了如何使用?Maven?打包,然后上傳至Linux服務(wù)器進(jìn)行部署,對(duì)SpringBoot打包部署上線過程感興趣的朋友一起看看吧2022-10-10
復(fù)雜JSON字符串轉(zhuǎn)換為Java嵌套對(duì)象的實(shí)現(xiàn)
這篇文章主要介紹了復(fù)雜JSON字符串轉(zhuǎn)換為Java嵌套對(duì)象的實(shí)現(xiàn),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09
hashMap擴(kuò)容時(shí)應(yīng)該注意這些死循環(huán)問題
今天給大家?guī)?lái)的是關(guān)于Java的相關(guān)知識(shí),文章圍繞著hashMap擴(kuò)容時(shí)的死循環(huán)問題展開,文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06
Java?guava框架LoadingCache及CacheBuilder本地小容量緩存框架總結(jié)
Guava?Cache本地緩存框架主要是一種將本地?cái)?shù)據(jù)緩存到內(nèi)存中,但數(shù)據(jù)量并不能太大,否則將會(huì)占用過多的內(nèi)存,本文給大家介紹Java?guava框架?LoadingCache及CacheBuilder?本地小容量緩存框架總結(jié),感興趣的朋友一起看看吧2023-12-12
Java獲取時(shí)間打印到控制臺(tái)代碼實(shí)例
這篇文章主要介紹了Java獲取時(shí)間打印到控制臺(tái)代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02
JFileChooser實(shí)現(xiàn)對(duì)選定文件夾內(nèi)圖片自動(dòng)播放和暫停播放實(shí)例代碼
這篇文章主要介紹了JFileChooser實(shí)現(xiàn)對(duì)選定文件夾內(nèi)圖片自動(dòng)播放和暫停播放實(shí)例代碼,需要的朋友可以參考下2017-04-04
如何發(fā)布jar包到maven中央倉(cāng)庫(kù)
這篇文章主要介紹了發(fā)布jar包到maven中央倉(cāng)庫(kù)的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-12-12

