分類
發燒車訊

Fork/Join框架詳解

Fork/Join框架詳解

Fork/Join框架是Java 7提供的一個用於并行執行任務的框架,是一個把大任務分割成若干個小任務,最終匯總每個小任務結果后得到大任務結果的框架。Fork/Join框架要完成兩件事情:

  • 任務分割:首先Fork/Join框架需要把大的任務分割成足夠小的子任務,如果子任務比較大的話還要對子任務進行繼續分割

  • 執行任務併合並結果:分割的子任務分別放到雙端隊列里,然後幾個啟動線程分別從雙端隊列里獲取任務執行。子任務執行完的結果都放在另外一個隊列里,啟動一個線程從隊列里取數據,然後合併這些數據

ForkJoinTask

使用Fork/Join框架,首先需要創建一個ForkJoin任務。該類提供了在任務中執行fork和join的機制。通常情況下我們不需要直接集成ForkJoinTask類,只需要繼承它的子類,Fork/Join框架提供了兩個子類:

  • RecursiveAction
    用於沒有返回結果的任務
  • RecursiveTask
    用於有返回結果的任務

ForkJoinPool

ForkJoinTask需要通過ForkJoinPool來執行。

任務分割出的子任務會添加到當前工作線程所維護的雙端隊列中,進入隊列的頭部。當一個工作線程的隊列里暫時沒有任務時,它會隨機從其他工作線程的隊列的尾部獲取一個任務(工作竊取算法);

Fork/Join框架的實現原理

ForkJoinPool由ForkJoinTask數組和ForkJoinWorkerThread數組組成,ForkJoinTask數組負責將存放程序提交給ForkJoinPool,而ForkJoinWorkerThread負責執行這些任務;

ForkJoinTask的fork方法的實現原理

當我們調用ForkJoinTask的fork方法時,程序會把任務放在ForkJoinWorkerThread的pushTask的workQueue中,異步地執行這個任務,然後立即返回結果,代碼如下:

public final ForkJoinTask<V> fork() {
    Thread t;
    if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
        ((ForkJoinWorkerThread)t).workQueue.push(this);
    else
        ForkJoinPool.common.externalPush(this);
    return this;
}

pushTask方法把當前任務存放在ForkJoinTask數組隊列里。然後再調用ForkJoinPool的signalWork()方法喚醒或創建一個工作線程來執行任務。代碼如下:

final void push(ForkJoinTask<?> task) {
    ForkJoinTask<?>[] a; ForkJoinPool p;
    int b = base, s = top, n;
    if ((a = array) != null) {    // ignore if queue removed
        int m = a.length - 1;     // fenced write for task visibility
        U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
        U.putOrderedInt(this, QTOP, s + 1);
        if ((n = s - b) <= 1) {
            if ((p = pool) != null)
                p.signalWork(p.workQueues, this);
        }
        else if (n >= m)
            growArray();
    }
}

ForkJoinTask的join方法的實現原理

Join方法的主要作用是阻塞當前線程並等待獲取結果。讓我們一起看看ForkJoinTask的join方法的實現,代碼如下:

public final V join() {
    int s;
    if ((s = doJoin() & DONE_MASK) != NORMAL){
        reportException(s);
    }
    return getRawResult();
}

它首先調用doJoin方法,通過doJoin()方法得到當前任務的狀態來判斷返回什麼結果,任務狀態有4種:已完成(NORMAL)、被取消(CANCELLED)、信號(SIGNAL)和出現異常(EXCEPTIONAL);
如果任務狀態是已完成,則直接返回任務結果;
如果任務狀態是被取消,則直接拋出CancellationException;
如果任務狀態是拋出異常,則直接拋出對應的異常;
doJoin方法的實現,代碼如下:

private int doJoin() {
    int s;
    Thread t;
    ForkJoinWorkerThread wt;
    ForkJoinPool.WorkQueue w;
    return (s = status) < 0 ? s :
            ((t = Thread.currentThread()) instanceof                                ForkJoinWorkerThread) ? (w = (wt =                                      (ForkJoinWorkerThread)t).workQueue).tryUnpush(this) && (s =                 doExec()) < 0 ? s : wt.pool.awaitJoin(w, this, 0L) :                externalAwaitDone();
}

doExec() :

final int doExec() {
    int s; 
    boolean completed;
    if ((s = status) >= 0) {
        try {
            completed = exec();
        } catch (Throwable rex) {
            return setExceptionalCompletion(rex);
        }
        if (completed){
            s = setCompletion(NORMAL);
        }
    }
    return s;
}

在doJoin()方法里,首先通過查看任務的狀態,看任務是否已經執行完成,如果執行完成,則直接返回任務狀態;如果沒有執行完,則從任務數組裡取出任務並執行。如果任務順利執行完成,則設置任務狀態為NORMAL,如果出現異常,則記錄異常,並將任務狀態設置為EXCEPTIONAL

Fork/Join框架的異常處理

ForkJoinTask在執行的時候可能會拋出異常,但是我們沒辦法在主線程里直接捕獲異常,所以ForkJoinTask提供了isCompletedAbnormally()方法來檢查任務是否已經拋出異常或已經被取消了,並且可以通過ForkJoinTask的getException方法獲取異常。代碼如下:

if(task.isCompletedAbnormally())
{
    System.out.println(task.getException());
}

getException方法返回Throwable對象,如果任務被取消了則返回CancellationException。如果任務沒有完成或者沒有拋出異常則返回null:

public final Throwable getException() {
    int s = status & DONE_MASK;
    return ((s >= NORMAL) ? null :
        (s == CANCELLED) ? new CancellationException() :
        getThrowableException());
}

DEMO

需求:求1+2+3+4的結果
分析:Fork/Join框架首先要考慮到的是如何分割任務,如果希望每個子任務最多執行兩個數的相加,那麼我們設置分割的閾值是2,由於是4個数字相加,所以Fork/Join框架會把這個任務fork成兩個子任務,子任務一負責計算1+2,子任務二負責計算3+4,然後再join兩個子任務的結果。因為是有結果的任務,所以必須繼承RecursiveTask,實現代碼如下:

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

/**
 *
 * @author aikq
 * @date 2018年11月21日 20:37
 */
public class ForkJoinTaskDemo {

    public static void main(String[] args) {
        ForkJoinPool pool = new ForkJoinPool();
        CountTask task = new CountTask(1,4);
        Future<Integer> result = pool.submit(task);
        try {
            System.out.println("計算結果=" + result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

class CountTask extends RecursiveTask<Integer>{
    private static final long serialVersionUID = -7524245439872879478L;

    private static final int THREAD_HOLD = 2;

    private int start;
    private int end;

    public CountTask(int start,int end){
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        //如果任務足夠小就計算
        boolean canCompute = (end - start) <= THREAD_HOLD;
        if(canCompute){
            for(int i=start;i<=end;i++){
                sum += i;
            }
        }else{
            int middle = (start + end) / 2;
            CountTask left = new CountTask(start,middle);
            CountTask right = new CountTask(middle+1,end);
            //執行子任務
            left.fork();
            right.fork();
            //獲取子任務結果
            int lResult = left.join();
            int rResult = right.join();
            sum = lResult + rResult;
        }
        return sum;
    }
}

通過這個例子,我們進一步了解ForkJoinTask,ForkJoinTask與一般任務的主要區別在於它需要實現compute方法,在這個方法里,首先需要判斷任務是否足夠小,如果足夠小就直接執行任務。如果不足夠小,就必須分割成兩個子任務,每個子任務在調用fork方法時,又會進入compute方法,看看當前子任務是否需要繼續分割成子任務,如果不需要繼續分割,則執行當前子任務並返回結果。使用join方法會等待子任務執行完並得到其結果

本文由博客一文多發平台 發布!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

ThreadLocal深度解析和應用示例

開篇明意

  ThreadLocal是JDK包提供的線程本地變量,如果創建了ThreadLocal<T>變量,那麼訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題。

  ThreadLocal很容易讓人望文生義,想當然地認為是一個“本地線程”。其實,ThreadLocal並不是一個Thread,而是Thread的一個局部變量,也許把它命名ThreadLocalVariable更容易讓人理解一些。

  來看看官方的定義:這個類提供線程局部變量。這些變量與正常的變量不同,每個線程訪問一個(通過它的get或set方法)都有它自己的、獨立初始化的變量副本。ThreadLocal實例通常是類中的私有靜態字段,希望將狀態與線程關聯(例如,用戶ID或事務ID)。

源碼解析

  1.核心方法之   set(T t)

 1     /**
 2      * Sets the current thread's copy of this thread-local variable
 3      * to the specified value.  Most subclasses will have no need to
 4      * override this method, relying solely on the {@link #initialValue}
 5      * method to set the values of thread-locals.
 6      *
 7      * @param value the value to be stored in the current thread's copy of
 8      *        this thread-local.
 9      */
10     public void set(T value) {
11         Thread t = Thread.currentThread();
12         ThreadLocalMap map = getMap(t);
13         if (map != null)
14             map.set(this, value);
15         else
16             createMap(t, value);
17     }

解析:

  當調用ThreadLocal的set(T t)的時候,代碼首先會獲取當前線程的 ThreadLocalMap(ThreadLocal中的靜態內部類,同時也作為Thread的成員變量存在,後面會進一步了解ThreadLocalMap),如果ThreadLocalMap存在,將ThreadLocal作為map的key,要保存的值作為value來put進map中(如果map不存在就先創建map,然後再進行put);

  2.核心方法值 get()

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);        //此處和set方法一致,也是通過當前線程獲取對應的成員變量ThreadLocalMap,map中存放的是Entry(ThreadLocalMap的內部類(繼承了弱引用))
    
if (map != null) {
      ThreadLocalMap.Entry e = map.getEntry(this);
      if (e != null) {
        @SuppressWarnings("unchecked")
        T result = (T)e.value;
        return result;
      }
    }
    return setInitialValue();
}

解析:

  剛才把對象放到set到map中,現在根據key將其取出來,值得注意的是這裏的map裏面存的可不是鍵值對,而是繼承了WeakReference<ThreadLocal<?>> 的Entry對象,關於ThreadLocalMap.Entry類,後面會有更加詳盡的講述。

核心方法之  remove()

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

解析:

  通過getMap方法獲取Thread中的成員變量ThreadLocalMap,在map中移除對應的ThreadLocal,由於ThreadLocal(key)是一種弱引用,弱引用中key為空,gc會回收變量value,看一下核心的m.remove(this);方法

        /**
         * Remove the entry for key.
         */
        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //定義Entry在數組中的標號 for (Entry e = tab[i];              //通過循環的方式remove掉Thread中所有的Entry
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {   
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        } 

 

 

 

靈魂提問

  問:threadlocal是做什麼用的,用在哪些場景當中?  

    結合官方對ThreadLocal類的定義,threadLocal主要滿足某些變量或者示例是線程隔離的,但是在相同線程的多個類或者方法中都能使用的到,並且當線程結束時該變量也應該銷毀。通俗點講:ThreadLocal保證每個線程有自己的數據副本,當線程結束后可  以獨立回收。由於ThreadLocal的特性,同一線程在某地方進行設置,在隨後的任意地方都可以獲取到。從而可以用來保存線程上下文信息。常用的比如每個請求怎麼把一串後續關聯起來,就可以用ThreadLocal進行set,在後續的任意需要記錄日誌的方法裏面進行get獲取到請求id,從而把整個請求串起來。     使用場景有很多,比如:

  • 基於用戶請求線程的數據隔離(每次請求都綁定userId,userId的值存在於ThreadLoca中)
  • 跟蹤一個請求,從接收請求,處理到返回的整個流程,有沒有好的辦法   思考:微服務中的鏈路追蹤是否利用了ThreadLocal特性
  • 數據庫的讀寫分離
  • 還有比如Spring的事務管理,用ThreadLocal存儲Connection,從而各個DAO可以獲取同一Connection,可以進行事務回滾,提交等操作。

    
問:如果我啟動另外一個線程。那麼在主線程設置的Threadlocal值能被子線程拿到嗎?     原始的ThreadLocal是不具有繼承(或者說傳遞)特性的     
問:那該如何解決ThreadLocal無法傳遞的問題呢?     用ThreadLocal的子類 InheritableThreadLocal,InheritableThreadLocal是具有傳遞性的

  /**
  * 重寫Threadlocal類中的getMap方法,在原Threadlocal中是返回
  * t.theadLocals,而在這麼卻是返回了inheritableThreadLocals,因為
  * Thread類中也有一個要保存父子傳遞的變量
  */ ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals; }
    /**  * 同理,在創建ThreadLocalMap的時候不是給t.threadlocal賦值  *而是給inheritableThreadLocals變量賦值  *  */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

解析:因為InheritableThreadLocal重寫了ThreadLocal中的getMap 和createMap方法,這兩個方法維護的是Thread中的另外一個成員變量  inheritableThreadLocals,線程在創建的時候回複製inheritableThreadLocals中的值 ;

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
  //Thread類中維護的成員變量,ThreadLocal會維護該變量
ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */
//Thread中維護的成員變量 ,
InheritableThreadLocal 中維護該變量
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;


 

//Thread init方法中的關鍵代碼,簡單來說是將父類中inheritableThreadLocals中的值拷貝到當前線程的inheritableThreadLocals中(淺拷貝,拷貝的是value的地址引用)
 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

總結

