Java併發包JUC核心原理解析

CS-LogN思維導圖:記錄CS基礎 面試題
開源地址:https://github.com/FISHers6/CS-LogN

JUC

分類

線程管理

  • 線程池相關類

    • Executor、Executors、ExecutorService
    • 常用的線程池:FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadExecutor
  • 能獲取子線程的運行結果

    • Callable、Future、FutureTask

併發流程管理

  • CountDwonLatch、CyclicBarrier、Semaphore、Condition

實現線程安全

  • 互斥同步(鎖)

    • Synchronzied、及工具類Vector、Collections
    • Lock接口的相關類:ReentrantLock、讀寫鎖
  • 非互斥同(原子類)

    • 原子基本類型、引用類型、原子升級、累加器
  • 併發容器

    • ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue
  • 無同步與不可變方案

    • final關鍵字、ThreadLocal棧封閉

線程池

使用線程池的作用好處

  • 降低資源消耗

    • 重複利用已創建的線程降低線程創建和銷毀造成的消耗
  • 提高響應速度

    • 任務到達,可以不需要等到線程創建就能立即執行
  • 提高線程的可管理性

    • 使用線程池可以進行統一的分配,調優和監控

線程池的參數

  • corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler

  • 圖示

常用線程池的創建與規則

  • 線程添加規則

    • 1.如果線程數量小於corePoolSize,即使工作線程處於空閑狀態,也會創建一個新線程來運行新任務,創建方法是使用threadFactory

    • 2.如果線程數量大於corePoolSize但小於maximumPoolSize,則將任務放入隊列

    • 3.如果workQueue隊列已滿,並且線程數量小於maxPoolSize,則開闢一個非核心新線程來運行任務

    • 4.如果隊列已滿,並且線程數大於或等於maxPoolSize,則拒絕該任務,執行handler

    • 圖示(分別與3個參數比較)

  • 常用線程池

    • newFixedThreadPool

      • 創建固定大小的線程池,使用無界隊列會發生OOM
    • newSingleThreadExecutor

      • 創建一個單線程的線程池,線程數為1
    • newCachedThreadPool

      • 創建一個可緩存的線程池,60s會回收部分空閑的線程。採用直接交付的隊列 SynchronousQueue ,隊列容量為0,來一個創建一個線程
    • newScheduledThreadPool

      • 創建一個大小無限的線程池。此線程池支持定時以及周期性執行任務的需求
  • 如何設置初始化線程池的大小?

    • 可根據線程池中的線程
      處理任務的不同進行分別估計

      • CPU 密集型任務

        • 大量的運算,無阻塞
          通常 CPU 利用率很高
          應配置盡可能少的線程數量
          設置為 CPU 核數 + 1
      • IO 密集型任務

        • 這類任務有大量 IO 操作
          伴隨着大量線程被阻塞
          有利於并行提高CPU利用率
          配置更多數量: CPU 核心數 * 2
  • 使用線程池的注意事項

    • 1.避免任務堆積(無界隊列會OOM)、2.避免線程數過多(cachePool直接交付隊列)、3.排查線程泄露

線程池的狀態和常用方法

  • 線程池的狀態

    • RUNNING(接受並處理任務中)、
      SHUTDOWN(不接受新任務但處理排隊任務)、
      STOP(不接受新任務 也不處理排隊任務 並中斷正在進行的任務)、
      TIDYING、TEMINATED(運行完成)
  • 線程池停止

    • shutdown

      • 通知有序停止,先前提交的任務務會執行
    • shutdownNow

      • 嘗試立即停止,忽略隊列里等待的任務

線程池的源碼解析

  • 線程池的組成

    • 1.線程池管理器
      2.工作線程
      3.任務隊列:無界、有界、直接交付隊列
      4.任務接口Task

    • 圖示

  • Executor家族

    • Executor頂層接口,只有一個execute方法

    • ExecutorService繼承了Executor,增加了一些新的方法,比如shutdown擁有了初步管理線程池的功能方法

    • Executors工具類,來創建,類似Collections

    • 圖示

  • 線程池實現任務復用的原理

    • 線程池對線程作了包裝,不需要啟動線程,不需要重複start線程,只是調用已有線程固定數量的線程來跑傳進來的任務run方法

    • 添加工作線程

      • 4步:1. 獲取線程池狀態、4.判斷是否進入任務隊列 3.根據狀態檢測是否增加工作線程4.執行拒絕handler
    • 重複利用線程執行不同的任務

面試題

  • 為什麼要使用線程池?
  • 如何使用線程池?
  • 線程池有哪些核心參數?
  • 初始化線程池的大小的如何算?
  • shutdown 和 shutdownNow 有什麼區別?

ThreadLocal

ThreadLocal的作用好處

  • 為每個線程提供存儲自身獨立的局部變量,實現線程間隔離
  • 即:達到線程安全,不需要加鎖節省開銷,減少參數傳遞

ThreadLocal的使用場景

  • 1.每個線程需要一個獨享的對象,如 線程不安全的工具類,(線程隔離)
  • 2.每個線程內需要保存全局變量,如 攔截器中的用戶信息參數,讓不同方法直接使用,避免參數傳遞過多,(局部變量安全,參數傳遞)

ThreadLocal的實現原理

  • 每個 Thread 維護着一個 ThreadLocalMap 的引用;ThreadLocalMap 是 ThreadLocal 的內部類,用 Entry 來進行存儲;key就對應一個個ThreadLocal

  • get方法:取出當前線程的ThreadLocalMap,然後調用map.getEntry方法,把ThreadLocal作為key參數傳入,取出對應的value

  • set方法:往 ThreadLocalMap 設置ThreadLocal對應值
    initalValue方法:延遲加載,get的時候設置初始化

  • 圖示

缺陷注意

  • value內存泄漏

    • 原因:ThreadLocal 被 ThreadLocalMap 中的 entry 的 key 弱引用。如果 ThreadLocal 沒有被強引用, 那麼 GC 時 Entry 的 key 就會被回收,但是對應的 value 卻不會回收,就會造成內存泄漏

    • 解決方案:每次使用完 ThreadLocal,都調用它的 remove () 方法,清除value數據。

    • 源碼圖示

面試題

  • ThreadLocal 的作用是什麼?
  • 講一講ThreadLocal的實現原理(組成結構)
  • ThreadLocal有什麼風險?

Callable與Future

Callable

  • 引入目的

    • 解決Runnable的缺陷

      • 1.沒有返回值,因為返回類型為void
      • 2.不能拋出異常,因為沒有繼承Execption接口
  • 是什麼如何使用

    • Callable是類似於Runnable的接口,實現Callable接口的類和實現Runnable的類都是可被其它線程執行的任務。
    • 實現Call方法,可以有返回值

Future

  • 引入目的

    • Future的核心思想是:一個方法的計算過程可能非常耗時,一直在原地等待方法返回,顯然不明智。可以把該計算過程放到子線程去執行,並通過Future去控制方法的計算過程,在計算出結果后直接獲取該結果。
  • 常用方法

    • get方法:獲取結果,在沒有計算出結果前,會進入阻塞態
  • 使用場景

    • 用法1:線程池的submit方法返回Future對象
    • 用法2:用FutureTask來創建Future
  • 注意點

    • 當for循環批量獲取future的結果時,容易block,get方法調用時應使用timeout限制
    • Future和Callable的生命周期不能後退
  • Callable和Future的關係

    • Future相當於一個存儲器,它存儲未來call()任務方法的返回值結果

    • 可以用Future.get方法來獲取Callable接口的執行結果,在call()未執行完畢之前沒調用get的線程會被阻塞

    • 線程池傳入Callable,submit返回Future,get獲取值

  • FutureTask

    • FutureTask是一種包裝器,可以把Callable轉化成Future和Runnable,它同時實現了二者的接口。所以既可以作為Runnable任務被線程執行,又可以作為Future得到Callable的返回值

    • 圖示

