溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何理解多線程的并發(fā)問(wèn)題

發(fā)布時(shí)間:2021-10-11 09:57:22 來(lái)源:億速云 閱讀:135 作者:iii 欄目:編程語(yǔ)言

這篇文章主要講解了“如何理解多線程的并發(fā)問(wèn)題”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“如何理解多線程的并發(fā)問(wèn)題”吧!

一、多線程為什么會(huì)有并發(fā)問(wèn)題

為什么多線程同時(shí)訪問(wèn)(讀寫)同個(gè)變量,會(huì)有并發(fā)問(wèn)題?

  1. Java 內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中,每條線程有自己的工作內(nèi)存。

  2. 線程的工作內(nèi)存中保存了該線程中用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作都必須在工作內(nèi)存中進(jìn)行,而不能直接讀寫主內(nèi)存。

  3. 線程訪問(wèn)一個(gè)變量,首先將變量從主內(nèi)存拷貝到工作內(nèi)存,對(duì)變量的寫操作,不會(huì)馬上同步到主內(nèi)存。

  4. 不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量的傳遞均需要自己的工作內(nèi)存和主存之間進(jìn)行數(shù)據(jù)同步進(jìn)行。

二、Java 內(nèi)存模型(JMM)

Java 內(nèi)存模型(JMM) 作用于工作內(nèi)存(本地內(nèi)存)和主存之間數(shù)據(jù)同步過(guò)程,它規(guī)定了如何做數(shù)據(jù)同步以及什么時(shí)候做數(shù)據(jù)同步,如下圖。

如何理解多線程的并發(fā)問(wèn)題

三、并發(fā)編程三要素

原子性:在一個(gè)操作中,CPU 不可以在中途暫停然后再調(diào)度,即不被中斷操作,要么執(zhí)行完成,要么就不執(zhí)行。

可見性:多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程能夠立即看得到修改的值。

有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。

三、怎么做,才能解決止并發(fā)問(wèn)題?(重點(diǎn))

下面結(jié)合不同場(chǎng)景分析解決并發(fā)問(wèn)題的處理方式。

一、volatile

1.1 volatile 特性

保證可見性,不保證原子性

  1. 當(dāng)寫一個(gè)volatile變量時(shí),JVM會(huì)把本地內(nèi)存的變量強(qiáng)制刷新到主內(nèi)存中

  2. 這個(gè)寫操作導(dǎo)致其他線程中的緩存無(wú)效,其他線程讀,會(huì)從主內(nèi)存讀。volatile的寫操作對(duì)其它線程實(shí)時(shí)可見。

禁止指令重排序指令重排序是指編譯器和處理器為了優(yōu)化程序性能對(duì)指令進(jìn)行排序的一種手段,需要遵守一定規(guī)則:

  1. 不會(huì)對(duì)存在依賴關(guān)系的指令重排序,例如 a = 1;b = a; a 和b存在依賴關(guān)系,不會(huì)被重排序

  2. 不能影響單線程下的執(zhí)行結(jié)果。比如:a=1;b=2;c=a+b這三個(gè)操作,前兩個(gè)操作可以重排序,但是c=a+b不會(huì)被重排序,因?yàn)橐WC結(jié)果是3

1.2 使用場(chǎng)景

對(duì)于一個(gè)變量,只有一個(gè)線程執(zhí)行寫操作,其它線程都是讀操作,這時(shí)候可以用 volatile 修飾這個(gè)變量。

1.3 單例雙重鎖為什么要用到volatile?

public class TestInstance {
private static volatile TestInstance mInstance;
public static TestInstance getInstance(){ //1
 if (mInstance == null){ //2
 synchronized (TestInstance.class){ //3
 if (mInstance == null){ //4
 mInstance = new TestInstance(); //5
 }
 }
 }
 return mInstance;
}
復(fù)制代碼

}

假如沒有用volatile,并發(fā)情況下會(huì)出現(xiàn)問(wèn)題,線程A執(zhí)行到注釋5 new TestInstance() 的時(shí)候,分為如下幾個(gè)幾步操作:

  1. 分配內(nèi)存

  2. 初始化對(duì)象

  3. mInstance 指向內(nèi)存

這時(shí)候如果發(fā)生指令重排,執(zhí)行順序是132,執(zhí)行到第3的時(shí)候,線程B剛好進(jìn)來(lái)了,并且執(zhí)行到注釋2,這時(shí)候判斷mInstance 不為空,直接使用一個(gè)未初始化的對(duì)象。所以使用volatile關(guān)鍵字來(lái)禁止指令重排序。

