面試:在面試中關於List(ArrayList、LinkedList)集合會怎麼問呢?你該如何回答呢?

前言

在一開始基礎面的時候,很多面試官可能會問List集合一些基礎知識,比如:

  • ArrayList默認大小是多少,是如何擴容的?

  • ArrayList和LinkedList的底層數據結構是什麼?

  • ArrayList和LinkedList的區別?分別用在什麼場景?

  • 為什麼說ArrayList查詢快而增刪慢?

  • Arrays.asList方法后的List可以擴容嗎?

  • modCount在非線程安全集合中的作用?

  • ArrayList和LinkedList的區別、優缺點以及應用場景

 

ArrayList(1.8)

ArrayList是由動態再分配的Object[]數組作為底層結構,可設置null值,是非線程安全的。

ArrayList成員屬性

//默認的空的數組,在構造方法初始化一個空數組的時候使用
private static final Object[] EMPTY_ELEMENTDATA = {};
​
//使用默認size大小的空數組實例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
​
//ArrayList底層存儲數據就是通過數組的形式,ArrayList長度就是數組的長度。
transient Object[] elementData; 
​
//arrayList的大小
private int size;

那麼ArrayList底層數據結構是什麼呢?

很明顯,使用動態再分配的Object[]數組作為ArrayList底層數據結構了,既然是使用數組實現的,那麼數組特點就能說明為什麼ArrayList查詢快而增刪慢?

因為數組是根據下標查詢不需要比較,查詢方式為:首地址+(元素長度*下標),基於這個位置讀取相應的字節數就可以了,所以非常快;但是增刪會帶來元素的移動,增加數據會向後移動,刪除數據會向前移動,導致其效率比較低。

 

ArrayList的構造方法

  • 帶有初始化容量的構造方法

  • 無參構造方法

  • 參數為Collection類型的構造器

//帶有初始化容量的構造方法
public ArrayList(int initialCapacity) {
    //參數大於0,elementData初始化為initialCapacity大小的數組
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    //參數小於0,elementData初始化為空數組
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    //參數小於0,拋出異常
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}
​
//無參構造方法
public ArrayList() {
    //在1.7以後的版本,先構造方法中將elementData初始化為空數組DEFAULTCAPACITY_EMPTY_ELEMENTDATA
    //當調用add方法添加第一個元素的時候,會進行擴容,擴容至大小為DEFAULT_CAPACITY=10
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

那麼ArrayList默認大小是多少?

從無參構造方法中可以看出,一開始默認為一個空的實例elementData為上面的DEFAULTCAPACITY_EMPTY_ELEMENTDATA,當添加第一個元素的時候會進行擴容,擴容大小就是上面的默認容量DEFAULT_CAPACITY為10

ArrayList的Add方法

  • boolean add(E):默認直接在末尾添加元素

  • void add(int,E):在特定位置添加元素,也就是插入元素

  • boolean addAll(Collection<? extends E> c):添加集合

  • boolean addAll(int index, Collection<? extends E> c):在指定位置后添加集合

boolean add(E)
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

通過ensureCapacityInternal方法為確定容量大小方法。在添加元素之前需要確定數組是否能容納下,size是數組中元素個數,添加一個元素size+1。然後再數組末尾添加元素。

其中,ensureCapacityInternal方法包含了ArrayList擴容機制grow方法,當前容量無法容納下數據時1.5倍擴容,進行:

private void ensureCapacityInternal(int minCapacity) {
    //判斷當前的數組是否為默認設置的空數據,是否取出最小容量
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //包括擴容機制grow方法
    ensureExplicitCapacity(minCapacity);
}
​
private void ensureExplicitCapacity(int minCapacity) {
        //記錄著集合的修改次數,也就每次add或者remove它的值都會加1
        modCount++;
​
        //當前容量容納不下數據時(下標超過時),ArrayList擴容機制:擴容原來的1.5倍
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
​
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        //ArrayList擴容機制:擴容原來的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

ArrayList是如何擴容的?

根據當前的容量容納不下新增數據時,ArrayList會調用grow進行擴容:

//相當於int newCapacity = oldCapacity + oldCapacity/2
int newCapacity = oldCapacity + (oldCapacity >> 1);

擴容原來的1.5倍。

void add(int,E)
public void add(int index, E element) {
    //檢查index也就是插入的位置是否合理,是否存在數組越界
    rangeCheckForAdd(index);
    //機制和boolean add(E)方法一樣
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

ArrayList的刪除方法

  • remove(int):通過刪除指定位置上的元素,

  • remove(Object):根據元素進行刪除,

  • clear():將elementData中每個元素都賦值為null,等待垃圾回收將這個給回收掉,

  • removeAll(collection c):批量刪除。

 

remove(int)
public E remove(int index) {
    //檢查下標是否超出數組長度,造成數組越界
    rangeCheck(index);
​
    modCount++;
    E oldValue = elementData(index);
    //算出數組需要移動的元素數量
    int numMoved = size - index - 1;
    if (numMoved > 0)
        //數組數據遷移,這樣會導致刪除數據時,效率會慢
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    //將--size上的位置賦值為null,讓gc(垃圾回收機制)更快的回收它。
    elementData[--size] = null; // clear to let GC do its work
    //返回刪除的元素
    return oldValue;
}

為什麼說ArrayList刪除元素效率低?

因為刪除數據需要將數據後面的元素數據遷移到新增位置的後面,這樣導致性能下降很多,效率低。

remove(Object)
public boolean remove(Object o) {
    //如果需要刪除數據為null時,會讓數據重新排序,將null數據遷移到數組尾端
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                //刪除數據,並遷移數據
                fastRemove(index);
                return true;
            }
    } else {
        //循環刪除數組中object對象的值,也需要數據遷移
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}

可以看出,arrayList是可以存放null值。

 

LinkedList(1.8)

LinkedList是一個繼承於AbstractSequentialList的雙向鏈表。它也可以被當做堆棧、隊列或雙端隊列進行使用,而且LinkedList也為非線程安全, jdk1.6使用的是一個帶有 header節頭結點的雙向循環鏈表, 頭結點不存儲實際數據 ,在1.6之後,就變更使用兩個節點first、last指向首尾節點。

LinkedList的主要屬性

//鏈表節點的個數 
transient int size = 0; 
//鏈表首節點
 transient Node<E> first; 
//鏈表尾節點
 transient Node<E> last; 
//Node節點內部類定義
private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
​
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

一旦變量被transient修飾,變量將不再是對象持久化的一部分,該變量內容在序列化后無法獲得訪問

LinkedList構造方法

無參構造函數, 默認構造方法聲明也不做,first和last節點會被默認初始化為null。

*
/** Constructs an empty list. \*/*
​
public LinkedList() {}
​

LinkedList插入

由於LinkedList由雙向鏈表作為底層數據結構,因此其插入無非由三大種

  • 尾插: add(E e)、addLast(E e)、addAll(Collection<? extends E> c)

  • 頭插: addFirst(E e)

  • 中插: add(int index, E element)

可以從源碼看出,在鏈表首尾添加元素很高效,在中間添加元素比較低效,首先要找到插入位置的節點,在修改前後節點的指針。

 

 

尾插-add(E e)和addLast(E e)
//常用的添加元素方法
public boolean add(E e) {
    //使用尾插法
    linkLast(e);
    return true;
}
​
//在鏈表尾部添加元素
public void addLast(E e) {
        linkLast(e);
    }
​
//在鏈表尾端添加元素
void linkLast(E e) {
        //尾節點
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        //判斷是否是第一個添加的元素
        //如果是將新節點賦值給last
        //如果不是把原首節點的prev設置為新節點
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        //將集合修改次數加1
        modCount++;
    }
​
頭插-addFirst(E e)
public void addFirst(E e) {
    //在鏈表頭插入指定元素
    linkFirst(e);
}
​
private void linkFirst(E e) {
         //獲取頭部元素,首節點
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        //鏈表頭部為空,(也就是鏈表為空)
        //插入元素為首節點元素
        // 否則就更新原來的頭元素的prev為新元素的地址引用
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        //
        size++;
        modCount++;
    }
中插-add(int index, E element)

當index不為首尾的的時候,實際就在鏈表中間插入元素。

 // 作用:在指定位置添加元素
    public void add(int index, E element) {
        // 檢查插入位置的索引的合理性
        checkPositionIndex(index);
​
        if (index == size)
            // 插入的情況是尾部插入的情況:調用linkLast()。
            linkLast(element);
        else
            // 插入的情況是非尾部插入的情況(中間插入):linkBefore
            linkBefore(element, node(index));
    }
​
    private void checkPositionIndex(int index) {
        if (!isPositionIndex(index))
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
​
    private boolean isPositionIndex(int index) {
        return index >= 0 && index <= size;
    }
​
    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;  // 得到插入位置元素的前繼節點
        final Node<E> newNode = new Node<>(pred, e, succ);  // 創建新節點,其前繼節點是succ的前節點,後接點是succ節點
        succ.prev = newNode;  // 更新插入位置(succ)的前置節點為新節點
        if (pred == null)
            // 如果pred為null,說明該節點插入在頭節點之前,要重置first頭節點 
            first = newNode;
        else
            // 如果pred不為null,那麼直接將pred的後繼指針指向newNode即可
            pred.next = newNode;
        size++;
        modCount++;
    }

LinkedList 刪除

刪除和插入一樣,其實本質也是只有三大種方式,

  • 刪除首節點:removeFirst()

  • 刪除尾節點:removeLast()

  • 刪除中間節點 :remove(Object o)、remove(int index)

在首尾節點刪除很高效,刪除中間元素比較低效要先找到節點位置,再修改前後指針指引。

 

 

刪除中間節點-remove(int index)和remove(Object o)
remove(int index)和remove(Object o)都是使用刪除指定節點的unlink刪除元素

 public boolean remove(Object o) {
     //因為LinkedList允許存在null,所以需要進行null判斷        
     if (o == null) {
         //從首節點開始遍歷
         for (Node<E> x = first; x != null; x = x.next) {
             if (x.item == null) {
                 //調用unlink方法刪除指定節點
                 unlink(x);
                 return true;
             }
         }
     } else {
         for (Node<E> x = first; x != null; x = x.next) {
             if (o.equals(x.item)) {
                 unlink(x);
                 return true;
             }
         }
     }
    return false;
 } 
​
//刪除指定位置的節點,其實和上面的方法差不多
    //通過node方法獲得指定位置的節點,再通過unlink方法刪除
    public E remove(int index) {
        checkElementIndex(index);
       
        return unlink(node(index));
    }
​
 //刪除指定節點
    E unlink(Node<E> x) {
        //獲取x節點的元素,以及它上一個節點,和下一個節點
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;
        //如果x的上一個節點為null,說明是首節點,將x的下一個節點設置為新的首節點
        //否則將x的上一節點設置為next,將x的上一節點設為null
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }
        //如果x的下一節點為null,說明是尾節點,將x的上一節點設置新的尾節點
        //否則將x的上一節點設置x的上一節點,將x的下一節點設為null
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        //將x節點的元素值設為null,等待垃圾收集器收集
        x.item = null;
        //鏈表節點個數減1
        size--;
        //將集合修改次數加1
        modCount++;
        //返回刪除節點的元素值
        return element;
    }
刪除首節點-removeFirst()
//刪除首節點
public E remove() {
        return removeFirst();
    }
 //刪除首節點
 public E removeFirst() {
      final Node<E> f = first;
      //如果首節點為null,說明是空鏈表,拋出異常
      if (f == null)
          throw new NoSuchElementException();
      return unlinkFirst(f);
  }
  //刪除首節點
  private E unlinkFirst(Node<E> f) {
      //首節點的元素值
      final E element = f.item;
      //首節點的下一節點
      final Node<E> next = f.next;
      //將首節點的元素值和下一節點設為null,等待垃圾收集器收集
      f.item = null;
      f.next = null; // help GC
      //將next設置為新的首節點
      first = next;
      //如果next為null,說明說明鏈表中只有一個節點,把last也設為null
      //否則把next的上一節點設為null
      if (next == null)
          last = null;
      else
          next.prev = null;
      //鏈表節點個數減1
      size--;
      //將集合修改次數加1
      modCount++;
      //返回刪除節點的元素值
      return element;
 }
刪除尾節點-removeLast()
  
 //刪除尾節點
    public E removeLast() {
        final Node<E> l = last;
        //如果首節點為null,說明是空鏈表,拋出異常
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }
    private E unlinkLast(Node<E> l) {
        //尾節點的元素值
        final E element = l.item;
        //尾節點的上一節點
        final Node<E> prev = l.prev;
        //將尾節點的元素值和上一節點設為null,等待垃圾收集器收集
        l.item = null;
        l.prev = null; // help GC
        //將prev設置新的尾節點
        last = prev;
        //如果prev為null,說明說明鏈表中只有一個節點,把first也設為null
        //否則把prev的下一節點設為null
        if (prev == null)
            first = null;
        else
            prev.next = null;
        //鏈表節點個數減1
        size--;
        //將集合修改次數加1
        modCount++;
        //返回刪除節點的元素值
        return element;
    }

其他方法也是類似的,比如查詢方法 LinkedList提供了get、getFirst、getLast等方法獲取節點元素值。

modCount屬性的作用?

modCount屬性代表為結構性修改( 改變list的size大小、以其他方式改變他導致正在進行迭代時出現錯誤的結果)的次數,該屬性被Iterator以及ListIterator的實現類所使用,且很多非線程安全使用modCount屬性。

初始化迭代器時會給這個modCount賦值,如果在遍歷的過程中,一旦發現這個對象的modCount和迭代器存儲的modCount不一樣,Iterator或者ListIterator 將拋出ConcurrentModificationException異常,

這是jdk在面對迭代遍歷的時候為了避免不確定性而採取的 fail-fast(快速失敗)原則:

在線程不安全的集合中,如果使用迭代器的過程中,發現集合被修改,會拋出ConcurrentModificationExceptions錯誤,這就是fail-fast機制。對集合進行結構性修改時,modCount都會增加,在初始化迭代器時,modCount的值會賦給expectedModCount,在迭代的過程中,只要modCount改變了,int expectedModCount = modCount等式就不成立了,迭代器檢測到這一點,就會拋出錯誤:urrentModificationExceptions。

 

總結

ArrayList和LinkedList的區別、優缺點以及應用場景

區別:

  • ArrayList是實現了基於動態數組的數據結構,LinkedList是基於鏈表結構。

  • 對於隨機訪問的get和set方法查詢元素,ArrayList要優於LinkedList,因為LinkedList循環鏈表尋找元素。

  • 對於新增和刪除操作add和remove,LinkedList比較高效,因為ArrayList要移動數據。

優缺點:

  • 對ArrayList和LinkedList而言,在末尾增加一個元素所花的開銷都是固定的。對ArrayList而言,主要是在內部數組中增加一項,指向所添加的元素,偶爾可能會導致對數組重新進行分配;而對LinkedList而言,這個開銷是 統一的,分配一個內部Entry對象。

  • 在ArrayList集合中添加或者刪除一個元素時,當前的列表移動元素後面所有的元素都會被移動。而LinkedList集合中添加或者刪除一個元素的開銷是固定的。

  • LinkedList集合不支持 高效的隨機隨機訪問(RandomAccess),因為可能產生二次項的行為。

  • ArrayList的空間浪費主要體現在在list列表的結尾預留一定的容量空間,而LinkedList的空間花費則體現在它的每一個元素都需要消耗相當的空間

應用場景:

ArrayList使用在查詢比較多,但是插入和刪除比較少的情況,而LinkedList用在查詢比較少而插入刪除比較多的情況

各位看官還可以嗎?喜歡的話,動動手指點個,點個關注唄!!謝謝支持!

歡迎掃碼關注,原創技術文章第一時間推出

 

 

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

【其他文章推薦】

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

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

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

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

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

11_條件隨機場

  今天是2020年6月14號星期日。從這篇開始,發布時間就正常了。前邊的文章是在寒假寫好的,後來因為趕進度和改小論文,沒有及時整理。3月到6月,兩個半月的時間做了些什麼?真的好怕浪費了時間~《統計學習方法》這本書(第二版),大概在四月底就看個差不多了,半生不熟的好歹是通篇過了一遍,當然不止一遍,除了潛在狄利克雷分配。期間夾雜着手撕代碼的過程,因為腦容量有限的原因,寫代碼的時候,又要把書面內容重新過一遍。就連發布,也要重新過一遍,把重要的地方加上不同顏色…於是這個假期,《統計學習方法》這本書的每個章節,不知道反反覆復的過了多少遍,肯定不超過十遍…也就是看的多,不代表學的多…但是看了總歸是看了,也比不看強…

  CRF條件隨機場~本節說難,挺難;說簡單,也簡單。為什麼我覺得CRF和LR很像呢?可能有很多人在應用場景上反駁我,LR做數據分類問題,CRF是針對序列標註問題。我覺得像,提供一下幾個觀點(遞進着看):

  1.LR和CRF都是判別模型。這句話要從計算方式上來理解:LR是通過線性函數,對特徵向量擬合不同的權值,最後加和的線性函數值作為sigmoid函數的輸入,轉換為概率值(對數幾率)。CRF在公式(11.10)中,不,在更明顯的公式(11.15)中,wkfk(y,x)對1->K加和。個人膚淺的理解:公式(11.15)就是對各個特徵擬合不同的權值wk,加和的線性函數值作為某個“求概率公式”的輸入。

  2.從觀點1出發,思考一下“特徵”。在LR中,“特徵”很明顯,就是特徵向量的各個分量,相當於一個分量一個特徵。LR擬合的w,其實就是看看每個特徵分量對分類結果占什麼樣的貢獻。在CRF中,“特徵”是定義在最大團上的,拋去“最大團”“勢函數”“特徵函數”這些抽象概念,CRF的“特徵”其實不就是定義在相互聯繫的兩個節點上嘛?相當於“成對”構成一個特徵。轉移特徵也好,狀態特徵也罷,線性鏈條件隨機場不就限定了“特徵”要產生在“成對”的關聯關係上。然後,公式(11.15)告訴我,[權值×特徵==》加和],CRF和LR計算線性函數值的方式沒區別啊(區別在特徵的定義上)。

  3.從觀點1出發,思考一下“概率的計算”。不知道大家有沒有仔細思考LR中,第六章公式(6.6)分母位置的1是怎麼計算的,或者為什麼要在分母添加這一項。是不是可以理解為exp(0*wx)=1,這樣能夠把分母作為歸一化項Z(x)看待。分母的1+exp(wx)看作Z(x)=exp(0*wx)+exp(1*wx),P(Y=1|x)=exp(1*wx)/Z(x),另一個類別Y=0的概率P(Y=0|x)=exp(0*wx)/Z(x),怎麼樣,香不香?這裏不就是兩個項的線性函數值進行了指數歸一化。在這個角度上,看CRF的概率計算方式,公式(11.15)和公式(11.16)不就是多了幾項嘛…多個項(K項)進行了歸一化,把數值轉換成了概率值,這這這,和softmax()指數歸一化不就是一回事嘛…然後再考慮考慮為什麼CRF中勢函數是嚴格正的…

  暫時先這三點,實際上都是圍繞[判別模型]提出的。

GitHub:https://github.com/wangzycloud/statistical-learning-method

條件隨機場

引入

  書中對條件隨機場CRF的描述是:給定一組輸入隨機變量條件下另一組輸出隨機變量的條件概率分佈模型,其特點是假設輸出隨機變量構成馬爾可夫隨機場。先拋去“輸出隨機變量構成馬爾可夫隨機場”這個限定條件不看,該模型是條件概率模型,並且是一組隨機變量,在另外一組隨機變量條件下的條件概率模型。

  組這個概念,限定了模型中發生作用的因素要產生在多個隨機變量上。類似這樣,形象不(僅代表個人理解哈~不能保證正確):

  像示意圖中表示的,如果一組隨機變量之間,關係是散亂無序的,我們該怎樣求解呢?各個組上隨機變量的關係都發現不了,何談尋找A組在B組條件下的條件概率分佈?因此,考慮對輸出組的隨機變量施加限定條件。人類最常做的就是發現規律,總結經驗。比如說,讓輸出組的隨機變量兩兩之間產生聯繫,會不會簡化問題?

  於是,模型變成了這樣。又或者,讓輸入組隨機變量和輸出組隨機變量,都變成序列關係。這時,問題變成了由輸入序列對輸出序列預測的判別模型。具體的,讓輸出組隨機變量構成馬爾可夫隨機場。需要的知識包括圖與無向圖、馬爾可夫性、無向圖模型的因子分解、團與最大團…

概率無向圖

  概率無向圖模型,又稱為馬爾可夫隨機場,是一個可以由無向圖表示的聯合概率分佈。“馬爾可夫隨機場”正式出場了,簡單講它是一個聯合概率分佈,是由無向圖表示的。想象無向圖的樣子,由結點和邊構成,且邊沒有方向(無箭頭)。深入一點,一個結點表示一個隨機變量,如果兩個結點之間有邊關聯,就認為這兩個結點具有關聯關係,並且是“相互關聯”(無向邊),相互產生影響。

  可以看到,“有向變無向”這個操作,讓限定條件變的更寬泛了,也就是讓CRF的應用場景增多。當然,無向圖不只是上圖的線性序列關係。根據馬爾可夫性的不同,概率無向圖模型也有很多種類。接下來由圖開始,具體介紹一下模型的定義。

  看一下馬爾可夫性,我的理解是這樣:在無向圖G上給定Cs(與集合C有關的一堆結點),As和Bs條件獨立。As和Bs之間沒有邊關聯,就算利用Cs作為結點把As和Bs連接了起來,兩者仍然保持獨立關係。

  有三種類型,分別是成對馬爾可夫性、局部馬爾可夫性、全局馬爾可夫性,具體定義較晦澀。如下:

  由以上馬爾可夫性,概率無向圖模型定義為:

  有了概率無向圖的定義,實際上我們更關心的是如何求解各個隨機變量的聯合概率分佈。那麼,對給定的概率無向圖模型,我們能不能找到將整體的聯合概率寫成若干個子聯合概率的乘積的方法?也就是將聯合概率進行因子分解(類似:6=2×3,整數6可以分解成因子2乘以因子3),就能方便模型的學習與計算了。事實上,概率無向圖模型最大的特點就是易於因子分解。這裏留幾個疑問,“整體的”變成若干個“子聯合概率的”這句話,子聯合概率我不是可以理解成“子集”的,這個子集要如何劃分?我們知道,圖結構動不動就會像漁網一樣,橫七豎八的全是連接邊。

  另外,聯合概率的因子分解,不僅要能解決無向圖上劃分小子集的問題,還要保證各個小子集乘積的聯合概率要等於整體的聯合概率。

  這裏劃分小子集的問題,由無向圖上的“最大團”解決的,定義如下:

  例子中描述的,應該是挺清楚的,最大團中的任何兩個結點均有邊連接。於是,概率無向圖模型的聯合概率分佈可以表示為其最大團上的隨機變量的函數的乘積,這種操作稱為概率無向圖模型的因子分解。接下來,考慮聯合概率如何計算。

  不管整體的聯合概率也好,子集的聯合概率也好,無非是表示一大撮和一小撮隨機變量的區別。問題在於,這個東西它是概率啊。怎麼在圖結構上體現概率啊…公式(11.15)和公式(11.16)給出了答案,對每個Yi(N個狀態序列中的一個序列Yi)求分值,將各個“得分”歸一化后的結果作為該無向圖Yi的聯合概率。實際上也就是每一個無向圖Yi是所有狀態序列中的一種情況,這個Yi的得分佔所有情況的比例,就認為是Yi的聯合概率。

  這裏Yi的得分,是如何反映的呢?可以從公式(11.15)看出,圖中所有最大團上各個最大團勢函數的乘積,作為Yi的得分。

  串到CRF特徵抓取的過程中,我認為這裏的勢函數,就是抓取最大團集合中的結點特徵,並且是各個結點之間的關聯特徵。由於訓練過程中擬合參數wk,所以這裏只需要將每個特徵作為正的“量”就可以了。勢函數僅作標示“特徵”的度量(metric),擬合的wk決定該特徵對“得分”影響的正負、大小程度(對應開頭的觀點3)。

  現在利用的這種方式,不是空穴來風,是有定理來保證的:

條件隨機場的定義與形式

  條件隨機場是給定隨機變量X條件下,隨機變量Y的馬爾可夫隨機場。我們要用到的CRF指的是定義在線性鏈上的特殊的條件隨機場,稱為線性鏈條件隨機場。一般情況下,在條件概率模型P(Y|X)中,Y是輸出變量,表示標記序列;X是輸入變量,表示觀測序列。這裏,我們也把標記序列稱為狀態序列。在學習時,利用訓練數據通過極大似然估計或正則化的極大似然估計得到條件概率模型;預測時,對於給定的輸入序列x,求出條件概率模型最大的輸出序列y。

  我認為在給定輸入序列X的條件下,計算輸出序列Y的條件概率(且序列Y構成線性鏈條件隨機場)的場景是與圖11.4相符合的。文中描述的,現實中一般假設X和Y有相同的圖結構,形如圖11.5這樣,這就好像對模型進行了簡化,讓模型更容易實現。換一個角度看,圖11.4中的X表達的是輸入序列整體對輸出序列Y的影響,具體到如何產生影響,如圖11.5表達的這樣,輸入序列X的每個分量Xi,與相同時刻的Yi相互影響。

  這裏留兩個疑問,(Xi,Yi)的相互影響關係,會不會傳遞到t時刻?與時刻t有沒有關係?

  看一下線性鏈條件隨機場的具體定義:

  圖示如下,公式(11.9)實際上表達了:馬爾可夫性規定Yi(Y3)只與相鄰的結點有關聯關係(藍框代表等式左側,紅框代表等式右側)。

  根據定理11.1,可以給出線性鏈條件隨機場P(Y|X)的因子分解式,各因子是定義在相鄰兩個結點(最大團)上的勢函數。兩個疑問的解答,相信後面部分的描述中可以找到答案。接下來看一下條件隨機場的三種形式。

1)條件隨機場的參數化形式

  該基本形式,表示給定輸入序列x,對輸出序列y預測的條件概率。實際上現在的具體定義,就是把最大團、勢函數、概率求解等概念落實下來,變成可以量化的東西。公式(11.11)為歸一化項,相當於把Y的每種情況(各種Y序列)考慮到,作為求概率時的分母(每種情況下,各個得分加和)。在某個情況Y下,計算該Y的概率,需要先得到所有勢函數構成的特徵和,也就是“得分分值”。

  這裏的特徵來自兩部分,yi-1與yi橫向上的轉移特徵tk(·)和yi與x豎向上的狀態特徵sl(·)。

  看一下具體定義:

  這裏tk和sl的取值,類似於最大熵模型中特徵函數的定義。我的理解,取1或者0,是標記這個特徵有還是沒有。至於此時該特徵的貢獻是大是小,是正是負,這取決於模型訓練時擬合的參數情況(λ、μ)。

  舉個例子,看下計算時的具體過程:

  該標記序列y=(y1,y2,y3)的非規範化概率,實際上就是通過存在的特徵×對應權重,然後加和。符合[權值×特徵==》加和]這個方式。