  • ThreadLocal類封裝了getMap()、Set()、Get()、Remove()4個核心方法。
  • 通過getMap()獲取每個子線程Thread持有自己的ThreadLocalMap實例, 因此它們是不存在併發競爭的。可以理解為每個線程有自己的變量副本。
  • ThreadLocalMap中Entry[]數組存儲數據,初始化長度16,後續每次都是1.5倍擴容。主線程中定義了幾個ThreadLocal變量,Entry[]才有幾個key。
  • Entry的key是對ThreadLocal的弱引用,當拋棄掉ThreadLocal對象時,垃圾收集器會忽略這個key的引用而清理掉ThreadLocal對象, 防止了內存泄漏。

    tips:上面四個總結來源於其他技術博客,個人認為總結的比較合理所以直接摘抄過來了

拓展:

  ThreadLocal在線程池中使用容易發生的問題: 內存泄漏,先看下圖

  

  每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal實例置為null以後,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連接過來的強引用. 只有當前thread結束以後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.

  所以得出一個結論就是只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設為null和線程結束這段時間不會被回收的,就發生了我們認為的內存泄露。其實這是一個對概念理解的不一致,也沒什麼好爭論的。最要命的是線程對象不被回收的情況,這就發生了真正意義上的內存泄露。比如使用線程池的時候,線程結束是不會銷毀的,會再次使用的。就可能出現內存泄露。  

  PS.Java為了最小化減少內存泄露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map里所有key為null的value。所以最怕的情況就是,threadLocal對象設null了,開始發生“內存泄露”,然後使用線程池,這個線程結束,線程放回線程池中不銷毀,這個線程一直不被使用,或者分配使用了又不再調用get,set方法,那麼這個期間就會發生真正的內存泄露。 

 

  1. JVM利用設置ThreadLocalMap的Key為弱引用,來避免內存泄露。
  2. JVM利用調用remove、get、set方法的時候,回收弱引用。
  3. 當ThreadLocal存儲很多Key為null的Entry的時候,而不再去調用remove、get、set方法,那麼將導致內存泄漏。
  4. 當使用static ThreadLocal的時候,延長ThreadLocal的生命周期,那也可能導致內存泄漏。因為,static變量在類未加載的時候,它就已經加載,當線程結束的時候,static變量不一定會回收。那麼,比起普通成員變量使用的時候才加載,static的生命周期加長將更容易導致內存泄漏危機。

 

  參考鏈接:

 

在線程池中使用ThreadLocal

通過上面的分析可以知道InheritableThreadLocal是通過Thread()的inint方法實現父子之間的傳遞的,但是線程池是統一創建線程並實現復用的,這樣就好導致下面的問題發生:

  •   線程不會銷毀,ThreadLocal也不會被銷毀,這樣會導致ThreadLoca會隨着Thread的復用而復用
  •   子線程無法通過InheritableThreadLocal實現傳遞性(因為沒有單獨的調用Thread的Init方法進行map的複製),子線程中get到的是null或者是其他線程復用的錯亂值(疑問點還沒搞清楚原因,後續補充::在異步線程中會出現null的情況,同步線程不會出現)     

    ps:線程池中的線程是什麼時候創建的?

 

  解決方案:

    下面兩個鏈接有詳細的說明,我就不重複寫了,後續我會將本文進一般優化並添加一些例子來幫助說明,歡迎收藏,關於本文有不同的意見歡迎評論指正……

    

    

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

分類
發燒車訊

關於GC(中):Java垃圾回收相關基礎知識

Java內存模型

(圖源: )

區域名 英文名 訪問權限 作用 備註
程序計數器 Program Counter Register 線程隔離 標記待取的下一條執行的指令 執行Native方法時為空; JVM規範中唯一不會發生OutOfMemoryError的區域
虛擬機棧 VM Stack 線程隔離 每個Java方法執行時創建,用於存儲局部變量表,操作棧,動態鏈接,方法出口等信息 方法執行的內存模型
本地方法棧 Native Method Stack 線程隔離 Native方法執行時使用 JVM規範沒有強制規定,如Hotspot將VM和Native兩個方法棧合二為一
Java堆 Java Heap 線程共享 存放對象實例 更好的回收內存 vs 更快的分配內存
方法區 Method Area 線程共享 存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據 JVM規範不強制要求做垃圾收集
運行時常量池 Runtime Constant Pool 線程共享 方法區的一部分
直接內存 Direct Memory 堆外內存,通過堆的DirectByteBuffer訪問 不是運行時數據區的一部分,但也可能OutOfMemoryError

對象的創建——new的時候發生了什麼

討論僅限於普通Java對象,不包括數組和Class對象。

  1. 常量池查找類的常量引用,如果沒有先做類加載
  2. 分配內存,視堆內存是否是規整(由垃圾回收器是否具有壓縮功能而定)而使用“指針碰撞”或“空閑列表”模式
  3. 內存空間初始化為零值,可能提前在線程創建時分配TLAB時做初始化
  4. 設置必要信息,如對象是哪個類的示例、元信息、GC分代年齡等
  5. 調用<init>方法

垃圾回收器總結

垃圾回收,針對的都是堆。

分代

  • 新生代:適合使用複製算法, 以下三個區一般佔比為8:1:1
    • Eden 新對象誕生區
    • From Survivor 上一次GC的倖存者(見“GC種類-minor GC”)
    • To Survivor 本次待存放倖存者的區域
  • 老年代:存活時間較久的,大小較大的對象,因此使用標記-整理或標記-清除算法比較合適
  • 永久代:存放類信息和元數據等不太可能回收的信息。Java8中被元空間(Metaspace)代替,不再使用堆,而是物理內存。

分代的原因

  • 不同代的對象生命周期不同,可以針對性地使用不同的垃圾回收算法
  • 不同代可以分開進行回收

回收算法

名稱 工作原理 優點 缺點
標記-清除 對可回收對對象做一輪標記,標記完成后統一回收被標記的對象 易於理解,內存利用率高 效率問題;內存碎片;分配大對象但無空間時提前GC
複製 內存均分兩塊,只使用其中一塊。回收時將這一塊存活對象全部複製到另一塊 效率高 可用空間減少; 空間不夠時需老年代分配擔保
標記-整理 對可回收對對象做一輪標記,標記完成后將存活對象統一左移,清理掉邊界外內存 內存利用率高 效率問題

標記-X算法適用於老年代,複製算法適用於新生代。

GC種類