final與不變性

什麼是不變性(Immutable)

  • 如果對象在被創建后,狀態就不能被修改,那麼它就是不可變的。
  • 具有不變性的對象一定是線程安全的,我們不需要對其採取任何額外的安全措施,也能保證線程安全。

final的作用

  • 類防止被繼承、方法防止被重寫、變量防止被修改
  • 天生是線程安全的(因為不能修改),而不需要額外的同步開銷

final的3種用法:修飾變量、方法、類

  • final修飾變量

    • 被final修飾的變量,意味着值不能被修改。
      如果變量是對象,那麼對象的引用不能變,但是對象自身的內容依然可以變化。

    • 賦值時機

      • 屬性被聲明為final后,該變量則只能被賦值一次。且一旦被賦值,final的變量就不能再被改變,如論如何也不會變。

      • 區分為3種

        • final instance variable(類中的final屬性)

          • 等號右側、構造函數、初始化代碼塊
        • final static variable(類中的static final屬性)

          • 等號右側、靜態初始化代碼塊
        • final local variable(方法中的final變量)

          • 使用前複製即可
      • 為什麼規定時機

        • 根據JVM對類和成員變量、靜態成員變量的加載規則來看:如果初始化不賦值,後續賦值,就是從null變成新的賦值,這就違反final不變的原則了!
  • final修飾方法(構造方法除外)

    • 不可被重寫,也就是不能被override,即便是子類有同樣名字的方法,那也不是override,與static類似*
  • final修飾類

    • 不可被繼承,例如典型的String類就是final的

棧封閉 實現線程安全

  • 在方法里新建的局部便咯,實際上是存儲在每個線程私有的棧空間,線程棧不能被其它線程訪問,所以不會有線程安全問題,如ThreadLocal

面試題

CAS

什麼是CAS

  • 我認為V的值應該是A,如果是的話那我就把它改成B,如果不是A(說明被別人修改過了),那我就不修改了,避免多人同時修改導致出錯。
  • CAS有三個操作數:內存值V、預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,才將內存值修改為B,否則什麼都不做。最後返回現在的V值。
  • 最終執行CPU處理機提供的的原子指令

缺點

  • ABA問題

    • 我認為 V的值為A,有其它線程在這期間修改了值為B,但它又修改成了A,那麼CAS只是對比最終結果和預期值,就檢測不出是否修改過
  • CAS+自旋,導致自旋時間過長

  • 改進:通過版本號的機制來解決。每次變量更新的時候,版本號加 1,如AtomicStampedReference的compareAndSet ()

應用場景

  • 1 樂觀鎖:數據庫、git版本號; 自旋 2 concurrentHashMap:CAS+自旋
    3 原子類

CAS底層實現

  • 通過Unsafe獲取待修改變量的內存遞增,
    比較預期值與結果,調用彙編cmpxchg指令

以AtomicInteger為例,分析在Java中是如何利用CAS實現原子操作的?

  • 1.使用Unsafe類拿到value的內存遞增,通過偏移量 直接操作內存數據
  • 2.Unsafe的getAndAddInt方法,使用CAS+自旋嘗試修改數據
  • CAS的參數通過 預期值 與 實際拿到的值進行比較,相同就修改,不相同就自旋
  • Unsafe提供硬件級別的原子操作,最終調用原子彙編指令的cmpxchg指令

鎖

鎖的分類

Lock鎖接口

  • 簡介

    • Lock鎖是一種工具,用於控制對共享資源的訪問
    • 如:ReentrantLock
  • Lock和Synchronized的異同點

    • 相同點

      • 都能達到線程安全的目的
    • 不同點

      • Lock 有比 synchronized 更精確的線程語義和更好的性能;高級功能

      • 1 實現原理不同

        • Synchronized 是關鍵字,屬於 JVM 層面,底層是通過 monitorenter 和 monitorexit 完成,依賴於 monitor 對象來完成;
        • Lock 是 java.util.concurrent.locks.lock 包下的,底層是AQS
      • 2 靈活性不同

        • Synchronized 代碼完成之後系統自動讓線程釋放鎖;ReentrantLock 需要用戶手動釋放鎖,加鎖解鎖靈活
      • 3 等待時是否可以中斷

        • Synchronized 不可中斷,除非拋出異常或者正常運行完成;ReentrantLock 可以中斷。一種是通過 tryLock,另一種是 lockInterruptibly () 放代碼塊中,調用 interrupt () 方法進行中斷;
  • 可見性

    • happens-before規則約定;Lock與Synchronized一致都可以保證可見性
    • 即下一個線程加鎖時可以看到上一個釋放鎖的線程發生的所有操作

樂觀鎖與悲觀鎖

  • 悲觀鎖(互斥同步鎖)

    • 思想

      • 鎖住數據,讓別人無法訪問,確保數據萬無一失
    • 實例

      • Synchronized、Lock相關類
      • 應用實例:select 把庫鎖住,屬於悲觀鎖,更新期間其它人不能修改
    • 缺點

      • 在阻塞和喚醒性能開銷大(用戶態核心態切換、上下文切換、檢查是否有線程被喚醒)
      • 持有鎖的線程被阻塞時無法釋放,有可能造成永久阻塞
  • 樂觀鎖

    • 思想

      • 認為自己在操作數據時不會有其它線程干擾,所以不需要鎖住被操作對象
      • 在更新數據的時候,去對比修改期間有沒有被其它人改變過,沒改過就正常修改(類似CAS思想)
      • 樂觀鎖一般由CAS實現:CAS在一個原子操作內把數據對比且交換,在此期間不能被打斷的
    • 實例

      • 原子類、併發容器
      • 應用實例:數據庫版本號控制、git版本號
    • 優缺點對比

      • 悲觀鎖一旦切換就不用再考慮切換CPU等操作了,一勞永逸,開銷固定
      • 樂觀鎖,會一步步嘗試自旋來獲取鎖,自旋開銷
  • 對比

可重入鎖與非可重入鎖

  • 什麼是可重入

    • 拿到鎖的線程又請求這把鎖,允許通過
  • 可重入的好處

    • 避免死鎖(拿到鎖的線程內部又請求該鎖)
    • 提升封裝性,避免一次次加鎖
  • 可重入鎖ReentrantLock與非可重入鎖ThreadPoolExecutor的Worker類對比

公平鎖和非公平鎖

  • 公平鎖

    • 介紹

      • 公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖
    • 優點

      • 公平鎖的優點是公平執行,等待鎖的線程不會餓死
    • 缺點

      • 缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大
  • 非公平鎖

    • 介紹

      • 多個線程加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現后申請鎖的線程先獲取鎖的場景
    • 優點

      • 減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程
    • 缺點

      • 處於等待隊列中的線程可能會餓死,或者等很久才會獲得鎖
  • 優缺點對比

  • 源碼分析

共享鎖和排他鎖

  • 排他鎖

    • 介紹

      • 排他鎖,獲取鎖后,既能讀又能寫,但是此時其它線程不能獲取這個鎖了,只能由當前線程修改數據獨享鎖,保證了線程安全,synchronized
      • 又稱為 獨佔鎖,寫鎖
  • 共享鎖

    • 介紹

      • 獲取共享鎖后,其它線程也可以獲取共享鎖完成讀操作,但都不能修改刪除數據
      • 又成為 讀鎖
  • ReentrantReadWriteLock

    • 讀寫鎖的作用

      • 共享鎖減少了多個讀都加鎖的開銷,線程也安全
      • 在讀的地方使用讀鎖,在寫的地方寫鎖;在沒有寫鎖的情況下,讀操作無阻塞,提高程序效率
    • 讀寫鎖的規則

      • 要麼可以多讀,要麼只能一寫
      • 讀寫鎖只是一把鎖,可以通過兩個方式鎖定:讀鎖定 或 寫鎖定
    • 一把鎖兩種方式鎖定

      • readLock() 讀鎖
      • writeLock() 寫鎖
    • 讀線程插隊策略(非公平下)

      • 寫鎖可以隨時插隊,參与競爭
      • 讀鎖僅在等待隊列頭節點為寫的時候不允許插隊;當隊頭為讀的時候可以去插隊。
    • 鎖升級

      • 引入場景

        • 假如一開始持有寫鎖,但我寫需求完了,後面都是讀的需求了,如果還佔用寫鎖就浪費資源開銷
      • 策略

        • 只允許降級,不允許升級
    • 適合場景

      • 讀多寫少,提高併發效率