2)條件隨機場的簡化形式

  注意到條件隨機場公式(11.10)中同一個特徵在各個位置都有定義,可以對同一個特徵在各個位置求和,從而將局部特徵轉化為一個全局特徵函數。這樣就可以將條件隨機場寫成權值向量和特徵向量的內積形式,也就是現在要描述的簡化形式。實際上,這更進一步貼近了[權值×特徵==》加和]這種方式。

  如上圖所示,假設每個位置i都有這個局部特徵(沒有該特徵的話,特徵值為0),每個i都要針對這一個局部特徵求一個參數,這個工作量似乎有點大,並且重複。那麼,是不是可以把該局部變量上升到全局特徵,每個局部位置特徵值加和,讓這一個特徵在全局上學習一個權重參數。一來減少沒必要的參數估計,二來可以把重點放在“特徵”的增加和估計上。

  這裏公式(11.13)表明了全局特徵是通過各個位置i的局部特徵加和得到的。

  接下來,用向量形式進行表示:

  這樣就得到了條件隨機場的簡化形式。經過知識點細化,再抽象,看CRF公式(11.19)和公式(11.20)和LR中的公式(6.5)和公式(6.6)多像。各個特徵的線性函數值(得分),通過指數歸一化轉化成概率,學習的過程擬合各個參數。

3)條件隨機場的矩陣形式

  先看定義:

  這裏先記一個要點,對每個標記序列引進特殊的起點和終點狀態標記,這時標註序列的概率Pw(y|x)可以通過矩陣形式表示並有效計算。划重點:引進特殊起點和重點標記之後,才可以通過矩陣形式計算。

  不知道怎麼分析這個地方,現在從以下幾個問題開始:

  ①公式(11.21)中Mi的下標i和yi的下標i是否表示同一個意思?

  我認為這兩個下標i指的不是同一個意思,yi的i表示該矩陣中狀態的取值;Mi中的i表示第i個矩陣,實際上每個位置都有對應的矩陣。同時矩陣的維度是M×M維的,因為yi可以取到M個狀態(yi有M個狀態,yi-1的某個狀態可以轉移到M个中任意一個,yi-1有M個狀態,所以轉移有M×M種)。例M2(1,3)表示在狀態序列的第2個位置上(t=2),由t=1時刻的“狀態1”轉移到t=2的“狀態3”的非規範化概率。

  ②Mi中的元素是怎麼計算的,是什麼?

  計算方式見公式(11.22)和公式(11.23)。我先想到的是,Mi矩陣為yi-1到yi的轉移矩陣,每個時刻yi都是M種狀態,轉移也都是yi-1到yi這些轉移方式,計算方式都由公式(11.23)計算。那麼,不同的Mi之間的區別是什麼呢。我的理解是針對不同的時刻t、不同的x,特徵函數是否存在和相應的權值大小,決定了Mi不同。

  ③公式(11.24)的分子部分n+1個矩陣的適當元素的乘積,是什麼?

  仔細看一下CRF的簡化形式中公式(11.15)的分子部分,利用公式(11.13)對i進行展開,有:

  是不是就變成了公式(11.24)的分子部分n+1個矩陣的適當元素乘積的形式。也就是說,CRF的矩陣形式來源於簡化形式,至於為什麼會有這種方式,我覺得是便於計算吧,下邊前後向算法會用到這種形式。

  ④公式(11.25)規範化因子,是什麼?

  看上去是對所有序列的非規範化概率的總和。其實追根究底④這個問題是想知道矩陣運算在這裏計算的是什麼。

  綜上,條件隨機場矩陣形式的要點有:

  以2×2的Mi矩陣為例(例11.2),具體表示如下:

  看一下例11.2:

  以狀態序列(1,2,1)為例:

  解析如下:

  接下來看一下求規範化因子的過程:

  上面提到的問題④,n+1個矩陣連乘后,得到的結果仍然是M×M維的。但第1行第1列的元素,正好是所有路徑上的非規範化概率之和。

  了解完概率無向圖、條件隨機場的定義和各種表示方法之後,與隱馬爾可夫模型類似的,接下來介紹條件隨機場的3個基本問題:概率計算問題、學習問題、預測問題。

