【Java面试高频-集合】- 读写的场景设计集合是怎么样?对于读多写少要如何设计的呢?对于读少写多又该如何设计呢?
Java集合-讀寫的場景
1 讀多寫少的場景
**CopyOnWrite的思想。**讀數據不加鎖,寫數據的時候利用拷貝的副本來執行。
ReadWriteLock的思想也可以。
那么 寫線程現在把副本數組給修改完了,現在怎么才能讓讀線程感知到這個變化呢?
配合volatile關鍵字的使用,核心就是讓一個變量被寫線程給修改之后,立馬讓其他線程可以讀到這個變量引用的最近的值。 所以一旦線程搞定了副本數組的修改之后,那么就可以用volatile寫的方式,把這個副本數組賦值給volatile修飾的那個數組的引用變量了。只要一賦值給那個volatile修飾的變量,立馬就會對讀線程可見,大家就能看到最新的數組了。
那么要是多個線程要同時更新呢?那搞出來 多個副本會不會有什么問題?
不會,因為加入一個lock鎖的機制,也就是同一時間只能有一個線程可以更新。
2 讀少寫多的場景
優化思路總結:
- 給每個用戶信息配一把鎖,把用戶信息存在數組中,array[drive_id]=driver_info.如果數據量不大,這種方案可行。如果數據量非常大,會非常耗內存;
- 水平切分,按照driver_id進行哈希,比如對10000進行取模,則把司機信息切分成1w組,用1w個map來存放司機信息。面對大數據時這種方法非常適合;
- 去掉鎖,存放的時候仍然以KV的map來存放,但是value不是在單獨的driver_info, 而是先給driver_info生成一個簽名,然后將簽名+driver_info分兩步寫到定長的內存中,作為value存放。取得時候,對value中的driver_info先生成簽名,然后跟取出來的簽名比較。如果一樣,說明driver_info沒有發生竟態寫。
(1) 需求緣起
【業務場景】
有一類寫多讀少的場景:大部分請求是對數據進行修改,少部分請求對數據讀取;
例子1: 例子1:滴滴打車,某個司機地理位置信息的變化(可能每幾秒鐘有一個修改),以及司機地理位置的讀取(用戶打車的時候查看某個司機的地理位置)。
void SetDriverInfo(long driver_id, DriverInfoi); // 大量請求調用修改司機信息,可能主要是GPS位置的修改
DriverInfo GetDriverInfo(long driver_id); // 少量請求查詢司機信息
【底層實現】
具體到底層實現往往就是一個Map(本質上是一個定長key,定長value的緩存結構)來存儲司機的信息,或者某個類型的計數。
【臨界資源】
這個Map存儲了所有信息,當并發讀寫訪問時,它作為臨界資源,在讀寫之前,一般要進行加鎖操作,以司機信息為例:
void SetDriverInfo(long driver_id, DriverInfoinfo){WriteLock (m_lock);Map= info;UnWriteLock(m_lock);}DriverInfo GetDriverInfo(long driver_id){DriverInfo t;ReadLock(m_lock);t= Map;UnReadLock(m_lock);return t;}【并發鎖瓶頸】
假設滴滴有100w司機同時在線,每個司機沒5秒更新一次經緯度狀態,那么每秒就有20w次寫并發操作。假設滴滴日訂單1000w個,平均每秒大概也有300個下單,對應到查詢并發量,可能是1000級別的并發讀操作。
上述實現方案沒有任何問題,但在并發量很大的時候(每秒20w寫,1k讀),鎖m_lock會成為潛在瓶頸,在這類高并發環境下寫多讀少的業務倉井,如何來進行優化,是本文將要討論的問題。
(2) 水平切分+鎖粒度優化
上文中之所以鎖沖突嚴重,是因為所有司機都公用一把鎖,鎖的粒度太粗(可以認為是一個數據庫的“庫級別鎖”),是否可能進行水平拆分(類似于數據庫里的分庫),把一個庫鎖變成多個庫鎖,來提高并發,降低鎖沖突呢?顯然是可以的,把1個Map水平切分成多個Map即可:
void SetDriverInfo(long driver_id, DriverInfoinfo){i= driver_id % N; // 水平拆分成N份,N個Map,N個鎖WriteLock (m_lock [i]); //鎖第i把鎖Map[i]<driver_id>= info; // 操作第i個MapUnWriteLock (m_lock[i]); // 解鎖第i把鎖 }每個Map的并發量(變成了1/N)和數據量都降低(變成了1/N)了,所以理論上,鎖沖突會成平方指數降低。
分庫之后,仍然是庫鎖,有沒有辦法變成數據庫層面所謂的“行級鎖”呢,難道要把x條記錄變成x個Map嗎,這顯然是不現實的。
(3) Map變Array+最細鎖粒度優化
假設driver_id是遞增生成的,并且緩存的內存比較大,是可以把Map優化成Array,而不是拆分成N個Map,是有可能把鎖的粒度細化到最細的(每個記錄一個鎖)。
void SetDriverInfo(long driver_id, DriverInfoinfo){index= driver_id;WriteLock (m_lock [index]); //超級大內存,一條記錄一個鎖,鎖行鎖Array[index]= info; //driver_id就是Array下標UnWriteLock (m_lock[index]); // 解鎖行鎖 }和上一個方案相比,這個方案使得鎖沖突降到了最低,但鎖資源大增,在數據量非常大的情況下,一般不這么搞。數據量比較小的時候,可以一個元素一個鎖的(典型的是連接池,每個連接有一個鎖表示連接是否可用)。
上文中提到的另一個例子,用戶操作類型計數,操作類型是有限的,即使一個type一個鎖,鎖的沖突也可能是很高的,還沒有方法進一步提高并發呢?
(3)把鎖去掉,變成無鎖緩存
【無鎖的結果】
void AddCountByType(long type /*, int count*/){//不加鎖Array[type]++; // 計數++//Array[type] += count; // 計數增加count }如果這個緩存不加鎖,當然可以達到最高的并發,但是多線程對緩存中同一塊定長數據進行操作時,有可能出現不一致的數據塊,這個方案為了提高性能,犧牲了一致性。在讀取計數時,獲取到了錯誤的數據,是不能接受的(作為緩存,允許cache miss,卻不允許讀臟數據)。
加上簽名之后,不但緩存要寫入定長value本身,還要寫入定長簽名(例如16bitCRC校驗):
(1)線程1對緩存進行操作,對key想要寫入value1,寫入簽名v1-sign;
(2)線程2對緩存進行操作,對key想要寫入value2,寫入簽名v2-sign;
(3)如果不加鎖,線程1和線程2對同一個定長區域進行一個并發的寫操作,可能每個線程寫成功一半,導致出現臟數據產生,最終的結果即不是value1也不是value2,而是一個亂七八糟的不符合預期的值value-unexpected,但簽名,一定是v1-sign或者v2-sign中的任意一個;
畫外音:16bit/32bit的寫可以保證原子性。
(4)數據讀取的時候,不但要取出value,還要像消息接收方收到消息一樣,校驗一下簽名,如果發現簽名不一致,緩存則返回NULL,即cache miss;
總結
以上是生活随笔為你收集整理的【Java面试高频-集合】- 读写的场景设计集合是怎么样?对于读多写少要如何设计的呢?对于读少写多又该如何设计呢?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设节屏幕亮度
- 下一篇: 高考决定命运吗?高中毕业,我用十年从深圳