自旋鎖和阻塞鎖

  • 阻塞鎖

    • 思想

      • 沒拿到鎖之前,會直接把線程阻塞,直到被喚醒
    • 開銷缺陷

      • 阻塞或喚醒一個線程需要操作系統切換CPU狀態來完成,恢復現場等需要消耗處理機時間;如果同步代碼塊的內容過於簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長,得不償失
  • 自旋鎖

    • 思想

      • 讓當前搶鎖失敗的線程進行自旋,如果在自旋完成后前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷
    • 開銷缺陷

      • 自旋佔用時間長,起始開銷低,但消耗CPU資源開銷會線性增長
  • 源碼分析

    • atomic包下的類基本都是自旋鎖的實現

    • AtomicInteger的實現:自旋鎖實現原理是CAS,Atomic調用Unsafe進行自增add的源碼中的do-while循環就是一個自旋操作,使用CAS如果修改過程中遇到其它線程修改導致沒有秀嘎四成功,就在while里死循環,直至修改成功

    • 圖示

  • 適用場景

    • 多核、臨界區短小

可中斷鎖

  • 介紹

    • 線程B等待線程A釋放鎖時,線程B不想等待了,想處理其它事情,我們可以中斷它
  • 使用場景

    • synchronized是不可中斷鎖,Lock是可中斷鎖(tryLock(time) 和 lockInterruptibly)響應中斷

鎖優化

  • JDK1.6 后對synchronized鎖的優化

    • JDK1.6 對鎖的實現引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。

    • 偏向鎖

      • 無競爭條件下,消除整個同步互斥,連CAS都不操作;即這個鎖會偏向於第一個獲得它的線程
    • 輕量級鎖

      • 無競爭條件下,通過CAS消除同步互斥,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。
    • 重量級鎖

      • 互斥同步鎖
    • 自旋鎖

      • 為了減少線程狀態改變帶來的消耗,不停地執行當前線程
    • 自適應自旋鎖

      • 自旋的時間不固定了,如設置自旋次數
    • 鎖消除

      • 不可能存在共享數據競爭的鎖進行消除;
    • 鎖粗化

      • 鎖粗化就是增大鎖的作用域;如解決加鎖操作在循環體內的頻開銷
  • 寫代碼時的優化

    • 縮小同步代碼塊、如不要鎖住方法
    • 減少鎖的請求次數, 如一批一批請求
    • 參考LongAdder的思想,每個段有自己的計數器,最後才合併

面試題

  • 什麼是公平鎖?什麼是非公平鎖?
  • 自旋鎖解決什麼問題?自旋鎖的原理是什麼?自旋的缺點?
  • 說說 JDK1.6 之後的synchronized 關鍵字底層做了哪些優化,可以詳細介紹一下這些優化嗎?
  • 說說 synchronized 和 java.util.concurrent.locks.Lock 的異同?

原子類atomic包

原子類的作用

  • 原子類的作用和鎖類似,都是為了保證併發下線程安全
  • 粒度更細,變量級別
  • 效率更高,除了高度競爭外

原子類的種類

  • Atomic*基本類型原子類:AtomicInteger、AtomicLong、AtomicBoolean
  • Atomic*Array數組類型原子類:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • Atomic*Reference 引用類型原子類:AtomicReference等
  • AtomicIntegerFiledUpdate等升級類型原子類
  • Adder累加器、Accumlator累加器

AtomicInteger

  • 常用方法

    • get、getAndSet、getAndIncrement、compareAndSet(int expect,int update)
  • 實現原理

    • AtomicInteger 內部使用 CAS 原子語義來處理加減等操作。CAS通過判斷內存某個位置的值是否與預期值相等,如果相等則進行值更新
    • CAS 是內部是通過 Unsafe 類實現,而 Unsafe 類的方法都是 native 的,在 JNI 里是藉助於一個 CPU 指令完成的,屬於原子操作。
  • 缺點

    • 循環開銷大。如果 CAS 失敗,會一直嘗試
    • 只能保證單個共享變量的原子操作,對於多個共享變量,CAS 無法保證,引出原子引用類
    • 用CAS存在 ABA 問題

Adder累加器

  • 引入目的/改進思想

    • AtomicLong在每一次加法都要flush和refresh主存,與JMM內存模型有關。工作線程之間不能直接通信,需要通過主內存間接通信
  • 設計思想

    • Java8引入,高併發下LongAdder比AtomicLong效率高,本質是空間換時間
    • 競爭激烈時,LongAdder把不同線程對應到不同的Cell單元上進行修改,降低了衝突的概率,是多段鎖的理念,提高了併發性
    • 每個線程都有自己的一個計數器,不存在競爭
    • sum源碼分析:最終把每一個Cell的計數器與base主變量相加

面試題

  • AtomicInteger 怎麼實現原子操作的?
  • AtomicInteger 有哪些缺點?

併發容器

ConcurrentHashMap

  • 集合類歷史

    • Vector的方法被synchronizd修飾,同步鎖;不允許多個線程同時執行。併發量大的時候性能不好
    • Hashtable是線程安全的HashMap,方法也是被synchronized修飾,同步但併發性能差
    • Collections工具類,提高的有synchronizedList和synchronizedMap,代碼內使用sync互斥變量加鎖
  • 為什麼需要

    • 為什麼不用HashMap

      • 1.多線程下同時put碰撞導致數據丟失
      • 2.多線程下同時put擴容導致數據丟失
      • 3.死循環造成的CPU100%
    • 為什麼不用Collection.synchronizedMap

      • 同步鎖併發性能低
  • 數據結構與併發策略

    • JDK1.7

      • 數組+鏈表,拉鏈法解決衝突
      • 採用分段鎖,每個數組結點是一個獨立的ReentrantLock鎖,可以支持同時併發寫
    • JDK1.8

      • 數組+鏈表+紅黑樹,拉鏈法和樹化解決衝突
      • 採用CAS+synchronized鎖細化
    • 1.7到1.8改變後有哪些優點

      • 1.數據結構由鏈表變為紅黑樹,樹查詢效率更高
      • 2.減少了Hash碰撞,1.7拉鏈法
      • 3.保證了併發安全和性能,分段鎖改成CAS+synchronized
      • 為什麼超過8要轉為紅黑樹,因為紅黑樹存儲空間是結點的兩倍,經過泊松分佈,8衝突概率低
  • 注意事項

    • 組合操作線程不安全,應使用putIfAbsent提供的原子性操作

CopyOnWriteArrayList

  • 引入目的

    • Vector和SynchronizedList鎖的粒度太大併發效率低,並且迭代時無法編輯exceptMod!=Count
  • 適合場景

    • 讀多寫少,如黑名單管理每日更新
  • 讀寫規則

    • 是對讀寫鎖的升級:讀取完全不用加鎖,讀時寫入也不會阻塞。只有寫入和寫入之間需要同步
  • 實現原理

    • 創建數據的新副本,實現讀寫分離,修改時整個副本進行一次複製,完成后最後再替換回去;由於讀寫分離,舊容器不變,所以線程安全無需鎖
    • 在計算機內存中修改不直接修改主內存,而是修改緩存(cache、對拷貝的副本進行修改),再進行同步(指針指向新數據)。
  • 缺點

    • 1.數據一致性問題,拷貝不能保證數據實時一致,只能保證數據最終一致性
    • 2.內存佔用問題,寫複製機制,寫操作時內存會同時駐紮兩個對象的內存

