<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
目標:
synchronized回顧(鎖分類–>多執行緒)
概念
synchronized:是Java中的關鍵字,是一種同步鎖。
Java中鎖分為以下幾種:
樂觀鎖、悲觀鎖(syn)
獨享鎖(syn)、共用鎖
公平鎖、非公平鎖(syn)
互斥鎖(syn)、讀寫鎖
可重入鎖(syn)
分段鎖
synchronized JDK1.6鎖升級(無鎖 -> 偏向鎖 (非鎖)-> 輕量級鎖 -> 重量級鎖(1.6前都是)【面試常問】
tips:
為什麼用到鎖?大家肯定會想到多執行緒(並行)
接下來,我們一起簡單回顧下多執行緒特性
多執行緒特性回顧(面試常問)原子性
:指一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執
行可見性
:是指多個執行緒存取一個資源時,該資源的狀態、值資訊等對於其他執行緒都是可見的。有序性
:指程式中程式碼的執行順序 (編譯器會重排)
原子性實現回顧
保證了原子性?
com.syn.com.syn.th.SyncAtomicity
package com.syn.com.syn.th; import java.util.concurrent.TimeUnit; /* 目標:測試原子性問題 1、呼叫正常(不加鎖)方法;兩個執行緒都可以正常執行 2、呼叫加鎖方法,只能有一個執行緒正常執行,其他執行緒排隊等候 */ public class SyncAtomicity { public static void main(String[] args) throws InterruptedException { SyncAtomicity syncAtomicity = new SyncAtomicity(); //synchronized修飾實體方法 //new Thread(()->syncAtomicity.testSYNC()).start(); //new Thread(()->syncAtomicity.testSYNC()).start(); //synchronized修飾靜態方法 new Thread(() -> SyncAtomicity.testSYNCForStatic()).start(); new Thread(() -> SyncAtomicity.testSYNCForStatic()).start(); //正常方法 //new Thread(() -> syncAtomicity.test()).start(); //new Thread(() -> syncAtomicity.test()).start(); } //加鎖方法 public synchronized void testSYNC() { System.out.println("進入testSYNC方法>>>>>>>>>>>>>>>>>>>>>"); try { //模擬方法體尚未執行完畢 TimeUnit.HOURS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } //加鎖方法 public synchronized static void testSYNCForStatic() { System.out.println("進入testSYNC方法>>>>>>>>>>>>>>>>>>>>>"); try { //模擬方法體尚未執行完畢 TimeUnit.HOURS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } //正常方法 public void test() { System.out.println("進入test方法>>>>>>>>>>>>>>>>>>>>>"); try { //模擬方法體尚未執行完畢 TimeUnit.HOURS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }
總結
我們發現在同一時刻確實只有一個執行緒進入,保證了原子性
這是什麼原理呢?
目標 通過javap反組合看一下synchronized到底是怎麼加鎖的
com.syn.BTest
public class BTest { private static Object object = new Object(); public synchronized void testMethod() { System.out.println("Hello World -synchronized method "); } public static void main(String[] args) { synchronized (object) { System.out.println("Hello World -synchronized block "); } } }
反組合後,我們將看到什麼?
JDK自帶的一個工具: javap ,對位元組碼進行反組合:
//com.syn.BTest javap -v -c BTest.class
反組合後
解釋
被synchronized修飾的程式碼塊,多了兩個指令
monitorenter、monitorexit
即JVM使用monitorenter和monitorexit兩個指令實現同步
解釋
被synchronized修飾的方法;增加 了ACC_SYNCHRONIZED 修飾。會隱式呼叫monitorenter和
monitorexit。
monitorenter原理(重要)
monitorenter首先我們來看一下JVM規範中對於monitorenter的描述
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
翻譯如下:
每一個物件都會和一個監視器monitor關聯。
監視器被佔用時會被鎖住,其他執行緒無法來獲取該monitor。
當JVM執行某個執行緒的某個方法內部的monitorenter時,它會嘗試去獲取當前物件對應的monitor的所有權。其過程如下:
monitor,不再擁有monitor的所有權,此時其他被這個monitor阻塞的執行緒可以嘗試去獲取這個
monitor的所有權
monitorexit釋放鎖。
monitorexit插入在方法結束處和異常處,JVM保證每個monitorenter必須有對應的monitorexit。
tips(重要)
上面文字太多,杜絕去念!!!!!!
用圖說話!!!! !!!!!!!!
類:com.syn.BTest
public static void main(String[] args) { synchronized (object) { System.out.println("Hello World -synchronized block "); } }
總結:
通過上面的流程我們發現
1、synchronized是靠Monitor關聯拿到鎖的
2、如果競爭的時候拿不到鎖,執行緒就去競爭佇列
3、如果拿到鎖了,第二次拿,它又拿到鎖,其他執行緒進入阻塞佇列
4、如果拿到鎖的執行緒呼叫了wait方法,其他執行緒進入等待佇列
5、釋放鎖,需要將計數器減減操作
6、出現異常,也釋放鎖。
synchronized是Java中的關鍵字,無法通過JDK原始碼檢視它的實現,它是由JVM提供支援的,所以如果想要了解具體的實現需要檢視JVM原始碼
目標:JVM虛擬機器器原始碼下載
http://hg.openjdk.java.net/jdk8/jdk8/hotspot/
或者
http://hg.openjdk.java.net/jdk8/jdk8/hotspot/archive/tip.zip
解壓檢視即可,無需環境搭建
目標: 通過JVM虛擬機器器原始碼分析synchronized監視器Monitor是怎麼生成的
tips:
c++原始碼只看重點、弄懂原理
c++重要嗎?不重要
但是面試時很重要,面試過去了就不重要!!!!!!!!!!!!
學別人不會的東西你才有價值!!!!你會、大家都會,沒啥意思!!
在HotSpot虛擬機器器中,monitor監視器是由ObjectMonitor實現的。
構造器程式碼src/share/vm/runtime/objectMonitor.hpp
hpp可以include包含cpp的東西,兩者都是c++的程式碼
//構造器 ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; // 遞迴:執行緒的重入次數,典型的System.out.println _object = NULL; // 對應synchronized (object)對應裡面的object _owner = NULL; // 標識擁有該monitor的執行緒 _WaitSet = NULL; // 因為呼叫object.wait()方法而被阻塞的執行緒會被放在該佇列中 _WaitSetLock = 0 ; _Responsible = NULL; _succ = NULL; _cxq = NULL; // 競爭佇列,所有請求鎖的執行緒首先會被放在這個佇列中 FreeNext = NULL; _EntryList = NULL; // 阻塞;第二輪競爭鎖仍然沒有搶到的執行緒(偏向/可重入) _SpinFreq = 0; _SpinClock = 0; OwnerIsThread = 0; }
結論:正好印證了上面的流程圖
目標: 通過JVM虛擬機器器原始碼分析synchronized多個執行緒搶奪鎖,拿到鎖之後要幹什麼?
monitorenter指令執行:
JVM原始碼:src/share/vm/interpreter/interpreterRuntime.cpp
JVM函數: InterpreterRuntime::monitorenter函數
//鎖競爭InterpreterRuntime::monitorenter IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif if (PrintBiasedLockingStatistics) { Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); } Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); //偏向鎖(非鎖:jdk14廢棄) if (UseBiasedLocking) { // Retry fast entry if bias is revoked to avoid unnecessary inflation ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else { // 重量級鎖,最終呼叫了objectMonitor.cpp中的ObjectMonitor::enter ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); ...略
最終呼叫objectMonitor.cpp檔案中的 ObjectMonitor::enter
src/share/vm/runtime/objectMonitor.cpp
//重量級鎖入口 void ATTR ObjectMonitor::enter(TRAPS) { Thread * const Self = THREAD ; void * cur ; // 1、通過CAS(原子操作)操作嘗試把monitor的_owner欄位設定為當前執行緒(開始競爭) cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ; if (cur == NULL) { // Either ASSERT _recursions == 0 or explicitly set _recursions = 0. assert (_recursions == 0 , "invariant") ; assert (_owner == Self, "invariant") ; // CONSIDER: set or assert OwnerIsThread == 1 return ; } // 2、拿到鎖;計數+1,recursions++ if (cur == Self) { _recursions ++ ;//第一次進入(計數+1) return ; } if (Self->is_lock_owned ((address)cur)) { assert (_recursions == 0, "internal state error"); _recursions = 1 ; _owner = Self ; OwnerIsThread = 1 ; return ; } assert (Self->_Stalled == 0, "invariant") ; Self->_Stalled = intptr_t(this) ; if (Knob_SpinEarly && TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_recursions == 0 , "invariant") ; assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ; Self->_Stalled = 0 ; return ; } assert (_owner != Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (Self->is_Java_thread() , "invariant") ; JavaThread * jt = (JavaThread *) Self ; assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ; assert (jt->thread_state() != _thread_blocked , "invariant") ; assert (this->object() != NULL , "invariant") ; assert (_count >= 0, "invariant") ; Atomic::inc_ptr(&_count); EventJavaMonitorEnter event; { JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this); DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt); if (JvmtiExport::should_post_monitor_contended_enter()) { JvmtiExport::post_monitor_contended_enter(jt, this); } OSThreadContendState osts(Self->osthread()); ThreadBlockInVM tbivm(jt); Self->set_current_pending_monitor(this); for (;;) { jt->set_suspend_equivalent(); // cleared by handle_special_suspend_equivalent_condition() // or java_suspend_self() // 3、獲取鎖失敗的執行緒,則等待!!!!!!!!!!!!!!!!!!!!!!!! EnterI (THREAD) ; if (!ExitSuspendEquivalent(jt)) break ; _recursions = 0 ; _succ = NULL ; exit (false, Self) ; jt->java_suspend_self(); } Self->set_current_pending_monitor(NULL); }
總結
目標: 通過JVM虛擬機器器原始碼分析synchronized拿不到鎖的執行緒他們都去幹什麼了?
還是 /objectMonitor.cpp
還是EnterI函數
路徑:src/share/vm/runtime/objectMonitor.cpp的
//拿不到鎖的執行緒他們都去幹什麼了?? void ATTR ObjectMonitor::EnterI (TRAPS) { Thread * Self = THREAD ; assert (Self->is_Java_thread(), "invariant") ; assert (((JavaThread *) Self)->thread_state() == _thread_blocked , "invariant") ; // 沒拿到鎖,還是要嘗試TryLock一次 if (TryLock (Self) > 0) { //拿到鎖執行,在返回 assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ;//成功獲取 } DeferredInitialize () ; //沒拿到鎖,開始TrySpin自旋(CAS,while迴圈) if (TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } assert (_succ != Self , "invariant") ; assert (_owner != Self , "invariant") ; assert (_Responsible != Self , "invariant") ; // 實在拿不到鎖;當前執行緒被封裝成ObjectWaiter物件node,狀態設定成ObjectWaiter::TS_CXQ //即將放入競爭佇列 ObjectWaiter node(Self) ; Self->_ParkEvent->reset() ; node._prev = (ObjectWaiter *) 0xBAD ; node.TState = ObjectWaiter::TS_CXQ ; ObjectWaiter * nxt ; for (;;) { node._next = nxt = _cxq ; //使用核心函數cmpxchg_ptr 將沒有拿到鎖執行緒(node)放到競爭佇列 if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ; if (TryLock (Self) > 0) { assert (_succ != Self , "invariant") ; assert (_owner == Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; } } if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) { Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; } TEVENT (Inflated enter - Contention) ; int nWakeups = 0 ; int RecheckInterval = 1 ; //將競爭佇列執行緒掛起 for (;;) { // 執行緒在被掛起前做一下掙扎,看能不能獲取到鎖 if (TryLock (Self) > 0) break ; assert (_owner != Self, "invariant") ; if ((SyncFlags & 2) && _Responsible == NULL) { Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ; } // park self if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; // Increase the RecheckInterval, but clamp the value. RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; // 掛起!!!!!!::通過park將當前執行緒掛起(不被執行了),等待被喚 醒!!!!!!!!!!! Self->_ParkEvent->park() ; } //當該執行緒被喚醒時,執行TryLock----->ObjectMonitor::TryLoc !!!!!!!!!!!!!!!!!!!!! if (TryLock(Self) > 0) break ;
當該執行緒被喚醒時,會從掛起的點繼續執行,通過 ObjectMonitor::TryLock 嘗試獲取鎖
總結
4. 競爭失敗的執行緒被封裝成ObjectWaiter物件node,狀態設定成ObjectWaiter::TS_CXQ(競爭隊
列)
5. 在for迴圈中,通過CAS把node節點push到_cxq列表中,(競爭佇列)
6. node節點push到_cxq列表之後,通過自旋嘗試獲取鎖,如果還是沒有獲取到鎖,則通過park將當
前執行緒掛起,等待被喚醒。
7. 當該執行緒被喚醒時,會從掛起的點繼續執行,通過 ObjectMonitor::TryLock 嘗試獲取鎖。
一句話總結:沒拿到,嘗試拿一次、在自旋去拿、實在拿不到就去競爭佇列、等待喚醒
目標: 通過JVM虛擬機器器原始碼分析synchronized拿到鎖的執行緒最後是怎麼釋放鎖的?
執行monitorexit指令
還是 /objectMonitor.cpp
裡面的exit函數
Osrc/share/vm/runtime/objectMonitor.cpp
//執行緒釋放呼叫exit方法 void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) { Thread * Self = THREAD ; if (THREAD != _owner) { if (THREAD->is_lock_owned((address) _owner)) { assert (_recursions == 0, "invariant") ; _owner = THREAD ; _recursions = 0 ; OwnerIsThread = 1 ; } else { TEVENT (Exit - Throw IMSX) ; assert(false, "Non-balanced monitor enter/exit!"); if (false) { THROW(vmSymbols::java_lang_IllegalMonitorStateException()); } return; } } //_recursions計數不等於0;說明還沒出程式碼塊;進入減減操作, if (_recursions != 0) { _recursions--; // this is simple recursive enter TEVENT (Inflated exit - recursive) ; return ; } if ((SyncFlags & 4) == 0) { _Responsible = NULL ; } #if INCLUDE_TRACE if (not_suspended && Tracing::is_event_enabled(TraceJavaMonitorEnterEvent)) { _previous_owner_tid = SharedRuntime::get_java_tid(Self); } #endif for (;;) { assert (THREAD == _owner, "invariant") ; if (Knob_ExitPolicy == 0) { OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock OrderAccess::storeload() ; // See if we need to wake a successor if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { TEVENT (Inflated exit - simple egress) ; return ; } TEVENT (Inflated exit - complex egress) ; if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) { return ; } TEVENT (Exit - Reacquired) ; } else { if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) { OrderAccess::release_store_ptr (&_owner, NULL) ; // drop the lock OrderAccess::storeload() ; // Ratify the previously observed values. if (_cxq == NULL || _succ != NULL) { TEVENT (Inflated exit - simple egress) ; return ; } if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) { TEVENT (Inflated exit - reacquired succeeded) ; return ; } TEVENT (Inflated exit - reacquired failed) ; } else { TEVENT (Inflated exit - complex egress) ; } } guarantee (_owner == THREAD, "invariant") ; // 計數為0;開始喚醒cq競爭佇列、enteryList阻塞佇列 ObjectWaiter * w = NULL ;//w就是被喚醒的執行緒 int QMode = Knob_QMode ; // qmode = 2:直接繞過EntryList阻塞佇列,從cxq(競爭)佇列中獲取執行緒用於競爭鎖 if (QMode == 2 && _cxq != NULL) { w = _cxq ; assert (w != NULL, "invariant") ; assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ; ExitEpilog (Self, w) ; return ; } // qmode =3:cxq(競爭)佇列插入EntryList(阻塞)尾部; if (QMode == 3 && _cxq != NULL) { w = _cxq ; for (;;) { assert (w != NULL, "Invariant") ; ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; } assert (w != NULL , "invariant") ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; } // Append the RATs to the EntryList // TODO: organize EntryList as a CDLL so we can locate the tail in constant-time. ObjectWaiter * Tail ; for (Tail = _EntryList ; Tail != NULL && Tail->_next != NULL ; Tail = Tail->_next) ; if (Tail == NULL) { _EntryList = w ; } else { Tail->_next = w ; w->_prev = Tail ; } } // qmode =4:cxq佇列插入到_EntryList頭部 if (QMode == 4 && _cxq != NULL) { // Aggressively drain cxq into EntryList at the first opportunity. // This policy ensure that recently-run threads live at the head of EntryList. // Drain _cxq into EntryList - bulk transfer. // First, detach _cxq. // The following loop is tantamount to: w = swap (&cxq, NULL) w = _cxq ; for (;;) { assert (w != NULL, "Invariant") ; ObjectWaiter * u = (ObjectWaiter *) Atomic::cmpxchg_ptr (NULL, &_cxq, w) ; if (u == w) break ; w = u ; } assert (w != NULL , "invariant") ; ObjectWaiter * q = NULL ; ObjectWaiter * p ; for (p = w ; p != NULL ; p = p->_next) { guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ; p->TState = ObjectWaiter::TS_ENTER ; p->_prev = q ; q = p ; } // Prepend the RATs to the EntryList if (_EntryList != NULL) { q->_next = _EntryList ; _EntryList->_prev = q ; } _EntryList = w ; // Fall thru into code that tries to wake a successor from EntryList } w = _EntryList ; if (w != NULL) { assert (w->TState == ObjectWaiter::TS_ENTER, "invariant") ; ExitEpilog (Self, w) ;//喚醒w!!!!!!!!!!!!!!!!!!!!!! ------->當前 類的ExitEpilog return ; }
實現如下
void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) { assert (_owner == Self, "invariant") ; _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ; ParkEvent * Trigger = Wakee->_event ; Wakee = NULL ; // Drop the lock OrderAccess::release_store_ptr (&_owner, NULL) ; OrderAccess::fence() ; // ST _owner vs LD in unpark() if (SafepointSynchronize::do_call_back()) { TEVENT (unpark before SAFEPOINT) ; } DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self); // 喚醒之前被park()掛起的執行緒. Trigger->unpark() ;// invoke ObjectMonitor::EnterI 方法,繼續競爭 if (ObjectMonitor::_sync_Parks != NULL) { ObjectMonitor::_sync_Parks->inc() ; } }
被喚醒的執行緒,回到 ObjectMonitor::EnterI (TRAPS) 的第600行,繼續執行monitor 的競爭。
// park self if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; // Increase the RecheckInterval, but clamp the value. RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; Self->_ParkEvent->park() ; } //喚醒之後就開始搶奪鎖 if (TryLock(Self) > 0) break ;
TryLock方 法實現如下:
//執行緒嘗試獲取鎖(or 執行緒被喚醒後獲取) int ObjectMonitor::TryLock (Thread * Self) { for (;;) { void * own = _owner ; if (own != NULL) return 0 ; //獲取 if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) { // Either guarantee _recursions == 0 or set _recursions = 0. assert (_recursions == 0, "invariant") ; assert (_owner == Self, "invariant") ; // 嘗試拿到鎖返回1 return 1 ; } //拿不到鎖返回-1 if (true) return -1 ; } }
總結
1、先進入減減操作,直到為0
2、為0後,喚醒競爭佇列的執行緒
3、喚醒執行緒後,繼續爭奪鎖,迴圈前面的步驟(鎖競爭-----等待----釋放)
一句話總結:釋放後,進入減減操作、直到為0然後喚醒佇列,讓他們去爭奪鎖,迴圈前面步驟
到此這篇關於Java同步鎖Synchronized底層原始碼和原理剖析的文章就介紹到這了,更多相關Java同步鎖Synchronized內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45