條件隨機場的概率計算問題

  與隱馬爾可夫模型類似,引進前向-後向向量,遞歸的計算概率(遞歸的計算過程是非常不同的)。

  先看前向計算過程。注意CRF作為無向圖模型,拋去了HMM的方向性,我們要從矩陣乘法的角度進行分析。仔細看一下公式(11.28),轉換成矩陣語言如下(以m=5為例,這裏將時刻的下標標記為t,用來區分yi的狀態和時刻t:αi->αt,Mi->Mt):

  具體到轉換過程中(考慮矩陣乘法的過程):

  可以看到,α1t的得出,是結合了t-1時刻各個αi的結果。再來理解一下“αt(yi|x)表示在位置t的標記是yi並且從1到t的前部分標記序列的非規範化概率”這句話,見下圖:

  實際上,t時刻α向量中的某一個分量,αi可以視作終點狀態取yi時的非規範化概率,並且這個概率是1->t時刻的整個過程中,所有可能序列的非規範化概率之和(從start到stop所有路徑上的非規範化概率之和)。如圖中,α1t也就是從start=y1到stop=y1過程中,所有可能序列的非規範化概率之和。因此,每個αi是start=y1到stop=yi的規範化因子Zi。這樣就能看出與HMM特別不同了,一個應用矩陣乘法,一個應用條件概率公式。

  我理解的大概就是這個樣子,不知道能不能寫清楚,接下來看下後向算法的計算過程。從公式(11.31)開始:

  具體到轉換過程中(考慮矩陣乘法的過程):

  於是,βt(yi|x)可以表示從t時刻yt:start=y1到yn+1:stop=y1,所有路徑上的非規範化概率之和(共T-t個結點狀態的序列)。前向算法也好,後向算法也好,這裏的箭頭指向僅表示乘的方向,不是有向圖結構。

  與HMM類似的,接下來看幾個概率計算。

  公式(11.32)和公式(11.33)還是好理解的,看一下示意圖:

  通過示意圖,先來看一下Z(X),Z(X)既可以通過前向向量,又可以通過後向向量來求。實際上,不管前向向量αn也好,後向向量β1也好,Z(X)的計算矩陣過程,實際上是把m個值加和,也就是得到所有狀態序列的規範化因子。

  分開看公式(11.32)分子部分,我覺得是兩個值(α、β)進行了相乘。第一個α值,代表了從0時刻start=y1到t時刻stop=yi的非規範化概率;第二個β值,代表了從t時刻start=yi到n+1時刻stop=y1的非規範化概率。公式(11.33)是類似分析方法。

  再來看一下幾個期望的計算,就不具體分析了:

 

 

條件隨機場的學習問題

  學習問題實際上討論的是在給定訓練數據集上估計模型參數的問題。條件隨機場模型實際上定義在時序數據上的對數線性模型(是不是與LR像),學習方法包括極大似然估計等,具體的有改進的迭代尺度法IIS、梯度下降法及擬牛頓法。

  具體的方法目前先不弄了(有大概了解,但了解的程度,不足以寫出來),趕趕刷題的進度去了…等找着工作了,準備畢業的時候再把這些方法整理上~

  話說這章的代碼沒寫,因為不會…不知道從什麼地方下手。

條件隨機場的預測問題

  同HMM,大名鼎鼎的維特比算法。

PASS

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

【其他文章推薦】

※USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

※台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

QNX Message Passing,一個讓人頭禿的 IPC BUG

問題描述

QNX系統中 Client 與 Server 通過 QNX Message Passing 進行進程間通信。正式開發前,寫過測試程序 (Client),和 Server 通信一切正常。但把同樣的代碼拷貝到正式的 Client 中,結果發現調用 MsgSend() 后無響應,pidin 显示 Client 處於 REPLY PENDING 的狀態。

幾番嘗試,發現一個讓人很難接受的事實:只能在 Client 的主線程中調用 MsgSend() 才能收到 Server 的回復,如果在 Client 的對等線程中調用MsgSend(),則會因為收不到 Server 的回復而 Pending。

問題分析

測試程序能正常工作,所以起初懷疑是 Client 的問題。面對龐大的 Client,始終沒想到突破口。甚至曾一度懷疑是否是 QNX 的系統限制,MsgSend() 只能在主線程中調用,官網文檔翻了一圈並沒有發現這樣的限制。自己寫了個 IPC Server 來測試,發現並沒有這個問題。

Client Server 結果
測試程序,主線程中調用 MsgSend() 正式 Server OK
正式 Client,對等線程中調用 MsgSend() 正式 Server REPLY PENDING
正式 Client,對等線程中調用 MsgSend() 簡化版測試 Server OK

最後不得不懷疑起 Server,莫非 Server 端有什麼機制能檢測到消息是發自主線程還是對等線程,然後只回復來自主線程的 IPC 請求?

一個典型的 IPC Server 示例代碼如下:

While(1) {
   int rcvid = MsgReceive(chid, &recvBuf, sizeof(recvBuf), NULL);
   /* … process the request based on recvBuf … */
   MsgReply(rcvid, EOK, &replyBuf, sizeof(replyBuf)); // reply to unblock the IPC client
}

如果是從 Client 的主線程發送來的消息,rcvid 是一個很小的数字,如 1, 3, 5… 如果是從Client 的對等線程發送來的消息,rcvid 是一個大於 65535 的数字,如 65538, 65540, 65542… 

讓人意外的是 Server 用了一個 int16_t 來保存 rcvid,直接導致後續的 MsgReply() 無法正確的將消息回給 Client,從而導致 Client 一直處於 REPLY PENDING 狀態。

Reference

  • http://www.qnx.com/developers/docs/7.0.0/#com.qnx.doc.neutrino.sys_arch/topic/ipc_Robust.html  

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

【其他文章推薦】

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

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

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

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

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

※超省錢租車方案

Docker_01

目錄

  • 1.1 Docker簡介
    • 1.1.1 為什麼會有Docker的出現?
    • 1.1.2 Docker理念
    • 1.1.3 Docker or 虛擬機?
  • 2.1 Docker安裝
  • 3.1 Docker基本使用
    • 3.1.1 什麼是鏡像?
    • 3.1.2 Docker鏡像加載原理
    • 3.1.3 常用命令
  • 4.1 重要知識點

1.1 Docker簡介

1.1.1 為什麼會有Docker的出現?

一款產品從開發到上線,從操作系統,到運行環境,再到應用配置。作為開發+運維之間的協作我們需要關心很多東西,這也是很多互聯網公司都不得不面對的問題,特別是各種版本的迭代之後,不同版本環境的兼容,對運維人員都是考驗
Docker之所以發展如此迅速,也是因為它對此給出了一個標準化的解決方案。
環境配置如此麻煩,換一台機器,就要重來一次,費力費時。很多人想到,能不能從根本上解決問題。軟件可以帶環境安裝?1. 也就是說,安裝的時候,把原始環境一模一樣地複製過來。開發人員利用 Docker 可以消除協作編碼時“在我的機器上可正常工作”的問題。

1.1.2 Docker理念

Docker是基於Go語言實現的雲開源項目。
Docker的主要目標是“Build,Ship and Run Any App,Anywhere”,也就是通過對應用組件的封裝、分發、部署、運行等生命周期的管理,使用戶的APP(可以是一個WEB應用或數據庫應用等等)及其運行環境能夠做到“一次封裝,到處運行”。
Linux 容器技術的出現就解決了這樣一個問題,而 Docker 就是在它的基礎上發展過來的。將應用運行在 Docker 容器上面,而 Docker 容器在任何操作系統上都是一致的,這就實現了跨平台、跨服務器。只需要一次配置好環境,換到別的機子上就可以一鍵部署好,大大簡化了操作。

總而言之:解決了運行環境和配置問題軟件容器,方便做持續集成並有助於整體發布的容器虛擬化技術。

1.1.3 Docker or 虛擬機?

虛擬機(virtual machine)就是帶環境安裝的一種解決方案。
它可以在一種操作系統裏面運行另一種操作系統,比如在Windows 系統裏面運行Linux 系統。應用程序對此毫無感知,因為虛擬機看上去跟真實系統一模一樣,而對於底層系統來說,虛擬機就是一個普通文件,不需要了就刪掉,對其他部分毫無影響。這類虛擬機完美的運行了另一套系統,能夠使應用程序,操作系統和硬件三者之間的邏輯不變。 
但是虛擬機有很多缺點:1.資源佔用多2.冗餘步驟多3.啟動慢

由於前面虛擬機存在這些缺點,Linux 發展出了另一種虛擬化技術:Linux 容器(Linux Containers,縮寫為 LXC)。
Linux 容器不是模擬一個完整的操作系統,而是對進程進行隔離。有了容器,就可以將軟件運行所需的所有資源打包到一個隔離的容器中。容器與虛擬機不同,不需要捆綁一整套操作系統,只需要軟件工作所需的庫資源和設置。系統因此而變得高效輕量並保證部署在任何環境中的軟件都能始終如一地運行。
 
比較了 Docker 和傳統虛擬化方式的不同之處:

  • 傳統虛擬機技術是虛擬出一套硬件后,在其上運行一個完整操作系統,在該系統上再運行所需應用進程;
  • 而容器內的應用進程直接運行於宿主的內核,容器內沒有自己的內核,而且也沒有進行硬件虛擬。因此容器要比傳統虛擬機更為輕便。
  • 每個容器之間互相隔離,每個容器有自己的文件系統 ,容器之間進程不會相互影響,能區分計算資源。

對於開發和運維來說Docker的特點:

  • 更快速的應用交付和部署:傳統的應用開發完成后,需要提供一堆安裝程序和配置說明文檔,安裝部署后需根據配置文檔進行繁雜的配置才能正常運行。Docker化之後只需要交付少量容器鏡像文件,在正式生產環境加載鏡像並運行即可,應用安裝配置在鏡像里已經內置好,大大節省部署配置和測試驗證時間。
  • 更便捷的升級和擴縮容:隨着微服務架構和Docker的發展,大量的應用會通過微服務方式架構,應用的開發構建將變成搭樂高積木一樣,每個Docker容器將變成一塊“積木”,應用的升級將變得非常容易。當現有的容器不足以支撐業務處理時,可通過鏡像運行新的容器進行快速擴容,使應用系統的擴容從原先的天級變成分鐘級甚至秒級。
  • 更簡單的系統運維:應用容器化運行后,生產環境運行的應用可與開發、測試環境的應用高度一致,容器會將應用程序相關的環境和狀態完全封裝起來,不會因為底層基礎架構和操作系統的不一致性給應用帶來影響,產生新的BUG。當出現程序異常時,也可以通過測試環境的相同容器進行快速定位和修復。
  • 更高效的計算資源利用: Docker是內核級虛擬化,其不像傳統的虛擬化技術一樣需要額外的Hypervisor支持,所以在一台物理機上可以運行很多個容器實例,可大大提升物理服務器的CPU和內存的利用率。

2.1 Docker安裝