併發隊列

  • 為什麼使用隊列

    • 用隊列可以在線程間傳遞數據,緩存數據
    • 考慮鎖等線程安全問題的重任轉移到了“隊列”上
  • 併發隊列關係圖示

  • BlockingQueue阻塞隊列

    • 阻塞隊列是局由自動阻塞功能的隊列,線程安全;take方法移除隊頭,若隊列無數據則阻塞直到有數據;put方法插入元素,如果隊列已滿就無法繼續插入則阻塞直到隊列里有了空閑空間

    • ArrayBlockQueue

      • 有界可指定容量、可公平
      • Put源碼加鎖,可中斷的上鎖方法。沒滿才可以入隊,否則一直await等待。
    • LinkedBlockingQueue

      • 無界容量為MAX_VALUE,內部結構Node
      • 使用了兩把鎖take鎖和put鎖互補干擾
    • PriorityBlockingQueue

      • 支持優先級,無界隊列
    • SynchronousQueue

      • 直接傳遞的隊列,容量0,效率高線程池的CacheExecutorPool使用其作為工作隊列
    • DelayQueue

      • 無界隊列,根據延遲時間排序
  • 非阻塞隊列

    • ConcurrentLinkedQueue

      • 使用鏈表作為隊列存儲結構
      • 使用Unsafe的CAS非阻塞方法來實現線程安全,無需阻塞,適合對性能要求較高的併發場景
  • 選擇合適的隊列

    • 邊界上看

      • ArrayBlockQueue有界;LinkedBlockQueue無界適合容量大容量激增
    • 內存上看

      • ArrayBlockQueue內部結構是array,從內存存儲上看,連續存儲更加整齊。而LinkedBlockQueue採用鏈表結點,可以非連續存儲。
    • 吞吐量上看

      • 從性能上看LinkedBlockQueue的put鎖和鎖分開,鎖粒度更細,所以優於ArrayBlockQueue

總結併發容器對比

  • 分為3類:Concurrent、CopyOnWrite、Blocking*
  • Concurrent*的特定是大部分使用CAS併發;而CopyOnWrite通過複製一份元數據寫加鎖實現;Blocking通過ReentLock鎖底層AQS實現

併發流程控制工具類

控制併發流程工具類的作用

  • 控制併發流程的工具類,作用是幫助程序員更容易讓線程之間相互配合,來滿足業務邏輯

  • 併發工具類圖示

CountDownLatch倒計時門閂

  • 作用(事件)

    • 一個線程等多個線程、或多個線程等一個線程完成到達,才能繼續執行
  • 常用方法

    • 構造函數中傳入倒數值、await、countDown

Semaphore信號量

  • 作用

    • 用來限制管理數量有限的資源的使用情況,相當於一定數量的“許可證”
  • 常用方法

    • 構造函數中傳入數量、acquire、release

Condition條件對象

  • 作用

    • 等待條件滿足才放行,否則阻塞;一個鎖可以對應多個條件
  • 常用方法

    • lock.newCondition、await、signal

CyclicBarrier循環柵欄

  • 作用(線程)

    • 多個線程互相等待,直到達到同一個同步點(屏障),再繼續一起執行
  • 常用方法

    • 構造函數中傳入個數、await

AQS

AQS的作用

  • AQS是一個用於構建鎖、同步器、協作工具類的框架,有了AQS后,更多的協作工具類都可以很方便的寫出來

AQS的應用場景

  • Exclusive(獨佔)

    • ReentrantLock 公平和非公平鎖
  • Share(共享)

    • Semaphore/CountDownLatch/CyclicBarrier

AQS原理解析

  • 核心三要素

    • 1.sate

      • 使用一個 int 成員變量來表示同步狀態 state,被volatile修飾,會被併發修改,各方法如getState、setState等使用CAS保證線程安全
      • 在ReentrantLock中,表示可重入的次數
      • 在Semaphore中,表示剩餘許可證信號的數量
      • 在CountDownLatch中,表示還需要倒數的個數
    • 2.控制線程搶鎖和配合的FIFO隊列

      • 獲取資源線程的排隊工作
    • 3.期望協作工具類去實現的“獲取/釋放”等喚醒分配的方法策略

  • AQS的用法

    • 第一步:寫一個類,想好協作的邏輯,實現獲取/釋放方法
    • 第二步:內部寫一個Sync類繼承AbstractQueueSynchronizer
    • 第三步:Sync類根據獨佔還是共享重寫tryAcquire/tryRelease或tryAcquireShared和tryReleaseShared等方法,在之前寫的獲取/釋放方法中調用AQS的acquire/release或則Shared方法

AQS應用實例源碼解析

  • AQS在CountDownLatch的應用

    • 內部類Sync繼承AQS

    • 1.state表示門閂倒數的count數量,對應getCount方法獲取

    • 2.釋放方法,countDown方法會讓state減1,直到減為0時就喚醒所有線程。countDown方法調用releaseShared,它調用sync實現的tryReleaseShared,其使用CAS+自旋鎖,來實現安全的計數-1

    • 3.阻塞方法,await會調用sync提供的aquireSharedInterruptly方法,當state不等於0時,最終調用LockUpport的park,它利用Unsafe的park,native方法,把線程加入阻塞隊列

    • 總結

  • AQS在Semphore的應用

    • state表示信號量允許的剩餘許可數量

    • tryAcquire方法,判斷信號量大於0就成功獲取,使用CAS+自旋改變state狀態。如果信號量小於0了,再請求時tryAcquireShared返回負數,調用aquireSharedInterruptly方法就進入阻塞隊列

    • release方法,調用sync實現的releaseShared,會利用AQS去阻塞隊列喚醒一個線程

    • 總結

  • AQS在ReentrantLock的應用

    • state表示已重入的次數,獨佔鎖權保存在AQS的Thread類型的exclusiveOwnerThread變量中
    • 釋放鎖: unlock方法調用sync實現的release方法,會調用tryRelease,使用setState而不是CAS來修改重入次數state,當state減到0完全釋放鎖
    • 加鎖lock方法:調用sync實現的lock方法。CAS嘗試修改鎖的所有權為當前線程,如果修改失敗就要調用acquire方法再次嘗試獲取,acquire方法調用了AQS的tryAcquire,這個實現在ReentantLock的裏面,獲取失敗加入到阻塞隊列

通過AQS自定義同步器

  • 自定義同步器在實現時只需要根據業務邏輯需求,實現共享資源 state 的獲取與釋放方式策略即可
  • 至於具體線程等待隊列的維護(如獲取資源失敗入隊 / 喚醒出隊等),AQS 已經在頂層實現好了

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

設計模式系列之外觀模式(Facade Pattern)——提供統一的入口

說明:設計模式系列文章是讀劉偉所著《設計模式的藝術之道(軟件開發人員內功修鍊之道)》一書的閱讀筆記。個人感覺這本書講的不錯,有興趣推薦讀一讀。詳細內容也可以看看此書作者的博客https://blog.csdn.net/LoveLion/article/details/17517213

模式概述

絕大多數B/S系統都有一個首頁或者導航頁面,大部分C/S系統都提供了菜單或者工具欄,在這裏,首頁和導航頁面就充當了B/S系統的外觀角色,而菜單和工具欄充當了C/S系統的外觀角色,通過它們用戶可以快速訪問子系統,增強了軟件的易用性。