1.4 volatile 原理

在JVM底層volatile是采用 內(nèi)存屏障 來(lái)實(shí)現(xiàn)的,內(nèi)存屏障會(huì)提供3個(gè)功能:

  1. 它確保指令重排序時(shí)不會(huì)把其后面的指令排到內(nèi)存屏障之前的位置,也不會(huì)把前面的指令排到內(nèi)存屏障的后面;即在執(zhí)行到內(nèi)存屏障這句指令時(shí),在它前面的操作已經(jīng)全部完成;

  2. 它會(huì)強(qiáng)制將緩存的修改操作立即寫到主內(nèi)存

  3. 寫操作會(huì)導(dǎo)致其它CPU中的緩存行失效,寫之后,其它線程的讀操作會(huì)從主內(nèi)存讀。

1.5 volatile 的局限性

**volatile 只能保證可見性,不能保證原子性。**寫操作對(duì)其它線程可見,但是不能解決多個(gè)線程同時(shí)寫的問(wèn)題。

二、Synchronized

2.1 Synchronized 使用場(chǎng)景

多個(gè)線程同時(shí)寫一個(gè)變量。

例如售票,余票是100張,窗口A和窗口B同時(shí)各賣出一張票, 假如余票變量用 volatile 修飾,是有問(wèn)題的。

A窗口獲取余票是100,B窗口獲取余票也是100,A賣出一張變成99,刷新回主內(nèi)存,同時(shí)B賣出一張變成99,也刷新回主內(nèi)存,會(huì)導(dǎo)致最終主內(nèi)存余票是99而不是98。

前面說(shuō)到 volatile 的局限性,就是多個(gè)線程同時(shí)寫的情況,這種情況一般可以使用 Synchronized 。

Synchronized 可以保證同一時(shí)刻,只有一個(gè)線程可執(zhí)行某個(gè)方法或某個(gè)代碼塊。

2.2 Synchronized 原理

public class SynchronizedTest {
public static void main(String[] args) {
 synchronized (SynchronizedTest.class) {
 System.out.println("123");
 }
 method();
}
private static void method() {
}
}
復(fù)制代碼

將這段代碼先用 javac 命令編譯,再 java p -v SynchronizedTest.class 命令查看字節(jié)碼,部分字節(jié)碼如下

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
 stack=2, locals=3, args_size=1
 0: ldc #2 // class com/lanshifu/opengldemo/test/SynchronizedTest
 2: dup
 3: astore_1
 4: monitorenter
 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
 8: ldc #4 // String 123
 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
 13: aload_1
 14: monitorexit
 15: goto 23
 18: astore_2
 19: aload_1
 20: monitorexit
 21: aload_2
 22: athrow
 23: invokestatic #6 // Method method:()V
 26: return
復(fù)制代碼

可以看到 4: monitorenter 和 14: monitorexit ,中間是打印的語(yǔ)句。

執(zhí)行同步代碼塊,首先會(huì)執(zhí)行 monitorenter 指令,然后執(zhí)行同步代碼塊中的代碼,退出同步代碼塊的時(shí)候會(huì)執(zhí)行 monitorexit 指令 。

使用Synchronized進(jìn)行同步,其關(guān)鍵就是必須要對(duì)對(duì)象的監(jiān)視器monitor進(jìn)行獲取,當(dāng)線程獲取monitor后才能繼續(xù)往下執(zhí)行,否則就進(jìn)入同步隊(duì)列,線程狀態(tài)變成BLOCK,同一時(shí)刻只有一個(gè)線程能夠獲取到monitor,當(dāng)監(jiān)聽到monitorexit被調(diào)用,隊(duì)列里就有一個(gè)線程出隊(duì),獲取monitor。

每個(gè)對(duì)象擁有一個(gè)計(jì)數(shù)器,當(dāng)線程獲取該對(duì)象鎖后,計(jì)數(shù)器就會(huì)加一,釋放鎖后就會(huì)將計(jì)數(shù)器減一,所以只要這個(gè)鎖的計(jì)數(shù)器大于0,其它線程訪問(wèn)就只能等待。

2.3 Synchronized 鎖的升級(jí)

大家對(duì)Synchronized的理解可能就是重量級(jí)鎖,但是Java1.6對(duì) Synchronized 進(jìn)行了各種優(yōu)化之后,有些情況下它就并不那么重,Java1.6 中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖。

偏向鎖:大多數(shù)情況下,鎖不僅不存在多線程競(jìng)爭(zhēng),而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。