  • Minor GC,只回收新生代,將Eden和From Survivor區的存活對象複製到To Survivor
  • Major GC,清理老年代。但因為伴隨着新生代的對象生命周期升級到老年代,一般也可認為伴隨着FullGC。
  • FullGC,整個堆的回收
  • Mixed GC,G1特有,可能會發生多次回收,可以參考

垃圾回收器小結

垃圾回收器名稱 特性 目前工作分代 回收算法 可否與Serial配合 可否與ParNew配合 可否與ParallelScavenge配合 可否與SerialOld配合 可否與ParallelOld配合 可否與CMS配合 可否與G1配合
Serial 單線程 新生代 複製 Y N Y N/A
ParNew 多線程 新生代 複製 N N Y N/A
ParallelScavenge 多線程, 更關注吞吐量可調節 新生代 複製 N N Y N/A
SerialOld 單線程 老年代 標記-整理 Y Y N N/A
ParallelOld 多線程 老年代 標記-整理 N N Y N/A
CMS 多線程,併發收集,低停頓。但無法處理浮動垃圾,標記-清除會產生內存碎片較多 老年代 標記-清除 Y Y N Y N/A
G1 并行併發收集,追求可預測但回收時間,整體內存模型有所變化 新生代/老年代 整體是標記-整理,局部(兩Region)複製 N N N N N N

在本系列的上一篇文章中,減少FullGC的方式是使用G1代替CMS,計劃在下一篇文章中對比CMS和G1的區別。

理解GC日誌

只舉比較簡單的例子,具體各項的格式視情況分析,不同回收器也會有差異。

2019-11-22T10:28:32.177+0800: 60188.392: [GC (Allocation Failure) 2019-11-22T10:28:32.178+0800: 60188.392: [ParNew: 1750382K->2520K(1922432K), 0.0312604 secs] 1945718K->198045K(4019584K), 0.0315892 secs] [Times: user=0.09 sys=0.01, real=0.03 secs]

開始時間-(方括號[)-發生區域(ParNew,命名和GC回收器有關)-回收前大小-回收后大小-(方括號])-GC前堆已使用容量-GC后堆已使用容量大小-回收時間-使用時間詳情(用戶態時間-內核時間-牆上時鐘時間)

注意這裏沒有包括“2019-11-22T10:28:32.177+0800: 60188.392: [GC (Allocation Failure)”這部分的分析。

可借鑒的編程模式

對象分配的併發控制

對象創建是很頻繁的,在線程共享的堆中會遇到併發的問題。兩種解決辦法:

  1. 同步鎖定:CAS+失敗重試,確保原子性
  2. 堆中預先給每個線程劃分一小塊內存區域——本地線程分配緩衝(TLAB),TLAB使用完並分配新的TLAB時才做同步鎖定。可看作1的優化。

CAS: Conmpare And Swap,用於實現多線程同步的原子指令。 將內存位置的內容與給定值進行比較,只有在相同的情況下,將該內存位置的內容修改為新的給定值。關於CAS可以參考:

對象訪問的定位方式

前提條件:通過上本地變量表的reference訪問中的對象及它在方法區的對象類型數據(類信息)
主流的兩種方式,這兩種方式各有優點,可以看出方式2是方式1的優化,但並不是全面超越方式1,無法完全取代。
這裏可以看到要權衡垃圾回收和訪問速度兩方面。

方式1: 直接指針訪問實例數據

reference直接存放對象實例地址,只需要一次訪問即可,執行效率較高。

方式2: 使用句柄池

reference中地址穩定,對象被移動時只需要改句柄池的地址。相對的,訪問實例需要兩次指針定位。

參考資料

  1. 周志明.著《深入理解JAVA虛擬機》

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

[NLP] Adaptive Softmax

1. Overview

Adaptive softmax算法在鏈接1中的論文中提出,該算法目的是為了提高softmax函數的運算效率,適用於一些具有非常大詞彙量的神經網絡。

在NLP的大部分任務中,都會用到softmax,但是對於詞彙量非常大的任務,每次進行完全的softmax會有非常大的計算量,很耗時(每次預測一個token都需要O(|V|)的時間複雜度)。

所以paper中提出adaptive softmax來提升softmax的運算效率。

1) 該算法的提出利用到了單詞分佈不均衡的特點(unbalanced word distribution)來形成clusters, 這樣在計算softmax時可以避免對詞彙量大小的線性依賴關係,降低計算時間;

2) 另外通過結合現代架構和矩陣乘積操作的特點,通過使其更適合GPU單元的方式進一步加速計算。

2. Introduction

2.1 一般降低softmax計算複雜度的兩種方式

1) 考慮原始分佈:近似原始概率分佈或近似原始概率分佈的子集

2) 構造近似模型,但是產生準確的概率分佈。比如:hierarchical softmax.

(上述兩個方法可以大致區分為:一是準確的模型產生近似概率分佈,二是用近似模型產生準確的概率分佈)

2.2 本文貢獻點

paper中主要採用的上述(2)的方式,主要借鑒的是hierarchical softmax和一些變型。與以往工作的不同在於,本paper結合了GPU的特點進行了計算加速。主要

1. 定義了一種可以生成 近似層次模型 的策略,該策略同時考慮了矩陣乘積運算的計算時間。這個計算時間並非與矩陣的維數是簡單線性關係。

2. 在近來的GPU上對該模型進行了實證分析。在所提出的優化算法中也包含了對實際計算時間模型的定義。

3. 與一般的softmax相比,有2× 到 10× 的加速。這等價於在同等算力限制下提高了準確度。另外一點非常重要,該paper所提出的這種高效計算的方法與一些通過并行提高效率的方法相比,在給定一定量的training data下,對準確度並沒有損失。

3. Adaptive Softmax 

3.1 矩陣乘積運算的計算時間模型

hidden states: (B × d); Word representation: (d × k); 這兩個矩陣的乘積運算時間: g(k, B)。其中B是batch size, d: hidden layer 的size, k vectors的數量。

(1) 固定B,d, 探究k值大小與g(k)的關係:

在K40、 M40兩種GPU model中進行實驗,發現k在大約為$k_0 \approx 50$的範圍之內,g(k)是常數級別的,在$k > k_0$后,呈線性依賴關係。其計算模型為:

同樣的,關於B的計算時間的函數也呈現這樣的關係。這可以表示,矩陣相乘時,當其中一個維度很小時,矩陣乘法是低效的。

如何理解呢?比如對於 $k_1 = 2^2$, $k_2 = 2^4$,它們均在同樣的常量時間內完成運算,那麼顯然對於$k1$量級有算力被浪費了。

那麼這也說明,一些words的層次結構,其每個節點只具有少量的子節點(比如huffman 編碼的tree),是次優的。

(2) 探究batch size B值大小與g(B)的關係:

同樣的,當探究計算用時與batch size $B$,的關係時,發現在矩陣乘法運算中,其中一個維度很小時計算並不高效。因此會導致:

(i) 在層次結構中,因為每個節點只有少數一些孩子節點 (比如Huffman 樹),其計算效率是次優的;

(ii) 對於按照詞頻劃分clusters后,那些只包含稀有詞的cluster,被選擇的概率為p, 並且其batch size也縮減為 $p B$,也會出現以上所述的低效的矩陣乘積運算問題。

(3) 本文的計算模型

所以paper中綜合考慮了k與B,提出了下面的計算模型:

對於Adaptive Softmax, 其核心思想是根據詞頻大小,將不同詞頻區間的詞分為不同的clusters,按照詞頻高的優先訪問的原則,來對每個cluster整體,進而對cluster中的每個詞進行訪問,進行softmax操作。

那麼paper中首先以2個clusters為例,對該模型進行講解,之後推廣到多個clusters的一般情況。

3.2 Two-Clusters Case

根據Zipf定律,在Penn TreeBank中 20%的詞,可以覆蓋一般文檔中87%的出現過的詞。

直觀地,可以將字典$V$中的詞分為$V_h$、$V_t$兩個部分。其中$V_h$表示高頻詞集合(head),$V_t$表示低頻詞集合(tail)。一般地,$|V_h| << |V_t|$,$P(V_h) >> P(V_t)$。

(1) Clusters 的組織

直觀地可以想到對於這兩個clusters有兩種組織方式:(i) 兩個clusters都是2層的樹結構,(ii) 將head部分用一個短列表保存,對tail部分用二層樹結構。何種方式更優可以就其計算效率和準確率進行比較:

在準確率方面,(i) 較之 (ii) 一般有 5 ~ 10 %的下降。原因如下:

對於屬於cluster $c$的每個詞$w$的概率計算為: 

若採用(i):  $P(c | h) * P(w | c, h)$

若採用(ii): 對於高頻詞head部分可以直接計算獲得 $P(w | h)$,其更為簡單直接。

因此,除非(i)(ii)的計算時間有非常大的差別,否則選擇(ii)的組織方式。

(2) 對計算時間的縮短

 

圖2. Two clusters示意圖

如圖2所示,$k_h = |V_h|, k_t = k – k_h, p_t = 1 – p_h$

(i) 對於第一層:對於batch size 為B的輸入,在head中對$k_h$個高頻詞以及1個單位的二層cluster的向量(加陰影部分),共$k_h + 1$個向量,做softmax,

這樣占文檔中$p_h$的詞可以基本確定下來,在head 的 short list 部分找到對應的單詞;

而如果與陰影部分進行softmax值更大,表明待預測的詞,在第二層的低頻詞部分;

第一層的計算開銷為:$g(k_h + 1, B)$

(ii) 在short list 中確定了 $p_h × B$ 的內容后,還剩下 $p_t B$的輸入需要在tail中繼續進行softmax來確定所預測的詞。

第二層中需要對$k_t$個向量做softmax;

第二層的計算開銷為:$g(k_t, pB)$

綜上,總的計算開銷為:$$C = g(k_h + 1, B) + g(k_t, p_t B)$$

相比於最初的softmax,分母中需要對詞典中的每個詞的向量逐一進行計算;採用adaptive softmax可以使得該過程分為兩部分進行,每部分只需對該部分所包含的詞的向量進行計算即可,降低了計算用時。

論文中的Figure 2显示了,對於$k_h$的合理劃分,最高可以實現相較於完全softmax的5x加速。

(3) 不同cluster的分類器能力不同

由於每個cluster基本是獨立計算的,它們並不要求有相同的能力(capacity)。

一般可以為高頻詞的cluster予以更高的capacity,而可以適當降低詞頻詞的cluster的capacity。因為低頻詞在文檔中很少出現,因此對其capacity的適當降低並不會非常影響總體的性能。

在本文中,採取了為不同的clusters共享隱層,通過加映射矩陣的方式降低分類器的輸入大小。一般的,tail部分的映射矩陣將其大小從$d$維縮小到$d_t = d / 4$維。

3.3 一般性情況

在3.2中以分為2個cluster為例,介紹了adaptive softmax如何組織並計算的,這裏將其拓展為一般情況,即可以分為多個clusters時的處理方式。

假設將整個詞典分為一個head部分和J個詞頻詞cluster部分,那麼有:

$$V = V_h \cup V_1 … V_j, V_i \cap V_j = \phi$$

