Java多線程之Semaphore實(shí)現(xiàn)信號(hào)燈
前言:
Semaphore是計(jì)數(shù)信號(hào)量。Semaphore管理一系列許可證。每個(gè)acquire方法阻塞,直到有一個(gè)許可證可以獲得然后拿走一個(gè)許可證;每個(gè)release方法增加一個(gè)許可證,這可能會(huì)釋放一個(gè)阻塞的acquire方法。然而,其實(shí)并沒有實(shí)際的許可證這個(gè)對(duì)象,Semaphore只是維持了一個(gè)可獲得許可證的數(shù)量。
Semaphore可以維護(hù)當(dāng)前訪問(wèn)自身的線程個(gè)數(shù),并提供了同步機(jī)制。使用Semaphore可以控制同時(shí)訪問(wèn)資源的線程個(gè)數(shù),例如,實(shí)現(xiàn)一個(gè)文件允許的并發(fā)訪問(wèn)數(shù)。
1 Semaphore的主要方法
Semaphore(int permits):構(gòu)造方法,創(chuàng)建具有給定許可數(shù)的計(jì)數(shù)信號(hào)量并設(shè)置為非公平信號(hào)量。
Semaphore(int permits,boolean fair):構(gòu)造方法,當(dāng)fair等于true時(shí),創(chuàng)建具有給定許可數(shù)的計(jì)數(shù)信號(hào)量并設(shè)置為公平信號(hào)量。
void acquire():當(dāng)前線程嘗試去阻塞的獲取1個(gè)許可證。
此過(guò)程是阻塞的,它會(huì)一直等待許可證,直到發(fā)生以下任意一件事:
- 當(dāng)前線程獲取了1個(gè)可用的許可證,則會(huì)停止等待,繼續(xù)執(zhí)行。
- 當(dāng)前線程被中斷,則會(huì)拋出
InterruptedException異常,并停止等待,繼續(xù)執(zhí)行。
void acquire(int n):從此信號(hào)量獲取給定數(shù)目許可,在提供這些許可前一直將線程阻塞。
當(dāng)前線程嘗試去阻塞的獲取多個(gè)許可證。
此過(guò)程是阻塞的,它會(huì)一直等待許可證,直到發(fā)生以下任意一件事:
- 當(dāng)前線程獲取了n個(gè)可用的許可證,則會(huì)停止等待,繼續(xù)執(zhí)行。
- 當(dāng)前線程被中斷,則會(huì)拋出
InterruptedException異常,并停止等待,繼續(xù)執(zhí)行。
void release():釋放一個(gè)許可,將其返回給信號(hào)量。
void release(int n):釋放n個(gè)許可。
int availablePermits():當(dāng)前可用的許可數(shù)。
void acquierUninterruptibly():當(dāng)前線程嘗試去阻塞的獲取1個(gè)許可證(不可中斷的)。
此過(guò)程是阻塞的,它會(huì)一直等待許可證,直到發(fā)生以下任意一件事:
- 當(dāng)前線程獲取了1個(gè)可用的許可證,則會(huì)停止等待,繼續(xù)執(zhí)行。
void acquireUninterruptibly(permits):當(dāng)前線程嘗試去阻塞的獲取多個(gè)許可證。
此過(guò)程是阻塞的,它會(huì)一直等待許可證,直到發(fā)生以下任意一件事:
- 當(dāng)前線程獲取了n個(gè)可用的許可證,則會(huì)停止等待,繼續(xù)執(zhí)行。
boolean tryAcquire()當(dāng)前線程嘗試去獲取1個(gè)許可證。
- 此過(guò)程是非阻塞的,它只是在方法調(diào)用時(shí)進(jìn)行一次嘗試。
- 如果當(dāng)前線程獲取了1個(gè)可用的許可證,則會(huì)停止等待,繼續(xù)執(zhí)行,并返回
true。 - 如果當(dāng)前線程沒有獲得這個(gè)許可證,也會(huì)停止等待,繼續(xù)執(zhí)行,并返回
false。
boolean tryAcquire(permits):當(dāng)前線程嘗試去獲取多個(gè)許可證。
- 此過(guò)程是非阻塞的,它只是在方法調(diào)用時(shí)進(jìn)行一次嘗試。
- 如果當(dāng)前線程獲取了
permits個(gè)可用的許可證,則會(huì)停止等待,繼續(xù)執(zhí)行,并返回true。 - 如果當(dāng)前線程沒有獲得
permits個(gè)許可證,也會(huì)停止等待,繼續(xù)執(zhí)行,并返回false。
boolean tryAcquire(timeout,TimeUnit):當(dāng)前線程在限定時(shí)間內(nèi),阻塞的嘗試去獲取1個(gè)許可證。
此過(guò)程是阻塞的,它會(huì)一直等待許可證,直到發(fā)生以下任意一件事:
- 當(dāng)前線程獲取了可用的許可證,則會(huì)停止等待,繼續(xù)執(zhí)行,并返回
true。 - 當(dāng)前線程等待時(shí)間
timeout超時(shí),則會(huì)停止等待,繼續(xù)執(zhí)行,并返回false。 - 當(dāng)前線程在
timeout時(shí)間內(nèi)被中斷,則會(huì)拋出InterruptedException一次,并停止等待,繼續(xù)執(zhí)行。
boolean tryAcquire(permits,timeout,TimeUnit):當(dāng)前線程在限定時(shí)間內(nèi),阻塞的嘗試去獲取permits個(gè)許可證。
此過(guò)程是阻塞的,它會(huì)一直等待許可證,直到發(fā)生以下任意一件事:
- 當(dāng)前線程獲取了可用的
permits個(gè)許可證,則會(huì)停止等待,繼續(xù)執(zhí)行,并返回true。 - 當(dāng)前線程等待時(shí)間
timeout超時(shí),則會(huì)停止等待,繼續(xù)執(zhí)行,并返回false。 - 當(dāng)前線程在
timeout時(shí)間內(nèi)被中斷,則會(huì)拋出InterruptedException一次,并停止等待,繼續(xù)執(zhí)行。
2 實(shí)例講解
public class SemaphoreTest {
private static final Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
Executor executor = Executors.newCachedThreadPool();
String[] name = {"Jack", "Pony", "Larry", "Martin", "James", "ZhangSan","Tree"};
int[] age = {21,22,23,24,25,26,27};
for(int i=0;i<7;i++)
{
Thread t1=new InformationThread(name[i],age[i]);
executor.execute(t1);
}
}
private static class InformationThread extends Thread {
private final String name;
private final int age;
public InformationThread(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()
+ ":大家好,我是" + name + "我今年" + age +
"當(dāng)前時(shí)間段為:" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(name + "要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:" + System.currentTimeMillis());
System.out.println("當(dāng)前可使用的許可數(shù)為:" + semaphore.availablePermits());
System.out.println("是否有正在等待許可證的線程:" + semaphore.hasQueuedThreads());
System.out.println("正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):" + semaphore.getQueueLength());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
pool-1-thread-1:大家好,我是Jack我今年21當(dāng)前時(shí)間段為:1543498535306
pool-1-thread-3:大家好,我是Larry我今年23當(dāng)前時(shí)間段為:1543498535306
pool-1-thread-2:大家好,我是Pony我今年22當(dāng)前時(shí)間段為:1543498535306
Pony要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498536310
Jack要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498536310
當(dāng)前可使用的許可數(shù)為:0
Larry要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498536310
是否有正在等待許可證的線程:true
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):4
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):4
當(dāng)前可使用的許可數(shù)為:0
pool-1-thread-4:大家好,我是Martin我今年24當(dāng)前時(shí)間段為:1543498536311
是否有正在等待許可證的線程:true
pool-1-thread-5:大家好,我是James我今年25當(dāng)前時(shí)間段為:1543498536311
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):2
pool-1-thread-6:大家好,我是ZhangSan我今年26當(dāng)前時(shí)間段為:1543498536312
James要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498537315
Martin要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498537315
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):1
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:false
pool-1-thread-7:大家好,我是Tree我今年27當(dāng)前時(shí)間段為:1543498537316
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):0
ZhangSan要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498537317
當(dāng)前可使用的許可數(shù)為:1
是否有正在等待許可證的線程:false
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):0
Tree要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498538319
當(dāng)前可使用的許可數(shù)為:2
是否有正在等待許可證的線程:false
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):0
以上是非公平信號(hào)量,將建立Semaphore對(duì)象的語(yǔ)句改為如下語(yǔ)句:
private static final Semaphore semaphore=new Semaphore(3,true);
pool-1-thread-1:大家好,我是Jack我今年21當(dāng)前時(shí)間段為:1543498810563
pool-1-thread-3:大家好,我是Larry我今年23當(dāng)前時(shí)間段為:1543498810564
pool-1-thread-2:大家好,我是Pony我今年22當(dāng)前時(shí)間段為:1543498810563
Jack要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498811564
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):4
pool-1-thread-4:大家好,我是Martin我今年24當(dāng)前時(shí)間段為:1543498811564
Larry要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498811568
當(dāng)前可使用的許可數(shù)為:0
Pony要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498811568
是否有正在等待許可證的線程:true
當(dāng)前可使用的許可數(shù)為:0
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):3
是否有正在等待許可證的線程:true
pool-1-thread-5:大家好,我是James我今年25當(dāng)前時(shí)間段為:1543498811568
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):2
pool-1-thread-6:大家好,我是ZhangSan我今年26當(dāng)前時(shí)間段為:1543498811568
Martin要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498812566
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):1
pool-1-thread-7:大家好,我是Tree我今年27當(dāng)前時(shí)間段為:1543498812566
James要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498812572
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:false
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):0
ZhangSan要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498812572
當(dāng)前可使用的許可數(shù)為:1
是否有正在等待許可證的線程:false
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):0
Tree要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543498813568
當(dāng)前可使用的許可數(shù)為:2
是否有正在等待許可證的線程:false
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):0
實(shí)現(xiàn)單例模式
將創(chuàng)建信號(hào)量對(duì)象語(yǔ)句修改如下:
private static final Semaphore semaphore=new Semaphore(1);
運(yùn)行程序,結(jié)果如下:
pool-1-thread-1:大家好,我是Jack我今年21當(dāng)前時(shí)間段為:1543499053898
Jack要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543499054903
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):6
pool-1-thread-2:大家好,我是Pony我今年22當(dāng)前時(shí)間段為:1543499054904
Pony要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543499055907
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):5
pool-1-thread-3:大家好,我是Larry我今年23當(dāng)前時(shí)間段為:1543499055907
Larry要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543499056909
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):4
pool-1-thread-4:大家好,我是Martin我今年24當(dāng)前時(shí)間段為:1543499056909
Martin要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543499057913
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):3
pool-1-thread-5:大家好,我是James我今年25當(dāng)前時(shí)間段為:1543499057913
James要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543499058914
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):2
pool-1-thread-6:大家好,我是ZhangSan我今年26當(dāng)前時(shí)間段為:1543499058915
ZhangSan要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543499059919
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:true
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):1
pool-1-thread-7:大家好,我是Tree我今年27當(dāng)前時(shí)間段為:1543499059919
Tree要準(zhǔn)備釋放許可證了,當(dāng)前時(shí)間為:1543499060923
當(dāng)前可使用的許可數(shù)為:0
是否有正在等待許可證的線程:false
正在等待許可證的隊(duì)列長(zhǎng)度(線程數(shù)量):0
如上可知,如果將給定許可數(shù)設(shè)置為1,就如同一個(gè)單例模式,即單個(gè)停車位,只有一輛車進(jìn),然后這輛車出來(lái)后,下一輛車才能進(jìn)。
3 源碼解析
Semaphore有兩種模式,公平模式和非公平模式。公平模式就是調(diào)用acquire的順序就是獲取許可證的順序,遵循FIFO;而非公平模式是搶占式的,也就是有可能一個(gè)新的獲取線程恰好在一個(gè)許可證釋放時(shí)得到了這個(gè)許可證,而前面還有等待的線程。
構(gòu)造方法
Semaphore有兩個(gè)構(gòu)造方法,如下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
獲取許可
先從獲取一個(gè)許可看起,并且先看非公平模式下的實(shí)現(xiàn)。首先看acquire方法,acquire方法有幾個(gè)重載,但主要是下面這個(gè)方法
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
從上面可以看到,調(diào)用了Sync的acquireSharedInterruptibly方法,該方法在父類AQS中,如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //如果線程被中斷了,拋出異常
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) //獲取許可失敗,將線程加入到等待隊(duì)列中
doAcquireSharedInterruptibly(arg);
}
AQS子類如果要使用共享模式的話,需要實(shí)現(xiàn)tryAcquireShared方法,下面看NonfairSync的該方法實(shí)現(xiàn):
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
該方法調(diào)用了父類中的nonfairTyAcquireShared方法,如下:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//獲取剩余許可數(shù)量
int available = getState();
//計(jì)算給完這次許可數(shù)量后的個(gè)數(shù)
int remaining = available - acquires;
//如果許可不夠或者可以將許可數(shù)量重置的話,返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
從上面可以看到,只有在許可不夠時(shí)返回值才會(huì)小于0,其余返回的都是剩余許可數(shù)量,這也就解釋了,一旦許可不夠,后面的線程將會(huì)阻塞??赐炅朔枪降墨@取,再看下公平的獲取,
代碼如下:
protected int tryAcquireShared(int acquires) {
for (;;) {
//如果前面有線程再等待,直接返回-1
if (hasQueuedPredecessors())
return -1;
//后面與非公平一樣
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
從上面可以看到,FairSync與NonFairSync的區(qū)別就在于會(huì)首先判斷當(dāng)前隊(duì)列中有沒有線程在等待,如果有,就老老實(shí)實(shí)進(jìn)入到等待隊(duì)列;而不像NonfairSync一樣首先試一把,說(shuō)不定就恰好獲得了一個(gè)許可,這樣就可以插隊(duì)了。
看完了獲取許可后,再看一下釋放許可。
釋放許可
釋放許可也有幾個(gè)重載方法,但都會(huì)調(diào)用下面這個(gè)帶參數(shù)的方法,
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
releaseShared方法在AQS中,如下:
public final boolean releaseShared(int arg) {
//如果改變?cè)S可數(shù)量成功
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
AQS子類實(shí)現(xiàn)共享模式的類需要實(shí)現(xiàn)tryReleaseShared類來(lái)判斷是否釋放成功,
實(shí)現(xiàn)如下:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
//獲取當(dāng)前許可數(shù)量
int current = getState();
//計(jì)算回收后的數(shù)量
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//CAS改變?cè)S可數(shù)量成功,返回true
if (compareAndSetState(current, next))
return true;
}
}
從上面可以看到,一旦CAS改變?cè)S可數(shù)量成功,那么就會(huì)調(diào)用doReleaseShared()方法釋放阻塞的線程。
減小許可數(shù)量
Semaphore還有減小許可數(shù)量的方法,該方法可以用于用于當(dāng)資源用完不能再用時(shí),這時(shí)就可以減小許可證。代碼如下:
protected void reducePermits(int reduction) {
if (reduction < 0) throw new IllegalArgumentException();
sync.reducePermits(reduction);
}
可以看到,委托給了Sync,Sync的reducePermits方法如下:
final void reducePermits(int reductions) {
for (;;) {
//得到當(dāng)前剩余許可數(shù)量
int current = getState();
//得到減完之后的許可數(shù)量
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
//如果CAS改變成功
if (compareAndSetState(current, next))
return;
}
}
從上面可以看到,就是CAS改變AQS中的state變量,因?yàn)樵撟兞看碓S可證的數(shù)量。
獲取剩余許可數(shù)量
Semaphore還可以一次將剩余的許可數(shù)量全部取走,該方法是drain方法,
如下:
public int drainPermits() {
return sync.drainPermits();
}
Sync的實(shí)現(xiàn)如下:
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
可以看到,就是CAS將許可數(shù)量置為0。
4、總結(jié)
Semaphore是信號(hào)量,用于管理一組資源。其內(nèi)部是基于AQS的共享模式,AQS的狀態(tài)表示許可證的數(shù)量,在許可證數(shù)量不夠時(shí),線程將會(huì)被掛起;而一旦有一個(gè)線程釋放一個(gè)資源,那么就有可能重新喚醒等待隊(duì)列中的線程繼續(xù)執(zhí)行。
到此這篇關(guān)于Java多線程之Semaphore實(shí)現(xiàn)信號(hào)燈的文章就介紹到這了,更多相關(guān)Java多線程 Semaphore實(shí)現(xiàn)信號(hào)燈內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java開源項(xiàng)目jeecgboot的超詳細(xì)解析
JeecgBoot是一款基于BPM的低代碼平臺(tái),下面這篇文章主要給大家介紹了關(guān)于java開源項(xiàng)目jeecgboot的相關(guān)資料,文中通過(guò)圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10
Eclipse中導(dǎo)入Maven Web項(xiàng)目并配置其在Tomcat中運(yùn)行圖文詳解
這篇文章主要介紹了Eclipse中導(dǎo)入Maven Web項(xiàng)目并配置其在Tomcat中運(yùn)行圖文詳解,需要的朋友可以參考下2017-12-12
Java Swing實(shí)現(xiàn)JTable檢測(cè)單元格數(shù)據(jù)變更事件的方法示例
這篇文章主要介紹了Java Swing實(shí)現(xiàn)JTable檢測(cè)單元格數(shù)據(jù)變更事件的方法,結(jié)合完整實(shí)例形式分析了Swing實(shí)現(xiàn)JTable檢測(cè)單元格數(shù)據(jù)變更事件過(guò)程中出現(xiàn)的問(wèn)題與相關(guān)解決方法,需要的朋友可以參考下2017-11-11
Java基于Scanner對(duì)象的簡(jiǎn)單輸入計(jì)算功能示例
這篇文章主要介紹了Java基于Scanner對(duì)象的簡(jiǎn)單輸入計(jì)算功能,結(jié)合實(shí)例形式分析了Java使用Scanner對(duì)象獲取用戶輸入半徑值計(jì)算圓形面積功能,需要的朋友可以參考下2018-01-01
MyBatis傳入?yún)?shù)為L(zhǎng)ist對(duì)象的實(shí)現(xiàn)
這篇文章主要介紹了MyBatis傳入?yún)?shù)為L(zhǎng)ist對(duì)象的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03