當(dāng)一個(gè)線程A訪問(wèn)加了同步鎖的代碼塊時(shí),會(huì)在對(duì)象頭中存 儲(chǔ)當(dāng)前線程的id,后續(xù)這個(gè)線程進(jìn)入和退出這段加了同步鎖的代碼塊時(shí),不需要再次加鎖和釋放鎖。

輕量級(jí)鎖:在偏向鎖情況下,如果線程B也訪問(wèn)了同步代碼塊,比較對(duì)象頭的線程id不一樣,會(huì)升級(jí)為輕量級(jí)鎖,并且通過(guò)自旋的方式來(lái)獲取輕量級(jí)鎖。

重量級(jí)鎖:如果線程A和線程B同時(shí)訪問(wèn)同步代碼塊,則輕量級(jí)鎖會(huì)升級(jí)為重量級(jí)鎖,線程A獲取到重量級(jí)鎖的情況下,線程B只能入隊(duì)等待,進(jìn)入BLOCK狀態(tài)。

2.4 Synchronized 缺點(diǎn)

  1. 不能設(shè)置鎖超時(shí)時(shí)間

  2. 不能通過(guò)代碼釋放鎖

  3. 容易造成死鎖

三、ReentrantLock

上面說(shuō)到 Synchronized 的缺點(diǎn),不能設(shè)置鎖超時(shí)時(shí)間和不能通過(guò)代碼釋放鎖, ReentranLock就可以解決這個(gè)問(wèn)題。

在多個(gè)條件變量和高度競(jìng)爭(zhēng)鎖的地方,用ReentrantLock更合適,ReentrantLock還提供了 Condition ,對(duì)線程的等待和喚醒等操作更加靈活,一個(gè)ReentrantLock可以有多個(gè)Condition實(shí)例,所以更有擴(kuò)展性。

3.1 ReentrantLock 的使用

lock 和 unlock

ReentrantLock reentrantLock = new ReentrantLock();
 System.out.println("reentrantLock->lock");
 reentrantLock.lock();
 try {
 
 System.out.println("睡眠2秒...");
 Thread.sleep(2000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }finally {
 reentrantLock.unlock();
 System.out.println("reentrantLock->unlock");
 }
復(fù)制代碼

實(shí)現(xiàn)可定時(shí)的鎖請(qǐng)求:tryLock

public static void main(String[] args) {
 ReentrantLock reentrantLock = new ReentrantLock();
 Thread thread1 = new Thread_tryLock(reentrantLock);
 thread1.setName("thread1");
 thread1.start();
 Thread thread2 = new Thread_tryLock(reentrantLock);
 thread2.setName("thread2");
 thread2.start();
}
 static class Thread_tryLock extends Thread {
 ReentrantLock reentrantLock;
 public Thread_tryLock(ReentrantLock reentrantLock) {
 this.reentrantLock = reentrantLock;
 }
 @Override
 public void run() {
 try {
 System.out.println("try lock:" + Thread.currentThread().getName());
 boolean tryLock = reentrantLock.tryLock(3, TimeUnit.SECONDS);
 if (tryLock) {
 System.out.println("try lock success :" + Thread.currentThread().getName());
 System.out.println("睡眠一下:" + Thread.currentThread().getName());
 Thread.sleep(5000);
 System.out.println("醒了:" + Thread.currentThread().getName());
 } else {
 System.out.println("try lock 超時(shí) :" + Thread.currentThread().getName());
 }
 } catch (InterruptedException e) {
 e.printStackTrace();
 } finally {
 System.out.println("unlock:" + Thread.currentThread().getName());
 reentrantLock.unlock();
 }
 }
 }
復(fù)制代碼

打印的日志:

try lock:thread1
try lock:thread2
try lock success :thread2
睡眠一下:thread2
try lock 超時(shí) :thread1
unlock:thread1
Exception in thread "thread1" java.lang.IllegalMonitorStateException
	at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
	at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
	at com.lanshifu.demo_module.test.lock.ReentranLockTest$Thread_tryLock.run(ReentranLockTest.java:60)
醒了:thread2
unlock:thread2
復(fù)制代碼

上面演示了 trtLock 的使用, trtLock 設(shè)置獲取鎖的等待時(shí)間,超過(guò)3秒直接返回失敗,可以從日志中看到結(jié)果。 有異常是因?yàn)閠hread1獲取鎖失敗,不應(yīng)該調(diào)用unlock。

3.2 Condition 條件