安裝地址有兩個:

  • docker官網:(http://www.docker.com)
  • docker中文官網:(https://www.docker-cn.com/)
    跟github一樣docker也有自己的中心倉庫Docker Hub官網: https://hub.docker.com/
    Linux環境下安裝的話可能要注意套個CDN加速一般選擇阿里雲或者網易的鏡像庫。

安裝命令:

$ sudo yum install -y yum-utils

$ sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo
These repositories are included in the docker.repo file above but are disabled by default. You can enable them alongside the stable repository. The following command enables the nightly repository.

$ sudo yum-config-manager --enable docker-ce-nightly
To enable the test channel, run the following command:

$ sudo yum-config-manager --enable docker-ce-test
You can disable the nightly or test repository by running the yum-config-manager command with the --disable flag. To re-enable it, use the --enable flag. The following command disables the nightly repository.

$ sudo yum-config-manager --disable docker-ce-nightly

具體方法參考官網:https://docs.docker.com/engine/install/centos/

3.1 Docker基本使用

3.1.1 什麼是鏡像?

鏡像是一種輕量級、可執行的獨立軟件包,用來打包軟件運行環境和基於運行環境開發的軟件,它包含運行某個軟件所需的所有內容,包括代碼、運行時、庫、環境變量和配置文件。
在Docker打包一個鏡像的時候會使用一個叫UnionFS(聯合文件系統)的文件系統:UnionFS(聯合文件系統):Union文件系統(UnionFS)是一種分層、輕量級並且高性能的文件系統,它支持對文件系統的修改作為一次提交來一層層的疊加,同時可以將不同目錄掛載到同一個虛擬文件系統下(unite several directories into a single virtual filesystem)。Union 文件系統是 Docker 鏡像的基礎。鏡像可以通過分層來進行繼承,基於基礎鏡像(沒有父鏡像),可以製作各種具體的應用鏡像。
特性:一次同時加載多個文件系統,但從外面看起來,只能看到一個文件系統,聯合加載會把各層文件系統疊加起來,這樣最終的文件系統會包含所有底層的文件和目錄。

3.1.2 Docker鏡像加載原理

Docker鏡像加載原理:
docker的鏡像實際上由一層一層的文件系統組成,這種層級的文件系統UnionFS。
bootfs(boot file system)主要包含bootloader和kernel, bootloader主要是引導加載kernel, Linux剛啟動時會加載bootfs文件系統,在Docker鏡像的最底層是bootfs。這一層與我們典型的Linux/Unix系統是一樣的,包含boot加載器和內核。當boot加載完成之後整個內核就都在內存中了,此時內存的使用權已由bootfs轉交給內核,此時系統也會卸載bootfs。

rootfs (root file system) ,在bootfs之上。包含的就是典型 Linux 系統中的 /dev, /proc, /bin, /etc 等標準目錄和文件。rootfs就是各種不同的操作系統發行版,比如Ubuntu,Centos等等。

平時我們安裝進虛擬機的CentOS都是好幾個G,為什麼docker這裏才200M??
對於一個精簡的OS,rootfs可以很小,只需要包括最基本的命令、工具和程序庫就可以了,因為底層直接用Host的kernel,自己只需要提供 rootfs 就行了。由此可見對於不同的linux發行版, bootfs基本是一致的, rootfs會有差別, 因此不同的發行版可以公用bootfs。

分層的鏡像:以我們的pull為例,在下載的過程中我們可以看到docker的鏡像好像是在一層一層的在下載.

為什麼 Docker 鏡像要採用這種分層結構呢?
最大的一個好處就是 – 共享資源
 
比如:有多個鏡像都從相同的 base 鏡像構建而來,那麼宿主機只需在磁盤上保存一份base鏡像,
同時內存中也只需加載一份 base 鏡像,就可以為所有容器服務了。而且鏡像的每一層都可以被共享。

3.1.3 常用命令

1.幫助命令:

  • docker version
  • docker info
  • docker –help

2.鏡像命令:

  • docker images OPTIONS說明:-a :列出本地所有的鏡像(含中間映像層)-q :只显示鏡像ID –digests :显示鏡像的摘要信息 –no-trunc :显示完整的鏡像信息。

  • docker search 某個XXX鏡像名字 OPTIONS說明:–no-trunc : 显示完整的鏡像描述 -s : 列出收藏數不小於指定值的鏡像 –automated : 只列出 automated build類型的鏡像;

  • docker pull 某個XXX鏡像名字

  • docker rmi 某個XXX鏡像名字ID 刪除鏡像
    3.容器命令:

  • 新建並啟動容器:
    docker run [OPTIONS] IMAGE [COMMAND] [ARG…]
    OPTIONS說明(常用):有些是一個減號,有些是兩個減號
     
    –name=”容器新名字”: 為容器指定一個名稱;
    -d: 後台運行容器,並返回容器ID,也即啟動守護式容器;
    -i:以交互模式運行容器,通常與 -t 同時使用;
    -t:為容器重新分配一個偽輸入終端,通常與 -i 同時使用;
    -P: 隨機端口映射;
    -p: 指定端口映射,有以下四種格式
          ip:hostPort:containerPort
          ip::containerPort
          hostPort:containerPort
          containerPort

  • 列出當前所有正在運行的容器
    docker ps [OPTIONS]
    OPTIONS說明(常用):
     
    -a :列出當前所有正在運行的容器+歷史上運行過的
    -l :显示最近創建的容器。
    -n:显示最近n個創建的容器。
    -q :靜默模式,只显示容器編號。
    –no-trunc :不截斷輸出。

  • 退出容器
    exit:容器停止退出。 ctrl+P+Q:容器不停止退出

  • 啟動容器
    docker start 容器ID或者容器名

  • 重啟容器
    docker restart 容器ID或者容器名

  • 停止容器
    docker stop 容器ID或者容器名

  • 強制停止容器
    docker kill 容器ID或者容器名

  • 刪除已停止的容器
    docker rm 容器ID(一次性刪除多個容器:1.docker rm -f $(docker ps -a -q)2.docker ps -a -q | xargs docker rm)

4.1 重要知識點

1.關於docker ps -a命令它不會列出你後台啟動的進程也就是說你用守護進程方式啟動容器的話:docker run -d 容器名是不會被ps出來的。
這個是docker的機制問題,比如你的web容器,我們以nginx為例,正常情況下,我們配置啟動服務只需要啟動響應的service即可。例如service nginx start但是,這樣做,nginx為後台進程模式運行,就導致docker前台沒有運行的應用,這樣的容器後台啟動后,會立即自殺因為他覺得他沒事可做了.所以,最佳的解決方案是,將你要運行的程序以前台進程的形式運行。
2.如何重新進入已經啟動的容器
有兩個法子:

  • docker exec -it 容器ID bashShell
  • docker attach 容器ID

上述兩個區別:

  • attach 直接進入容器啟動命令的終端,不會啟動新的進程
  • exec 是在容器中打開新的終端,並且可以啟動新的進程
    3.從容器內拷貝文件到主機上
    docker cp 容器ID:容器內路徑 目的主機路徑。

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

【其他文章推薦】

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

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

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

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

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

※網頁設計最專業,超強功能平台可客製化

【原創】Linux中斷子系統(三)-softirq和tasklet

背景

  • Read the fucking source code! –By 魯迅
  • A picture is worth a thousand words. –By 高爾基

說明:

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

中斷子系統中有一個重要的設計機制,那就是Top-half和Bottom-half,將緊急的工作放置在Top-half中來處理,而將耗時的工作放置在Bottom-half中來處理,這樣確保Top-half能儘快完成處理,那麼為什麼需要這麼設計呢?看一張圖就明白了:

  • ARM處理器在進行中斷處理時,處理器進行異常模式切換,此時會將中斷進行關閉,處理完成后再將中斷打開;
  • 如果中斷不分上下半部處理,那麼意味着只有等上一个中斷完成處理后才會打開中斷,下一个中斷才能得到響應。當某个中斷處理處理時間較長時,很有可能就會造成其他中斷丟失而無法響應,這個顯然是難以接受的,比如典型的時鐘中斷,作為系統的脈搏,它的響應就需要得到保障;
  • 中斷分成上下半部處理可以提高中斷的響應能力,在上半部處理完成后便將中斷打開(通常上半部處理越快越好),這樣就可以響應其他中斷了,等到中斷退出的時候再進行下半部的處理;
  • 中斷的Bottom-half機制,包括了softirq、tasklet、workqueue、以及前文中提到過的中斷線程化處理等,其中tasklet又是基於softirq來實現的,這也是本文討論的主題;

在中斷處理過程中,離不開各種上下文的討論,了解不同上下文的區分有助於中斷處理的理解,所以,還是來一張老圖吧:

  • task_struct結構體中的thread_info.preempt_count用於記錄當前任務所處的context狀態;
  • PREEMPT_BITS:用於記錄禁止搶佔的次數,禁止搶佔一次該值就加1,使能搶佔該值就減1;
  • SOFTIRQ_BITS:用於同步處理,關掉下半部的時候加1,打開下半部的時候減1;
  • HARDIRQ_BITS:用於表示處於硬件中斷上下文中;

前戲結束了,直奔主題吧。

2. softirq

2.1 初始化

softirq不支持動態分配,Linux kernel提供了靜態分配,關鍵的結構體描述如下,可以類比硬件中斷來理解:

/* 支持的軟中斷類型,可以認為是軟中斷號, 其中從上到下優先級遞減 */
enum
{
	HI_SOFTIRQ=0,       /* 最高優先級軟中斷 */
	TIMER_SOFTIRQ,      /* Timer定時器軟中斷 */
	NET_TX_SOFTIRQ,     /* 發送網絡數據包軟中斷 */
	NET_RX_SOFTIRQ,     /* 接收網絡數據包軟中斷 */
	BLOCK_SOFTIRQ,      /* 塊設備軟中斷 */
	IRQ_POLL_SOFTIRQ,   /* 塊設備軟中斷 */
	TASKLET_SOFTIRQ,    /* tasklet軟中斷 */
	SCHED_SOFTIRQ,      /* 進程調度及負載均衡的軟中斷 */
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on thenumbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq, RCU相關的軟中斷 */

	NR_SOFTIRQS
};

/* 軟件中斷描述符,只包含一個handler函數指針 */
struct softirq_action {
	void	(*action)(struct softirq_action *);
};
/* 軟中斷描述符表,實際上就是一個全局的數組 */
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

/* CPU軟中斷狀態描述,當某個軟中斷觸發時,__softirq_pending會置位對應的bit */
typedef struct {
	unsigned int __softirq_pending;
	unsigned int ipi_irqs[NR_IPI];
} ____cacheline_aligned irq_cpustat_t;
/* 每個CPU都會維護一個狀態信息結構 */
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

/* 內核為每個CPU都創建了一個軟中斷處理內核線程 */
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);

來一張圖吧:

  • softirq_vec[]數組,類比硬件中斷描述符表irq_desc[],通過軟中斷號可以找到對應的handler進行處理,比如圖中的tasklet_action就是一個實際的handler函數;
  • 軟中斷可以在不同的CPU上并行運行,在同一個CPU上只能串行執行;
  • 每個CPU維護irq_cpustat_t狀態結構,當某個軟中斷需要進行處理時,會將該結構體中的__softirq_pending字段或上1UL << XXX_SOFTIRQ;

2.2 流程分析

2.2.1 軟中斷註冊

中斷處理流程中設備驅動通過request_irq/request_threaded_irq接口來註冊中斷處理函數,而在軟中斷處理流程中,通過open_softirq接口來註冊,由於它實在是太簡單了,我忍不住想把代碼貼上來:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

也就是將軟中斷描述符表中對應描述符的handler函數指針指向對應的函數即可,以便軟中斷到來時進行回調。

那麼,問題來了,什麼時候進行軟中斷函數回調呢?

2.2.2 軟中斷執行之一:中斷處理后

先看第一種情況,用圖片來回答問題:

  • Linux中斷子系統(二)-通用框架處理文章中講述了整个中斷處理流程,在接收到中斷信號后,處理器進行異常模式切換,並跳轉到異常向量表處進行執行,關鍵的流程為:el0_irq->irq_handler->handle_arch_irq(gic->handle_irq)->handle_domain_irq->__handle_domain_irq;
  • 在__handle_domain_irq函數中,irq_enter和irq_exit分別用於來標識進入和離開硬件中斷上下文處理,這個從preempt_count_add/preempt_count_sub來操作HARDIRQ_OFFSET可以看出來,這也對應到了上文中的Context描述圖;
  • 在離開硬件中斷上下文後,如果!in_interrupt() && local_softirq_pending為真,則進行軟中斷處理。這個條件有兩個含義:1)!in_interrupt()表明不能處在中斷上下文中,這個範圍包括in_nmi、in_irq、in_softirq(Bottom-half disable)、in_serving_softirq,凡是處於這幾種狀態下,軟中斷都不會被執行;2)local_softirq_pending不為0,表明有軟中斷處理請求;

軟中斷執行的入口就是invoke_softirq,繼續分析一波:

  • invoke_softirq函數中,根據中斷處理是否線程化進行分類處理,如果中斷已經進行了強制線程化處理(中斷強制線程化,需要在啟動的時候傳入參數threadirqs),那麼直接通過wakeup_softirqd喚醒內核線程來執行,否則的話則調用__do_softirq函數來處理;
  • Linux內核會為每個CPU都創建一個內核線程ksoftirqd,通過smpboot_register_percpu_thread函數來完成,其中當內核線程運行時,在滿足條件的情況下會執行run_ksoftirqd函數,如果此時有軟中斷處理請求,調用__do_softirq來進行處理;

上圖中的邏輯可以看出,最終的核心處理都放置在__do_softirq函數中完成:

  • local_softirq_pending函數用於讀取__softirq_pending字段,可以類比於設備驅動中的狀態寄存器,用於判斷是否有軟中斷處理請求;
  • 軟中斷處理時會關閉Bottom-half,處理完后再打開;
  • 軟中斷處理時,會打開本地中斷,處理完后關閉本地中斷,這個地方對應到上文中提到的Top-half和Bottom-half機制,在Bottom-half處理的時候,是會將中斷打開的,因此也就能繼續響應其他中斷,這個也就意味着其他中斷也能來打斷當前的Bottom-half處理;
  • while(softirq_bit = ffs(pending)),循環讀取狀態位,直到處理完每一個軟中斷請求;
  • 跳出while循環之後,再一次判斷是否又有新的軟中斷請求到來(由於它可能被中斷打斷,也就意味着可能有新的請求到來),有新的請求到來,則有三個條件判斷,滿足的話跳轉到restart處執行,否則調用wakeup_sotfirqd來喚醒內核線程來處理:
    1. time_before(jiffies, MAX_SOFTIRQ_TIME),軟中斷處理時間小於兩毫秒;
    2. !need_resched,當前沒有進程調度的請求;
    3. max_restart = MAX_SOFTIRQ_RESTART,跳轉到restart循環的次數不大於10次;
      這三個條件的判斷,是基於延遲和公平的考慮,既要保證軟中斷儘快處理,又不能讓軟中斷處理一直佔據系統,正所謂trade-off的藝術;

__do_softirq既然可以在中斷處理過程中調用,也可以在ksoftirqd中調用,那麼softirq的執行可能有兩種context,插張圖吧:

讓我們來思考最後一個問題:硬件中斷觸發的時候是通過硬件設備的電信號,那麼軟中斷的觸發是通過什麼呢?答案是通過raise_softirq接口:

  • 可以在中斷處理過程中調用raise_softirq來進行軟中斷處理請求,處理的實際也就是上文中提到過的irq_exit退出硬件中斷上下文之後再處理;
  • raise_softirq_irqoff函數中,最終會調用到or_softirq_pending,該函數會去讀取本地CPU的irq_stat中__softirq_pending字段,然後將對應的軟中斷號給置位,表明有該軟中斷的處理請求;
  • raise_softirq_irqoff函數中,會判斷當前的請求的上下文環境,如果不在中斷上下文中,就可以通過喚醒內核線程來處理,如果在中斷上下文中處理,那就不執行;
  • 多說一句,在軟中斷整個處理流程中,會經常看到in_interrupt()的條件判斷,這個可以確保軟中斷在CPU上的串行執行,避免嵌套;

2.2.3 軟中斷執行之二:Bottom-half Enable后

第二種軟中斷執行的時間點,在Bottom-half使能的時候,通常用於併發處理,進程空間上下文中進行調用:

  • 在討論併發專題的時候,我們談到過Bottom-half與進程之間能產生資源爭奪的情況,如果在軟中斷和進程之間有臨界資源(軟中斷上下文優先級高於進程上下文),那麼可以在進程上下文中調用local_bh_disable/local_bh_enable來對臨界資源保護;
  • 圖中左側的函數,都是用於打開Bottom-half的接口,可以看出是spin_lock_bh/read_lock_bh/write_lock_bh等併發處理接口的變種形式調用;
  • __local_bh_enable_ip函數中,首先判斷調用該本接口時中斷是否是關閉的,如果已經關閉了再操作BH接口就會告警;
  • preempt_count_sub需要與preempt_count_add配套使用,用於操作thread_info->preempt_count字段,加與減的值是一致的,而在__local_bh_enable_ip接口中,將cnt值的減操作分成了兩步:preempt_count_sub(cnt-1)和preempt_count_dec,這麼做的原因是執行完preempt_count_sub(cnt-1)后,thread_info->preempt_count字段的值保留了1,把搶佔給關閉了,當do_softirq執行完畢后,再調用preempt_count_dec再減去剩下的1,進而打開搶佔;
  • 為什麼在使能Bottom-half時要進行軟中斷處理呢?在併發處理時,可能已經把Bottom-half進行關閉了,如果此時中斷來了后,軟中斷不會被處理,在進程上下文中打開Bottom-half時,這時候就會檢查是否有軟中斷處理請求了;

3. tasklet

從上文中分析可以看出,tasklet是軟中斷的一種類型,那麼兩者有啥區別呢?先說結論吧:

  • 軟中斷類型內核中都是靜態分配,不支持動態分配,而tasklet支持動態和靜態分配,也就是驅動程序中能比較方便的進行擴展;
  • 軟中斷可以在多個CPU上并行運行,因此需要考慮可重入問題,而tasklet會綁定在某個CPU上運行,運行完后再解綁,不要求重入問題,當然它的性能也就會下降一些;

3.1 數據結構

  • DEFINE_PER_CPU(struct tasklet_head, tasklet_vec)為每個CPU都分配了tasklet_head結構,該結構用來維護struct tasklet_struct鏈表,需要放到該CPU上運行的tasklet將會添加到該結構的鏈表中,內核中為每個CPU維護了兩個鏈表tasklet_vec和tasklet_vec_hi,對應兩個不同的優先級,本文以tasklet_vec為例;
  • struct tasklet_struct為tasklet的抽象,幾個關鍵字段如圖所示,通過next來鏈接成鏈表,通過state字段來標識不同的狀態以確保能在CPU上串行執行,func函數指針在調用task_init()接口時進行初始化,並在最終觸發軟中斷時執行;

3.2 流程分析

  • tasklet本質上是一種軟中斷,所以它的調用流程與上文中討論的軟中斷流程是一致的;
  • 調度tasklet運行的接口是tasklet_schedule,如果tasklet沒有被調度則進行調度處理,將該tasklet添加到CPU對應的鏈表中,然後調用raise_softirq_irqoff來觸發軟中斷執行;
  • 軟中斷執行的處理函數是tasklet_action,這個在softirq_init函數中通過open_softirq函數進行註冊的;
  • tasklet_action函數,首先將該CPU上tasklet_vec中的鏈表挪到臨時鏈表list中,然後再對這個list進行遍歷處理,如果滿足執行條件則調用t->func()執行,並continue跳轉遍歷下一個節點。如果不滿足執行條件,則繼續將該tasklet添加回原來的tasklet_vec中,並再次觸發軟中斷;

3.3 接口

簡單貼一下接口吧:

/* 靜態分配tasklet */
DECLARE_TASKLET(name, func, data)

/* 動態分配tasklet */
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

/* 禁止tasklet被執行,本質上是增加tasklet_struct->count值,以便在調度時不滿足執行條件 */
void tasklet_disable(struct tasklet_struct *t);

/* 使能tasklet,與tasklet_diable對應 */
void tasklet_enable(struct tasklet_struct *t);

/* 調度tasklet,通常在設備驅動的中斷函數里調用 */
void tasklet_schedule(struct tasklet_struct *t);

/* 殺死tasklet,確保不被調度和執行, 主要是設置state狀態位 */
void tasklet_kill(struct tasklet_struct *t);

收工!

歡迎關注個人公眾號,不定期分享Linux內核機制文章。

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

【其他文章推薦】

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

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

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

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

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

Java中容易遺漏的小知識點( 一 )(為了和小白一樣馬上要考試的兄弟準備的,希望小白和大家高過不掛)

筆者csdn博客同文地址:https://blog.csdn.net/weixin_45791445/article/details/106597515

我是小康小白,一個平平無奇的Java小白。熱愛有趣的文字,生活和遠方。
個人博客:https://blog.csdn.net/weixin_45791445

小白最近快考試了,複習的順便總結一下一些小白認為容易忘記的知識點,希望能幫助到大家。

小白的總結系列二,三已上線Java中容易遺漏的常用的知識點( 二 )(為了和小白一樣馬上要考試的兄弟準備的,希望小白和大家高過不掛)
Java中容易遺漏的常用的知識點( 三 )(為了和小白一樣馬上要考試的兄弟準備的,希望小白和大家高過不掛)
  1. Java代碼運行機制

     1. 編寫源代碼
     	得到的是以“.java”為擴展名的源文件,源文件名應與該文件中被修飾為public的類名相同。
     2. 編譯(使用cmd命令編譯時使用的命令是javac 文件名.java。使用的javac編輯器)
     	編譯后的文件叫做類文件。得到的是以“.class”為擴展名的字節碼文件。
     	這裏出現的錯誤叫編譯時錯誤。
     	
     然後使用java命令運行,命令格式為:java 項目名(注意這裏沒有後綴))	
     3.類裝載ClassLoader
     4.字節碼(byte-code)校驗
     5. 解釋
     6. 運行
     這裏出現的錯誤叫運行時錯誤。
    
  2. 在一個簡單的Java程序中有一個獲以上的類組合而成。其次在一個可以獨立運行的Java程序中有且只有一個main()方法,它是程序運行的開端。程序的主體被包含在類中,這個起始的地方被稱為main方法。main方法之前必須加上public,static,void。static表示

  3. switch中的控製表達式必須為char,int,short或byte類型的。case後面的常量表達式必須和控製表達式的類型相同。(書上)

書上這一節後面的習題中有個選項說支持long類型是錯誤的。

switch表達式後面的數據類型只能是byte,short,char,int四種整形類型,枚舉類型和java.lang.String類型(從java 7才允許),不能是boolean類型。至於四種基本類型的包裝類是由於有自動拆箱的操作所以支持(實際)

  1. instanceof是對象運算符,用來判定一個對象是否屬於莫個指定的類(或其子類)的實例。

  2. 邏輯運算符分為標準邏輯運算符(!& ^ |)和條件邏輯運算符(&& ||)

  3. byte:1字節,short:2字節,int:4字節,long:8字節,float:4字節,double:8字節,char:2字節,boolean:書上此處為空。

switch的格式

do-while的格式(最後要加一個”;”)

foreach

  1. 運算符優先級
  2. 在判斷語句中的i++和–j是一個完整的獨立語句,先進行自加或者自減后再進行比對。
    (下面是debug測試)
  3. outer標籤

outer標籤是什麼,上面的文章講的很詳細。
小白在這裏補充一下break outer;和continue outer;的區別

break outer;是直接完全的跳出到outer標籤標記的循環外面。
![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200608102636433.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTc5MTQ0NQ==,size_16,color_FFFFFF,t_70#pic_center =500×500)
continue outer;的作用並非完全跳出外層循環,而是跳出outer標籤標記循環的本次循環,直接進行此循環的下一次循環;

![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200608102735262.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80NTc5MTQ0NQ==,size_16,color_FFFFFF,t_70#pic_center =500×500)
12. 構造方法

  1. 類是對象模板,只有實例化之後才可以使用。new運算符用於創建一個類的實例並返回對象的引用。
    默認構造方法對成員變量的初始化原則如下:
    對於整型成員變量,默認初值是0;對與浮點型,默認初值是0.0;對於boolean類型,默認初值是false;對於引用型,默認值是null。

  2. this關鍵字
    this可以出現在類的構造方法和非static修飾的成員方法(即實例方法)中,this代表實例對象自身,可以通過this來訪問實例對象的成員變量或調用成員方法。
    類方法中不可以使用this
    由於類方法可以通過類名字直接調用,這時可能還沒有任何對象產生,因此指代對象實例本身的this關鍵字不可以出現在類方法中。

  3. 	1)public訪問權限最具有開放性,可以用來修飾類、類與接口的成員(包括成員變量、成員方法)
    。由public類修飾的類或類成員可被任何類訪問。
    	2)protected可以用來修飾類的成員變量或方法。具有protected訪問特性的類成員可以被本類、
    本包中的其他類訪問,也可以被其他包中的子類繼承,它的可訪問性低於public,高於默認。
    	3)如果在定義類、接口、成員變量、成員方法時沒有指定訪問權限控制符,他們的權限就為默認權
    限。具有默認權限的類、接口、成員變量、成員方法,只能被本類和同一個包中的其他類、接口及成員方
    法引用,因此默認權限也被稱為包權限。
    	4)私有訪問控制符private用來聲明類的私有成員,它提供了最高的保護級別。用private修飾的
    成員變量或方法只能被該類自身所訪問和修改,而不能被任何其他類(包括該類的子類)來獲取和引用。
    

Java文件名應與修飾為public的類名相同。且Java文件中只能有一個public修飾的類

特殊情況:
只有被修飾為public訪問權限的成員變量和方法才可以被不同包的其他類使用,修飾為protected的成員變量和方法可以被不同包的子類繼承,訪問。不同包的其他類禁止訪問修飾為private和默認權限的成員變量和方法。

  1. super關鍵字
    子類在繼承父類時,可能會出現變量隱藏、方法覆蓋(overriding)等現象。變量隱藏指子類的成員變量與父類成員同名,此時,父類的成員變量被隱藏。方法覆蓋指子類的方法與父類的方法名相同,方法的返回值類型、入口參數的數目、類型、順序均相同,只是方法實現的功能不同,此時父類的方法被覆蓋。如果子類需要調用或訪問父類被隱藏的變量或被覆蓋的方法,可以使用super關鍵字實現。
    當用子類的構造方法創建一個子類對象時,子類的構造方法總會顯式或隱式地先調用父類的某個構造方法。如果子類的構造方法沒有明顯地指明調用父類的哪個構造方法,Java會默認調用父類的無參構造方法;子類也可以通過super關鍵字顯式調用父類指定的構造方法,具體調用哪個構造方法需要根據super()的參數類型決定。

  2. Java中的子類對象可以向上轉換為父類對象(也稱上轉型對象),允許將子類的實例賦值給父類的引用,也允許一個父類的引用指向子類對象。
    但是反過來,一個父類對象的類型未必可以向下轉換成子類對象,因為子類具有的信息,父類未必包含,這種轉換是不安全的。只有當父類引用實際上指向一個子類對象時,才可以進行這種轉換。

  3. 實例變量依附於對象,在使用new關鍵字創建對象時,Java運行系統為每個對象分配一個內存空間,不同的實例變量佔用不同的內存區域。類變量依附於類(而非對象),對於類變量來說,Java運行系統為類的類變量分配公共的存儲空間,該類的每個實例對象共享同一類變量的存儲空間。因此,每個對象對類變量的修改都會影響其他實例對象。
    (1)類變量可以通過類名直接訪問,而不需要創建對象。
    (2)任何一個對象對類變量的修改,都是在同一內存單元上完成的。因此,每個對象對類變量的修改都會影響其他實例對象。

    聲明為static的方法稱為類方法(或稱靜態方法),與此相對,沒有static修飾的成員方法則為實例方法。類方法的本質是該方法屬於整個類,而不是屬於某個實例,可以不創建實例,直接通過類名調用。 類方法的使用具有以下特點:
    (1)類方法可以通過類直接調用,而不需要創建實例對象。例如:Java Application的入口main()方法就被聲明為static類方法,可以不需要創建任何實例對象對調用。
    (2)類方法屬於整個類,被調用時可能還沒有創建任何對象實例,因此類方法內只可以訪問類變量,而不能直接訪問實例變量和實例方法。
    (3)類方法中不能使用this關鍵字,因為靜態方法不屬於任何一個實例。

  4. 抽象類和抽象方法(為什麼會有抽象類?)
    語法:

    abstract class <類名> [extends<父類>][implements<接口名>]{ <類主體> }
    

如果一個類A需要繼承抽象類,則該類必須實現抽象類中定義的所有抽象方法。否則,該類也必須修飾為抽象類。也就是說,抽象類的子類如果僅實現父類的部分抽象方法,子類也必須聲明為抽象類。

為什麼會有抽象類和抽象方法?
有時候,我們沒有足夠的信息去描述一個具體的對象,這樣就不能用正常的類來描述它,那麼這時候就需要抽象類了。抽象方法沒有方法體,為什麼呢?小白的理解是我們只知道這個對象有這樣的一種方法(或者理解為” 行為 “),但不知到它的這種方法能夠干什麼,就沒有寫具體的方法主體。因此,抽象類不能實例化(創建的人都不知道它具體是什麼,怎麼實例成一個具體的對象)
![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200607181353317.png#pic_center =240×240)

  1. 接口
    接口(interface)是Java所提供的另一種重要結構。接口是一種特殊的類,但接口與類存在着本質的區別。類有成員變量和成員方法,而接口卻只有常量和抽象方法,也就是說接口的成員變量必須初始化,同時接口中的所有方法必須聲明為abstract方法。

語法:

[接口修飾符] interface〈接口名〉[extends〈父類接口列表〉] { 接口體 }

用implements子句表示類實現某個接口。一個類可以同時實現多個接口,接口之間用逗號“,”分隔。 在類體中可以使用接口中定義的常量,由於接口中的方法為抽象方法,所以必須在類體中加入要實現接口方法的代碼,如果一個接口是從別的一個或多個父接口中繼承而來,則在類體中必須加入實現該接口及其父接口中所有方法的代碼。 在實現一個接口時,類中對方法的定義要和接口中的相應方法的定義相匹配,其方法名、方法的返回值類型、方法的訪問權限和參數的數目與類型信息要一致。

接口繼承中出現的方法重名和常量重名
(1)方法重名 如兩個方法完全一樣,只保留一個。 如果兩個方法有不同的參數(類型或個數),那麼子接口中包括兩個方法,方法被重載。 若兩個方法僅在返回值上不同,出現錯誤。
(2)常量重名 兩個重名常量全部保留,並使用原來的接口名作為前綴。

  1. 內部類:
    Java支持在一個類中聲明另一個類,這樣的類稱為內部類(InnerClass),而包含內部類的類稱為內部類的外部類(OuterClass)。內部類一般用來實現一些沒有通用意義的功能邏輯。

eaducoder裏面老用。小白表示心累ಥ_ಥಥ_ಥ。

一個類把內部類看成是自己的成員,外部類的成員變量在內部類中仍然有效,內部類可以直接使用外部類中的成員變量和方法,即使他們是private的,這也是內部類的一個好處。

  1. 匿名類(小白表示從未用過,但寫一下吧。萬一考呢。)
    使用類創建對象時,Java允許把類體與對象的創建組合在一起。也就是說,類創建對象時,除了構造方法還有類體,此類體被稱為匿名類。
    實戰:
一個匿名類:
Student3 liu = new Student3() { 
	void speak() { 
			System.out.println("這是匿名類中的方法"); 
		} 
};// 匿名類體結束

重點來了

  1. 泛型
    泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。這種參數類型可以用在類、接口和方法的創建中,分別稱為泛型類、泛型接口、泛型方法。

1)泛型類聲明:
創建一個簡單的泛型是非常容易的。首先,在一對尖括號(< >)中聲明類型變量,以逗號間隔變量名列表。在類的實例變量和方法中,可以在任何類型的地方使用那些類型變量。
格式:

class 泛型類名<泛型列表>{ 
	類體 
}

泛型類聲明時並不指明泛型列表是什麼類型的數據,可以是任何對象或接口,但不能是基本類型數據。泛型列表處的泛型可以作為類的成員變量的類型、方法的類型以及局部變量的類型。

2)使用泛型類聲明對象:
使用泛型類聲明對象時,必須指定類中使用泛型的具體具體類名,如: Chorus<Student,Button> model model = new Chorus<Student,Button>();

3)泛型接口:
格式:

Interface 泛型接口名<泛型列表>{
	 接口體 
}

小白沒想到書上對於泛型講的這麼多。(唉,不禁為我的菜雞感覺億點悲傷。)

小白實戰中常用的是:
在創建arraylist或者list集合時加泛型

ArrayList<String> listTest = new ArrayList<String>();

這樣限定了創建集合的類型。對於此小白的理解時泛型是在創建list或者arrayList集合時指定集合中的元素是同一種類型。例如你在創建時指定了創建的list集合中只能有String類型的變量,那麼你向其中添加數據時,如果不是String類型的數據,程序在編譯時就會報錯。如果不是用泛型這種錯誤只有在程序運行時才會報錯。而且,泛型讓我們在get獲得集合中的數據時,無需因為類型不同需要強轉而煩惱。

當你向加了泛型的語句中加入不同數據類型的數據時是會報錯的。但如果不加,這種錯誤就只能在程序運行是才報錯。

這次先寫到這裏小白明天接着寫。

Java中容易遺漏的常用的知識點( 二 )(為了和小白一樣馬上要考試的兄弟準備的,希望小白和大家高過不掛)
Java中容易遺漏的常用的知識點( 三 )(為了和小白一樣馬上要考試的兄弟準備的,希望小白和大家高過不掛)

![在這裏插入圖片描述](https://img-blog.csdnimg.cn/20200607181353317.png#pic_center =240×240)

小白的總結系列二已上線Java中容易遺漏的常用的知識點( 二 )(為了和小白一樣馬上要考試的兄弟準備的,希望小白和大家高過不掛)

兄弟們,小白編寫不易。希望各位兄弟們,點贊評論收藏加關注。小白在此謝謝各位老爺們,也祝福和我一樣面臨考試的兄弟們高分通過。

對於白嫖的兄弟們,

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

【其他文章推薦】

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

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

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

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

聽說你的資源被盜用了,那你知道 Nginx 怎麼防盜鏈嗎?

上一篇文章講了 Nginx 中的變量和運行原理,下面就來說一個主要提供變量並修改變量的值的模塊,也就是我們要講的防盜鏈模塊:referer 模塊。

簡單有效的防盜鏈手段

場景

如果做過個人站點的同學,可能會遇到別人盜用自己站點資源鏈接的情況,這就是盜鏈。說到盜鏈就要說一個 HTTP 協議的 頭部,referer 頭部。當其他網站通過 URL 引用了你的頁面,用戶在瀏覽器上點擊 URL 時,HTTP 請求的頭部會通過 referer 頭部將該網站當前頁面的 URL 帶上,告訴服務器本次請求是由誰發起的。

例如,在谷歌中搜索 Nginx 然後點擊鏈接:

在打開的新頁面中查看請求頭會發現,請求頭中包含了 referer 頭部且值是 https://www.google.com/。

像谷歌這種我們是允許的,但是有一些其他的網站想要引用我們自己網站的資源時,就需要做一些管控了,不然豈不是誰都可以拿到鏈接。

目的

這裏目的其實已經很明確了,就是要拒絕非正常的網站訪問我們站點的資源。

思路

  • invalid_referer 變量
    • referer 提供了這個變量,可以用來配置哪些 referer 頭部合法,也就是,你允許哪些網站引用你的資源。

referer 模塊

要實現上面的目的,referer 模塊可得算頭一號,一起看下 referer 模塊怎麼用的。

  • 默認編譯進 Nginx,通過 --without-http_referer_module 禁用

referer 模塊有三個指令,下面看一下。

Syntax: valid_referers none | blocked | server_names | string ...;
Default: —
Context: server, location

Syntax: referer_hash_bucket_size size;
Default: referer_hash_bucket_size 64; 
Context: server, location

Syntax: referer_hash_max_size size;
Default: referer_hash_max_size 2048; 
Context: server, location
  • valid_referers 指令,配置是否允許 referer 頭部以及允許哪些 referer 訪問。
  • referer_hash_bucket_size 表示這些配置的值是放在哈希表中的,指定哈希表的大小。
  • referer_hash_max_size 則表示哈希表的最大大小是多大。

這裏面最重要的是 valid_referers 指令,需要重點來說明一下。

valid_referers 指令

可以同時攜帶多個參數,表示多個 referer 頭部都生效。

參數值

  • none
    • 允許缺失 referer 頭部的請求訪問
  • block:允許 referer 頭部沒有對應的值的請求訪問。例如可能經過了反向代理或者防火牆
  • server_names:若 referer 中站點域名與 server_name 中本機域名某個匹配,則允許該請求訪問
  • string:表示域名及 URL 的字符串,對域名可在前綴或者後綴中含有 * 通配符,若 referer 頭部的值匹配字符串后,則允許訪問
  • 正則表達式:若 referer 頭部的值匹配上了正則,就允許訪問

invalid_referer 變量

  • 允許訪問時變量值為空
  • 不允許訪問時變量值為 1

實戰

下面來看一個配置文件。

server {
	server_name referer.ziyang.com;
    listen 80;

	error_log logs/myerror.log debug;
	root html;
	location /{
		valid_referers none blocked server_names
               		*.ziyang.com www.ziyang.org.cn/nginx/
               		~\.google\.;
		if ($invalid_referer) {
    			return 403;
		}
		return 200 'valid\n';
	}
}

那麼對於這個配置文件而言,以下哪些請求會被拒絕呢?

curl -H 'referer: http://www.ziyang.org.cn/ttt' referer.ziyang.com/
curl -H 'referer: http://www.ziyang.com/ttt' referer.ziyang.com/
curl -H 'referer: ' referer.ziyang.com/
curl referer.ziyang.com/
curl -H 'referer: http://www.ziyang.com' referer.ziyang.com/
curl -H 'referer: http://referer.ziyang.com' referer.ziyang.com/
curl -H 'referer: http://image.baidu.com/search/detail' referer.ziyang.com/
curl -H 'referer: http://image.google.com/search/detail' referer.ziyang.com/

我們需要先來解析一下這個配置文件。valid_referers 指令配置了哪些值呢?

 valid_referers none blocked server_names
        *.ziyang.com www.ziyang.org.cn/nginx/
        ~\.google\.;
  • none:表示沒有 referer 的可以訪問
  • blocked:表示 referer 沒有值的可以訪問
  • server_names:表示本機 server_name 也就是 referer.ziyang.com 可以訪問
  • *.ziyang.com:匹配上了正則的可以訪問
  • www.ziyang.org.cn/nginx/:該頁面發起的請求可以訪問
  • ~\.google\.:google 前後都是正則匹配

下面就實際看下響應:

# 返回 403,沒有匹配到任何規則
  ~ curl -H 'referer: http://www.ziyang.org.cn/ttt' referer.ziyang.com/
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
  ~ curl -H 'referer: http://image.baidu.com/search/detail' referer.ziyang.com/
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.17.8</center>
</body>
</html>
# 匹配到了 *.ziyang.com
  ~ curl -H 'referer: http://www.ziyang.com/ttt' referer.ziyang.com/
valid
  ~ curl -H 'referer: http://www.ziyang.com' referer.ziyang.com/
valid
# 匹配到了 server name
  ~ curl -H 'referer: http://referer.ziyang.com' referer.ziyang.com/
valid
# 匹配到了 blocked
  ~ curl -H 'referer: ' referer.ziyang.com/
valid
# 匹配到了 none
  ~ curl referer.ziyang.com/
valid
# 匹配到了 ~\.google\.
  ~ curl -H 'referer: http://image.google.com/search/detail' referer.ziyang.com/
valid

防盜鏈另外一種解決方案:secure_link 模塊

referer 模塊是一種簡單的防盜鏈手段,必須依賴瀏覽器發起請求才會有效,如果攻擊者偽造 referer 頭部的話,這種方式就失效了。

secure_link 模塊是另外一種解決的方案。

它的主要原理是,通過驗證 URL 中哈希值的方式防盜鏈。

基本過程是這個樣子的:

  • 由服務器(可以是 Nginx,也可以是其他 Web 服務器)生成加密的安全鏈接 URL,返回給客戶端
  • 客戶端使用安全 URL 訪問 Nginx,由 Nginx 的 secure_link 變量驗證是否通過

原理如下:

  • 哈希算法是不可逆的
  • 客戶端只能拿到執行過哈希算法的 URL
  • 僅生成 URL 的服務器,驗證 URL 是否安全的 Nginx,這兩者才保存原始的字符串
  • 原始字符串通常由以下部分有序組成:
    • 資源位置。如 HTTP 中指定資源的 URI,防止攻擊者拿到一個安全 URI 后可以訪問任意資源
    • 用戶信息。如用戶的 IP 地址,限制其他用戶盜用 URL
    • 時間戳。使安全 URL 及時過期
    • 密鑰。僅服務器端擁有,增加攻擊者猜測出原始字符串的難度

模塊:

  • ngx_http_secure_link_module
    • 未編譯進 Nginx,需要通過 –with-http_secure_link_module 添加
  • 變量
    • secure_link
    • secure_link_expires
Syntax: secure_link expression;
Default: —
Context: http, server, location

Syntax: secure_link_md5 expression;
Default: —
Context: http, server, location

Syntax: secure_link_secret word;
Default: —
Context: location

變量值及帶過期時間的配置示例

  • secure_link
    • 值為空字符串:驗證不通過
    • 值為 0:URL 過期
    • 值為 1:驗證通過
  • secure_link_expires
    • 時間戳的值

命令行生成安全鏈接

  • 生成 md5
echo -n '時間戳URL客戶端IP密鑰' | openssl md5 -binary | openssl base64 | tr +/ - | tr -d =
  • 構造請求 URL
/test1.txt?md5=md5生成值&expires=時間戳(如 2147483647)

Nginx 配置

  • secure_link $arg_md5,$arg_expires;
    • secure_link 後面必須跟兩個值,一個是參數中的 md5,一個是時間戳
  • secure_link_md5 “$secure_link_expires$uri$remote_addr secret”;
    • 按照什麼樣的順序構造原始字符串

實戰

下面是一個實際的配置文件,我這裏就不做演示了,感興趣的可以自己做下實驗。

server {
	server_name securelink.ziyang.com;
    listen 80;
	error_log  logs/myerror.log  info;
	default_type text/plain;
	location /{
		secure_link $arg_md5,$arg_expires;
        secure_link_md5 "$secure_link_expires$uri$remote_addr secret";

        if ($secure_link = "") {
            return 403;
        }

        if ($secure_link = "0") {
            return 410;
        }

		return 200 '$secure_link:$secure_link_expires\n';
	}

	location /p/ {
        secure_link_secret mysecret2;

        if ($secure_link = "") {
            return 403;
        }

        rewrite ^ /secure/$secure_link;
	}

	location /secure/ {
		alias html/;
    	internal;
	}
}

僅對 URI 進行哈希的簡單辦法

除了上面這種相對複雜的方式防盜鏈,還有一種相對簡單的防盜鏈方式,就是只對 URI 進行哈希,這樣當 URI 傳

  • 將請求 URL 分為三個部分:/prefix/hash/link
  • Hash 生成方式:對 “link 密鑰” 做 md5 哈希
  • 用 secure_link_secret secret; 配置密鑰

命令行生成安全鏈接

  • 原請求
    • link
  • 生成的安全請求
    • /prefix/md5/link
  • 生成 md5
    • echo -n 'linksecret' | openssl md5 –hex

Nginx 配置

  • secure_link_secret secret;

這個防盜鏈的方法比較簡單,那麼具體是怎麼用呢?大家都在網上下載過資源對吧,不管是电子書還是軟件,很多網站你點擊下載的時候往往會彈出另外一個頁面去下載,這個新的頁面其實就是請求的 Nginx 生成的安全 URL。如果這個 URL 被拿到的話,其實還是可以用的,所以需要經常的更新密鑰來確保 URL 不會被盜用。

今天這篇文章詳細講了防盜鏈的具體用法,最近的這兩篇文章都是說的已有的變量用法,下一篇文章講一下怎麼生成新的變量。

本文首發於我的個人博客:iziyang.github.io,所有配置文件我已經放在了 Nginx 配置文件,大家可以自取。

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

04 . Docker安全與Docker底層實現

Docker安全

Docker安全性時,主要考慮三個方面

# 1. 由內核的名字空間和控制組機制提供的容器內在安全
# 2. Docker程序(特別是服務端)本身的抗攻擊性
# 3. 內核安全性的加強機制對容器安全性的影響
內核命名空間

Docker容器和LXC容器很相似,所提供的安全特性也差不多。當用docker run啟動一個容器時,在後台Docker為容器創建了一個獨立的命名空間和控制組集合。

命名空間提供了最基礎也是最直接的隔離,在容器中運行的進程不會被運行在主機上的進程和其它容器發 現和作用。

每個容器都有自己獨有的網絡棧,意味着它們不能訪問其他容器的sockets或接口。不過,如果主機系統上做了相應的設置,容器可以像跟主機交互一樣的和其他容器交互。當指定公共端口或使用links來連接2個容器時,容器就可以相互通信了(可以根據配置來限制通信的策略).

從網絡架構的角度來看,所有的容器通過本地主機的網橋接口相互通信,就像物理機器通過物理交換機通信一樣。

那麼,內核中實現命名空間和私有網絡的代碼是否足夠成熟?

內核名字空間從2.6.15版本(2008年7月發布)之後被引入,數年間,這些機制的可靠性在諸多大型生產系統中被實踐驗證。

實際上,名字空間的想法和設計提出的時間要更早,最初是為了在內核中引入一種機制來實現 OpenVZ 的特性。而OpenVZ項目早在2005年就發布了,其設計和實現都已經十分成熟。

控制組

控制組是Linux容器機制的另外一個關鍵組件,負責實現資源的審計和限制.

它提供了很多有用的特性;以及確保各個容器可以公平地分享主機的內存、CPU、磁盤IO等資源;當然,更重要的是,控制組確保了當容器內的資源使用產生壓力時不會連累主機系統。

儘管控制組不負責隔離容器之間相互訪問、處理數據和進程,它在防止拒絕服務(DDOS)攻擊方面是必不可少的。尤其是在多用戶的平台(比如公有或私有的PaaS)上,控制組十分重要。例如,當某些應用程序表現異常的時候,可以保證一致地正常運行和性能。

控制組始於2006年,內核從2.6.24版本開始被引入.

Docker服務端的防護

運行一個容器或應用程序的核心是通過Docker服務端。Docker服務的運行目前需要root權限,因此其安全性十分關鍵。

首先,確保只有可信的用戶才可以訪問Docker服務。Docker允許用戶在主機和容器間共享文件夾,同時不需要限制容器的訪問權限,這就容易讓容器突破資源限制。例如,惡意用戶啟動容器的時候將主機的根目錄/映射到容器的 /host目錄中,那麼容器理論上就可以對主機的文件系統進行任意修改了。這聽起來很瘋狂?但是事實上幾乎所有虛擬化系統都允許類似的資源共享,而沒法禁止用戶共享主機根文件系統到虛擬機系統

這將會造成很嚴重的安全後果。因此,當提供容器創建服務時(例如通過一個web服務器),要更加註意進行參數的安全檢查,防止惡意的用戶用特定參數來創建一些破壞性的容器

為了加強對服務端的保護,Docker的REST API(客戶端用來跟服務端通信)在0.5.2之後使用本地的Unix套接字機制替代了原先綁定在 127.0.0.1 上的TCP套接字,因為後者容易遭受跨站腳本攻擊。現在用戶使用Unix權限檢查來加強套接字的訪問安全。

用戶仍可以利用HTTP提供REST API訪問。建議使用安全機制,確保只有可信的網絡或VPN,或證書保 護機制(例如受保護的stunnel和ssl認證)下的訪問可以進行。此外,還可以使用HTTPS和證書來加強 保護。

最近改進的Linux名字空間機制將可以實現使用非root用戶來運行全功能的容器。這將從根本上解決了容器和主機之間共享文件系統而引起的安全問題。

終極目標是改進 2 個重要的安全特性:

  • 將容器的root用戶映射到本地主機上的非root用戶,減輕容器和主機之間因權限提升而引起的安全問題;
  • 允許Docker服務端在非root權限下運行,利用安全可靠的子進程來代理執行需要特權權限的操作。這些子進程將只允許在限定範圍內進行操作,例如僅僅負責虛擬網絡設定或文件系統管理、配置操作等。
  • 最後,建議採用專用的服務器來運行Docker 和相關的管理服務(例如管理服務比如ssh監控和進程監控、管理工具nrpe、collectd等)。其它的業務服務都放到容器中去運行。
內核能力機制

能力機制(Capability)是Linux內核一個強大的特性,可以提供細粒度的權限訪問控制。 Linux內核自2.2版本起就支持能力機制,它將權限劃分為更加細粒度的操作能力,既可以作用在進程上,也可以作用在文件上。

例如,一個Web服務進程只需要綁定一個低於1024的端口的權限,並不需要root權限。那麼它只需要被授權 net_bind_service能力即可。此外,還有很多其他的類似能力來避免進程獲取root權限。

默認情況下,Docker啟動的容器被嚴格限制只允許使用內核的一部分能力.

使用能力機制對加強Docker容器的安全有很多好處。通常,在服務器上會運行一堆需要特權權限的進程,包括有ssh、cron、syslogd、硬件管理工具模塊(例如負載模塊)、網絡配置工具等等。容器跟這些進程是不同的,因為幾乎所有的特權進程都由容器以外的支持系統來進行管理。

# 1. ssh訪問被主機上ssh服務來管理;
# 2. cron通常應該作為用戶進程執行執行,權限交給使用他服務的應用來處理;
# 3. 日誌系統可由Docker或第三方服務管理;
# 4. 硬件管理無關緊要,容器中也就無需執行udevd以及類似服務;
# 5. 網絡管理也都在主機上設置,除非特殊需求,容器不需要對網絡進行配置.

從上面的例子可以看出,大部分情況下,容器並不需要真正的root權限,容器只需要少數的能力即可.為了加強安全,容器可以禁用一些沒必要的權限:

# 1. 完全禁止任何mount操作.
# 2. 禁止直接訪問本地主機的套接字.
# 3. 禁止訪問一些文件系統的操作,比如創建新的設備,修改文件屬性等.
# 4. 禁止模塊加載.

這樣,就算攻擊者在容器中取得了root權限,也不能獲得本地主機的較高權限,能進行的破壞也有限.

默認認情況下,Docker採用 白名單 機制,禁用必需功能之外的其它權限。 當然,用戶也可以根據自身需求來為Docker容器啟用額外的權限。

其他安全特性.

除了能力機制之外,還可以利用一些現有的安全機制來增強docker的安全性,例如TOMOYO,AppArmor,SELinux,GRSEC等.

Docker 當前默認只開啟了能力機制,用戶可以採用多種方案來加強Docker主機的安全,例如:

  1. 在內核中啟用GRSEC和PAX,這將增加很多編譯和運行時的安全檢查,通過地址隨機化避免惡意探測等,並且,啟用該特性不需要Docker進行任何配置.

  2. 使用一些有增強安全特性的容器模板,比如帶AppArmor的模板和Redhat帶Selinux策略的模板.這些模板提供了額外的安全特性.

  3. 用戶可以自定義訪問控制機制來定製安全策略.

跟其他添加Docker容器的第三方工具一樣(比如網絡拓撲和文件系統共享),有很多類似的機制,在不改變Docker內核情況下就可以加固現有的容器.

小結

總體來說,Docker容器還是十分安全的,特別是在容器不使用root權限來運行進程的話.

另外,用戶可以使用現有工具,比如Apparmor,SELinux,GRSEC來增強安全性,甚至自己在內核中實現更複雜的安全機制.

Docker底層實現

Docker底層的核心技術包括Linux上的命名空間(Namespaces)、控制組(ControlGroups),Union文件系統(Union file systems)和容器格式(Container format).

我們知道,傳統的虛擬機通過在宿主主機中運行hypervisor來模擬一套完整的硬件環境提供給虛擬機的操作系統.虛擬機系統看到的環境是可限制的,也是彼此隔離的,這種直接的做法實現了對資源完整的封裝,但很多時候往往意味着系統資源的浪費,例如,以宿主機和虛擬機系統都為linux系統為例,虛擬機中運行的應用其實是可以利用宿主機系統中的運行環境。

我們知道,在操作系統中,包括內核、文件系統、網絡、PID、UID、IPC、內存、硬盤、CPU等等,所有的資源都是應用進程直接共享的,要想實現虛擬化,除了要實現對內存、CPU、網絡IO、硬盤IO、存儲空間等的限制外,還要實現文件系統、網絡、PID、UID、IPC等等的相互隔離,前者相對容易實現一些,後者則需要宿主機系統的深入支持.

隨着Linux系統對於命名空間功能的完善實現,程序員可以實現上面的所有需要,讓某些進程在彼此隔離的命名空間中運行,大家雖然都共用一個內核和某些運行時環境(l例如一些系統命令和系統等),但是彼此卻看不到,大家都以為系統中只有自己的存在,這種機制就是容器,利用命名空間來做權限的隔離控制,利用cgroups來做資源分配.

容器的基本架構

Dcoker採用了c/s架構,包括客戶端和服務端,Docker守護進程(Daemon)作為服務端接受來自客戶端的請求,並處理這些請求(創建、運行、分發容器).

客戶端和服務端既可以運行在一個機器上,也可以通過socket或者RESTful API來進行通信.

Docker守護進程一般在宿主主機後台運行,等待來自客戶端的消息.

Docker客戶端則為用戶提供一系列可執行的命令,用戶用這些命令實現跟Docker守護進程交互.

命名空間

命名空間是Linux內核一個強大的特性,每個容器都有自己單獨的命名空間,運行在其中的應用都像是在獨立的操作系統運行一樣,命名空間保證了容器之間彼此互不影響.

pid命名空間

不同用戶的進程就是通過pid命名空間隔離開的,且不同命名空間可以有相同的pid,所有的LXC進程在Docker中的父進程為Docker進程,每個LXC進程具有不同的命名空間,同時由於嵌套,因此可以很方便的實現嵌套的Docker容器.

net命名空間

有了pid命名空間,每個命名空間的pid能夠實現相互隔離,但是網絡端口還是共享host的端口,網絡隔離是通過net命名空間實現的,每個net命名空間有單獨的網絡設備,IP地址,路由表,/proc/net目錄,這樣每個容器的網絡就能隔離開來,Docker默認採用veth的方式,將容器中的虛擬網卡host上的一個Docker網橋docker0連接在一起.

IPC命名空間

容器中進程交互採用了Linux常見的進程交互方法,包括信號量,消息隊列和共享內存等,然而同VM不同的是,容器的進程交互實際山還是host上具有相同pid命名空間的進程交互,因此需要在IPC資源中申請加入命名空間信息,每個IPC資源有一個唯一的32位id。

mnt命名空間

類似chroot,將一個進程放到一個特定的目錄執行,mnt命名空間允許不同命名空間的進程看到的文件結構不同,這樣每個命名空間中的進程所看到的文件目錄就被隔離開了,同chroot不同,每個命名空間的容器在/proc/mounts的信息只包含所在命名空間的mount point。

uts命名空間

UTS命名空間允許每個容器擁有獨立的hostname和domain name,使其在網絡上可以被視作一個獨立的節點而非主機上的一個進程.

每個容器可以有不同的用戶和組id,也就是說可以在容器內用容器內部的用戶執行程序而非主機上的用戶.

控制組

控制組(cgroups)是一個Linux內核的一個特性,主要用來對資源進行隔離、限制、審計等,只有能控制分配到容器的資源,才能避免當多個容器同時運行時對系統資源的競爭.

控制組技術最早由Google的程序員在2006年提出,Linux內核從2.6.24開始支持.

控制組可以提供對容器的內存、CPU、磁盤IO等資源的限制和審計管理.

聯合文件系統

聯合文件系統(UnionFS)是一種分層、輕量級並且高性能的文件系統,他支持對文件系統的修改作為一次提交來一層層的疊加,同時可以將不同目錄掛載到同一個虛擬文件系統.

聯合文件系統是Docker鏡像的基礎,鏡像可以通過分層來進行繼承,基於基礎鏡像(沒有父鏡像),可以製作各種具體的應用鏡像.

另外,不同Docker容器就可以共享一些基礎的文件系統層,同時加上自己獨有的改動層,大大提高了存儲的效率.

Docker中使用的AUFS就是一種聯合文件系統,AUFS支持為每一個成員目錄(類似Git的分支)設定只讀(readonly)、讀寫(readwrite)和寫出(whiteout-able)權限,同時AUFS里有一個類似分層的概念,對只讀權限的分支可以邏輯上進行增量的修改(不影響只讀部分的).

Docker目前支持的聯合文件系統包括OverlayFS,AUFS,BtrFS,VFS,ZFS和Device Mapper。

有可能的情況下,推薦使用overlay2存儲驅動,overlay2是目前Docker默認的存儲驅動,以前是aufs,可以通過配置以上提到的其他類型存儲驅動.

容器格式

最初,Docker採用了LXC中的容器格式,從0.7版本開始以後去除LXC,轉而使用自行開發的libcontainer,從1.11開始,進一步演進為runC和containerd。

網絡實現

Docker的網絡實現其實就是利用了Linux上的網絡命名空間和虛擬網絡設備(特別是vethpair).

基本原理

首先,要實現網絡通信,機器需要至少一個網絡接口(物理接口或虛擬接口)來收發數據包,此外,如果不同子網之間要進行通信,需要路由機制.

Docker中的網絡接口默認都是虛擬的接口,虛擬接口的優勢之一就是轉發效率較高,Linux通過在內核中進行數據複製來實現虛擬接口之間的數據轉發,發送接口的發送緩存中的數據包直接複製到接收接口的接受緩存中,對於本地系統和容器內系統來看就像是一個正常的以太網卡,只是他不需要真正同外部網絡設備通信,速度要快很多.

Docker容器網絡就是利用了這項技術,他在本地主機和容器內分別創建一個虛擬接口,並讓他們彼此連通(這樣的一對接口叫做veth pair).

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

【其他文章推薦】

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

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

※超省錢租車方案

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

※網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Java 異常處理的十個建議

前言

Java異常處理的十個建議,希望對大家有幫助~

本文已上傳github:

https://github.com/whx123/JavaHome

公眾號:撿田螺的小男孩

一、盡量不要使用e.printStackTrace(),而是使用log打印。

反例:

try{
  // do what you want  
}catch(Exception e){
  e.printStackTrace();
}

正例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦,{}",e);
}