其中$V_h$在第一層,其餘均在第二層,如圖Figure 3所示。

每個cluster中的詞向量的數目為$k_i = |V_i|$,單詞$w$屬於某個cluster的概率為:$p_i = \sum_{w \in V_i} p(w)$.

那麼,

計算head部分的時間開銷為:$C_h = g(J + k_h, B)$

計算每個二層cluster的時間開銷為:$\forall_i, C_i = g(k_i, p_i B)$

那麼總的時間開銷為:$C = g(J + k_h, B) + \sum_i g(k_i, p_i B) \tag8$

 

回顧公式(7):

$$g(k,B) = max(c + \lambda k_0 B_0, c + \lambda k B) \tag7$$

這裡有兩部分,一個是常數部分$c + \lambda k_0 B_0$,一個是線性變換部分$c + \lambda k B$。

通過3.1可知,在常數部分GPU的算力並未得到充分利用,因此應盡量避免落入該部分。那麼需要滿足:

$kB \geq k_0B_0$,這樣在求max時,可以利用以上的第二部分。

將(8)式代入(7)的第二部分得:

 

那麼接下來的目標就是根據(10) 以最小化時間開銷C. 

在(10)中,$J ,B$是固定的,那麼可以重點關注$\sum_i p_ik_i$與$k_h$。

(1) $\sum_i p_ik_i$

假設$p_{i + j} = p_i + p_j$, 那麼 $p_jk_j = (p_{i + j} – p_i) k_j$

$p_ik_i + p_jk_j = p_i(k_i – k_j) + p_{i + j}k_j \tag{11}$  

假設$k_i > k_j, p_{i + j}$, $k_j$均固定,那麼(11)中只有$p_i$可變,可以通過減小$p_i$的方式使(11)式減小。=> 也即$k_i > k_j$ 且 $p_i$盡量小,即包含越多詞的cluster,其概率越小。

(2) $k_h$

減小$k_h$可以使(10)式減小。 => 即為可以使高頻詞所在的cluster包含更少的詞。

綜上,給定了cluster的個數J,與batch size B,可以通過給大的cluster分配以更小的概率降低時間開銷C.

另外paper中講到可以用動態規劃的方法,在給定了J后,對$k_i$的大小進行劃分。

cluster的劃分個數$J$。paper中實驗了採用不同的J所導致的不同計算時間。如圖Figure 4所示。雖然在$10 ~ 15$區間內效果最好,但$ > 5$之後效果沒有非常明顯的提升。所以文中建議採用2~5個clusters。

實驗主要在text8, europarl, one billion word三個數據集上做的,通過比較ppl發現,adaptive softmax仍能保持低的ppl,並且相比於原模型有2x到10x的提速。

  

參考鏈接:

1. Efficient softmax approximation for GPUs: 

Some of us get dipped in flat, some in satin, some in gloss. But every once in a while you find someone who’s iridescent, and when you do, nothing will ever compare. ~ Flipped

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

分類
發燒車訊

類加載器 – ClassLoader詳解

獲得ClassLoader的途徑

  • 獲得當前類的ClassLoader
    • clazz.getClassLoader()
  • 獲得當前線程上下文的ClassLoader
    • Thread.currentThread().getContextClassLoader();
  • 獲得系統的ClassLoader
    • ClassLoader.getSystemClassLoader()
  • 獲得調用者的ClassLoader
    • DriverManager.getCallerClassLoader

ClassLoader源碼解析

概述

類加載器是用於加載類的對象,ClassLoader是一個抽象類。如果我們給定了一個類的二進制名稱,類加載器應嘗試去定位或生成構成定義類的數據。一種典型的策略是將給定的二進制名稱轉換為文件名,然後去文件系統中讀取這個文件名所對應的class文件。

每個Class對象都會包含一個定義它的ClassLoader的一個引用。

數組類的Class對象,不是由類加載器去創建的,而是在Java運行期JVM根據需要自動創建的。對於數組類的類加載器來說,是通過Class.getClassLoader()返回的,與數組當中元素類型的類加載器是一樣的;如果數組當中的元素類型是一個原生類型,數組類是沒有類加載器的【代碼一】。

應用實現了ClassLoader的子類是為了擴展JVM動態加載類的方式。

類加載器典型情況下時可以被安全管理器所使用去標識安全域問題。

ClassLoader類使用了委託模型來尋找類和資源,ClassLoader的每一個實例都會有一個與之關聯的父ClassLoader,當ClassLoader被要求尋找一個類或者資源的時候,ClassLoader實例在自身嘗試尋找類或者資源之前會委託它的父類加載器去完成。虛擬機內建的類加載器,稱之為啟動類加載器,是沒有父加載器的,但是可以作為一個類加載器的父類加載器【雙親委託機制】。

支持併發類加載的類加載器叫做并行類加載器,要求在初始化期間通過ClassLoader.registerAsParallelCapable 方法註冊自身,ClassLoader類默認被註冊為可以并行,但是如果它的子類也是并行加載的話需要單獨去註冊子類。

在委託模型不是嚴格的層次化的環境下,類加載器需要并行,否則類加載會導致死鎖,因為加載器的鎖在類加載過程中是一直被持有的。

通常情況下,Java虛擬機以平台相關的形式從本地的文件系統中加載類,比如在UNIX系統,虛擬機從CLASSPATH環境所定義的目錄加載類。
然而,有些類並不是來自於文件;它們是從其它來源得到的,比如網絡,或者是由應用本身構建【動態代理】。定義類(defineClass )方法會將字節數組轉換為Class的實例,這個新定義類的實例可以由Class.newInstance創建。

由類加載器創建的對象的方法和構造方法可能引用其它的類,為了確定被引用的類,Java虛擬機會調用最初創建類的類加載器的loadClass方法。

二進制名稱:以字符串參數的形式向CalssLoader提供的任意一個類名,必須是一個二進制的名稱,包含以下四種情況

  • “java.lang.String” 正常類
  • “javax.swing.JSpinner$DefaultEditor” 內部類
  • “java.security.KeyStore\(Builder\)FileBuilder$1″ KeyStore的內部類Builder的內部類FileBuilder的第一個匿名內部類
  • “java.net.URLClassLoader$3$1” URLClassLoader類的第三個匿名內部類的第一個匿名內部類

代碼一:

public class Test12 {
    public static void main(String[] args) {
        String[] strings = new String[6];
        System.out.println(strings.getClass().getClassLoader());
        // 運行結果:null

        Test12[] test12s = new Test12[1];
        System.out.println(test12s.getClass().getClassLoader());
        // 運行結果:sun.misc.Launcher$AppClassLoader@18b4aac2

        int[] ints = new int[2];
        System.out.println(ints.getClass().getClassLoader());
        // 運行結果:null
    }
}

loadClass方法

loadClass的源碼如下, loadClass方法加載擁有指定的二進制名稱的Class,默認按照如下順序尋找類:

  • 調用findLoadedClass(String)檢查這個類是否被加載
  • 調用父類加載器的loadClass方法,如果父類加載器為null,就會調用啟動類加載器
  • 調用findClass(String)方法尋找

使用上述步驟如果類被找到且resolve為true,就會去調用resolveClass(Class)方法

protected Class<?> loadClass(String name, boolean resolve)
  throws ClassNotFoundException
{
  synchronized (getClassLoadingLock(name)) {
      // First, check if the class has already been loaded
      Class<?> c = findLoadedClass(name);
      if (c == null) {
          long t0 = System.nanoTime();
          try {
              if (parent != null) {
                  c = parent.loadClass(name, false);
              } else {
                  c = findBootstrapClassOrNull(name);
              }
          } catch (ClassNotFoundException e) {
              // ClassNotFoundException thrown if class not found
              // from the non-null parent class loader
          }

          if (c == null) {
              // If still not found, then invoke findClass in order
              // to find the class.
              long t1 = System.nanoTime();
              c = findClass(name);

              // this is the defining class loader; record the stats
              sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
              sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
              sun.misc.PerfCounter.getFindClasses().increment();
          }
      }
      if (resolve) {
          resolveClass(c);
      }
      return c;
  }
}

findClass方法

findClass的源碼如下,findClass尋找擁有指定二進制名稱的類,JVM鼓勵我們重寫此方法,需要自定義加載器遵循雙親委託機制,該方法會在檢查完父類加載器之後被loadClass方法調用,默認返回ClassNotFoundException異常。

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

defineClass方法

defineClass的源碼如下,defineClass方法將一個字節數組轉換為Class的實例。

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                     ProtectionDomain protectionDomain)
    throws ClassFormatError
{
    protectionDomain = preDefineClass(name, protectionDomain);
    String source = defineClassSourceLocation(protectionDomain);
    Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
    postDefineClass(c, protectionDomain);
    return c;
}

自定義類加載器

/**
 * 繼承了ClassLoader,這是一個自定義的類加載器
 * @author 夜的那種黑丶
 */