public static void main(String[] args) {
 Thread_Condition thread_condition = new Thread_Condition();
 thread_condition.setName("測(cè)試Condition的線程");
 thread_condition.start();
 try {
 Thread.sleep(2000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 thread_condition.singal();
 }
static class Thread_Condition extends Thread {
 @Override
 public void run() {
 await();
 }
 private ReentrantLock lock = new ReentrantLock();
 public Condition condition = lock.newCondition();
 public void await() {
 try {
 System.out.println("lock");
 lock.lock();
 System.out.println(Thread.currentThread().getName() + ":我在等待通知的到來(lái)...");
 condition.await();//await 和 signal 對(duì)應(yīng)
 //condition.await(2, TimeUnit.SECONDS); //設(shè)置等待超時(shí)時(shí)間
 System.out.println(Thread.currentThread().getName() + ":等到通知了,我繼續(xù)執(zhí)行>>>");
 } catch (Exception e) {
 e.printStackTrace();
 } finally {
 System.out.println("unlock");
 lock.unlock();
 }
 }
 public void singal() {
 try {
 System.out.println("lock");
 lock.lock();
 System.out.println("我要通知在等待的線程,condition.signal()");
 condition.signal();//await 和 signal 對(duì)應(yīng)
 Thread.sleep(1000);
 } catch (InterruptedException e) {
 e.printStackTrace();
 } finally {
 System.out.println("unlock");
 lock.unlock();
 }
 }
 }
復(fù)制代碼

運(yùn)行打印日志

lock
測(cè)試Condition的線程:我在等待通知的到來(lái)...
lock
我要通知在等待的線程,condition.signal()
unlock
測(cè)試Condition的線程:等到通知了,我繼續(xù)執(zhí)行>>>
unlock
復(fù)制代碼

上面演示了 Condition的 await 和 signal 使用,前提要先lock。

3.3 公平鎖與非公平鎖

ReentrantLock 構(gòu)造函數(shù)傳true表示公平鎖。

公平鎖表示線程獲取鎖的順序是按照線程加鎖的順序來(lái)分配的,即先來(lái)先得的順序。而非公平鎖就是一種鎖的搶占機(jī)制,是隨機(jī)獲得鎖的,可能會(huì)導(dǎo)致某些線程一致拿不到鎖,所以是不公平的。

3.4 ReentrantLock 注意點(diǎn)

  1. ReentrantLock使用lock和unlock來(lái)獲得鎖和釋放鎖

  2. unlock要放在finally中,這樣正常運(yùn)行或者異常都會(huì)釋放鎖

  3. 使用condition的await和signal方法之前,必須調(diào)用lock方法獲得對(duì)象監(jiān)視器

四、并發(fā)包

通過(guò)上面分析,并發(fā)嚴(yán)重的情況下,使用鎖顯然效率低下,因?yàn)橥粫r(shí)刻只能有一個(gè)線程可以獲得鎖,其它線程只能乖乖等待。

Java提供了并發(fā)包解決這個(gè)問(wèn)題,接下來(lái)介紹并發(fā)包里一些常用的數(shù)據(jù)結(jié)構(gòu)。

4.1 ConcurrentHashMap

我們都知道HashMap是線程不安全的數(shù)據(jù)結(jié)構(gòu),HashTable則在HashMap基礎(chǔ)上,get方法和put方法加上Synchronized修飾變成線程安全,不過(guò)在高并發(fā)情況下效率底下,最終被 ConcurrentHashMap 替代。

ConcurrentHashMap 采用分段鎖,內(nèi)部默認(rèn)有16個(gè)桶,get和put操作,首先將key計(jì)算hashcode,然后跟16取余,落到16個(gè)桶中的一個(gè),然后每個(gè)桶中都加了鎖(ReentrantLock),桶中是HashMap結(jié)構(gòu)(數(shù)組加鏈表,鏈表過(guò)長(zhǎng)轉(zhuǎn)紅黑樹)。

所以理論上最多支持16個(gè)線程同時(shí)訪問(wèn)。

4.2 LinkBlockingQueue

鏈表結(jié)構(gòu)的阻塞隊(duì)列,內(nèi)部使用多個(gè)ReentrantLock

/** Lock held by take, poll, etc */
 private final ReentrantLock takeLock = new ReentrantLock();
 /** Wait queue for waiting takes */
 private final Condition notEmpty = takeLock.newCondition();
 /** Lock held by put, offer, etc */
 private final ReentrantLock putLock = new ReentrantLock();
 /** Wait queue for waiting puts */
 private final Condition notFull = putLock.newCondition();
private void signalNotEmpty() {
 final ReentrantLock takeLock = this.takeLock;
 takeLock.lock();
 try {
 notEmpty.signal();
 } finally {
 takeLock.unlock();
 }
 }
 /**
 * Signals a waiting put. Called only from take/poll.
 */
 private void signalNotFull() {
 final ReentrantLock putLock = this.putLock;
 putLock.lock();
 try {
 notFull.signal();
 } finally {
 putLock.unlock();
 }
 }
復(fù)制代碼

源碼不貼太多,簡(jiǎn)單說(shuō)一下 LinkBlockingQueue 的邏輯:

  1. 從隊(duì)列獲取數(shù)據(jù),如果隊(duì)列中沒有數(shù)據(jù),會(huì)調(diào)用 notEmpty.await(); 進(jìn)入等待。

  2. 在放數(shù)據(jù)進(jìn)去隊(duì)列的時(shí)候會(huì)調(diào)用 notEmpty.signal(); ,通知消費(fèi)者,1中的等待結(jié)束,喚醒繼續(xù)執(zhí)行。

  3. 從隊(duì)列里取到數(shù)據(jù)的時(shí)候會(huì)調(diào)用 notFull.signal(); ,通知生產(chǎn)者繼續(xù)生產(chǎn)。

  4. 在put數(shù)據(jù)進(jìn)入隊(duì)列的時(shí)候,如果判斷隊(duì)列中的數(shù)據(jù)達(dá)到最大值,那么會(huì)調(diào)用 notFull.await(); ,等待消費(fèi)者消費(fèi)掉,也就是等待3去取數(shù)據(jù)并且發(fā)出 notFull.signal(); ,這時(shí)候生產(chǎn)者才能繼續(xù)生產(chǎn)。

LinkBlockingQueue 是典型的生產(chǎn)者消費(fèi)者模式,源碼細(xì)節(jié)就不多說(shuō)。

4.3 原子操作類:AtomicInteger

內(nèi)部采用CAS(compare and swap)保證原子性

舉一個(gè)int自增的例子

AtomicInteger atomicInteger = new AtomicInteger(0);
 atomicInteger.incrementAndGet();//自增
復(fù)制代碼

源碼看一下

/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
 public final int incrementAndGet() {
 return U.getAndAddInt(this, VALUE, 1) + 1;
 }
復(fù)制代碼

U 是 Unsafe,看下 Unsafe#getAndAddInt

public final int getAndAddInt(Object var1, long var2, int var4) {
 int var5;
 do {
 var5 = this.getIntVolatile(var1, var2);
 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 return var5;
 }
復(fù)制代碼

通過(guò) compareAndSwapInt 保證原子性。

總結(jié)

面試中問(wèn)到多線程并發(fā)問(wèn)題,可以這么答:

  1. 當(dāng)只有一個(gè)線程寫,其它線程都是讀的時(shí)候,可以用 volatile 修飾變量

  2. 當(dāng)多個(gè)線程寫,那么一般情況下并發(fā)不嚴(yán)重的話可以用 Synchronized ,Synchronized并不是一開始就是重量級(jí)鎖,在并發(fā)不嚴(yán)重的時(shí)候,比如只有一個(gè)線程訪問(wèn)的時(shí)候,是偏向鎖;當(dāng)多個(gè)線程訪問(wèn),但不是同時(shí)訪問(wèn),這時(shí)候鎖升級(jí)為輕量級(jí)鎖;當(dāng)多個(gè)線程同時(shí)訪問(wèn),這時(shí)候升級(jí)為重量級(jí)鎖。所以在并發(fā)不是很嚴(yán)重的情況下,使用Synchronized是可以的。不過(guò)Synchronized有局限性,比如不能設(shè)置鎖超時(shí),不能通過(guò)代碼釋放鎖。

  3. ReentranLock 可以通過(guò)代碼釋放鎖,可以設(shè)置鎖超時(shí)。

  4. 高并發(fā)下,Synchronized、ReentranLock 效率低,因?yàn)橥粫r(shí)刻只有一個(gè)線程能進(jìn)入同步代碼塊,如果同時(shí)有很多線程訪問(wèn),那么其它線程就都在等待鎖。這個(gè)時(shí)候可以使用并發(fā)包下的數(shù)據(jù)結(jié)構(gòu),例如 ConcurrentHashMap , LinkBlockingQueue ,以及原子性的數(shù)據(jù)結(jié)構(gòu)如: AtomicInteger 。

感謝各位的閱讀,以上就是“如何理解多線程的并發(fā)問(wèn)題”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)如何理解多線程的并發(fā)問(wèn)題這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI