Java并發(fā)之Semaphore源碼解析(二)

在上一章,我們學(xué)習(xí)了信號(hào)量(Semaphore)是如何請(qǐng)求許可證的,下面我們來(lái)看看要如何歸還許可證。

可以看到當(dāng)我們要?dú)w還許可證時(shí),不論是調(diào)用release()或是release(int permits),都會(huì)調(diào)用AQS實(shí)現(xiàn)的releaseShared(int arg)方法。在releaseShared(int arg)方法中會(huì)先調(diào)用子類實(shí)現(xiàn)的tryReleaseShared(int arg)方法,這個(gè)方法會(huì)向信號(hào)量歸還許可證,在歸還完畢后,會(huì)調(diào)用doReleaseShared()方法嘗試喚醒信號(hào)量等待隊(duì)列中需要許可證的線程,這也印證了筆者之前所說(shuō)的線程在歸還信號(hào)量后,會(huì)嘗試喚醒等待隊(duì)列中等待許可證的線程。

那我們來(lái)看看信號(hào)量(Semaphore)靜態(tài)內(nèi)部類Sync實(shí)現(xiàn)的tryReleaseShared(int releases)是怎么完成歸還許可證,首先會(huì)調(diào)用getState()獲取信號(hào)量當(dāng)前剩余的許可證,加上外部線程歸還的許可證數(shù)量算出總許可證數(shù)量:current + releases,如果能用CAS的方式修改成功,則退出方法,否則一直輪詢直到歸還成功,這里CAS失敗的原因有可能是外部也在請(qǐng)求和歸還許可證,可能在執(zhí)行完代碼<1>處后和執(zhí)行代碼<2>處之前,信號(hào)量?jī)?nèi)部的許可證數(shù)量已經(jīng)變了,所以CAS失敗。歸還信號(hào)量成功后就會(huì)調(diào)用doReleaseShared(),這個(gè)方法前面已經(jīng)講解過(guò)了,這里就不再贅述了。

public class Semaphore implements java.io.Serializable {

? ? //...

? ? abstract static class Sync extends AbstractQueuedSynchronizer {

? ? ? ? //...

? ? ? ? protected final boolean tryReleaseShared(int releases) {

? ? ? ? ? ? for (;;) {

? ? ? ? ? ? ? ? int current = getState();//<1>

? ? ? ? ? ? ? ? int next = current + releases;

? ? ? ? ? ? ? ? if (next < current) // overflow

? ? ? ? ? ? ? ? ? ? throw new Error("Maximum permit count exceeded");

? ? ? ? ? ? ? ? if (compareAndSetState(current, next))//<2>

? ? ? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? //...

? ? }

? ? //...

? ? public void release() {

? ? ? ? sync.releaseShared(1);

? ? }

? ? //...

? ? public void release(int permits) {

? ? ? ? if (permits < 0) throw new IllegalArgumentException();

? ? ? ? sync.releaseShared(permits);

? ? }

? ? //...

}

public abstract class AbstractQueuedSynchronizer

? ? extends AbstractOwnableSynchronizer

? ? implements java.io.Serializable {

? ? //...

? ? public final boolean releaseShared(int arg) {

? ? ? ? if (tryReleaseShared(arg)) {

? ? ? ? ? ? doReleaseShared();

? ? ? ? ? ? return true;

? ? ? ? }

? ? ? ? return false;

? ? }

? ? //...

? ? protected boolean tryReleaseShared(int arg) {

? ? ? ? throw new UnsupportedOperationException();

? ? }

? ? //...

}

下面我們?cè)賮?lái)看看tryAcquire(long timeout, TimeUnit unit)和tryAcquire(int permits, long timeout, TimeUnit unit)的實(shí)現(xiàn),這兩個(gè)方法會(huì)在給定的時(shí)間范圍內(nèi)嘗試獲取許可證,如果獲取成功則返回true,獲取失敗則返回false。

這兩個(gè)方法都會(huì)調(diào)用AQS實(shí)現(xiàn)的tryAcquireSharedNanos(int arg, long nanosTimeout),這個(gè)方法其實(shí)和先前講得doAcquireShared(int arg)十分相似,只是多了一個(gè)超時(shí)返回的功能。