理由:

  • printStackTrace()打印出的堆棧日誌跟業務代碼日誌是交錯混合在一起的,通常排查異常日誌不太方便。
  • e.printStackTrace()語句產生的字符串記錄的是堆棧信息,如果信息太長太多,字符串常量池所在的內存塊沒有空間了,即內存滿了,那麼,用戶的請求就卡住啦~

二、catch了異常,但是沒有打印出具體的exception,無法更好定位問題

反例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦");
}

正例:

try{
  // do what you want  
}catch(Exception e){
  log.info("你的程序有異常啦,{}",e);
}

理由:

  • 反例中,並沒有把exception出來,到時候排查問題就不好查了啦,到底是SQl寫錯的異常還是IO異常,還是其他呢?所以應該把exception打印到日誌中哦~

三、不要用一個Exception捕捉所有可能的異常

反例:

public void test(){
    try{
        //…拋出 IOException 的代碼調用
        //…拋出 SQLException 的代碼調用
    }catch(Exception e){
        //用基類 Exception 捕捉的所有可能的異常,如果多個層次都這樣捕捉,會丟失原始異常的有效信息哦
        log.info(“Exception in test,exception:{}”, e);
    }
}

正例:

public void test(){
    try{
        //…拋出 IOException 的代碼調用
        //…拋出 SQLException 的代碼調用
    }catch(IOException e){
        //僅僅捕捉 IOException
        log.info(“IOException in test,exception:{}”, e);
    }catch(SQLException e){
        //僅僅捕捉 SQLException
        log.info(“SQLException in test,exception:{}”, e);
    }
}

理由:

  • 用基類 Exception 捕捉的所有可能的異常,如果多個層次都這樣捕捉,會丟失原始異常的有效信息哦

四、記得使用finally關閉流資源或者直接使用try-with-resource

反例:

FileInputStream fdIn = null;
try {
    fdIn = new FileInputStream(new File("/jay.txt"));
    //在這裏關閉流資源?有沒有問題呢?如果發生異常了呢?
    fdIn.close();
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}

正例1:

需要使用finally關閉流資源,如下

FileInputStream fdIn = null;
try {
    fdIn = new FileInputStream(new File("/jay.txt"));
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}finally {
    try {
        if (fdIn != null) {
            fdIn.close();
        }
    } catch (IOException e) {
        log.error(e);
    }
}

正例2:

當然,也可以使用JDK7的新特性try-with-resource來處理,它是Java7提供的一個新功能,它用於自動資源管理。

  • 資源是指在程序用完了之後必須要關閉的對象。
  • try-with-resources保證了每個聲明了的資源在語句結束的時候會被關閉
  • 什麼樣的對象才能當做資源使用呢?只要實現了java.lang.AutoCloseable接口或者java.io.Closeable接口的對象,都OK。
try (FileInputStream inputStream = new FileInputStream(new File("jay.txt")) {
    // use resources   
} catch (FileNotFoundException e) {
    log.error(e);
} catch (IOException e) {
    log.error(e);
}

理由:

  • 如果不使用finally或者try-with-resource,當程序發生異常,IO資源流沒關閉,那麼這個IO資源就會被他一直佔著,這樣別人就沒有辦法用了,這就造成資源浪費。

五、捕獲異常與拋出異常必須是完全匹配,或者捕獲異常是拋異常的父類

反例:

//BizException 是 Exception 的子類
public class BizException extends Exception {}
//拋出父類Exception
public static void test() throws Exception {}

try {
    test(); //編譯錯誤
} catch (BizException e) { //捕獲異常子類是沒法匹配的哦
    log.error(e);
}

正例:

//拋齣子類Exception
public static void test() throws BizException {}

try {
    test();
} catch (Exception e) {
    log.error(e);
}

六、捕獲到的異常,不能忽略它,至少打點日誌吧

反例:

public static void testIgnoreException() throws Exception {
    try {       
        // 搞事情
    } catch (Exception e) {     //一般不會有這個異常
        
    }
}

正例:

public static void testIgnoreException() {
    try {
        // 搞事情
    } catch (Exception e) {     //一般不會有這個異常
        log.error("這個異常不應該在這裏出現的,{}",e); 
    }
}

理由:

  • 雖然一個正常情況都不會發生的異常,但是如果你捕獲到它,就不要忽略呀,至少打個日誌吧~

七、注意異常對你的代碼層次結構的侵染(早發現早處理)

反例:

public UserInfo queryUserInfoByUserId(Long userid) throw SQLException {
    //根據用戶Id查詢數據庫
}

正例:

public UserInfo queryUserInfoByUserId(Long userid) {
    try{
        //根據用戶Id查詢數據庫
    }catch(SQLException e){
        log.error("查詢數據庫異常啦,{}",e);
    }finally{
        //關閉連接,清理資源
    }
}

理由:

  • 我們的項目,一般都會把代碼分 Action、Service、Dao 等不同的層次結構,如果你是DAO層處理的異常,儘早處理吧,如果往上 throw SQLException,上層代碼就還是要try catch處理啦,這就污染了你的代碼~

八、自定義封裝異常,不要丟棄原始異常的信息Throwable cause

我們常常會想要在捕獲一個異常后拋出另一個異常,並且希望把原始異常的信息保存下來,這被稱為異常鏈。公司的框架提供統一異常處理就用到異常鏈,我們自定義封裝異常,不要丟棄原始異常的信息,否則排查問題就頭疼啦

反例:

public class TestChainException {
    public void readFile() throws MyException{
        try {
            InputStream is = new FileInputStream("jay.txt");
            Scanner in = new Scanner(is);
            while (in.hasNext()) {
                System.out.println(in.next());
            }
        } catch (FileNotFoundException e) {
            //e 保存異常信息
            throw new MyException("文件在哪裡呢");
        }
    }
    public void invokeReadFile() throws MyException{
        try {
            readFile();
        } catch (MyException e) {
            //e 保存異常信息
            throw new MyException("文件找不到");
        }
    }
    public static void main(String[] args) {
        TestChainException t = new TestChainException();
        try {
            t.invokeReadFile();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}
//MyException 構造器
public MyException(String message) {
        super(message);
    }

運行結果如下,沒有了Throwable cause,不好排查是什麼異常了啦

正例:


public class TestChainException {
    public void readFile() throws MyException{
        try {
            InputStream is = new FileInputStream("jay.txt");
            Scanner in = new Scanner(is);
            while (in.hasNext()) {
                System.out.println(in.next());
            }
        } catch (FileNotFoundException e) {
            //e 保存異常信息
            throw new MyException("文件在哪裡呢", e);
        }
    }
    public void invokeReadFile() throws MyException{
        try {
            readFile();
        } catch (MyException e) {
            //e 保存異常信息
            throw new MyException("文件找不到", e);
        }
    }
    public static void main(String[] args) {
        TestChainException t = new TestChainException();
        try {
            t.invokeReadFile();
        } catch (MyException e) {
            e.printStackTrace();
        }
    }
}
//MyException 構造器
public MyException(String message, Throwable cause) {
        super(message, cause);
    }

九、運行時異常RuntimeException ,不應該通過catch 的方式來處理,而是先預檢查,比如:NullPointerException處理

反例:

try {
  obj.method() 
} catch (NullPointerException e) {
...
}

正例:

if (obj != null){
   ...
}

十、注意異常匹配的順序,優先捕獲具體的異常

注意異常的匹配順序,因為只有第一個匹配到異常的catch塊才會被執行。如果你希望看到,是NumberFormatException異常,就拋出NumberFormatException,如果是IllegalArgumentException就拋出IllegalArgumentException。

反例:

try {
    doSomething("test exception");
} catch (IllegalArgumentException e) {       
    log.error(e);
} catch (NumberFormatException e) {
    log.error(e);
}

正例:

try {
    doSomething("test exception");
} catch (NumberFormatException e) {       
    log.error(e);
} catch (IllegalArgumentException e) {
    log.error(e);
}

理由:

  • 因為NumberFormatException是IllegalArgumentException 的子類,反例中,不管是哪個異常,都會匹配到IllegalArgumentException,就不會再往下執行啦,因此不知道是否是NumberFormatException。所以需要優先捕獲具體的異常,把NumberFormatException放前面~

公眾號

  • 歡迎關注我個人公眾號,交個朋友,一起學習哈~
  • 如果答案整理有錯,歡迎指出哈,感激不盡~

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

Spring源碼系列(一)–詳細介紹bean組件

簡介

spring-bean 組件是 Spring IoC 的核心,我們可以使用它的 beanFactory 來獲取所需的對象,對象的實例化、屬性裝配和初始化等都可以交給 spring 來管理。

針對 spring-bean 組件,我計劃分成兩篇博客來講解。本文會詳細介紹這個組件,包括以下內容。下一篇再具體分析它的源碼。

  1. spring-bean 組件的相關概念:實例化、屬性裝配、初始化、bean、beanDefinition、beanFactory。
  2. bean 組件的使用:註冊bean、獲取bean、屬性裝配、處理器等。

項目環境說明

正文開始前,先介紹下示例代碼使用的環境等。

工程環境

JDK:1.8.0_231

maven:3.6.1

IDE:Spring Tool Suites4 for Eclipse 4.12

Spring:5.2.6.RELEASE

依賴引入

除了引入 spring,這裏還額外引入了日誌和單元測試。

    <properties>
        <spring.version>5.2.6.RELEASE</spring.version>
    </properties>
    
    <dependencies>
        <!-- spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
		<!-- junit -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!-- logback -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
            <type>jar</type>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <type>jar</type>
        </dependency>
    </dependencies>

幾個重要概念

實例化、屬性裝配和初始化

在 spring-bean 組件的設計中,這三個詞完整、有序地描述了生成一個新對象的整個流程,是非常重要的理論基礎。它們的具體含義如下:

  1. 實例化:創建出一個新對象。
  2. 屬性裝配:給對象的成員屬性賦值。
  3. 初始化:調用對象的初始化方法。

下面使用一段代碼來簡單演示下這個流程。

public class UserService implements IUserService {
    
    private UserDao userDao;
    
    
    public UserService() {
        super();
        System.err.println("UserService構造方法被調用");
        System.err.println("        ||");
        System.err.println("        \\/");
    }
    
    public void init() {
        System.err.println("UserService的init方法被調用");
        System.err.println("        ||");
        System.err.println("        \\/");
    }
    
    
    public UserDao getUserDao() {
        return userDao;
    }
    
    public void setUserDao(UserDao userDao) {
        System.err.println("UserService的屬性裝配中");
        System.err.println("        ||");
        System.err.println("        \\/");
        this.userDao = userDao;
    }
    
}

如果我們將這個 bean 交給 spring 管理,獲取 bean 時會在控制台打印以下內容:

什麼是bean

按照官方的說法, bean 是一個由 Spring IoC 容器實例化、組裝和管理的對象。我認為,這種表述是錯誤的,通過registerSingleton方式註冊的 bean,它就不是由 Spring IoC 容器實例化、組裝,所以,更準確的表述應該是這樣:

某個類的對象、FactoryBean 對象、描述對象或 FactoryBean 描述對象,被註冊到了 Spring IoC 容器,這時通過 Spring IoC 容器獲取的這個類的對象就是 bean。

舉個例子,使用了 Spring 的項目中, Controller 對象、Service 對象、DAO 對象等都屬於 bean。

至於什麼是 IoC 容器,在 spring-bean 組件中,我認為,beanFactory 就屬於 IoC 容器。

什麼是beanFactory

從客戶端來看,一個完整的 beanFactory 工廠包含以下基本功能:

  1. 註冊別名。對應下圖的AliasRegistry接口。
  2. 註冊單例對象。對應下圖的SingletonBeanRegistry接口。
  3. 註冊BeanDefinition對象。對應下圖的BeanDefinitionRegistry接口。
  4. 獲取 bean。對應下圖的BeanFactory接口。

在 spring-bean 組件中,DefaultListableBeanFactory就是一個完整的 beanFactory 工廠,也可以說是一個 IoC 容器。接下來的例子將直接使用它來作為 beanFactory。

至於其他的接口,這裏也補充說明下。HierarchicalBeanFactory用於提供父子工廠的支持,ConfigurableBeanFactory用於提供配置 beanFactory 的支持,ListableBeanFactory用於提供批量獲取 bean 的支持(不包含父工廠的 bean),AutowireCapableBeanFactory用於提供實例化、屬性裝配、初始化等一系列管理 bean 生命周期的支持。

什麼是beanDefinition

beanDefinaition 是一個描述對象,用來描述 bean 的實例化、初始化等信息。

在 spring-bean 組件中,beanDefinaition主要包含以下四種:

  1. RootBeanDefinition:beanFactory 中最終用於 createBean 的 beanDefinaition,不允許添加 parentName。在 BeanFactory 中以下三種實現類都會被包裝成RootBeanDefinition用於 createBean。
  2. ChildBeanDefinition:必須設置 parentName 的 beanDefinaition。當某個 Bean 的描述對象和另外一個的差不多時,我們可以直接定義一個ChildBeanDefinition,並設置它的 parentName 為另外一個的 beanName,這樣就不用重新設置一份。
  3. GenericBeanDefinition:通用的 beanDefinaition,可以設置 parentName,也可以不用設置。
  4. AnnotatedGenericBeanDefinition:在GenericBeanDefinition基礎上增加暴露註解數據的方法。

spring-bean 組件提供了BeanDefinitionBuilder用於創建 beanDefinaition,下面的例子會頻繁使用到。

使用例子

入門–簡單地註冊和獲取bean

下面通過一個入門例子來介紹註冊和獲取 bean 的過程。

    @Test
    public void testBase() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
    }

兩種註冊bean的方式

beanFactory 除了支持註冊 beanDefinition,還允許直接註冊 bean 實例,如下。和前者相比,後者的實例化、屬性裝配和初始化都沒有交給 spring 管理。

    @Test
    public void testRegisterWays() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 註冊Bean-- BeanDefinition方式
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 註冊Bean-- Bean實例方式
        beanFactory.registerSingleton("userService2", new UserService());
        
        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
        IUserService userService2 = (IUserService)beanFactory.getBean("userService2");
        System.err.println(userService2.get("userId"));
    }

當然,這種方式僅支持單例 bean 的註冊,多例的就沒辦法了。

註冊多例bean

默認情況下,我們從 beanFactory 獲取到的 bean 都是單例的,即每次 getBean 獲取到的都是同一個對象,實際項目中,有時我們需要獲取到多例的 bean,這個時候就可以通過設置 beanDefinition 的 scope 來處理。如下:

    @Test
    public void testScope() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 註冊Bean-- BeanDefinition方式
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        rootBeanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE);
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean--通過BeanType
        IUserService userService1 = beanFactory.getBean(IUserService.class);
        IUserService userService2 = beanFactory.getBean(IUserService.class);
        assertNotEquals(userService1, userService2);
    }

