摘要:而導(dǎo)致這個(gè)問(wèn)題的原因是線程并行執(zhí)行操作并不是原子的,存在線程安全問(wèn)題。表示自旋鎖,由于線程的阻塞和喚醒需要從用戶(hù)態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對(duì)來(lái)說(shuō)性能開(kāi)銷(xiāo)很大。
文章簡(jiǎn)介
synchronized想必大家都不陌生,用來(lái)解決線程安全問(wèn)題的利器。同時(shí)也是Java高級(jí)程序員面試比較常見(jiàn)的面試題。這篇文正會(huì)帶大家徹底了解synchronized的實(shí)現(xiàn)。
內(nèi)容導(dǎo)航什么時(shí)候需要用Synchronized
synchronized的使用
synchronized的實(shí)現(xiàn)原理分析
什么時(shí)候需要用Synchronized想必大家對(duì)synchronized都不陌生,主要作用是在多個(gè)線程操作共享數(shù)據(jù)的時(shí)候,保證對(duì)共享數(shù)據(jù)訪問(wèn)的線程安全性。
比如在下面這個(gè)圖片中,兩個(gè)線程對(duì)于i這個(gè)共享變量同時(shí)做i++遞增操作,那么這個(gè)時(shí)候?qū)τ趇這個(gè)值來(lái)說(shuō)就存在一個(gè)不確定性,也就是說(shuō)理論上i的值應(yīng)該是2,但是也可能是1。而導(dǎo)致這個(gè)問(wèn)題的原因是線程并行執(zhí)行i++操作并不是原子的,存在線程安全問(wèn)題。所以通常來(lái)說(shuō)解決辦法是通過(guò)加鎖來(lái)實(shí)現(xiàn)線程的串行執(zhí)行,而synchronized就是java中鎖的實(shí)現(xiàn)的關(guān)鍵字。
synchronized在并發(fā)編程中是一個(gè)非常重要的角色,在JDK1.6之前,它是一個(gè)重量級(jí)鎖的角色,但是在JDK1.6之后對(duì)synchronized做了優(yōu)化,優(yōu)化以后性能有了較大的提升(這塊會(huì)在后面做詳細(xì)的分析)。
先來(lái)看一下synchronized的使用
Synchronized的使用synchronized有三種使用方法,這三種使用方法分別對(duì)應(yīng)三種不同的作用域,代碼如下
修飾普通同步方法將synchronized修飾在普通同步方法,那么該鎖的作用域是在當(dāng)前實(shí)例對(duì)象范圍內(nèi),也就是說(shuō)對(duì)于 SyncDemosd=newSyncDemo();這一個(gè)實(shí)例對(duì)象sd來(lái)說(shuō),多個(gè)線程訪問(wèn)access方法會(huì)有鎖的限制。如果access已經(jīng)有線程持有了鎖,那這個(gè)線程會(huì)獨(dú)占鎖,直到鎖釋放完畢之前,其他線程都會(huì)被阻塞
public SyncDemo{ Object lock =new Object(); //形式1 public synchronized void access(){ // } //形式2,作用域等同于形式1 public void access1(){ synchronized(lock){ // } } //形式3,作用域等同于前面兩種 public void access2(){ synchronized(this){ // } } }修飾靜態(tài)同步方法
修飾靜態(tài)同步方法或者靜態(tài)對(duì)象、類(lèi),那么這個(gè)鎖的作用范圍是類(lèi)級(jí)別。舉個(gè)簡(jiǎn)單的例子,
SyncDemo sd=SyncDemo(); SyncDemo sd2=new SyncDemo();}
兩個(gè)不同的實(shí)例sd和sd2, 如果sd這個(gè)實(shí)例訪問(wèn)access方法并且成功持有了鎖,那么sd2這個(gè)對(duì)象如果同樣來(lái)訪問(wèn)access方法,那么它必須要等待sd這個(gè)對(duì)象的鎖釋放以后,sd2這個(gè)對(duì)象的線程才能訪問(wèn)該方法,這就是類(lèi)鎖;也就是說(shuō)類(lèi)鎖就相當(dāng)于全局鎖的概念,作用范圍是類(lèi)級(jí)別。
這里拋一個(gè)小問(wèn)題,大家看看能不能回答,如果不能也沒(méi)關(guān)系,后面會(huì)講解;問(wèn)題是如果sd先訪問(wèn)access獲得了鎖,sd2對(duì)象的線程再訪問(wèn)access1方法,那么它會(huì)被阻塞嗎?
public SyncDemo{ static Object lock=new Object(); //形式1 public synchronized static void access(){ // } //形式2等同于形式1 public void access1(){ synchronized(lock){ // } } //形式3等同于前面兩種 public void access2(){ synchronzied(SyncDemo.class){ // } } }步方法塊
public SyncDemo{ Object lock=new Object(); public void access(){ //do something synchronized(lock){ // } } }
通過(guò)演示3種不同鎖的使用,讓大家對(duì)synchronized有了初步的認(rèn)識(shí)。當(dāng)一個(gè)線程視圖訪問(wèn)帶有synchronized修飾的同步代碼塊或者方法時(shí),必須要先獲得鎖。當(dāng)方法執(zhí)行完畢退出以后或者出現(xiàn)異常的情況下會(huì)自動(dòng)釋放鎖。如果大家認(rèn)真看了上面的三個(gè)案例,那么應(yīng)該知道鎖的范圍控制是由對(duì)象的作用域決定的。對(duì)象的作用域越大,那么鎖的范圍也就越大,因此我們可以得出一個(gè)初步的猜想,synchronized和對(duì)象有非常大的關(guān)系。那么,接下來(lái)就去剖析一下鎖的原理Synchronized的實(shí)現(xiàn)原理分析
當(dāng)一個(gè)線程嘗試訪問(wèn)synchronized修飾的代碼塊時(shí),它首先要獲得鎖,那么這個(gè)鎖到底存在哪里呢?對(duì)象在內(nèi)存中的布局
synchronized實(shí)現(xiàn)的鎖是存儲(chǔ)在Java對(duì)象頭里,什么是對(duì)象頭呢?在Hotspot虛擬機(jī)中,對(duì)象在內(nèi)存中的存儲(chǔ)布局,可以分為三個(gè)區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)、對(duì)齊填充(Padding)
當(dāng)我們?cè)贘ava代碼中,使用new創(chuàng)建一個(gè)對(duì)象實(shí)例的時(shí)候,(hotspot虛擬機(jī))JVM層面實(shí)際上會(huì)創(chuàng)建一個(gè) instanceOopDesc對(duì)象。
Hotspot虛擬機(jī)采用OOP-Klass模型來(lái)描述Java對(duì)象實(shí)例,OOP(Ordinary Object Point)指的是普通對(duì)象指針,Klass用來(lái)描述對(duì)象實(shí)例的具體類(lèi)型。Hotspot采用instanceOopDesc和arrayOopDesc來(lái)描述對(duì)象頭,arrayOopDesc對(duì)象用來(lái)描述數(shù)組類(lèi)型
instanceOopDesc的定義在Hotspot源碼中的 instanceOop.hpp文件中,另外,arrayOopDesc的定義對(duì)應(yīng) arrayOop.hpp
class instanceOopDesc : public oopDesc { public: // aligned header size. static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; } // If compressed, the offset of the fields of the instance may not be aligned. static int base_offset_in_bytes() { // offset computation code breaks if UseCompressedClassPointers // only is true return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc); } static bool contains_field_offset(int offset, int nonstatic_field_size) { int base_in_bytes = base_offset_in_bytes(); return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize); } }; #endif // SHARE_VM_OOPS_INSTANCEOOP_HPP
從instanceOopDesc代碼中可以看到 instanceOopDesc繼承自oopDesc,oopDesc的定義載Hotspot源碼中的 oop.hpp文件中
class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata; // Fast access to barrier set. Must be initialized. static BarrierSet* _bs; ... }
在普通實(shí)例對(duì)象中,oopDesc的定義包含兩個(gè)成員,分別是 _mark和 _metadata
_mark表示對(duì)象標(biāo)記、屬于markOop類(lèi)型,也就是接下來(lái)要講解的Mark World,它記錄了對(duì)象和鎖有關(guān)的信息
_metadata表示類(lèi)元信息,類(lèi)元信息存儲(chǔ)的是對(duì)象指向它的類(lèi)元數(shù)據(jù)(Klass)的首地址,其中Klass表示普通指針、 _compressed_klass表示壓縮類(lèi)指針
Mark Word在前面我們提到過(guò),普通對(duì)象的對(duì)象頭由兩部分組成,分別是markOop以及類(lèi)元信息,markOop官方稱(chēng)為Mark Word
在Hotspot中,markOop的定義在 markOop.hpp文件中,代碼如下
class markOopDesc: public oopDesc { private: // Conversion uintptr_t value() const { return (uintptr_t) this; } public: // Constants enum { age_bits = 4, //分代年齡 lock_bits = 2, //鎖標(biāo)識(shí) biased_lock_bits = 1, //是否為偏向鎖 max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, //對(duì)象的hashcode cms_bits = LP64_ONLY(1) NOT_LP64(0), epoch_bits = 2 //偏向鎖的時(shí)間戳 }; ...
Mark word記錄了對(duì)象和鎖有關(guān)的信息,當(dāng)某個(gè)對(duì)象被synchronized關(guān)鍵字當(dāng)成同步鎖時(shí),那么圍繞這個(gè)鎖的一系列操作都和Mark word有關(guān)系。Mark Word在32位虛擬機(jī)的長(zhǎng)度是32bit、在64位虛擬機(jī)的長(zhǎng)度是64bit。
Mark Word里面存儲(chǔ)的數(shù)據(jù)會(huì)隨著鎖標(biāo)志位的變化而變化,Mark Word可能變化為存儲(chǔ)以下5中情況
鎖標(biāo)志位的表示意義
鎖標(biāo)識(shí) lock=00 表示輕量級(jí)鎖
鎖標(biāo)識(shí) lock=10 表示重量級(jí)鎖
偏向鎖標(biāo)識(shí) biased_lock=1表示偏向鎖
偏向鎖標(biāo)識(shí) biased_lock=0且鎖標(biāo)識(shí)=01表示無(wú)鎖狀態(tài)
到目前為止,我們?cè)倏偨Y(jié)一下前面的內(nèi)容,synchronized(lock)中的lock可以用Java中任何一個(gè)對(duì)象來(lái)表示,而鎖標(biāo)識(shí)的存儲(chǔ)實(shí)際上就是在lock這個(gè)對(duì)象中的對(duì)象頭內(nèi)。大家懂了嗎?
其實(shí)前面只提到了鎖標(biāo)志位的存儲(chǔ),但是為什么任意一個(gè)Java對(duì)象都能成為鎖對(duì)象呢?
首先,Java中的每個(gè)對(duì)象都派生自O(shè)bject類(lèi),而每個(gè)Java Object在JVM內(nèi)部都有一個(gè)native的C++對(duì)象 oop/oopDesc進(jìn)行對(duì)應(yīng)。
其次,線程在獲取鎖的時(shí)候,實(shí)際上就是獲得一個(gè)監(jiān)視器對(duì)象(monitor) ,monitor可以認(rèn)為是一個(gè)同步對(duì)象,所有的Java對(duì)象是天生攜帶monitor.
在hotspot源碼的 markOop.hpp文件中,可以看到下面這段代碼。
ObjectMonitor* monitor() const { assert(has_monitor(), "check"); // Use xor instead of &~ to provide one extra tag-bit check. return (ObjectMonitor*) (value() ^ monitor_value); }
多個(gè)線程訪問(wèn)同步代碼塊時(shí),相當(dāng)于去爭(zhēng)搶對(duì)象監(jiān)視器修改對(duì)象中的鎖標(biāo)識(shí),上面的代碼中ObjectMonitor這個(gè)對(duì)象和線程爭(zhēng)搶鎖的邏輯有密切的關(guān)系(后續(xù)會(huì)詳細(xì)分析)
鎖的升級(jí)前面提到了鎖的幾個(gè)概念,偏向鎖、輕量級(jí)鎖、重量級(jí)鎖。在JDK1.6之前,synchronized是一個(gè)重量級(jí)鎖,性能比較差。從JDK1.6開(kāi)始,為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗,synchronized進(jìn)行了優(yōu)化,引入了 偏向鎖和 輕量級(jí)鎖的概念。所以從JDK1.6開(kāi)始,鎖一共會(huì)有四種狀態(tài),鎖的狀態(tài)根據(jù)競(jìng)爭(zhēng)激烈程度從低到高分別是:無(wú)鎖狀態(tài)->偏向鎖狀態(tài)->輕量級(jí)鎖狀態(tài)->重量級(jí)鎖狀態(tài)。這幾個(gè)狀態(tài)會(huì)隨著鎖競(jìng)爭(zhēng)的情況逐步升級(jí)。為了提高獲得鎖和釋放鎖的效率,鎖可以升級(jí)但是不能降級(jí)。
下面就詳細(xì)講解synchronized的三種鎖的狀態(tài)及升級(jí)原理
在大多數(shù)的情況下,鎖不僅不存在多線程的競(jìng)爭(zhēng),而且總是由同一個(gè)線程獲得。因此為了讓線程獲得鎖的代價(jià)更低引入了偏向鎖的概念。偏向鎖的意思是如果一個(gè)線程獲得了一個(gè)偏向鎖,如果在接下來(lái)的一段時(shí)間中沒(méi)有其他線程來(lái)競(jìng)爭(zhēng)鎖,那么持有偏向鎖的線程再次進(jìn)入或者退出同一個(gè)同步代碼塊,不需要再次進(jìn)行搶占鎖和釋放鎖的操作。偏向鎖可以通過(guò) -XX:+UseBiasedLocking開(kāi)啟或者關(guān)閉
偏向鎖的獲取偏向鎖的獲取過(guò)程非常簡(jiǎn)單,當(dāng)一個(gè)線程訪問(wèn)同步塊獲取鎖時(shí),會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)偏向鎖的線程ID,表示哪個(gè)線程獲得了偏向鎖,結(jié)合前面分析的Mark Word來(lái)分析一下偏向鎖的獲取邏輯
首先獲取目標(biāo)對(duì)象的Mark Word,根據(jù)鎖的標(biāo)識(shí)為和epoch去判斷當(dāng)前是否處于可偏向的狀態(tài)
如果為可偏向狀態(tài),則通過(guò)CAS操作將自己的線程ID寫(xiě)入到MarkWord,如果CAS操作成功,則表示當(dāng)前線程成功獲取到偏向鎖,繼續(xù)執(zhí)行同步代碼塊
如果是已偏向狀態(tài),先檢測(cè)MarkWord中存儲(chǔ)的threadID和當(dāng)前訪問(wèn)的線程的threadID是否相等,如果相等,表示當(dāng)前線程已經(jīng)獲得了偏向鎖,則不需要再獲得鎖直接執(zhí)行同步代碼;如果不相等,則證明當(dāng)前鎖偏向于其他線程,需要撤銷(xiāo)偏向鎖。
CAS:表示自旋鎖,由于線程的阻塞和喚醒需要CPU從用戶(hù)態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對(duì)CPU來(lái)說(shuō)性能開(kāi)銷(xiāo)很大。同時(shí),很多對(duì)象鎖的鎖定狀態(tài)指會(huì)持續(xù)很短的時(shí)間,因此引入了自旋鎖,所謂自旋就是一個(gè)無(wú)意義的死循環(huán),在循環(huán)體內(nèi)不斷的重行競(jìng)爭(zhēng)鎖。當(dāng)然,自旋的次數(shù)會(huì)有限制,超出指定的限制會(huì)升級(jí)到阻塞鎖。偏向鎖的撤銷(xiāo)
當(dāng)其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí),持有偏向鎖的線程才會(huì)釋放偏向鎖,撤銷(xiāo)偏向鎖的過(guò)程需要等待一個(gè)全局安全點(diǎn)(所有工作線程都停止字節(jié)碼的執(zhí)行)。
首先,暫停擁有偏向鎖的線程,然后檢查偏向鎖的線程是否為存活狀態(tài)
如果線程已經(jīng)死了,直接把對(duì)象頭設(shè)置為無(wú)鎖狀態(tài)
如果還活著,當(dāng)達(dá)到全局安全點(diǎn)時(shí)獲得偏向鎖的線程會(huì)被掛起,接著偏向鎖升級(jí)為輕量級(jí)鎖,然后喚醒被阻塞在全局安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼
偏向鎖的獲取流程圖 輕量級(jí)鎖前面我們知道,當(dāng)存在超過(guò)一個(gè)線程在競(jìng)爭(zhēng)同一個(gè)同步代碼塊時(shí),會(huì)發(fā)生偏向鎖的撤銷(xiāo)。偏向鎖撤銷(xiāo)以后對(duì)象會(huì)可能會(huì)處于兩種狀態(tài)
一種是不可偏向的無(wú)鎖狀態(tài),簡(jiǎn)單來(lái)說(shuō)就是已經(jīng)獲得偏向鎖的線程已經(jīng)退出了同步代碼塊,那么這個(gè)時(shí)候會(huì)撤銷(xiāo)偏向鎖,并升級(jí)為輕量級(jí)鎖
一種是不可偏向的已鎖狀態(tài),簡(jiǎn)單來(lái)說(shuō)就是已經(jīng)獲得偏向鎖的線程正在執(zhí)行同步代碼塊,那么這個(gè)時(shí)候會(huì)升級(jí)到輕量級(jí)鎖并且被原持有鎖的線程獲得鎖
那么升級(jí)到輕量級(jí)鎖以后的加鎖過(guò)程和解鎖過(guò)程是怎么樣的呢?
輕量級(jí)鎖加鎖JVM會(huì)先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲(chǔ)鎖記錄的空間(LockRecord)
將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,稱(chēng)為Displaced Mark Word.
線程嘗試使用CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針
如果替換成功,表示當(dāng)前線程獲得輕量級(jí)鎖,如果失敗,表示存在其他線程競(jìng)爭(zhēng)鎖,那么當(dāng)前線程會(huì)嘗試使用CAS來(lái)獲取鎖,當(dāng)自旋超過(guò)指定次數(shù)(可以自定義)時(shí)仍然無(wú)法獲得鎖,此時(shí)鎖會(huì)膨脹升級(jí)為重量級(jí)鎖
輕量鎖解鎖嘗試CAS操作將所記錄中的Mark Word替換回到對(duì)象頭中
如果成功,表示沒(méi)有競(jìng)爭(zhēng)發(fā)生
如果失敗,表示當(dāng)前鎖存在競(jìng)爭(zhēng),鎖會(huì)膨脹成重量級(jí)鎖
一旦鎖升級(jí)成重量級(jí)鎖,就不會(huì)再恢復(fù)到輕量級(jí)鎖狀態(tài)。當(dāng)鎖處于重量級(jí)鎖狀態(tài),其他線程嘗試獲取鎖時(shí),都會(huì)被阻塞,也就是 BLOCKED狀態(tài)。當(dāng)持有鎖的線程釋放鎖之后會(huì)喚醒這些現(xiàn)場(chǎng),被喚醒之后的線程會(huì)進(jìn)行新一輪的競(jìng)爭(zhēng)重量級(jí)鎖
重量級(jí)鎖依賴(lài)對(duì)象內(nèi)部的monitor鎖來(lái)實(shí)現(xiàn),而monitor又依賴(lài)操作系統(tǒng)的MutexLock(互斥鎖)
大家如果對(duì)MutexLock有興趣,可以抽時(shí)間去了解,假設(shè)Mutex變量的值為1,表示互斥鎖空閑,這個(gè)時(shí)候某個(gè)線程調(diào)用lock可以獲得鎖,而Mutex的值為0表示互斥鎖已經(jīng)被其他線程獲得,其他線程調(diào)用lock只能掛起等待
為什么重量級(jí)鎖的開(kāi)銷(xiāo)比較大呢?
原因是當(dāng)系統(tǒng)檢查到是重量級(jí)鎖之后,會(huì)把等待想要獲取鎖的線程阻塞,被阻塞的線程不會(huì)消耗CPU,但是阻塞或者喚醒一個(gè)線程,都需要通過(guò)操作系統(tǒng)來(lái)實(shí)現(xiàn),也就是相當(dāng)于從用戶(hù)態(tài)轉(zhuǎn)化到內(nèi)核態(tài),而轉(zhuǎn)化狀態(tài)是需要消耗時(shí)間的
總結(jié)到目前為止,我們分析了synchronized的使用方法、以及鎖的存儲(chǔ)、對(duì)象頭、鎖升級(jí)的原理。如果有問(wèn)題,可以掃描二維碼留言
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/72574.html
摘要:原文地址游客前言金三銀四,很多同學(xué)心里大概都準(zhǔn)備著年后找工作或者跳槽。最近有很多同學(xué)都在交流群里求大廠面試題。 最近整理了一波面試題,包括安卓JAVA方面的,目前大廠還是以安卓源碼,算法,以及數(shù)據(jù)結(jié)構(gòu)為主,有一些中小型公司也會(huì)問(wèn)到混合開(kāi)發(fā)的知識(shí),至于我為什么傾向于混合開(kāi)發(fā),我的一句話就是走上編程之路,將來(lái)你要學(xué)不僅僅是這些,豐富自己方能與世接軌,做好全棧的裝備。 原文地址:游客kutd...
摘要:結(jié)構(gòu)型模式適配器模式橋接模式裝飾模式組合模式外觀模式享元模式代理模式。行為型模式模版方法模式命令模式迭代器模式觀察者模式中介者模式備忘錄模式解釋器模式模式狀態(tài)模式策略模式職責(zé)鏈模式責(zé)任鏈模式訪問(wèn)者模式。 主要版本 更新時(shí)間 備注 v1.0 2015-08-01 首次發(fā)布 v1.1 2018-03-12 增加新技術(shù)知識(shí)、完善知識(shí)體系 v2.0 2019-02-19 結(jié)構(gòu)...
摘要:為了避免一篇文章的篇幅過(guò)長(zhǎng),于是一些比較大的主題就都分成幾篇來(lái)講了,這篇文章是筆者所有文章的目錄,將會(huì)持續(xù)更新,以給大家一個(gè)查看系列文章的入口。 前言 大家好,筆者是今年才開(kāi)始寫(xiě)博客的,寫(xiě)作的初衷主要是想記錄和分享自己的學(xué)習(xí)經(jīng)歷。因?yàn)閷?xiě)作的時(shí)候發(fā)現(xiàn),為了弄懂一個(gè)知識(shí),不得不先去了解另外一些知識(shí),這樣以來(lái),為了說(shuō)明一個(gè)問(wèn)題,就要把一系列知識(shí)都了解一遍,寫(xiě)出來(lái)的文章就特別長(zhǎng)。 為了避免一篇...
摘要:為了避免一篇文章的篇幅過(guò)長(zhǎng),于是一些比較大的主題就都分成幾篇來(lái)講了,這篇文章是筆者所有文章的目錄,將會(huì)持續(xù)更新,以給大家一個(gè)查看系列文章的入口。 前言 大家好,筆者是今年才開(kāi)始寫(xiě)博客的,寫(xiě)作的初衷主要是想記錄和分享自己的學(xué)習(xí)經(jīng)歷。因?yàn)閷?xiě)作的時(shí)候發(fā)現(xiàn),為了弄懂一個(gè)知識(shí),不得不先去了解另外一些知識(shí),這樣以來(lái),為了說(shuō)明一個(gè)問(wèn)題,就要把一系列知識(shí)都了解一遍,寫(xiě)出來(lái)的文章就特別長(zhǎng)。 為了避免一篇...
閱讀 728·2023-04-25 20:32
閱讀 2287·2021-11-24 10:27
閱讀 4532·2021-09-29 09:47
閱讀 2251·2021-09-28 09:36
閱讀 3648·2021-09-22 15:27
閱讀 2768·2019-08-30 15:54
閱讀 380·2019-08-30 11:06
閱讀 1278·2019-08-30 10:58