這里筆者簡(jiǎn)單過(guò)一下這個(gè)方法的實(shí)現(xiàn):先在代碼<1>處算出超時(shí)時(shí)間,然后封裝線程對(duì)應(yīng)的節(jié)點(diǎn)Node并將其入隊(duì),如果判斷節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)是頭節(jié)點(diǎn),且申請(qǐng)?jiān)S可證成功,這里會(huì)調(diào)用setHeadAndPropagate(node, r)將頭節(jié)點(diǎn)指向當(dāng)前節(jié)點(diǎn),并嘗試喚醒下一個(gè)節(jié)點(diǎn)對(duì)應(yīng)的線程。如果申請(qǐng)?jiān)S可證失敗,會(huì)在<2>處算出還剩多少的阻塞時(shí)間nanosTimeout,如果剩余阻塞時(shí)間小于等于0,代表線程獲取許可證失敗,這里會(huì)調(diào)用<3>處的cancelAcquire(node) 將節(jié)點(diǎn)從等待隊(duì)列中移除,具體的移除邏輯可以看筆者寫的ReentrantLock源碼解析第二章。如果剩余阻塞時(shí)間大于0,則會(huì)執(zhí)行shouldParkAfterFailedAcquire(p, node)將前驅(qū)節(jié)點(diǎn)的等待狀態(tài)改為SIGNAL,在第二次循環(huán)時(shí),如果前驅(qū)節(jié)點(diǎn)的狀態(tài)為SIGNAL,且剩余阻塞時(shí)間大于SPIN_FOR_TIMEOUT_THRESHOLD(1000ns),則陷入阻塞,直到被中斷拋出異常,或者被喚醒,檢查是否能獲取許可證,如果不能獲取許可證且超時(shí),則會(huì)返回false表示在超時(shí)時(shí)間內(nèi)沒(méi)有獲取到許可證。

public class Semaphore implements java.io.Serializable {

? ? //...

? ? public boolean tryAcquire(int permits, long timeout, TimeUnit unit)

? ? ? ? throws InterruptedException {

? ? ? ? if (permits < 0) throw new IllegalArgumentException();

? ? ? ? return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));

? ? }

? ? //...

? ? public boolean tryAcquire(long timeout, TimeUnit unit)

? ? ? ? throws InterruptedException {

? ? ? ? return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));

? ? }

? ? //...

}

public abstract class AbstractQueuedSynchronizer

? ? extends AbstractOwnableSynchronizer

? ? implements java.io.Serializable {

? ? //...

? ? public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

? ? ? ? ? ? throws InterruptedException {

? ? ? ? if (Thread.interrupted())

? ? ? ? ? ? throw new InterruptedException();

? ? ? ? return tryAcquireShared(arg) >= 0 ||

? ? ? ? ? ? doAcquireSharedNanos(arg, nanosTimeout);

? ? }

? ? //...

? ? private boolean doAcquireSharedNanos(int arg, long nanosTimeout)

? ? ? ? ? ? throws InterruptedException {

? ? ? ? if (nanosTimeout <= 0L)

? ? ? ? ? ? return false;

? ? ? ? final long deadline = System.nanoTime() + nanosTimeout;//<1>

? ? ? ? final Node node = addWaiter(Node.SHARED);

? ? ? ? try {

? ? ? ? ? ? for (;;) {

? ? ? ? ? ? ? ? final Node p = node.predecessor();

? ? ? ? ? ? ? ? if (p == head) {

? ? ? ? ? ? ? ? ? ? int r = tryAcquireShared(arg);

? ? ? ? ? ? ? ? ? ? if (r >= 0) {

? ? ? ? ? ? ? ? ? ? ? ? setHeadAndPropagate(node, r);

? ? ? ? ? ? ? ? ? ? ? ? p.next = null; // help GC

? ? ? ? ? ? ? ? ? ? ? ? return true;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? nanosTimeout = deadline - System.nanoTime();//<2>

? ? ? ? ? ? ? ? if (nanosTimeout <= 0L) {

? ? ? ? ? ? ? ? ? ? cancelAcquire(node);//<3>

? ? ? ? ? ? ? ? ? ? return false;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? if (shouldParkAfterFailedAcquire(p, node) &&

? ? ? ? ? ? ? ? ? ? nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)

? ? ? ? ? ? ? ? ? ? LockSupport.parkNanos(this, nanosTimeout);

? ? ? ? ? ? ? ? if (Thread.interrupted())

? ? ? ? ? ? ? ? ? ? throw new InterruptedException();

? ? ? ? ? ? }

? ? ? ? } catch (Throwable t) {

? ? ? ? ? ? cancelAcquire(node);

? ? ? ? ? ? throw t;

? ? ? ? }

? ? }

? ? //...

}

下面我們對(duì)照一下FairSync和NonfairSync,其實(shí)NonfairSync基本沒(méi)有什么實(shí)現(xiàn),都是調(diào)用其父類Sync的方法,以非公平的方式競(jìng)爭(zhēng)許可證也是調(diào)用其父類nonfairTryAcquireShared(acquires)方法。而FairSync自身是有實(shí)現(xiàn)以公平的方式獲取許可證,實(shí)現(xiàn)邏輯也非常簡(jiǎn)單。先判斷信號(hào)量的等待隊(duì)列是否有節(jié)點(diǎn),有的話則返回獲取失敗,如果沒(méi)有再獲取當(dāng)前的可用許可證數(shù)量available,扣去申請(qǐng)的許可證數(shù)量available - acquires,用CAS的方式把扣減完的值remaining存放進(jìn)state,由于扣減的時(shí)候可能存在其他線程也在申請(qǐng)/歸還許可證,所以available的值并非一直有效,如果在獲取available后有其他線程也申請(qǐng)和歸還許可證,那么這里的CAS很可能會(huì)失敗,判斷CAS失敗后,又會(huì)開始新的一輪嘗試獲取許可證邏輯。

static final class FairSync extends Sync {

? ? private static final long serialVersionUID = 2014338818796000944L;

? ? FairSync(int permits) {

? ? ? ? super(permits);

? ? }

? ? protected int tryAcquireShared(int acquires) {

? ? ? ? for (;;) {

? ? ? ? ? ? if (hasQueuedPredecessors())

? ? ? ? ? ? ? ? return -1;

? ? ? ? ? ? int available = getState();

? ? ? ? ? ? int remaining = available - acquires;

? ? ? ? ? ? if (remaining < 0 ||

? ? ? ? ? ? ? ? compareAndSetState(available, remaining))

? ? ? ? ? ? ? ? return remaining;

? ? ? ? }

? ? }

}

static final class NonfairSync extends Sync {

? ? private static final long serialVersionUID = -2694183684443567898L;

? ? NonfairSync(int permits) {

? ? ? ? super(permits);

? ? }

? ? protected int tryAcquireShared(int acquires) {

? ? ? ? return nonfairTryAcquireShared(acquires);

? ? }

}

對(duì)照完公平FairSync和非公平NonfairSync的差別后,我們來(lái)看看Sync類實(shí)現(xiàn)的方法,Sync類的實(shí)現(xiàn)其實(shí)也不算復(fù)雜,主要就下面4個(gè)方法,其中:nonfairTryAcquireShared(int acquires)和tryReleaseShared(int releases)先前已經(jīng)將結(jié)果了,下面我們專注:reducePermits(int reductions)和drainPermits()。


abstract static class Sync extends AbstractQueuedSynchronizer {

? ? final int nonfairTryAcquireShared(int acquires) {

? ? ? ? //...

? ? }

? ? protected final boolean tryReleaseShared(int releases) {

? ? ? ? //...

? ? }

? ? final void reducePermits(int reductions) {

? ? ? ? //...

? ? }

? ? final int drainPermits() {

? ? ? ? //...

? ? }

}

Sync類實(shí)現(xiàn)的的reducePermits(int reductions)的作用是降低許可證數(shù)量,比如當(dāng)雙11來(lái)臨時(shí),淘寶京東可以對(duì)一些服務(wù)進(jìn)行擴(kuò)容和配置升級(jí),使得原本可以承受10W并發(fā)量的服務(wù)提高到可以承受50W,這里可以在不調(diào)用acquire()的前提下,調(diào)用release()方法增加信號(hào)量的許可證,當(dāng)雙11的壓力過(guò)去后,需要對(duì)服務(wù)進(jìn)行縮容,由50W的并發(fā)量回到10W,這里可以用reducePermits(int reductions)降低許可證數(shù)量。在這個(gè)方法中會(huì)先獲取當(dāng)前許可證數(shù)量,減去我們要扣除的許可證數(shù)量current - reductions,并判斷其結(jié)果是否溢出,如果溢出則拋出異常,沒(méi)有溢出用CAS的方式設(shè)置最新的許可證數(shù)量。


10

11

12

13

14

15

16

17

18

19

20

21

22

23

public class Semaphore implements java.io.Serializable {

? ? //...

? ? abstract static class Sync extends AbstractQueuedSynchronizer {

? ? ? ? //...

? ? ? ? final void reducePermits(int reductions) {

? ? ? ? ? ? for (;;) {

? ? ? ? ? ? ? ? int current = getState();

? ? ? ? ? ? ? ? int next = current - reductions;

? ? ? ? ? ? ? ? if (next > current) // underflow

? ? ? ? ? ? ? ? ? ? throw new Error("Permit count underflow");

? ? ? ? ? ? ? ? if (compareAndSetState(current, next))

? ? ? ? ? ? ? ? ? ? return;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? //...

? ? }

? ? //...

? ? protected void reducePermits(int reduction) {

? ? ? ? if (reduction < 0) throw new IllegalArgumentException();

? ? ? ? sync.reducePermits(reduction);

? ? }

? ? //...

}

需要注意兩點(diǎn):

這個(gè)方法的訪問(wèn)權(quán)限是protected,如果要使用此方法需要用一個(gè)類去繼承,并修改此方法的訪問(wèn)權(quán)限。

這個(gè)方法可能導(dǎo)致信號(hào)量的剩余許可證數(shù)量為負(fù),比如一個(gè)信號(hào)量原先的許可證數(shù)量為10,且被借走了9個(gè)許可證,當(dāng)前許可證數(shù)量為1。這時(shí)想把許可證數(shù)量從原先的10扣降到3,向reducePermits(int reduction)傳入7,此時(shí)current-reductions=1-7=-6,如果CAS成功,那么信號(hào)量目前的許可證數(shù)量為-6,不過(guò)沒(méi)關(guān)系,如果前面借走的9個(gè)許可證最終會(huì)歸還,信號(hào)量的許可證數(shù)量最終會(huì)回到3。

class MySemaphore extends Semaphore {

? ? public MySemaphore(int permits) {

? ? ? ? super(permits);

? ? }

? ? @Override

? ? public void reducePermits(int reduction) {

? ? ? ? super.reducePermits(reduction);

? ? }

}

public static void main(String[] args) {

? ? MySemaphore semaphore = new MySemaphore(8);

? ? System.out.println("初始信號(hào)量的許可證數(shù)量:" + semaphore.availablePermits());

? ? //初始化完信號(hào)量后,增加信號(hào)量的許可證數(shù)量

? ? int add = 2;

? ? semaphore.release(add);

? ? System.out.printf("增加%d個(gè)許可證后,許可證數(shù)量:%d\n", add, semaphore.availablePermits());

? ? //申請(qǐng)9個(gè)許可證

? ? int permits = 9;

? ? try {

? ? ? ? semaphore.acquire(permits);

? ? ? ? System.out.printf("申請(qǐng)%d個(gè)許可證后剩余許可證數(shù)量:%d\n", permits, semaphore.availablePermits());

? ? } catch (InterruptedException e) {

? ? ? ? e.printStackTrace();

? ? }

? ? //這里要將原先10個(gè)許可證扣除到只剩3個(gè),所以傳入7,扣除7個(gè)許可證

? ? semaphore.reducePermits(7);

? ? System.out.println("扣除7個(gè)許可證數(shù)量后,剩余許可證數(shù)量:" + semaphore.availablePermits());

? ? //歸還原先出借的9個(gè)許可證

? ? semaphore.release(permits);

? ? System.out.printf("歸還原先出借的%d信號(hào)量后,剩余信號(hào)量:%d\n", permits, semaphore.availablePermits());

}

執(zhí)行結(jié)果:

1

2

3

4

5

初始信號(hào)量的許可證數(shù)量:8

增加2個(gè)許可證后,許可證數(shù)量:10

申請(qǐng)9個(gè)許可證后剩余許可證數(shù)量:1

扣除7個(gè)許可證數(shù)量后,剩余許可證數(shù)量:-6

歸還原先出借的9信號(hào)量后,剩余信號(hào)量:3

Sync類實(shí)現(xiàn)的drainPermits()可以一次性扣除信號(hào)量目前所有的許可證數(shù)量并返回,通過(guò)這個(gè)API,我們可以得知資源目前最大的訪問(wèn)限度。還是拿上一章遠(yuǎn)程服務(wù)為例,判定服務(wù)能承受的并發(fā)是5000,用于限流的semaphore信號(hào)量的最大許可證數(shù)量也是5000。假設(shè)目前信號(hào)量剩余的許可證數(shù)量為2000,即有3000個(gè)線程正在并發(fā)訪問(wèn)遠(yuǎn)程服務(wù),我們可以通過(guò)drainPermits()方法獲取剩余的允許訪問(wèn)數(shù)量2000,然后創(chuàng)建2000個(gè)線程訪問(wèn)遠(yuǎn)程服務(wù),這個(gè)API一般用于計(jì)算量大且計(jì)算內(nèi)容比較獨(dú)立的場(chǎng)景。


public class Semaphore implements java.io.Serializable {

? ? //...

? ? abstract static class Sync extends AbstractQueuedSynchronizer {

? ? ? ? //...

? ? ? ? final int drainPermits() {

? ? ? ? ? ? for (;;) {

? ? ? ? ? ? ? ? int current = getState();

? ? ? ? ? ? ? ? if (current == 0 || compareAndSetState(current, 0))

? ? ? ? ? ? ? ? ? ? return current;

? ? ? ? ? ? }

? ? ? ? }

? ? ? ? //...

? ? }

? ? //...

? ? public int drainPermits() {

? ? ? ? return sync.drainPermits();

? ? }

? ? //...

}

最后,筆者介紹一個(gè)Semaphore在JDK1.6.0_17時(shí)期的BUG,便結(jié)束對(duì)Semaphore的源碼解析。

當(dāng)時(shí)AQS的setHeadAndPropagate(Node node, int propagate)和releaseShared(int arg) 兩個(gè)方法的實(shí)現(xiàn)是下面這樣的,這個(gè)代碼可能導(dǎo)致隊(duì)列被阻塞。


private void setHeadAndPropagate(Node node, int propagate) {

? ? setHead(node);

? ? if (propagate > 0 && node.waitStatus != 0) {

? ? ? ? Node s = node.next;

? ? ? ? if (s == null || s.isShared())

? ? ? ? ? ? unparkSuccessor(node);

? ? }

}

public final boolean releaseShared(int arg) {

? ? if (tryReleaseShared(arg)) {

? ? ? ? Node h = head;

? ? ? ? if (h != null && h.waitStatus != 0)

? ? ? ? ? ? unparkSuccessor(h);

? ? ? ? return true;

? ? }

? ? return false;

}

按照上面代碼的實(shí)現(xiàn),會(huì)讓下面的代碼出現(xiàn)隊(duì)列被阻塞的情況。t1和t2線程用于請(qǐng)求許可證,t3和t4線程用于歸還許可證,循環(huán)10000000次只是為了增加出現(xiàn)阻塞的概率,現(xiàn)在說(shuō)說(shuō)什么樣的場(chǎng)景下會(huì)出現(xiàn)隊(duì)列被阻塞的情況。

程序開始時(shí),信號(hào)量的許可證數(shù)量為0,所以t1和t2只能進(jìn)入隊(duì)列等待,t1和t2在隊(duì)列中的節(jié)點(diǎn)對(duì)應(yīng)N1和N2,節(jié)點(diǎn)的排序?yàn)椋篽ead->N1->N2(tail)。t3歸還許可證時(shí)發(fā)現(xiàn)頭節(jié)點(diǎn)不為null且頭節(jié)點(diǎn)的等待狀態(tài)為SIGNAL,于是會(huì)調(diào)用unparkSuccessor(h)方法喚醒頭節(jié)點(diǎn)的后繼節(jié)點(diǎn)N1對(duì)應(yīng)的線程t1,在執(zhí)行unparkSuccessor(h)的時(shí)候會(huì)把head的等待狀態(tài)改為0。

t1被喚醒后獲取到許可證,返回剩余許可證數(shù)量為0,即之后調(diào)用setHeadAndPropagate(Node node, int propagate)方法傳入的propagate為0,但尚未調(diào)用。此時(shí)t4也歸還了許可證,但發(fā)現(xiàn)head節(jié)點(diǎn)的等待狀態(tài)為0,就不會(huì)調(diào)用unparkSuccessor(h)。

t1執(zhí)行setHeadAndPropagate(Node node, int propagate),將頭節(jié)點(diǎn)指向自身線程對(duì)應(yīng)的節(jié)點(diǎn)N1,雖然此時(shí)信號(hào)量里有剩余的許可證,但t1原先拿到的propagate為0,所以不會(huì)執(zhí)行unparkSuccessor(node)喚醒t4。

那么新版本的setHeadAndPropagate(Node node, int propagate)和releaseShared(int arg)又是如何保證有許可證被歸還時(shí)喚醒隊(duì)列中被阻塞的線程呢?這里其實(shí)和PROPAGATE有關(guān),讓我們按照新版的setHeadAndPropagate和releaseShared走一遍上面的流程。

t1和t2進(jìn)入隊(duì)列中等待,t3歸還許可證發(fā)現(xiàn)頭節(jié)點(diǎn)不為null,且頭節(jié)點(diǎn)等待狀態(tài)為SIGNAL,于是調(diào)用unparkSuccessor(h)方法喚醒頭節(jié)點(diǎn)的后繼節(jié)點(diǎn)N1對(duì)應(yīng)的線程t1,在執(zhí)行unparkSuccessor(h)的時(shí)候會(huì)把head的等待狀態(tài)改為0。

t1被喚醒后獲取到許可證,返回剩余許可證數(shù)量為0,在調(diào)用setHeadAndPropagate(Node node, int propagate)之前,t4歸還了許可證,發(fā)現(xiàn)頭節(jié)點(diǎn)的等待狀態(tài)為0,將其改為PROPAGATE。

t1執(zhí)行setHeadAndPropagate(Node node, int propagate),獲取原先頭節(jié)點(diǎn)h,并將頭節(jié)點(diǎn)指向N1,此時(shí)雖然propagate為0,但原先頭節(jié)點(diǎn)h的等待狀態(tài)<0,可以執(zhí)行doReleaseShared()喚醒后繼節(jié)點(diǎn)N2對(duì)應(yīng)的線程t2。


import java.util.concurrent.Semaphore;

public class TestSemaphore {

? ? private static Semaphore sem = new Semaphore(0);

? ? private static class Thread1 extends Thread {

? ? ? ? @Override

? ? ? ? public void run() {

? ? ? ? ? ? sem.acquireUninterruptibly();

? ? ? ? }

? ? }

? ? private static class Thread2 extends Thread {

? ? ? ? @Override

? ? ? ? public void run() {

? ? ? ? ? ? sem.release();

? ? ? ? }

? ? }

? ? public static void main(String[] args) throws InterruptedException {

? ? ? ? for (int i = 0; i < 10000000; i++) {

? ? ? ? ? ? Thread t1 = new Thread1();

? ? ? ? ? ? Thread t2 = new Thread1();

? ? ? ? ? ? Thread t3 = new Thread2();

? ? ? ? ? ? Thread t4 = new Thread2();

? ? ? ? ? ? t1.start();

? ? ? ? ? ? t2.start();

? ? ? ? ? ? t3.start();

? ? ? ? ? ? t4.start();

? ? ? ? ? ? t1.join();

? ? ? ? ? ? t2.join();

? ? ? ? ? ? t3.join();

? ? ? ? ? ? t4.join();

? ? ? ? ? ? System.out.println(i);

? ? ? ? }

? ? }

}

USB Microphone https://www.soft-voice.com/

Wooden Speakers? https://www.zeshuiplatform.com/

亞馬遜測(cè)評(píng) www.yisuping.cn

深圳網(wǎng)站建設(shè)www.sz886.com

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 常見問(wèn)題:對(duì)某個(gè)知識(shí)點(diǎn)的理解或看法,一般從是什么,原理,好處與應(yīng)用場(chǎng)景來(lái)回答你對(duì)AQS的理解(想法)?CountD...
    _code_x閱讀 1,534評(píng)論 1 14
  • 前言 Semaphore(信號(hào)量)是用來(lái)控制同時(shí)訪問(wèn)特定資源的線程數(shù)量,它通過(guò)協(xié)調(diào)各個(gè)線程,以保證合理的使用公共資...
    nicktming閱讀 366評(píng)論 0 2
  • 我是黑夜里大雨紛飛的人啊 1 “又到一年六月,有人笑有人哭,有人歡樂(lè)有人憂愁,有人驚喜有人失落,有的覺得收獲滿滿有...
    陌忘宇閱讀 8,894評(píng)論 28 54
  • 信任包括信任自己和信任他人 很多時(shí)候,很多事情,失敗、遺憾、錯(cuò)過(guò),源于不自信,不信任他人 覺得自己做不成,別人做不...
    吳氵晃閱讀 6,391評(píng)論 4 8
  • 步驟:發(fā)微博01-導(dǎo)航欄內(nèi)容 -> 發(fā)微博02-自定義TextView -> 發(fā)微博03-完善TextView和...
    dibadalu閱讀 3,429評(píng)論 1 3

友情鏈接更多精彩內(nèi)容