public class ClassLoaderTest extends ClassLoader {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
       Class<?> clazz = loader.loadClass("classloader.Test01");
        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());
    }
    //------------------------------以上為測試代碼---------------------------------

    /**
     * 類加載器名稱,標識作用
     */
    private String classLoaderName;

    /**
     * 從磁盤讀物字節碼文件的擴展名
     */
    private String fileExtension = ".class";

    /**
     * 創建一個類加載器對象,將系統類加載器當做該類加載器的父加載器
     * @param classLoaderName 類加載器名稱
     */
    private ClassLoaderTest(String classLoaderName) {
        // 將系統類加載器當做該類加載器的父加載器
        super();
        this.classLoaderName = classLoaderName;
    }

    /**
     * 創建一個類加載器對象,显示指定該類加載器的父加載器
     * 前提是需要有一個類加載器作為父加載器
     * @param parent 父加載器
     * @param classLoaderName 類加載器名稱
     */
    private ClassLoaderTest(ClassLoader parent, String classLoaderName) {
        // 显示指定該類加載器的父加載器
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    /**
     * 尋找擁有指定二進制名稱的類,重寫ClassLoader類的同名方法,需要自定義加載器遵循雙親委託機制
     * 該方法會在檢查完父類加載器之後被loadClass方法調用
     * 默認返回ClassNotFoundException異常
     * @param className 類名
     * @return Class的實例
     * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        byte[] data = this.loadClassData(className);
        /*
         * 通過defineClass方法將字節數組轉換為Class
         * defineClass:將一個字節數組轉換為Class的實例,在使用這個Class之前必須要被解析
         */
        return this.defineClass(className, data, 0 , data.length);
    }

    /**
     * io操作,根據類名找到對應文件,返回class文件的二進制信息
     * @param className 類名
     * @return class文件的二進制信息
     * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
     */
    private byte[] loadClassData(String className) throws ClassNotFoundException {
        InputStream inputStream = null;
        byte[] data;
        ByteArrayOutputStream byteArrayOutputStream = null;

        try {
            this.classLoaderName = this.classLoaderName.replace(".", "/");
            inputStream = new FileInputStream(new File(className + this.fileExtension));
            byteArrayOutputStream = new ByteArrayOutputStream();

            int ch;
            while (-1 != (ch = inputStream.read())) {
                byteArrayOutputStream.write(ch);
            }

            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            throw new ClassNotFoundException();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (byteArrayOutputStream != null) {
                    byteArrayOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }
}

以上是一段自定義類加載器的代碼,我們執行這段代碼

classloader.Test01@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2

可以看見,這段代碼中進行類加載的類加載器還是系統類加載器(AppClassLoader)。這是因為jvm的雙親委託機製造成的,private ClassLoaderTest(String classLoaderName)將系統類加載器當做我們自定義類加載器的父加載器,jvm的雙親委託機制使自定義類加載器委託系統類加載器完成加載。

改造以下代碼,添加一個path屬性用來指定類加載位置:

public class ClassLoaderTest extends ClassLoader {
    public static void main(String[] args) throws Exception {
        ClassLoaderTest loader = new ClassLoaderTest("loader");
        loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
        Class<?> clazz = loader.loadClass("classloader.Test01");
        System.out.println("class:" + clazz);

        Object object = clazz.newInstance();
        System.out.println(object);
        System.out.println(object.getClass().getClassLoader());
    }
    //------------------------------以上為測試代碼---------------------------------

    /**
     * 從指定路徑加載
     */
    private String path;

    ......
    
    /**
     * io操作,根據類名找到對應文件,返回class文件的二進制信息
     * @param className 類名
     * @return class文件的二進制信息
     * @throws ClassNotFoundException 如果類不能被找到,拋出此異常
     */
    private byte[] loadClassData(String className) throws ClassNotFoundException {
        InputStream inputStream = null;
        byte[] data;
        ByteArrayOutputStream byteArrayOutputStream = null;

        className = className.replace(".", "/");

        try {
            this.classLoaderName = this.classLoaderName.replace(".", "/");
            inputStream = new FileInputStream(new File(this.path + className + this.fileExtension));
            byteArrayOutputStream = new ByteArrayOutputStream();

            int ch;
            while (-1 != (ch = inputStream.read())) {
                byteArrayOutputStream.write(ch);
            }

            data = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            throw new ClassNotFoundException();
        } finally {
            try {
                if (inputStream != null) {
                    inputStream.close();
                }
                if (byteArrayOutputStream != null) {
                    byteArrayOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    public void setPath(String path) {
        this.path = path;
    }
}

運行一下

class:class classloader.Test01
classloader.Test01@7f31245a
sun.misc.Launcher$AppClassLoader@18b4aac2

修改一下測試代碼,並刪除工程下的Test01.class文件

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
   loader.setPath("/home/fanxuan/桌面/");
    Class<?> clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz);

    Object object = clazz.newInstance();
    System.out.println(object);
    System.out.println(object.getClass().getClassLoader());
}

運行一下

class:class classloader.Test01
classloader.Test01@135fbaa4
classloader.ClassLoaderTest@7f31245a

分析

改造后的兩塊代碼,第一塊代碼中加載類的是系統類加載器AppClassLoader,第二塊代碼中加載類的是自定義類加載器ClassLoaderTest。是因為ClassLoaderTest會委託他的父加載器AppClassLoader加載class,第一塊代碼的path直接是工程下,AppClassLoader可以加載到,而第二塊代碼的path在桌面目錄下,所以AppClassLoader無法加載到,然後ClassLoaderTest自身嘗試加載並成功加載到。如果第二塊代碼工程目錄下的Test01.class文件沒有被刪除,那麼依然是AppClassLoader加載。

再來測試一塊代碼

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
    Class<?> clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest("loader");
    loader2.setPath("/home/fanxuan/Study/java/jvmStudy/out/production/jvmStudy/");
    Class<?> clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

結果顯而易見,類由系統類加載器加載,並且clazz和clazz2是相同的。

class:2133927002
sun.misc.Launcher$AppClassLoader@18b4aac2
class:2133927002
sun.misc.Launcher$AppClassLoader@18b4aac2

在改造一下

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/桌面/");
    Class<?> clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest("loader2");
    loader2.setPath("/home/fanxuan/桌面/");
    Class<?> clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

運行結果

class:325040804
classloader.ClassLoaderTest@7f31245a
class:621009875
classloader.ClassLoaderTest@45ee12a7

ClassLoaderTest是顯而易見,但是clazz和clazz2是不同的,這是因為類加載器的命名空間的原因。

我們可以通過設置父類加載器來讓loader和loader2處於同一命名空間

public static void main(String[] args) throws Exception {
    ClassLoaderTest loader = new ClassLoaderTest("loader");
    loader.setPath("/home/fanxuan/桌面/");
    Class<?> clazz = loader.loadClass("classloader.Test01");
    System.out.println("class:" + clazz.hashCode());

    Object object = clazz.newInstance();
    System.out.println(object.getClass().getClassLoader());

    ClassLoaderTest loader2 = new ClassLoaderTest(loader, "loader2");
    loader2.setPath("/home/fanxuan/桌面/");
    Class<?> clazz2 = loader2.loadClass("classloader.Test01");
    System.out.println("class:" + clazz2.hashCode());

    Object object2 = clazz2.newInstance();
    System.out.println(object2.getClass().getClassLoader());
}

運行結果

class:325040804
classloader.ClassLoaderTest@7f31245a
class:325040804
classloader.ClassLoaderTest@7f31245a

擴展:命名空間

  • 每個類加載器都有自己的命名空間,命名空間由該加載器及所有的父加載器所加載的類組成
  • 在同一命名空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
  • 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

Spring框架學習總結(上)

目錄

@

1、Spring的概述

在學習SSM框架中,我建議初學者最好先學Spring框架,其次mybatis接着springMVC,先學mybatis當然也是可以的,今天我們就以絕對優雅的姿態闖進Spring世界,系好安全帶,準備好了嗎,出發了哦!!!咳咳….平時開發接觸最多的估計就是IOC容器,它可以裝載bean(所謂的bean也就是我們java中的類,當然也包括servicedao裏面),有了這個機制,我們就不用在每次使用這個類的時候為它初始化,很少看到鍵字new。另外spring的aop,事務管理等等都是我們經常用到的,可見Spring的尤為重要的作用Spring的核心是控制反轉(IoC)和面向切面(AOP)

1.1什麼是Spring

肯定有熊dei會問SE/EE開發的一站式框架所謂的一站式是什麼意思,(哼,人類,我早就猜到你會問了)
所謂一站式框架指的是有EE開發的每一層解決方案。

WEB層 :SpringMVC

Service層 :Spring的Bean管理,Spring聲明式事務

DAO層 :Spring的Jdbc模板,Spring的ORM模塊

1.2為什麼學習Spring

俗話說,人狠話不多(兄嘚看圖)

1.3Spring的版本

Spring3.x、Spring4.x和Spring5.x

1.4Spring的體繫結構

正所謂,人狠話不多(兄嘚看圖)

2、Spring的入門(IOC)

2.1什麼IOC

一說起IOC我就想起了武哥對IOC的理解的幾個例子,可謂通俗易懂,非常適合剛入門Spring的兄嘚!有興趣的可以去了解了解武哥,武哥博客:https://blog.csdn.net/eson_15

IOC(Inverse of Control):控制反轉,也可以稱為依賴倒置。
控制反轉:將對象的創建權反轉給(交給)Spring。

所謂依賴,從程序的角度看,就是比如A要調用B的方法,那麼A就依賴於B,反正A要用到B,則A依

賴於B。所謂倒置,你必須理解如果不倒置,會怎麼著,因為A必須要有B,才可以調用B,如果不倒

置,意思就是A主動獲取B的實例:B b = new B(),這就是最簡單的獲取B實例的方法(當然還有各種

設計模式可以幫助你去獲得B的實例,比如工廠、Locator等等),然後你就可以調用b對象了。所

以,不倒置,意味着A要主動獲取B,才能使用B;到了這裏,就應該明白了倒置的意思了。倒置就是

A要調用B的話,A並不需要主動獲取B,而是由其它人自動將B送上門來。

2.2通俗理解IOC

形象的舉例就是:
通常情況下,假如你有一天在家裡口渴了,要喝水,那麼你可以到你小區的小賣部去,告訴他們,你需要一瓶水,然後小賣部給你一瓶水!這本來沒有太大問題,關鍵是如果小賣部很遠,那麼你必須知道:從你家如何到小賣部;小賣部里是否有你需要的水;你還要考慮是否開着車去;等等等等,也許有太多的問題要考慮了。也就是說,為了一瓶水,你還可能需要依賴於車等等這些交通工具或別的工具,問題是不是變得複雜了?那麼如何解決這個問題呢?
解決這個問題的方法很簡單:小賣部提供送貨上門服務,凡是小賣部的會員,你只要告知小賣部你需要什麼,小賣部將主動把貨物給你送上門來!這樣一來,你只需要做兩件事情,你就可以活得更加輕鬆自在:
第一:向小賣部註冊為會員。
第二:告訴小賣部你需要什麼。

這和Spring的做法很類似!Spring就是小賣部,你就是A對象,水就是B對象
第一:在Spring中聲明一個類:A
第二:告訴Spring,A需要B

假設A是UserAction類,而B是UserService類

<bean id="userService" class="org.leadfar.service.UserService"/>
<bean id="documentService" class="org.leadfar.service.DocumentService"/>
<bean id="orgService" class="org.leadfar.service.OrgService"/>
 
<bean id="userAction" class="org.leadfar.web.UserAction">
     <property name="userService" ref="userService"/>
</bean>

在Spring這個商店(工廠)中,有很多對象/服務:userService,documentService,orgService,也有很多會員:userAction等等,聲明userAction需要userService即可,Spring將通過你給它提供的通道主動把userService送上門來,因此UserAction的代碼示例類似如下所示:

package org.leadfar.web;
public class UserAction{
     private UserService userService;
     public String login(){
          userService.valifyUser(xxx);
     }
     public void setUserService(UserService userService){
          this.userService = userService;
     }
}

在這段代碼裏面,你無需自己創建UserService對象(Spring作為背後無形的手,把UserService對象通過你定義的setUserService()方法把它主動送給了你,這就叫依賴注入!),當然咯,我們也可以使用註解來注入。Spring依賴注入的實現技術是:動態代理

2.3下載Spring的開發包以及解壓說明

官網下載:http://spring.io/
什麼?不會下載?what???
好吧,已打包好了QAQ:https://pan.baidu.com/s/18wyE-5SRWcCu12iPOX56pg
什麼?沒有網盤?what???
有事請燒香謝謝…

解壓之後,文件說明:
docs :Spring的開發規範和API
libs :Spring的開發的jar和源碼
schema :Spring的配置文件的約束

2.4創建web項目,引入jar包

2.5創建普通接口和實現類

創建普通接口,定義一個eat方法

package com.gx.sping;

public interface IUserDao {

    public void eat();
}

創建普通實現類

package com.gx.sping;

public class UserDaoimpl implements IUserDao {
  @Override
    public void eat() {
        // TODO Auto-generated method stub
        System.out.println(用戶eat了");
    }
}

2.6Spring的IOC底層實現原理

創建普通接口和類出現的問題:
如果底層的實現切換了,需要修改源代碼,能不能不修改程序源代碼對程序進行擴展?
重點來了,要想不改變源碼,Spring的IOC就能實現!如下圖:Spring的IOC底層實現

2.7將實現類交給Spring管理

1、在classpath下(也就是src)創建一個XML文件

2、文件名最好統一叫applicationContext.xml

3、其xml文件的內容頭為schema約束

4、約束文件位置在spring的解壓路徑下lspring-framework-4.2.4.RELEASE\docs\spring-framework-reference\html\xsd-configuration.htm

5、不要求xml文件的內容頭能夠背出來,但要了解的是你要知道它是怎麼來的

6、xml文件的內容頭添加后,將實現類交給Spring管理

applicationContext.xml配置文件如下

<?xml version="1.0" encoding="UTF-8"?>
<beans 
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd">

 <!-- 實現類UserDaoimpl交給Spring管理 -->
     <bean id="IuserDao" class="com.gx.Ioc.UserDaoimpl" ></bean>
</beans>

2.8編寫測試類

package com.gx.Ioc;

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpingDemo1 {
    @Test
    public void demo11() {
        // 面向接口傳統方式
        UserDaoimpl userdao = new UserDaoimpl();
        userdao.eat();
    }
       //Spring的bean管理方式
    @Test
    public void demo22() {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        IUserDao userdao = (IUserDao) applicationContext.getBean("IuserDao");
        userdao.eat();

    }

}

兄嘚,如果測試不成功最好看看二者是否對應!!!

兄dei,到這裏,Spring的入門(IOC)算是入門了,是不是覺得很有成就感啊?

拉倒吧! 我都不好意思說了.(兄dei,我錯了,是我飄了,呀呀呀,兄dei別打臉鴨QAQ)

但是我依舊是阻止不了你驕傲的心.

那就頂我,讓我感受感受你的驕傲!哈哈哈QAQ

2.9 IOC和DI

IOC不是什麼技術,而是一種設計思想,IOC能指導我們如何設計出松耦合、更優良的程序。傳統應用程序都是由我們在類內部主動創建依賴對象,從而導致類與類之間高耦合,難於測試;有了IoC容器后,把創建和查找依賴對象的控制權交給了Spring容器,由容器進行注入組合對象,所以對象與對象之間是鬆散耦合,這樣利於功能復用,更重要的是使得程序的整個體繫結構變得非常靈活。
IOC:控制反轉,將對象的創建權反轉給了Spring。
DI:依賴注入,前提必須有IOC的環境,Spring管理這個類的時候將類的依賴的屬性注入(設置)進來。比如說下面講到的Spring的屬性注入其實就是典型的DI

所謂繼承:is a

Class A{

}
Class B extends A{

}

所謂依賴:

Class A{

}
Class B{
    public void xxx(A a){

}
}

所謂聚合:has a

3、Spring的工廠類

3.1Spring工廠類的結構

3.2老版本的工廠類:BeanFactory

BeanFactory:調用getBean的時候,才會生成類的實例。

3.3新版本的工廠類:ApplicationContext

ApplicationContext:加載配置文件的時候,就會將Spring管理的類都實例化
ApplicationContext有兩個實現類
1、ClassPathXmlApplicationContext :加載類路徑下的配置文件
2、FileSystemXmlApplicationContext :加載文件系統下的配置文件

4、Spring的配置

4.1XML的提示配置(Schema的配置)

在XML文件中要使用各種標籤來給spring進行配置,博主我這佩奇腦袋怎麼可能記住spring中所有的標籤呢,不怕不怕,博主我會配置XML的提示配置QAQ,會了這一招就算兄dei你是喬治腦袋也不用擔心(再說了我看兄dei各各英俊瀟洒,玉樹臨風,聰明絕頂…咳咳,暴露了暴露了)

4.2Bean的相關的配置(< bean >標籤的id和name的配置)

id :使用了約束中的唯一約束。裏面不能出現特殊字符的。上面提及到了要與getbean參數值對應

name :沒有使用約束中的唯一約束(理論上可以出現重複的,但是實際開發不能出現的)。裏面可以出現特殊字符。

4.3Bean的生命周期的配置(了解)

init-method :Bean被初始化的時候執行的方法
destroy-method :Bean被銷毀的時候執行的方法(Bean是單例創建,工廠關閉)

4.4Bean的作用範圍的配置(重點)

scope屬性Bean的作用範圍

scope屬性值如下(主要用的是前二者)
singletonscope屬性的默認值,Spring會採用單例模式創建這個對象。
prototype多例模式。(Struts2和Spring整合一定會用到)
request :應用在web項目中,Spring創建這個類以後,將這個類存入到request範圍中。
session :應用在web項目中,Spring創建這個類以後,將這個類存入到session範圍中。
globalsession :應用在web項目中,必須在porlet環境下使用。但是如果沒有這種環境,相對於session。

5、Spring的屬性注入

首先,創建幾個普通類

com.gx.spring.demo.Car
public class Car {
    private String name;
    private Double price;
    
    public Car(String name, Double price) {
        super();
        this.name = name;
        this.price = price;
    }
    @Override
    public String toString() {
        return "Car [name=" + name + ", price=" + price + "]";
    }
}
com.gx.spring.demo.Car2
/**
 * 用作set方法的屬性注入類
 */
public class Car2 {
    private String name;
    private Double price;
    public void setName(String name) {
        this.name = name;
    }
    public void setPrice(Double price) {
        this.price = price;
    }
    @Override
    public String toString() {
        return "Car2 [name=" + name + ", price=" + price + "]";
    }
    
}
com.gx.spring.demo.Person 
/**
 * 用作set方法的對象屬性注入類
 */
public class Person {
    private String name;
    private Car2 car2;
    public void setName(String name) {
        this.name = name;
    }
    public void setCar2(Car2 car2) {
        this.car2 = car2;
    }
    @Override
    public String toString() {
        return "Employee [name=" + name + ", car2=" + car2 + "]";
    }
}

5.1構造方法的方式的屬性注入

構造方法的屬性注入
constructor-arg 標籤用於配置構造方法的屬性注入
name :參數的名稱
value:設置普通數據
ref:引用數據,一般是另一個bean id值

當然了,構造方法的方式的屬性注入也支持對象屬性的注入,標籤中對應屬性也是ref
如果只有一個有參數的構造方法並且參數類型與注入的bean類型匹配,那就會注入到該構造方法中

applicationContext.xml中配置:

<!-- 構造方法的方式 -->
    <bean id="car" class="com.gx.spring.demo.Car">
        <constructor-arg name="name" value="瑪莎拉蒂"/>
        <constructor-arg name="price" value="800000"/>
    </bean>

測試方法:

    /**
     * 構造方法方式的普通屬性注入方法
     */
    public void demo1(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Car car = (Car) applicationContext.getBean("car");
        System.out.println(car);
    }

5.2Set方法的方式的屬性注入【開發常用】

Set方法的普通屬性注入
property 標籤用於配置Set方法的屬性注入
name :參數的名稱
value:設置普通數據
ref:引用數據,一般是另一個bean id值

applicationContext.xml中配置:

<!-- set方法的方式 -->
<bean id="car2" class="com.gx.spring.demo.Car2">
        <property name="name" value="法拉利黃金跑車"/>
        <property name="price" value="10000000"/>
</bean> 

測試方法:

@Test
    /**
     * set方法方式的屬性注入
     */
    public void demo2(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Car2 car2 = (Car2) applicationContext.getBean("car2");
        System.out.println(car2);
    }

Set方法設置對象類型的屬性
applicationContext.xml中配置:

<!-- set方法注入對象類型的屬性 -->
<bean id="Person" class="com.gx.spring.demo.Person">
            <!-- value:設置普通類型的值,ref:設置其他的類的id或name-->
        <property name="name" value="濤哥"/>
        <property name="car2" ref="car2"/>
    </bean> 

測試方法:

@Test
    /**
     * set方法注入對象類型
     */
    public void demo3(){
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
        Person person= (Person) applicationContext.getBean("Person");
        System.out.println(person);
    }

5.3註解的方式屬性注入【開發常用】

@Component (作用在類上通用:組件)
@Component(“userService”)相當於< bean id=”userService” class=”…”>

衍生:
@Controller Web層
@Service 業務層
@Repository 持久層
這三個註解是為了讓標註類本身的用途清晰

屬性注入的註解 ( 可以沒有set方法
普通類型屬性:@Value

對象類型屬性:@Resource對應bean中的id)= @Autowired(類型)+ @Qualifier(名稱)

5.3.1註解的理解

額,初學框架,註解二字可能對於大部分熊dei來說,太過於陌生,註解其實就是在一個類、方法、屬性上,使用@註解名稱,就比如是我們最熟悉的接實現口中的方法默認會有一個 @Override (熊dei,這樣理解能接受?)

5.3.2註解的jar包導入

Spring3.x註解的jar包
在Spring3.x的版本中,使用註解開發,只需要 spring核心基礎四包外 + log4j包 + 1個依賴包 即可

Spring4.x註解的jar包
然而在Spring4.x版本之後則需在 再添加一個要引入 spring-aop 的 jar 包,因為,spring4.x版本中一些註解的依賴方法封裝在spring-aop 的 jar 包中

5.3.3引入註解的context約束

所謂約束就是就是就是約束啦(搽汗),其中bean約束是最基本的約束!(下圖也可以看出)

引入約束:(引入 context 的約束):

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans 
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context 
http://www.springframework.org/schema/context/spring-context.xsd">
</beans>
5.3.4編寫相關的類
public interface UserDao {
public void sayHello();
}
public class UserDaoImpl implements UserDao {
@Override
public void sayHello() {
System.out.println("Hello Spring...");
} }
5.3.5配置註解掃描

Spring的註解開發:組件掃描(不使用類上註解的時候可以不用組件掃描
使用註解方式,需要開啟組件掃描< context:component-scan base-package=直接包名或者包名.類名/>,當然開發中一般都是base-package=包名,畢竟這樣可以掃描整個包,方便開發
Spring 的註解開發:組件掃描(類上註解: 可以直接使用屬性注入的註解)

<!-- Spring 的註解開發:組件掃描(類上註解: 可以直接使用屬性注入的註解) -->
<context:component-scan base-package="com.gx.spring.demo1"/>
5.3.6 在相關的類上添加註解

1、使用類上註解方式@Component(value=“userDao”),相當於< bean id=”userDao class=”com.gx.類名”>< /bean>
當然value屬性名可以省去直接@Component(”userDao”),當然@Component(“value值任意寫建議取的要有意義”)
2、註解方式可以沒有set方法

@Component(value="userDao")  //相當於配置了<bean id="userDao" class="com.gx.UserDaoImpl "></bean>
public class UserDaoImpl implements UserDao {
@Override
public void sayHello() {
System.out.println("Hello Spring Annotation...");
} }
5.3.7 編寫測試類
@Test
public void demo3() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) applicationContext.getBean("userDao");
userDao.sayHello();
}

5.4P名稱空間的屬性注入(Spring2.5以後)

通過引入p名稱空間完成屬性的注入:
寫法:
普通屬性 p:屬性名=”值”
對象屬性 p:屬性名-ref=”值”

P名稱空間的約束引入

使用p名稱空間

5.5 SpEL的屬性注入(Spring3.0以後)

SpEL:Spring Expression Language,Spring的表達式語言。
語法: #{SpEL}

5.6集合類型屬性注入(了解)

集合類型屬性配置:
集合的注入都是在< property>標籤中添加子標籤
數組:< array >
List:< list >
Set:< set >
Map:< map > ,map存放k/v 鍵值對,使用 描述
Properties:< props> < prop key=””>< /prop>
普通數據:< value >
引用數據:< ref >

    <!-- Spring的集合屬性的注入============================ -->
    <!-- 注入數組類型 -->
    <bean id="collectionBean" class="com.gx.spring.demo.CollectionBean">
        <!-- 數組類型 -->
        <property name="arrs">
            <list>
                <value>天才</value>
                <value>王二</value>
                <value>冠希</value>
            </list>
        </property>
        
        <!-- 注入list集合 -->
        <property name="list">
            <list>
                <value>李兵</value>
                <value>趙如何</value>
                <value>鄧鳳</value>
            </list>
        </property>
        
        <!-- 注入set集合 -->
        <property name="set">
            <set>
                <value>aaa</value>
                <value>bbb</value>
                <value>ccc</value>
            </set>
        </property>
        
        <!-- 注入Map集合 -->
        <property name="map">
            <map>
                <entry key="aaa" value="111"/>
                <entry key="bbb" value="222"/>
                <entry key="ccc" value="333"/>
            </map>
        </property>
    </bean>

6、Spring的分模塊開發的配置

分模塊配置:
在加載配置文件的時候,加載多個,沒錯,這就是傳說中的騷操作,堪稱開掛級別的操作(當然,這是可以的不是開掛)

在一個配置文件中引入多個配置文件,簡直優秀!!!

到這裏,恭喜恭喜,各位熊dei以優雅的儀式感闖進Spring世界,對Spring的IOC以及DI有了一定了解了,是不是也很期待Spring的Aop吶,畢竟Spring的核心是控制反轉(IOC)和面向切面(AOP)。

(哎哎,別打..別打..別打臉….)

如果本文對你有一點點幫助,就請點個讚唄,手留余香,謝謝!

最後,歡迎各位關注我的公眾號,一起探討技術,嚮往技術,追求技術…

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

分類
發燒車訊

Spring Boot2 系列教程(二十六)Spring Boot 整合 Redis

在 Redis 出現之前,我們的緩存框架各種各樣,有了 Redis ,緩存方案基本上都統一了,關於 Redis,松哥之前有一個系列教程,尚不了解 Redis 的小夥伴可以參考這個教程:

使用 Java 操作 Redis 的方案很多,Jedis 是目前較為流行的一種方案,除了 Jedis ,還有很多其他解決方案,如下:

除了這些方案之外,還有一個使用也相當多的方案,就是 Spring Data Redis。

在傳統的 SSM 中,需要開發者自己來配置 Spring Data Redis ,這個配置比較繁瑣,主要配置 3 個東西:連接池、連接器信息以及 key 和 value 的序列化方案。

在 Spring Boot 中,默認集成的 Redis 就是 Spring Data Redis,默認底層的連接池使用了 lettuce ,開發者可以自行修改為自己的熟悉的,例如 Jedis。

Spring Data Redis 針對 Redis 提供了非常方便的操作模板 RedisTemplate 。這是 Spring Data 擅長的事情,那麼接下來我們就來看看 Spring Boot 中 Spring Data Redis 的具體用法。

方案一:Spring Data Redis

創建工程

創建工程,引入 Redis 依賴:

創建成功后,還需要手動引入 commos-pool2 的依賴,因此最終完整的 pom.xml 依賴如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
</dependencies>

這裏主要就是引入了 Spring Data Redis + 連接池。

配置 Redis 信息

接下來配置 Redis 的信息,信息包含兩方面,一方面是 Redis 的基本信息,另一方面則是連接池信息:

spring.redis.database=0
spring.redis.password=123
spring.redis.port=6379
spring.redis.host=192.168.66.128
spring.redis.lettuce.pool.min-idle=5
spring.redis.lettuce.pool.max-idle=10
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=1ms
spring.redis.lettuce.shutdown-timeout=100ms

自動配置

當開發者在項目中引入了 Spring Data Redis ,並且配置了 Redis 的基本信息,此時,自動化配置就會生效。

我們從 Spring Boot 中 Redis 的自動化配置類中就可以看出端倪:

@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(
                    RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
            RedisTemplate<Object, Object> template = new RedisTemplate<>();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
    }
    @Bean
    @ConditionalOnMissingBean
    public StringRedisTemplate stringRedisTemplate(
                    RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(redisConnectionFactory);
            return template;
    }
}

這個自動化配置類很好理解:

  1. 首先標記這個是一個配置類,同時該配置在 RedisOperations 存在的情況下才會生效(即項目中引入了 Spring Data Redis)
  2. 然後導入在 application.properties 中配置的屬性
  3. 然後再導入連接池信息(如果存在的話)
  4. 最後,提供了兩個 Bean ,RedisTemplate 和 StringRedisTemplate ,其中 StringRedisTemplate 是 RedisTemplate 的子類,兩個的方法基本一致,不同之處主要體現在操作的數據類型不同,RedisTemplate 中的兩個泛型都是 Object ,意味者存儲的 key 和 value 都可以是一個對象,而 StringRedisTemplate 的 兩個泛型都是 String ,意味者 StringRedisTemplate 的 key 和 value 都只能是字符串。如果開發者沒有提供相關的 Bean ,這兩個配置就會生效,否則不會生效。

使用

接下來,可以直接在 Service 中注入 StringRedisTemplate 或者 RedisTemplate 來使用:

@Service
public class HelloService {
    @Autowired
    RedisTemplate redisTemplate;
    public void hello() {
        ValueOperations ops = redisTemplate.opsForValue();
        ops.set("k1", "v1");
        Object k1 = ops.get("k1");
        System.out.println(k1);
    }
}

Redis 中的數據操作,大體上來說,可以分為兩種:

  1. 針對 key 的操作,相關的方法就在 RedisTemplate 中
  2. 針對具體數據類型的操作,相關的方法需要首先獲取對應的數據類型,獲取相應數據類型的操作方法是 opsForXXX

調用該方法就可以將數據存儲到 Redis 中去了,如下:

k1 前面的字符是由於使用了 RedisTemplate 導致的,RedisTemplate 對 key 進行序列化之後的結果。

RedisTemplate 中,key 默認的序列化方案是 JdkSerializationRedisSerializer 。

而在 StringRedisTemplate 中,key 默認的序列化方案是 StringRedisSerializer ,因此,如果使用 StringRedisTemplate ,默認情況下 key 前面不會有前綴。

不過開發者也可以自行修改 RedisTemplate 中的序列化方案,如下:

@Service
public class HelloService {
    @Autowired
    RedisTemplate redisTemplate;
    public void hello() {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        ValueOperations ops = redisTemplate.opsForValue();
        ops.set("k1", "v1");
        Object k1 = ops.get("k1");
        System.out.println(k1);
    }
}

當然也可以直接使用 StringRedisTemplate:

@Service
public class HelloService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    public void hello2() {
        ValueOperations ops = stringRedisTemplate.opsForValue();
        ops.set("k2", "v2");
        Object k1 = ops.get("k2");
        System.out.println(k1);
    }
}

另外需要注意 ,Spring Boot 的自動化配置,只能配置單機的 Redis ,如果是 Redis 集群,則所有的東西都需要自己手動配置,關於如何操作 Redis 集群,松哥以後再來和大家分享。

方案二:Spring Cache

通過 Spring Cache 的形式來操作 Redis,Spring Cache 統一了緩存江湖的門面,這種方案,松哥之前有過一篇專門的文章介紹,小夥伴可以移步這裏:。

方案三:回歸原始時代

第三種方案,就是直接使用 Jedis 或者 其他的客戶端工具來操作 Redis ,這種方案在 Spring Boot 中也是支持的,雖然操作麻煩,但是支持,這種操作松哥之前也有介紹的文章,因此這裏就不再贅述了,可以參考 。

總結

Spring Boot 中,Redis 的操作,這裏松哥給大家總結了三種方案,實際上前兩個使用廣泛一些,直接使用 Jedis 還是比較少,基本上 Spring Boot 中沒見過有人直接這麼搞。

好了,本文就說到這裏,有問題歡迎留言討論。

相關案例已經上傳到 GitHub,歡迎小夥伴們們下載:

掃碼關注松哥,公眾號後台回復 2TB,獲取松哥獨家 超2TB 免費 Java 學習乾貨

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

PL真有意思(一):引言

前言

斷斷續續學編譯原理到之前發過寫一個編譯器和正則表達式引擎系列文章也有一段時間了,然後最近看完PLP這本書,這本書應該算是入門書,但是對我這種半吊子收穫很大。所以為了彌補最近學操作系統和接外包摸的魚,就想寫寫看完這本書的收穫。(為拙劣的標題道歉

程序設計語言的譜系

現在的新語言都是一撮一撮的出來,但是基本都可以用他們的計算模型來分成兩類,一類是更關心計算機做什麼的說明式,一類是更關心計算機怎麼做的命令式

一般認為像函數式邏輯式語言都算是說明式,而馮諾依曼式和面向對象的都被認為是命令式

函數式

函數式是基於函數的遞歸定義的計算模型,一般從本質上來看,函數式把程序認為是一種從輸入到輸出的函數,使用更簡單的函數通過逐步細化的過程來定義

邏輯式

邏輯式里經典的應該就是Prolog了,邏輯式一般將計算看作是一種找出滿足某些特定關係的值的嘗試過程

馮諾依曼式

馮諾依曼式最重要的就是通過副作用也就是修改寄存器里的值來對後續計算產生影響,像C和Fortran都屬於馮諾依曼式

幾個例子

如果C語言來實現求最大公約數,可以發現C語言偏向通過迭代和反覆修改變量的值來實現

int gcd(int a, intb) {
    while (a != b) {
      if (a > b) {
        a = a - b;
      } else {
        b = b - a;
      }
    }
}

從lisp來看,則更加關注輸入和輸出的數學關係,要算出最大公約數,需要對函數的不斷的擴充和精簡

(define gcd
  (lambda (a b)
    (cond ((= a b) a)
          ((> a b) (gcd (- a b) b))
          (else (gcd (- b a) a)))))

對於像C或者Java入門的人,看到Prolog可能頭都大了,因為Prolog和命令式的思考邏輯完全不同,邏輯式更傾向給出一組公理和檢測規則,期望系統能給出相應合理的值,我也僅限於能看懂這些小程序

gcd(A,B,G) :- A = B, G = A.
gcd(A,B,G) :- A > B, C is A - B, gcd(C,B,G)
gcd(A,B,G) :- B > A, C is B-A
gcd(C,A,G)

編譯和解釋

下面再看兩個概念,編譯和解釋。

編譯一般是指從一個語言到另一個語言的翻譯,無論是高級語言到彙編還是高級語言到高級語言都算是編譯。而解釋就是直接執行代碼,但是現代的解釋器,一般還有虛擬機一層,即翻譯到虛擬機指令再由虛擬機進行執行

自舉

很多語言的編譯器都是由自己編譯而成的,所以問題就是,最開始的編譯器是怎麼編譯的?

假設現在要為Java編寫一個編譯器,我們可以先用C語言編寫一個Java小子集的編譯器,然後再手動將C語言翻譯到這個Java子集,就可以由這個子集編譯運行自己,然後就可以不斷擴充這個編譯器

編譯概覽

其實這個在之前那個寫編譯器的系列是說得最詳細的,這個系列是想要寫寫筆記對實踐和語言設計結合的說。

  • 詞法分析

有關詞法分析其實就是將字符流化成單詞流,記錄每個單詞的信息,然後通常還會有其它更多的信息讓編譯器更好的生成錯誤信息

  • 語法分析

語法分析通常會用到一個概念:上下文無關文法,就是用來定義語法形式的,比如while語句就可以這樣表示

while-statment := WHILE ( expr ) statment

一般語法分析過程最後的輸出都是樹狀結構

  • 語義分析和中間代碼生成

語法分析只保證源代碼語法格式的正確,但是卻不能保證其它的正確性,比如對於靜態類型的語言,可能就會在編譯時直接檢測出類型錯誤

而在語義分析過程一般還需要藉助一個叫做符號表的數據結構,這個符號表將每個標識符都映射到關於它的已知信息

中間代碼的生成通常是會將樹形結構翻譯成更接近彙編的中間線性格式,但是中間格式不是必須的,比如之前那個系列里還寫了C的解釋器,雖然很殘缺,它是沒有中間代碼的,連虛擬機都沒有,是直接進行遍歷語法樹進行執行解釋的

  • 代碼優化

有些代碼改進是機器無關的,即可以在中間格式上就進行優化,但是有的代碼優化是平台相關的,所以就需要在最後對目標語言優化

  • 目標代碼生成

代碼生成階段就是根據之前生成的線性結構和符號表信息對目標代碼的生成

小結

這一篇主要說了幾個範式的語言和編譯過程的概括,對摸魚進行懺悔

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理【其他文章推薦】

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

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

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

分類
發燒車訊

電動車環島不是夢,特斯拉7月將於台灣完成250座充電站

一台特斯拉隨隨便便就要兩、三百萬元台幣的價碼,讓不少喜愛電動車有效率、環保特性的人無法輕易擁有。特斯拉宣布他們跟台灣本地銀行提供的優惠方案,最低每月負擔36,000 元台幣,就可以將特斯拉車子開回家。另外特斯拉的超級充電站也將遍布全台,開著特斯拉環島將不是夢想。

在上週五(6/16)特斯拉Model X 交車派對上,特斯拉香港、澳門及台灣地區區域總監范菁怡表示,在台灣由星展銀行提供的優惠方案,每月最少花36,000 元,就可以開著特斯拉回家。或車主開了3 年之後,除了繼續繳每月的費用,也可以選擇回購專案,將車子賣回給特斯拉。特斯拉也有與車行合作模式,可採租賃模式駕駛特斯拉車。以上幾項不同的方案,供車主依自己的需求選擇最適合的方案。

特斯拉表示,到了7 月,開特斯拉車環島也不會是問題。台灣中部、南部的台南、高雄都將在高速公路交流道一帶設置超級充電站,而東部也會有超級充電站。未來開著特斯拉享受環島行程不再是夢想。Tesla 於今年第二季末正式在台完成超過250 座目的地充電站,分布於全台80 個地點。

▲ 在派對上交車的車輛,5~7 人座都有。

另外不少車主期待的自動輔助駕駛2.0 版與導航功能也將在台啟用,為全世界第一批啟用國家之一。不論是自動輔助轉向、跟車、防撞預警、自動換導、自動停車或是召換功能,都能在台灣使用。未來Model S 或Model X 開在台灣的道路上,透過OTA 機制更新,能夠累積台灣路況,提供最好的駕駛體驗。

▲ 特斯拉與車行合作,提供駕駛特斯拉車的不同方式。

儘管在交車派對上車主是主角,但特斯拉宣布了不少優惠專案。對車主來說,可在一個月內免費申請安裝家用充電設備,另外還有機會到SpaceX 參觀!

 

▲ 特斯拉在台灣與星展銀行合作,推出特斯拉優惠方案。

(合作媒體:。圖片出處:科技新報)

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

【其他文章推薦】

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

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

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

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

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

分類
發燒車訊

美國原油均價下跌,市場憂產油過剩

美國能源資訊局(EIA)公佈,6月26日全美普通汽油平均零售價格為每加侖2.288美元,創下半年新低;較前週下跌3美分,較去年同期下跌4.1美分。各地區零售汽油均價全面下跌,西岸地區的零售汽油均價最高達每加侖2.826美元,較前週下跌3.4美分;墨西哥灣地區的零售汽油均價最低為每加侖2.053美元,較前週下跌3.8美分。NYMEX原油期貨上週下跌3.9%,因擔憂油市過剩以及美國產量持續增長的影響。

美國汽車協會(AAA)報告表示,6月26日全美普通無鉛汽油平均零售價格為每加侖2.26美元,較前週下跌3美分,較一個月前下跌11美分,較去年同期下跌4美分。AAA表示,包括美國煉油廠原油加工量處在新高水平、汽油以及原油庫存高企,以及今年以來的需求表現較為疲弱等,都是造成零售汽油均價下跌的主因。美國汽油需求已經有所回升,6月16日當週,美國汽油日均需求較前週926.9萬桶增至981.6萬桶,逼近5月底創下新高的982.2萬桶。

AAA表示,即將到來的美國獨立紀念日假期(7月4日),預計將有創同期新高的4,420萬人出遊(離家超過50英里),比去年還要增加125萬人或2.9%。其中,預計將有3,750萬人開車出遊,同樣較去年同期增加2.9%。AAA資深副總裁Bill Sutherland表示,就業市場強勁、薪資增加以及消費信心提高等,都是今年出遊人數將創下歷年同期新高的主要原因。AAA表示,當前美國零售汽油均價逼近歷年的同期新低,但鑑於下週的假期來臨,零售汽油均價可能會有小幅上漲。

《Oilprice.com》報導,相比十年前在油田自然衰竭的影響下,市場認為全球的產油上限即將到來;如今市場更多的是認為石油消費的巔峰將會到來,主要因為電動車興起的影響。一份調查顯示,如果電動車的年增長率維持在60%,則2023年的全球石油日需求量將會比當前減少200萬桶;如果年增長率為30%,則2025年的全球石油日需求量會比當前減少200萬桶。

不過,實際數據顯示,至少目前為止石油需求增長並未下滑;過去十年全球石油日需求量年均增長110萬桶,過去五年則年均增長140萬桶,2016年則增長160萬桶,而去年的電動車銷售增長41%。報導認為,包括人口以及中產階級增長,以及開發中國家汽車銷售持續增加等,都是令全球石油需求仍持續攀高的主因。

(本文內容由授權使用。圖片出處:wikipedia)

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

【其他文章推薦】

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

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

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