在軟件開發中,有時候為了完成一項較為複雜的功能,一個客戶類需要和多個業務類交互,而這些需要交互的業務類經常會作為一個整體出現,由於涉及到的類比較多,導致使用時代碼較為複雜,此時,特別需要一個類似服務員一樣的角色,由它來負責和多個業務類進行交互,而客戶類只需與該類交互。外觀模式通過引入一個外觀角色(Facade)來簡化客戶端與子系統(Subsystem)之間的交互,為複雜的子系統調用提供一個統一的入口,降低子系統與客戶端的耦合度,使得客戶端調用非常方便。

模式定義

外觀模式中,一個子系統的外部與其內部的通信通過一個統一的外觀類進行,外觀類將客戶類與子系統的內部複雜性分隔開,使得客戶類只需要與外觀角色打交道,而不需要與子系統內部的很多對象打交道。

外觀模式(Facade Pattern):為子系統中的一組接口提供一個統一的入口。外觀模式定義了一個高層接口,這個接口使得這一子系統更加容易使用

外觀模式又稱為門面模式,它是一種對象結構型模式。外觀模式是迪米特法則的一種具體實現,通過引入一個新的外觀角色可以降低原有系統的複雜度,同時降低客戶類與子系統的耦合度。

模式結構圖

外觀模式沒有一個一般化的類圖描述,下圖所示的類圖也可以作為描述外觀模式的結構圖:

外觀模式包含如下兩個角色:

  • Facade(外觀角色):在客戶端可以調用它的方法,在外觀角色中可以知道相關的(一個或者多個)子系統的功能和責任;在正常情況下,它將所有從客戶端發來的請求委派到相應的子系統去,傳遞給相應的子系統對象處理。

  • SubSystem(子系統角色):在軟件系統中可以有一個或者多個子系統角色,每一個子系統可以不是一個單獨的類,而是一個類的集合,它實現子系統的功能;每一個子系統都可以被客戶端直接調用,或者被外觀角色調用,它處理由外觀類傳過來的請求;子系統並不知道外觀的存在,對於子系統而言,外觀角色僅僅是另外一個客戶端而已。

模式偽代碼

外觀模式中所指的子系統是一個廣義的概念,它可以是一個類、一個功能模塊、系統的一個組成部分或者一個完整的系統。子系統類通常是一些業務類,實現了一些具體的、獨立的業務功能,其典型代碼如下:

public class SubSystemA {

    public void methodA() {
        //業務實現代碼
    }
}

public class SubSystemB {

    public void methodB() {
        //業務實現代碼
    }
}

public class SubSystemC {

    public void methodC() {
        //業務實現代碼
    }
}

引入外觀類,與子系統業務類之間的交互統一由外觀類來完成

public class Facade {
    private SubSystemA obj1 = new SubSystemA();
    private SubSystemB obj2 = new SubSystemB();
    private SubSystemC obj3 = new SubSystemC();

    public void method() {
        obj1.methodA();
        obj2.methodB();
        obj3.methodC();
    }
}

由於在外觀類中維持了對子系統對象的引用,客戶端可以通過外觀類來間接調用子系統對象的業務方法,而無須與子系統對象直接交互。引入外觀類后,客戶端代碼變得非常簡單,典型代碼如下:

public static void main(String[] args) {
    Facade facade = new Facade();
    facade.method();
}

模式改進

在標準的外觀模式中,如果需要增加、刪除或更換與外觀類交互的子系統類,必須修改外觀類或客戶端的源代碼,這將違背開閉原則,因此可以通過引入抽象外觀類來對系統進行改進,在一定程度上可以解決該問題。在引入抽象外觀類之後,客戶端可以針對抽象外觀類進行編程,對於新的業務需求,不需要修改原有外觀類,而對應增加一個新的具體外觀類,由新的具體外觀類來關聯新的子系統對象。

定義抽象外觀類

public abstract class AbstractFacade {
    public abstract void method();
}

根據具體的場景,實現具體的外觀類

public class Facade1 extends AbstractFacade {

    private SubSystemA obj1 = new SubSystemA();
    private SubSystemB obj2 = new SubSystemB();

    @Override
    public void method() {
        obj1.methodA();
        obj2.methodB();
    }
}

public class Facade2 extends AbstractFacade {

    private SubSystemB obj1 = new SubSystemB();
    private SubSystemC obj2 = new SubSystemC();

    @Override
    public void method() {
        obj1.methodB();
        obj2.methodC();
    }
}

客戶端針對抽象外觀類進行編程,代碼片段如下:

public static void main(String[] args) {
    AbstractFacade facade = new Facade1();
    // facade = new Facade2();
    facade.method();
}

模式應用