多種獲取bean的方式

beanFactory 提供了多種方式來獲取 bean 實例,如下。如果同時使用 beanName 和 beanType,獲取到指定 beanName 的 bean 後會進行類型檢查和類型類型,如果都不通過,將會報錯。

    @Test
    public void testGetBeanWays() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", rootBeanDefinition);
        
        // 獲取Bean--通過BeanName
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
        // 獲取Bean--通過BeanType
        IUserService userService2 = beanFactory.getBean(IUserService.class);
        System.err.println(userService2.get("userId"));
        // 獲取Bean--通過BeanName+BeanType的方式
        IUserService userService3 = beanFactory.getBean("userService", IUserService.class);
        System.err.println(userService3.get("userId"));
    }

不同形式的beanName

通過 name 獲取 bean,這個 name 包含以下三種形式:

  1. beanName,即註冊 bean 時用的 beanName。這是使用最多的形式,需要注意一點,如果 beanName 對應的 bean 是FactoryBean,並不會返回FactoryBean的實例,而是會返回FactoryBean.getObject方法的返回結果。
  2. alias,即我們通過SimpleAliasRegistry.registerAlias(name, alias)方法註冊到 beanFactory 的別名。這時,需要將 name 解析為 alias 對應的 beanName 來獲取 bean。
  3. ‘&’ + factorybeanName,這時為了獲取FactoryBean的一種特殊格式。
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 註冊Bean--註冊的是一個 FactoryBean
        UserServiceFactoryBean userServiceFactoryBean = new UserServiceFactoryBean();
        beanFactory.registerSingleton("userServiceFactoryBean", userServiceFactoryBean);

        // 註冊BeanName的別名
        beanFactory.registerAlias("userServiceFactoryBean", "userServiceAlias01");

        // 通過BeanName獲取
        assertEquals(userServiceFactoryBean.getObject(), beanFactory.getBean("userServiceFactoryBean"));

        // 通過別名獲取
        assertEquals(userServiceFactoryBean.getObject(), beanFactory.getBean("userServiceAlias01"));

        // 通過&+FactoryBeanName的方式
        assertEquals(userServiceFactoryBean, beanFactory.getBean("&userServiceFactoryBean"));

bean衝突的處理

通過 beanType 的方式獲取 bean,如果存在多個同類型的 bean且無法確定最優先的那一個,就會報錯。

    @Test
    public void testPrimary() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); 
        
        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        
        // 註冊Bean
        beanFactory.registerBeanDefinition("UserRegisterBeanDefinition", rootBeanDefinition);
        beanFactory.registerSingleton("UserRegisterSingleton", new User("zzs002", 19));
        beanFactory.registerSingleton("UserRegisterSingleton2", new User("zzs002", 18));
        
        // 獲取Bean--通過BeanType
        User user = beanFactory.getBean(User.class);
        System.err.println(user);
    }

運行以上方法,將出現 NoUniqueBeanDefinitionException 的異常。

針對上面的這種問題,spring 的處理方法如下:

  1. 檢查是否存在唯一一個通過registerBeanDefinition且isPrimary = true的(存在多個會報錯),存在的話將它作為匹配到的唯一 beanName;
  2. 通過我們註冊的OrderComparator來確定優先值最小的作為唯一 beanName。注意,通過registerSingleton註冊的和通過registerBeanDefinition註冊的,比較的對象是不一樣的,前者比較的對象是 bean 實例,後者比較的對象是 bean 類型,另外,這種方法最好不要存在相同優先級的 bean。

所以,為了解決這種衝突,可以設置BeanDefinition對象的 isPrimary = true,或者為 beanFactory 設置OrderComparator,代碼如下:

    @Test
    public void testPrimary() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 為BeanFactory設置比較器
        beanFactory.setDependencyComparator(new OrderComparator() {

            @Override
            public Integer getPriority(Object obj) {
                return obj.hashCode();
            }
        });

        // 創建BeanDefinition對象
        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        // rootBeanDefinition.setPrimary(true); // 設置BeanDefinition對象為isPrimary

        // 註冊Bean
        beanFactory.registerBeanDefinition("userRegisterBeanDefinition", rootBeanDefinition);
        beanFactory.registerSingleton("userRegisterSingleton", new User("zzs002", 19));
        beanFactory.registerSingleton("userRegisterSingleton2", new User("zzs003", 18));

        // 獲取Bean--通過BeanType
        User user = beanFactory.getBean(User.class);
        System.err.println(user);
    }

使用TypeConverter獲取自定義類型的對象

當我們使用 beanType 來獲取 bean 時,如果獲取到的 bean 不是指定的類型,這時,不會立即報錯,beanFactory 會嘗試使用我們註冊的TypeConverter來強制轉換。而這個類型轉換器我們可以自定義設置,如下。

    @Test
    public void testTypeConverter() {
        
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        // 註冊類型轉換器
        beanFactory.setTypeConverter(new TypeConverterSupport() {
            @SuppressWarnings("unchecked")
            @Override
            public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
                    @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
                // 將User轉換為UserVO
                if(UserVO.class.equals(requiredType) && User.class.isInstance(value)) {
                    User user = (User)value;
                    return (T)new UserVO(user);
                }
                return null;
            }
        });

        BeanDefinition rootBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(User.class).getBeanDefinition();
        beanFactory.registerBeanDefinition("User", rootBeanDefinition);

        UserVO bean = beanFactory.getBean("User", UserVO.class);
        Assert.assertTrue(UserVO.class.isInstance(bean));
    }

屬性裝配

beanFactory 在進行屬性裝配時,會讀取 beanDefinition 對象中的PropertyValues中的propertyName=propertyValue,所以,我們想要對 bean 注入什麼參數,只要在定義 beanDefinition 時指定就行。

    @Test
    public void testPopulate() {
        // 創建BeanFactory對象
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 定義userService的beanDefinition
        AbstractBeanDefinition userServiceBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        // 定義userDao的beanDefinition
        AbstractBeanDefinition userDaoBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserDao.class).getBeanDefinition();
        // 給userService設置裝配屬性
        userServiceBeanDefinition.getPropertyValues().add("userDao", userDaoBeanDefinition);
        // userServiceBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        // userServiceBeanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);

        // 註冊Bean
        beanFactory.registerBeanDefinition("userService", userServiceBeanDefinition);
        beanFactory.registerBeanDefinition("userDao", userDaoBeanDefinition);

        // 獲取Bean
        IUserService userService = (IUserService)beanFactory.getBean("userService");
        userService.save(null);
    }

運行以上方法,發現 userDao 對象被成功注入到了 userService 對象中!

這裏補充一點,beanFactory 除了通過 beanDefinition 中的PropertyValues獲取 propertyName=propertyValue,還可以讀取 bean 中的屬性來自動定義 propertyName=propertyValue,只要設置 beanDefinition 的 autowireMode 就可以了。

bean 實例化、屬性裝配和初始化的處理器

前面講到,我們將 bean 的實例化、屬性裝配和初始化都交給了 spring 處理,然而,有時我們需要在這些節點對 bean 進行自定義的處理,這時就需要用到 beanPostProcessor。

這裏我簡單演示下如何添加處理器,以及處理器的執行時機,至於處理器的具體實現,我就不多擴展了。

    @Test
    public void testPostProcessor() {
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

        // 添加實例化處理器
        beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() {
            // 如果這裏我們返回了對象,則beanFactory會將它作為bean直接返回,不再進行bean的實例化、屬性裝配和初始化等操作
            public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
                if(UserService.class.equals(beanClass)) {
                    System.err.println("bean實例化之前的處理。。 --> ");
                }
                return null;
            }

            // 這裏通過返回的布爾值判斷是否需要繼續對bean進行屬性裝配和初始化等操作
            public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("bean實例化之後的處理。。 --> ");
                }
                return true;
            }
        });

        // 添加裝配處理器
        beanFactory.addBeanPostProcessor(new InstantiationAwareBeanPostProcessor() {

            // 這裏可以在屬性裝配前對參數列表進行調整
            public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("屬性裝配前對參數列表進行調整 --> ");
                }
                return InstantiationAwareBeanPostProcessor.super.postProcessProperties(pvs, bean, beanName);
            }

        });

        // 添加初始化處理器
        beanFactory.addBeanPostProcessor(new BeanPostProcessor() {

            // 初始化前對bean進行改造
            public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("初始化前,對Bean進行改造。。 --> ");
                }
                return bean;
            }

            // 初始化后對bean進行改造
            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
                if(UserService.class.isInstance(bean)) {
                    System.err.println("初始化后,對Bean進行改造。。 --> ");
                }
                return bean;
            }
        });
        // 定義userService的beanDefinition
        AbstractBeanDefinition userServiceBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserService.class).getBeanDefinition();
        // 定義userDao的beanDefinition
        AbstractBeanDefinition userDaoBeanDefinition = BeanDefinitionBuilder.rootBeanDefinition(UserDao.class).getBeanDefinition();
        // 給userService添加裝配屬性
        userServiceBeanDefinition.getPropertyValues().add("userDao", userDaoBeanDefinition);
        // 給userService設置初始化方法
        userServiceBeanDefinition.setInitMethodName("init");
        
        // 註冊bean
        beanFactory.registerBeanDefinition("userService", userServiceBeanDefinition);
        beanFactory.registerBeanDefinition("userDao", userDaoBeanDefinition);

        IUserService userService = (IUserService)beanFactory.getBean("userService");
        System.err.println(userService.get("userId"));
    }

運行以上方法,控制台打印出了整個處理流程。實際開發中,我們可以通過設置處理器來改變改造生成的 bean 。

以上,基本介紹完 spring-bean 組件的使用,下篇博客再分析源碼,如果在分析過程中發現有其他特性,也會在這篇博客的基礎上擴展。

相關源碼請移步: spring-beans

本文為原創文章,轉載請附上原文出處鏈接:https://www.cnblogs.com/ZhangZiSheng001/p/13126053.html

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

※產品缺大量曝光嗎?你需要的是一流包裝設計!

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