個人認為外觀模式某些情況下可以看成是對既有系統的再次封裝,所以各種類庫、工具庫(比如hutool)、框架基本都有外觀模式的影子。外觀模式讓調用方更加簡潔,不用關心內部的實現,與此同時,也讓越來越多的程序猿多了個調包俠的昵稱(當然了這其中也包括筆者●´ω`●行無際)。

所以,你可能在很多開源代碼中看到類似XxxBootstrap、XxxContext、XxxMain等類似的Class,再追進去看一眼,你可能發現裏面關聯了一大堆的複雜的對象,這些對象對於外層調用者來說幾乎是透明的。

例子太多,以致於不知道舉啥例子(實際是偷懶的借口O(∩_∩)O哈哈~)。

模式總結

外觀模式並不給系統增加任何新功能,它僅僅是簡化調用接口。在幾乎所有的軟件中都能夠找到外觀模式的應用。所有涉及到與多個業務對象交互的場景都可以考慮使用外觀模式進行重構。

主要優點

(1) 它對客戶端屏蔽了子系統組件,減少了客戶端所需處理的對象數目,並使得子系統使用起來更加容易。通過引入外觀模式,客戶端代碼將變得很簡單,與之關聯的對象也很少。

(2) 它實現了子系統與客戶端之間的松耦合關係,這使得子系統的變化不會影響到調用它的客戶端,只需要調整外觀類即可。

(3) 一個子系統的修改對其他子系統沒有任何影響,而且子系統內部變化也不會影響到外觀對象。

適用場景

(1) 當要為訪問一系列複雜的子系統提供一個簡單入口時可以使用外觀模式。

(2) 客戶端程序與多個子系統之間存在很大的依賴性。引入外觀類可以將子系統與客戶端解耦,從而提高子系統的獨立性和可移植性。

(3) 在層次化結構中,可以使用外觀模式定義系統中每一層的入口,層與層之間不直接產生聯繫,而通過外觀類建立聯繫,降低層之間的耦合度。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

Java 多線程基礎(十二)生產者與消費者

 Java 多線程基礎(十二)生產者與消費者

一、生產者與消費者模型

生產者與消費者問題是個非常典型的多線程問題,涉及到的對象包括“生產者”、“消費者”、“倉庫”和“產品”。他們之間的關係如下:

①、生產者僅僅在倉儲未滿時候生產,倉滿則停止生產。
②、消費者僅僅在倉儲有產品時候才能消費,倉空則等待。
③、當消費者發現倉儲沒產品可消費時候會通知生產者生產。
④、生產者在生產出可消費產品時候,應該通知等待的消費者去消費。

生產者消費者模型具體來講,就是在一個系統中,存在生產者和消費者兩種角色,他們通過內存緩衝區進行通信,生產者生產消費者需要的資料,消費者把資料做成產品。生產消費者模式如下圖。

在日益發展的服務類型中,譬如註冊用戶這種服務,它可能解耦成好幾種獨立的服務(賬號驗證,郵箱驗證碼,手機短信碼等)。它們作為消費者,等待用戶輸入數據,在前台數據提交之後會經過分解併發送到各個服務所在的url,分發的那個角色就相當於生產者。消費者在獲取數據時候有可能一次不能處理完,那麼它們各自有一個請求隊列,那就是內存緩衝區了。做這項工作的框架叫做消息隊列。

二、生產者與消費者實現

下面通過生產包子的例子及wait()/notify()方式實現該模型(後面學習線程池相關內容之後,再通過其它方式實現生產/者消費者模型)。

麵包類:

public class Bread {
    private int capacity;    // 麵包的容量
    private int size;        // 麵包的實際數量
    public Bread(int capacity) {
        this.capacity = capacity;
        this.size = 0;
    }
    
    // 生產麵包
    public synchronized void produce(int val) {
        try {
             // left 表示“想要生產的數量”(有可能生產量太多,需多此生產)
            int left = val;
            while (left > 0) {
                // 庫存已滿時,等待“消費者”消費產品。
                while (size >= capacity)
                    wait();
                // 獲取“實際生產的數量”(即庫存中新增的數量)
                // 如果“庫存”+“想要生產的數量”>“總的容量”,則“實際增量”=“總的容量”-“當前容量”。(此時填滿倉庫)
                // 否則“實際增量”=“想要生產的數量”
                int inc = (size+left)>capacity ? (capacity-size) : left;
                size += inc;
                left -= inc;
                System.out.printf("%s produce(%3d) --> left=%3d, inc=%3d, size=%3d\n",
                        Thread.currentThread().getName(), val, left, inc, size);
                // 通知“消費者”可以消費了。
                notifyAll();
            }
        }catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
    // 消費麵包
    public synchronized void consume(int val) {
        try {
             // left 表示“客戶要消費數量”(有可能消費量太大,庫存不夠,需多此消費)
            int left = val;
            while (left > 0) {
                // 庫存為0時,等待“生產者”生產產品。
                while (size <= 0)
                    wait();
                // 獲取“實際消費的數量”(即庫存中實際減少的數量)
                // 如果“庫存”<“客戶要消費的數量”,則“實際消費量”=“庫存”;
                // 否則,“實際消費量”=“客戶要消費的數量”。
                int dec = (size<left) ? size : left;
                size -= dec;
                left -= dec;
                System.out.printf("%s consume(%3d) <-- left=%3d, dec=%3d, size=%3d\n",
                        Thread.currentThread().getName(), val, left, dec, size);
                notifyAll();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

生產者類

public class Producer{
    Bread bread;
    public Producer(Bread bread) {
        this.bread = bread;
    }
    public void produce(final int val) {
        new Thread(() -> {
            bread.produce(val);
        }).start();;
    }
}

消費者類

public class Customer {
    private Bread bread;
    public Customer(Bread bread) {
        this.bread = bread;
    }
    public void consume(final int val) {
        new Thread(() -> {
            bread.consume(val);
        }).start();;
    }
}

測試類代碼

public class Demo {
    public static void main(String[] args) {
        Bread bread = new Bread(100);
        Producer producer = new Producer(bread);
        Cunstomer customer = new Customer(bread);
        
        producer.produce(60);
        producer.produce(120);
        consumer.consume(90);
        consumer.consume(150);
        producer.produce(110);
    }
}
// 運行結果
Thread-1 produce( 60) --> left=  0, inc= 60, size= 60
Thread-5 produce(110) --> left= 70, inc= 40, size=100
Thread-4 consume(150) <-- left= 50, dec=100, size=  0
Thread-2 produce(120) --> left= 20, inc=100, size=100
Thread-3 consume( 90) <-- left=  0, dec= 90, size= 10
Thread-4 consume(150) <-- left= 40, dec= 10, size=  0
Thread-5 produce(110) --> left=  0, inc= 70, size= 70
Thread-4 consume(150) <-- left=  0, dec= 40, size= 30
Thread-2 produce(120) --> left=  0, inc= 20, size= 50

說明:

①、Producer是“生產者”類,它與“麵包(bread)”關聯。當調用“生產者”的produce()方法時,它會新建一個線程並向“麵包類”中生產產品。
②、Customer是“消費者”類,它與“麵包(bread)”關聯。當調用“消費者”的consume()方法時,它會新建一個線程並消費“麵包類”中的產品。
③、Bread是麵包類,記錄“麵包的產量(capacity)”以及麵包當前實際數目(size)”。
        麵包類的生產方法produce()和消費方法consume()方法都是synchronized方法,進入synchronized方法體,意味着這個線程獲取到了該“麵包”對象的同步鎖。這也就是說,同一時間,生產者和消費者線程只能有一個能運行。通過同步鎖,實現了對“殘酷”的互斥訪問。
       對於生產方法 produce() 而言:當麵包量滿時,生產者線程等待,需要等待消費者消費產品之後,生產線程才能生產;生產者線程生產完麵包之後,會通過 notifyAll() 喚醒同步鎖上的所有線程,包括“消費者線程”,即我們所說的“通知消費者進行消費”。
      對於消費方法consume()而言:當倉庫為空時,消費者線程等待,需要等待生產者生產產品之後,消費者線程才能消費;消費者線程消費完產品之後,會通過 notifyAll() 喚醒同步鎖上的所有線程,包括“生產者線程”,即我們所說的“通知生產者進行生產”。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

基於用戶的協同過濾來構建推薦系統

1.概述

之前介紹了如何構建一個推薦系統,今天給大家介紹如何基於用戶的協同過濾來構建推薦的實戰篇。

2.內容

協同過濾技術在推薦系統中應用的比較廣泛,它是一個快速發展的研究領域。它比較常用的兩種方法是基於內存(Memory-Based)和基於模型(Model-Based)。

  • 基於內存:主要通過計算近似度來進行推薦,比如基於用戶(Used-Based)和基於物品(Item-Based)的協同過濾,這兩個模式中都會首先構建用戶交互矩陣,然後矩陣的行向量和列向量可以用來表示用戶和物品,然後計算用戶和物品的相似度來進行推薦;
  • 基於模型:主要是對交互矩陣進行填充,預測用戶購買某個物品的可能性。

為了解決這些問題,可以通過建立協同過濾模型,利用購買數據向客戶推薦產品。下面,我們通過基於用戶的協同過濾(基於內存),通過實戰來一步步實現其中的細節。基於用戶的系統過濾體現在具有相似特徵的人擁有相似的喜好。比如,用戶A向用戶B推薦了物品C,而B購買過很多類似C的物品,並且評價也高。那麼,在未來,用戶B也會有很大的可能會去購買物品C,並且用戶B會基於相似度度量來推薦物品C。

2.1 基於用戶與用戶的協同過濾

這種方式識別與查詢用戶相似的用戶,並估計期望的評分為這些相似用戶評分的加權平均值。實戰所使用的Python語言,這裏需要依賴的庫如下:

  • pandas
  • numpy
  • sklearn

Python環境:

  • 版本3.7.6
  • Anaconda3

2.2 評分函數

這裏給非個性化協同過濾(不包含活躍用戶的喜歡、不喜歡、以及歷史評分),返回一個以用戶U和物品I作為輸入參數的分數。該函數輸出一個分數,用於量化用戶U喜歡 / 偏愛物品I的程度。這通常是通過對與用戶相似的人的評分來完成的。涉及的公式如下:

 

 這裏其中s為預測得分,u為用戶,i為物品,r為用戶給出的評分,w為權重。在這種情況下,我們的分數等於每個用戶對該項目的評價減去該用戶的平均評價再乘以某個權重的總和,這個權重表示該用戶與其他用戶有多少相似之處,或者對其他用戶的預測有多少貢獻。這是用戶u和v之間的權重,分數在0到1之間,其中0是最低的,1是最高的。理論上看起來非常完美,那為啥需要從每個用戶的評分中減去平均評分,為啥要使用加權平均而不是簡單平均?這是因為我們所處理的用戶類型,首先,人們通常在不同的尺度上打分,用戶A可能是一個积極樂觀的用戶,會給用戶A自己喜歡的電影平均高分(例如4分、或者5分)。而用戶B是一個不樂觀或者對評分標準比較高的用戶,他可能對最喜歡的電影評分為2分到5分之間。用戶B的2分對應到用戶A的4分。改進之處是可以通過規範化用戶評分來提高算法效率。一種方法是計算s(u,i)的分數,它是用戶對每件物品的平均評價加上一些偏差。通過使用餘弦相似度來計算上述公式中給出的權重,同時,按照上述方式對數據進行歸一化,在pandas中進行一些數據分析。

2.2.1 導入Python依賴包

import pandas as pd
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.metrics import pairwise_distances

2.2.2 加載數據源

加載數據示例代碼如下所示:

movies = pd.read_csv("data/movies.csv")
Ratings = pd.read_csv("data/ratings.csv")
Tags = pd.read_csv("data/tags.csv")

結果預覽如下:

print(movies.head())
print(Ratings.head())
print(Tags.head())

 

 構建數據:

Mean = Ratings.groupby(by="userId", as_index=False)['rating'].mean()
Rating_avg = pd.merge(Ratings, Mean, on='userId')
Rating_avg['adg_rating'] = Rating_avg['rating_x'] - Rating_avg['rating_y']
print(Rating_avg.head())

結果如下:

 

2.3 餘弦相似度

 對於上面的公式,我們需要找到有相似想法的用戶。找到一個喜歡和不喜歡的用戶聽起來很有意思,但是我們如何找到相似性呢?那麼這裏我們就需要用到餘弦相似度,看看用戶有多相似。它通常是根據用戶過去的評分來計算的。

這裏使用到Python的的sklearn的cosine_similarity函數來計算相似性,並做一些數據預處理和數據清洗。實例代碼如下:

check = pd.pivot_table(Rating_avg,values='rating_x',index='userId',columns='movieId')
print(check.head())
final = pd.pivot_table(Rating_avg,values='adg_rating',index='userId',columns='movieId')
print(final.head())

結果如下:

上圖中包含了很多NaN的值,這是因為每個用戶都沒有看過所有的電影,所以這種類型的矩陣被稱為稀疏矩陣。類似矩陣分解的方法被用來處理這種稀疏性,接下來,我們來對這些NaN值做相關替換。

這裏通常有兩種方式:

  1. 使用行上的用戶平均值;
  2. 用戶在列上的電影平均值

代碼如下:

# Replacing NaN by Movie Average
final_movie = final.fillna(final.mean(axis=0))
print(final_movie.head())

# Replacing NaN by user Average
final_user = final.apply(lambda row: row.fillna(row.mean()), axis=1)
print(final_user.head())

結果如下:

 

 接着,我們開始計算用戶之間的相似性,代碼如下:

# user similarity on replacing NAN by item(movie) avg
cosine = cosine_similarity(final_movie)
np.fill_diagonal(cosine, 0)
similarity_with_movie = pd.DataFrame(cosine, index=final_movie.index)
similarity_with_movie.columns = final_user.index
# print(similarity_with_movie.head())

# user similarity on replacing NAN by user avg
b = cosine_similarity(final_user)
np.fill_diagonal(b, 0 )
similarity_with_user = pd.DataFrame(b,index=final_user.index)
similarity_with_user.columns=final_user.index
# print(similarity_with_user.head())

結果如下:

 

 然後,我們來檢驗一下我們的相似度是否有效,代碼如下:

def get_user_similar_movies( user1, user2 ):
    common_movies = Rating_avg[Rating_avg.userId == user1].merge(
    Rating_avg[Rating_avg.userId == user2],
    on = "movieId",
    how = "inner" )
    return common_movies.merge( movies, on = 'movieId' )

a = get_user_similar_movies(370,86309)
a = a.loc[ : , ['rating_x_x','rating_x_y','title']]
print(a.head())

結果如下:

 

 從上圖中,我們可以看出產生的相似度幾乎是相同的,符合真實性。

2.4 相鄰用戶

在2.3中計算了所有用戶的相似度,但是在大數據領域,推薦系統與大數據相結合是至關重要的。以電影推薦為例子,構建一個矩陣(862 * 862),這個與實際的用戶數據(百萬、千萬或者更多)相比,這是一個很小的矩陣。因此在計算任何物品的分數時,如果總是查看所有其他用戶將不是一個好的解決方案或者方法。因此,採用相鄰用戶的思路,對於特定用戶,只取K個類似用戶的集合。

下面,我們對K取值30,所有的用戶都有30個相鄰用戶,代碼如下:

def find_n_neighbours(df,n):
    order = np.argsort(df.values, axis=1)[:, :n]
    df = df.apply(lambda x: pd.Series(x.sort_values(ascending=False)
           .iloc[:n].index, 
          index=['top{}'.format(i) for i in range(1, n+1)]), axis=1)
    return df

# top 30 neighbours for each user
sim_user_30_u = find_n_neighbours(similarity_with_user,30)
print(sim_user_30_u.head())

sim_user_30_m = find_n_neighbours(similarity_with_movie,30)
print(sim_user_30_m.head())

結果如下:

 

 2.5 計算最後得分

實現代碼如下所示:

def User_item_score(user,item):
    a = sim_user_30_m[sim_user_30_m.index==user].values
    b = a.squeeze().tolist()
    c = final_movie.loc[:,item]
    d = c[c.index.isin(b)]
    f = d[d.notnull()]
    avg_user = Mean.loc[Mean['userId'] == user,'rating'].values[0]
    index = f.index.values.squeeze().tolist()
    corr = similarity_with_movie.loc[user,index]
    fin = pd.concat([f, corr], axis=1)
    fin.columns = ['adg_score','correlation']
    fin['score']=fin.apply(lambda x:x['adg_score'] * x['correlation'],axis=1)
    nume = fin['score'].sum()
    deno = fin['correlation'].sum()
    final_score = avg_user + (nume/deno)
    return final_score

score = User_item_score(320,7371)
print("score (u,i) is",score)

結果如下:

 

這裏我們算出來的預測分數是4.25,因此可以認為用戶(370),可能喜歡ID(7371)的電影。接下來,我們給用戶(370)做電影推薦,實現代碼如下:

Rating_avg = Rating_avg.astype({"movieId": str})
Movie_user = Rating_avg.groupby(by = 'userId')['movieId'].apply(lambda x:','.join(x))

def User_item_score1(user):
    Movie_seen_by_user = check.columns[check[check.index==user].notna().any()].tolist()
    a = sim_user_30_m[sim_user_30_m.index==user].values
    b = a.squeeze().tolist()
    d = Movie_user[Movie_user.index.isin(b)]
    l = ','.join(d.values)
    Movie_seen_by_similar_users = l.split(',')
    Movies_under_consideration = list(set(Movie_seen_by_similar_users)-set(list(map(str, Movie_seen_by_user))))
    Movies_under_consideration = list(map(int, Movies_under_consideration))
    score = []
    for item in Movies_under_consideration:
        c = final_movie.loc[:,item]
        d = c[c.index.isin(b)]
        f = d[d.notnull()]
        avg_user = Mean.loc[Mean['userId'] == user,'rating'].values[0]
        index = f.index.values.squeeze().tolist()
        corr = similarity_with_movie.loc[user,index]
        fin = pd.concat([f, corr], axis=1)
        fin.columns = ['adg_score','correlation']
        fin['score']=fin.apply(lambda x:x['adg_score'] * x['correlation'],axis=1)
        nume = fin['score'].sum()
        deno = fin['correlation'].sum()
        final_score = avg_user + (nume/deno)
        score.append(final_score)
    data = pd.DataFrame({'movieId':Movies_under_consideration,'score':score})
    top_5_recommendation = data.sort_values(by='score',ascending=False).head(5)
    Movie_Name = top_5_recommendation.merge(movies, how='inner', on='movieId')
    Movie_Names = Movie_Name.title.values.tolist()
    return Movie_Names

user = int(input("Enter the user id to whom you want to recommend : "))
predicted_movies = User_item_score1(user)
print(" ")
print("The Recommendations for User Id : 370")
print("   ")
for i in predicted_movies:
    print(i)

結果如下:

 

3.總結

基於用戶的協同過濾,流程簡述如下:

  1. 採集數據 & 存儲數據
  2. 加載數據
  3. 數據建模(數據預處理 & 數據清洗)
  4. 計算相似性(餘弦相似度、相鄰計算)
  5. 得分預測(預測和最終得分計算)
  6. 物品推薦

4.結束語

這篇博客就和大家分享到這裏,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或發送郵件給我,我會盡我所能為您解答,與君共勉!

另外,博主出書了《Kafka並不難學》和《Hadoop大數據挖掘從入門到進階實戰》,喜歡的朋友或同學, 可以在公告欄那裡點擊購買鏈接購買博主的書進行學習,在此感謝大家的支持。關注下面公眾號,根據提示,可免費獲取書籍的教學視頻

,

這篇博客就和大家分享到這裏,如果大家在研究學習的過程當中有什麼問題,可以加群進行討論或發送郵件給我,我會盡我所能為您解答,與君共勉!

另外,博主出書了《Kafka並不難學》和《Hadoop大數據挖掘從入門到進階實戰》,喜歡的朋友或同學, 可以在公告欄那裡點擊購買鏈接購買博主的書進行學習,在此感謝大家的支持。關注下面公眾號,根據提示,可免費獲取書籍的教學視頻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

特斯拉與 BMW 談合作 可望發展碳纖維與電池技術

特斯拉(Tesla)明星執行長 Elon Musk 爆料,德國豪華車品牌 BMW 與特斯拉曾洽商合作,範圍涵蓋電池與輕量化車體零件。   Musk 在接受德國媒體 Der Spiegel 專訪時表示,對 BMW 以碳纖維材料強化車體感到非常有興趣,認為相當具成本效益,雙方在非正式場合會面時,有討論過合作的可能,但尚未達成決議。   據路透社報導,BMW 為發展碳纖維原料而成立合資公司 SGL,i3 電動車與 i8 油電混合動力跑車的駕駛艙與後蓋零件均採用到碳纖維材料。除此之外,Musk 還透露有意與 BMW 一起搞電池科技,以及電動車充電站,他甚至於計畫 5~6 年內在德國蓋一座電池廠。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※教你寫出一流的銷售文案?

不需充電的零排放氫燃料電池機車 北市府試用評估成效

    繼新北市電動機車 E-bike 於 10 月上路後,台北市政府也採用能源局補助業者研發旳氫燃料電池機車,每台配備 2 支金屬儲氫罐,交換費用 30 元,行駛中排水,但二氧化碳排放為 0,續航力定速時可達 86 公里,不過,若在行駛市區時,遇上紅綠燈走走停停,續航力會降為 50 公里。環保局表示,廠商提供試騎的 15 輛氫燃料電池機車將用於環保稽查、工地巡查、土地丈量等業務。   環保局表示,氫燃料電池首創用於機車,金屬容器包覆氫氧化物粉末,相較氣體狀較安全,業者提供的 15 部試騎機車將停於於市府公務停車場,也會提供交換氣體,試用 3 個月後評估成效。   負責研發的亞太燃料電池科技專案經理陳建豪表示,氣燃料電池不須充電,而是利用能源轉換,將氫氣透過觸媒轉換成電能,轉換過程僅會排放水,該款氫燃料電池機車時速最快可達 60 公里。業者說,量產後機車售價約 7 萬元,但免燃料稅。陳建豪表示,全球各國多有氫燃料電池的安全使用規範,但台灣沒有,盼政府能協助立法。     (Source:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

鎳礦出口禁令再提前 印尼宣布立即停止

摘錄自2019年10月28日中央社報導

印尼投資協調委員會主席巴赫利爾(Bahlil Lahadalia)28日指出,鎳礦業者已和政府達成協議,將立即停止鎳礦出口。這項禁令是為振興印尼冶煉產業發展。印尼上個月已將停止鎳礦出口的時間從2022年提早到明年1月實施,如今這項決定讓時程再度提前。

鎳礦產業協會秘書長梅迪(Meidy Lengkey)告訴路透社,只要政府能協助,讓鎳礦在國內市場能以公平售價賣出,鎳礦業者對於禁止出口的政策沒有意見。

 

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※幫你省時又省力,新北清潔一流服務好口碑

※別再煩惱如何寫文案,掌握八大原則!

鯨鯊嘴巴裡驚見新物種 外型像蝦子

摘錄自2019年10月28日中央社報導

日本研究團隊在一隻鯨鯊的嘴巴裡發現外型像是蝦子的新物種,研究人員推測,因鯨鯊嘴巴內有新鮮海水和食物湧入,提供此物種很好的棲息地。

法新社報導,新物種屬鉤蝦亞目。鉤蝦亞目物種在高山或深海等環境中,都有堅強的生存能力。研究人員表示,新物種有長約5毫米的咖啡色身體,還有毛茸茸的腳,這能幫助牠捕食有機物質。富川光表示,這個物種之所以會選擇住在這麼出乎意料的地方,有一些很好的理由。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※新北清潔公司,居家、辦公、裝潢細清專業服務

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

中興通訊每年投逾50億 進軍新能源汽車

中國手機廠商中興通訊推出的自主研發的大功率無線充電系統,已進入公共交通領域。2014年10月,中興通訊攜手蜀都客車共同發佈全球首個無線充電城市微循環解決方案。該方案可以在不專門征地的情況下,在社區附近將任何現有停車位或路面直接改造為安全快捷的充電站。   “目前我們已經與10多個城市簽約,包括四川、河北、雲南、河南等省的城市,與當地政府就公共汽車的無線充電進行合作。”中興通訊副總裁孫枕戈稱,下一步將會把無線充電落實到私家車領域。據孫枕戈介紹,今後新能源汽車有可能將以PPP(政府與私人組織之間合作建設城市基礎設施專案)模式進行運營,而且將獲得中央政府和地方政府的補貼。   孫枕戈表示,從2015年開始,中興通訊每年將投入至少10億元人民幣(約合新臺幣50.6億)進行無線充電設施的商業運作。中興通訊未來的戰略是將通訊技術與各行各業相連,即M-ICT戰略,無線充電項目是其中的一項,該項目是中興與大專院校、研究機構共同合作的結果。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

傳小米電動車已進入量產 售價新台幣 20 萬有找

小米在智慧型手機及資訊領域攻城掠地後,顯然創辦人雷軍並未因此滿足,打從更早之前就一直有傳聞小米想要打造車子,而且還是高技術門檻的電動車,如今又有進一步消息指稱,小米電動車已經進入量產階段。   身為 Tesla 在中國市場的第一批車主,雷軍也曾多次親自拜訪 Tesla 的執行長 Elon Musk,交流許多關於電動車的未來、以及對智慧車輛的看法,近期中國網路上盛傳,內部代號「米斯拉」的小米牌電動車已經開始量產,甚至連其代工合作夥伴為比亞迪等資訊都被揭露。   消息也指出,米斯拉除了會是台電動車外,還會搭載小米自家的 MIUI 系統,將擁有豐富的智慧聯網功能;更驚人的是,這樣的一台高科技智慧電動車,售價僅要 39,999 人民幣,折合新台幣不過 20 萬有找,若此消息為真,小米將會在車界掀起另一波破壞巨浪。    

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※新北清潔公司,居家、辦公、裝潢細清專業服務

※推薦評價好的iphone維修中心