Python3 源碼閱讀 – 垃圾回收機制

Python的垃圾回收機制包括了兩大部分:

  • 引用計數(大部分在 Include/object.h 中定義)
  • 標記清除+隔代回收(大部分在 Modules/gcmodule.c 中定義)

1. 引用計數機制

python中萬物皆對象,他的核心結構是:PyObject

typedef __int64 ssize_t;

typedef ssize_t         Py_ssize_t;

typedef struct _object {
    _PyObject_HEAD_EXTRA
    Py_ssize_t ob_refcnt;   // Py_ssize_t __int64
    struct _typeobject *ob_type;
} PyObject;

typedef struct {
    PyObject ob_base;
    Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;

PyObject是每個對象的底層數據結構,其中ob_refcnt就是作為引用計數。當一個對象有新的引用時, 它的ob_refcnt就會增加,當引用它的對象被刪除,它的ob_refcnt就會減少,當引用技術為0時,該對象的生命結束了。

  1. 引用計數+1的情況
    • 對象被創建 eg: a=2
    • 對象被引用 eg: b=a
    • 對象被作為參數,傳入到一個函數中,例如func(a)
    • 對象作為一個元素,存儲在容器中,例如list1=[a, b]
  2. 引用計數-1的情況
    • 對象的別名被显示的銷毀 eg: del a
    • 對象的別名被賦予新的對象 eg: a=34
    • 一個對象離開它的作用域, 例如f函數執行完畢時,func函數中的局部變量(全局變量不會)
    • 對象所在的容器被銷毀,或者從容器中刪除

如何查看對象的引用計數

import sys
a = 'hello'
sys.getrefcount(a)   
// 注意: getrefcount(a) 傳入a時, a的引用計數會加1

1.1 什麼時候觸發回收

當一個對象的引用計數變為了 0, 會直接進入釋放空間的流程

/* cpython/Include/object.h */
static inline void _Py_DECREF(const char *filename, int lineno,
                              PyObject *op)
{
    _Py_DEC_REFTOTAL;
    if (--op->ob_refcnt != 0) {
#ifdef Py_REF_DEBUG
        if (op->ob_refcnt < 0) {
            _Py_NegativeRefcount(filename, lineno, op);
        }
#endif
    }
    else {
    	/* // _Py_Dealloc 會找到對應類型的 descructor, 並且調用這個 descructor
        destructor dealloc = Py_TYPE(op)->tp_dealloc;
        (*dealloc)(op);
        */
        _Py_Dealloc(op);
    }
}

2. 常駐內存對象

引用計數機制所帶來的維護引用計數的額外操作,與python運行中所進行的內存分配、釋放、引用賦值的次數是成正比的,這一點,相對於主流的垃圾回收技術,比如標記–清除(mark--sweep)、停止–複製(stop--copy)等方法相比是一個弱點,因為它們帶來額外操作只和內存數量有關,至於多少人引用了這塊內存則不關心。因此為了與引用計數搭配、在內存的分配和釋放上獲得最高的效率,python設計了大量的內存池機制,比如小整數對象池、字符串的intern機制,列表的freelist緩衝池等等,這些大量使用的面向特定對象的內存池機制正是為了彌補引用計數的軟肋。

2.1 小整數對象池

#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS           257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS           5
#endif

#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* Small integers are preallocated in this array so that they
   can be shared.
   The integers that are preallocated are those in the range
   -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

Py_INCREF(op)  增加對象引用計數

Py_DECREF(op)  減少對象引用計數, 如果計數位0, 調用_Py_Dealloc

_Py_Dealloc(op) 調用對應類型的 tp_dealloc 方法

小整數對象池就是一個PyLongObject 數組, 大小=257+5=262, 範圍是[-5, 257) 注意左閉右開.

python對小整數的定義是[-5, 257), 這些整數對象是提前建立好的,不會被垃圾回收,在一個python程序中,所有位於這個範圍內的整數使用的都是同一個對象

2.2 大整數對象池

疑惑:《Python源碼剖析》提到的整數對象池block_list應該已經不存在了(因為PyLongObject為變長對象)。Python2中的PyIntObject實際是對c中的long的包裝。所以Python2也提供了專門的緩存池,供大整數輪流使用,避免每次使用不斷的malloc分配內存帶來的效率損耗,可參考劉志軍老師的講解。既然沒有池了,malloc/free會帶來的不小性能損耗。Guido認為Py3.0有極大的優化空間,在字符串和整形操作上可以取得很好的優化結果。

/* Allocate a new int object with size digits.
   Return NULL and set exception if we run out of memory. */

#define MAX_LONG_DIGITS \
    ((PY_SSIZE_T_MAX - offsetof(PyLongObject, ob_digit))/sizeof(digit))

PyLongObject *
_PyLong_New(Py_ssize_t size)
{
    PyLongObject *result;
    /* Number of bytes needed is: offsetof(PyLongObject, ob_digit) +
       sizeof(digit)*size.  Previous incarnations of this code used
       sizeof(PyVarObject) instead of the offsetof, but this risks being
       incorrect in the presence of padding between the PyVarObject header
       and the digits. */
    if (size > (Py_ssize_t)MAX_LONG_DIGITS) {
        PyErr_SetString(PyExc_OverflowError,
                        "too many digits in integer");
        return NULL;
    }
    result = PyObject_MALLOC(offsetof(PyLongObject, ob_digit) +
                             size*sizeof(digit));
    if (!result) {
        PyErr_NoMemory();
        return NULL;
    }
    return (PyLongObject*)PyObject_INIT_VAR(result, &PyLong_Type, size);
}

result = PyObject_MALLOC(offsetof(PyLongObject, ob_digit) + size*sizeof(digit));

每一個大整數,均創建一個新的對象。id(num)均不同。

2.4 字符串的intern機制

Objects/unicodeobject.c
Objects/codeobject.c

PyStringObject對象的intern機制之目的是:對於被intern之後的字符串,比如“Ruby”,在整個Python的運行期間,系統中都只有唯一的一個與字符串“Ruby”對應的PyStringObject對象。這樣當判斷兩個PyStringObject對象是否相同時,如果它們都被intern了,那麼只需要簡單地檢查它們對應的PyObject*是否相同即可。這個機制既節省了空間,又簡化了對PyStringObject對象的比較,嗯,可謂是一箭雙鵰哇。

摘自:《Python源碼剖析》 — 陳儒

Python3中PyUnicodeObject對象的intern機制和Python2的PyStringObject對象intern機制一樣,主要為了節省內存的開銷,利用字符串對象的不可變性,對存在的字符串對象重複利用

In [50]: a = 'python'

In [51]: b = 'python'

In [52]: id(a)
Out[52]: 442782398256

In [53]: id(b)
Out[53]: 442782398256

In [54]: b = 'hello python'

In [55]: a = 'hello python'

In [56]: id(a)
Out[56]: 442808585520

In [57]: id(b)
Out[57]: 442726541488

什麼樣的字符串會使用intern機制?

intern機制跟編譯時期有關,相關代碼在Objects/codeobject.c

/* Intern selected string constants */
static int
intern_string_constants(PyObject *tuple)
{
    int modified = 0;
    Py_ssize_t i;

    for (i = PyTuple_GET_SIZE(tuple); --i >= 0; ) {
        PyObject *v = PyTuple_GET_ITEM(tuple, i);
        if (PyUnicode_CheckExact(v)) {
            if (PyUnicode_READY(v) == -1) {
                PyErr_Clear();
                continue;
            }
            if (all_name_chars(v)) {
                PyObject *w = v;
                PyUnicode_InternInPlace(&v);
                if (w != v) {
                    PyTuple_SET_ITEM(tuple, i, v);
                    modified = 1;
                }
            }
        }
        /*....*/
}
    
/* all_name_chars(s): true iff s matches [a-zA-Z0-9_]* */
static int
all_name_chars(PyObject *o)
{
    const unsigned char *s, *e;

    if (!PyUnicode_IS_ASCII(o))
        return 0;

    s = PyUnicode_1BYTE_DATA(o);
    e = s + PyUnicode_GET_LENGTH(o);
    for (; s != e; s++) {
        if (!Py_ISALNUM(*s) && *s != '_')
            return 0;
    }
    return 1;
}

可見 all_name_chars 決定了是否會 intern,簡單來說就是 ascii 字母,数字和下劃線組成的字符串會被緩存。但是不僅如此。2.5還會說

/* This dictionary holds all interned unicode strings.  Note that references
   to strings in this dictionary are *not* counted in the string's ob_refcnt.
   When the interned string reaches a refcnt of 0 the string deallocation
   function will delete the reference from this dictionary.

   Another way to look at this is that to say that the actual reference
   count of a string is:  s->ob_refcnt + (s->state ? 2 : 0)
*/
static PyObject *interned = NULL;
/*省略*/
void
PyUnicode_InternInPlace(PyObject **p)
{
    PyObject *s = *p;
    PyObject *t;
#ifdef Py_DEBUG
    assert(s != NULL);
    assert(_PyUnicode_CHECK(s));
#else
    if (s == NULL || !PyUnicode_Check(s))
        return;
#endif
    /* If it's a subclass, we don't really know what putting
       it in the interned dict might do. */
    if (!PyUnicode_CheckExact(s))
        return;
    // [1]
    if (PyUnicode_CHECK_INTERNED(s))
        return;
    if (interned == NULL) {
        interned = PyDict_New();
        if (interned == NULL) {
            PyErr_Clear(); /* Don't leave an exception */
            return;
        }
    }
    Py_ALLOW_RECURSION
    // [2]
    t = PyDict_SetDefault(interned, s, s);
    Py_END_ALLOW_RECURSION
    if (t == NULL) {
        PyErr_Clear();
        return;
    }
    // [3]
    if (t != s) {
        Py_INCREF(t);
        Py_SETREF(*p, t);
        return;
    }
    // [4]
    /* The two references in interned are not counted by refcnt.
       The deallocator will take care of this */
    Py_REFCNT(s) -= 2;
    _PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}

通過函數我們可以得知,python中維護這一個interned變量的指針,這個變量指向PyDict_New創建的對象,而PyDict_New實際上創建了一個PyDictObject對象,是Python中dict類型的對象。實際上intern機制就是維護一個字典,這個字典中記錄著被intern機制處理過的字符串對象,[1]PyUnicode_CHECK_INTERNED宏檢查字符串對象的state.interned是否被標記,

如果字符串對象的state.interned被標記了,就直接返回;[2]嘗試把沒有被標記的字符串對象s作為key-value加入interned字典中;[3]處表示字符串對象s已經在interned字典中(對應的value值是字符串對象t),(通過Py_SETREF宏來改變p指針的指向),且原字符串對象p會因引用計數為零被回收。Py_SETREF宏在Include/object.h定義着:

/* Safely decref `op` and set `op` to `op2`.
 *
 * As in case of Py_CLEAR "the obvious" code can be deadly:
 *
 *     Py_DECREF(op);
 *     op = op2;
 *
 * The safe way is:
 *
 *      Py_SETREF(op, op2);
 *
 * That arranges to set `op` to `op2` _before_ decref'ing, so that any code
 * triggered as a side-effect of `op` getting torn down no longer believes
 * `op` points to a valid object.
 *
 * Py_XSETREF is a variant of Py_SETREF that uses Py_XDECREF instead of
 * Py_DECREF.
 */

#define Py_SETREF(op, op2)                      \
    do {                                        \
        PyObject *_py_tmp = (PyObject *)(op);   \
        (op) = (op2);                           \
        Py_DECREF(_py_tmp);                     \
    } while (0)

[4]中把新加入interned字典中的字符串對象做減引用操作,並把state.interned標記成SSTATE_INTERNED_MORTALSSTATE_INTERNED_MORTAL表示字符串對象被intern機制處理,但會隨着引用計數被回收;interned標記還有另外一種SSTATE_INTERNED_IMMORTAL,表示被intern機制處理但對象不可銷毀,會與Python解釋器同在。PyUnicode_InternInPlace只能創建SSTATE_INTERNED_MORTAL狀態的字符串,要想創建SSTATE_INTERNED_IMMORTAL狀態的字符串需要通過另外一個接口,強制改變intern的狀態

void
PyUnicode_InternImmortal(PyObject **p)
{
    PyUnicode_InternInPlace(p);
    if (PyUnicode_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) {
        _PyUnicode_STATE(*p).interned = SSTATE_INTERNED_IMMORTAL;
        Py_INCREF(*p);
    }
}

為什麼引用Py_REFCNT(s) -= 2;要-2呢?

PyDict_SetDefault(PyObject *d, PyObject *key, PyObject *defaultobj)
{
    PyDictObject *mp = (PyDictObject *)d;
    PyObject *value;
    Py_hash_t hash;

    /*...*/
    if (ix == DKIX_EMPTY) {
        /*...*/
        Py_ssize_t hashpos = find_empty_slot(mp->ma_keys, hash);
        ep0 = DK_ENTRIES(mp->ma_keys);
        ep = &ep0[mp->ma_keys->dk_nentries];
        dictkeys_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries);
        Py_INCREF(key);
        Py_INCREF(value);
        /*...*/
    return value;
}

對於被intern機制處理了的PyStringObject對象,Python採用了特殊的引用計數機制。在將一個PyStringObject對象a的PyObject指針作為key和value添加到interned中時,PyDictObject對象會通過這兩個指針對a的引用計數進行兩次加1的操作。但是Python的設計者規定在interned中a的指針不能被視為對象a的有效引用,因為如果是有效引用的話,那麼a的引用計數在Python結束之前永遠都不可能為0,因為interned中至少有兩個指針引用了a,那麼刪除a就永遠不可能了,這顯然是沒有道理的。
摘自:《Python源碼剖析》 — 陳儒

注意:實際上,即使Python會對一個字符串進行intern機制的處理,也會先創建一個PyUnicodeObject對象,然後檢查在interned字典中是否有值和其相同,存在的話就將interned字典保存的value值返回,之前臨時創建的字符串對象會由於引用計數為零而回收。

是否可以直接對C原生對象做intern的動作呢?不需要創建臨時對象

事實上CPython確實提供了以char * 為參數的intern機制相關函數,但是,也是一樣的創建temp在設置intern.

PyUnicode_InternImmortal(PyObject **p)
{
    PyUnicode_InternInPlace(p);
    if (PyUnicode_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) {
        _PyUnicode_STATE(*p).interned = SSTATE_INTERNED_IMMORTAL;
        Py_INCREF(*p);
    }
}

為什麼需要臨時對象?

因為PyDict_SetDefault() 操作的是PyDictObject對象,而該對象必須以PyObject*指針作為鍵

2.5 字符緩衝池(單字符)

python為小整數對象準備了小整數對象池,當然對於常用的字符,python對應的也建了字符串緩衝池,因為 python3 中通過 unicode_latin1[256] 將長度為 1 的 ascii 的字符也緩存了

/* Single character Unicode strings in the Latin-1 range are being
   shared as well. */
static PyObject *unicode_latin1[256] = {NULL};

unicode_decode_utf8(){
    /*省略*/
    /* ASCII is equivalent to the first 128 ordinals in Unicode. */
    if (size == 1 && (unsigned char)s[0] < 128) {
        if (consumed)
            *consumed = 1;
        return get_latin1_char((unsigned char)s[0]);
    }
    /*省略*/
}


static PyObject*
get_latin1_char(unsigned char ch)
{
    PyObject *unicode = unicode_latin1[ch];
    if (!unicode) {
        unicode = PyUnicode_New(1, ch);
        if (!unicode)
            return NULL;
        PyUnicode_1BYTE_DATA(unicode)[0] = ch;
        assert(_PyUnicode_CheckConsistency(unicode, 1));
        unicode_latin1[ch] = unicode;
    }
    Py_INCREF(unicode);
    return unicode;
}
In [46]: a = 'p'

In [47]: b = 'p'

In [48]: id(a)
Out[48]: 442757120384

In [49]: id(b)
Out[49]: 442757120384

當然單字符也包括空字符。

/* The empty Unicode object is shared to improve performance. */
static PyObject *unicode_empty = NULL;
In [8]: a = 'hello' + 'python'

In [9]: b = 'hellopython'

In [10]: a is b
Out[10]: True

In [11]: a = 'hello ' + 'python'

In [12]: b = 'hello python'

In [13]: id(a)
Out[13]: 118388503536

In [14]: id(b)
Out[14]: 118387544240

In [15]: 'hello ' + 'python' is 'hello python'
Out[15]: False

In [16]: 'hello_' + 'python' is 'hello_python'
Out[16]: True

2.6 小結:

  • 小整數[-5, 257)共用對象,常駐內存

  • 單個字母,長度為 1 的 ascii 的字符latin1會被interned, 包括空字符,共用對象,常駐內存

  • 由字母、数字、下劃線([a-zA-Z0-9_])組成的字符串,不可修改,默認開啟intern機制,共用對象,引用計數為0時,銷毀

  • 字符串(含有空格),不可修改,沒開啟intern機制,不共用對象,引用計數為0,銷毀

3. 標記清除+分代回收

為了防止出現循環引用的致命性問題,python採用的是引用計數機製為主,標記-清除和分代收集兩種機製為輔的策略

我們設置 n1.next 指向 n2,同時設置 n2.prev 指回 n1,現在,我們的兩個節點使用循環引用的方式構成了一個`雙向鏈表`,同時請注意到 ABC 以及 DEF 的引用計數值已經增加到了2,現在,假定我們的程序不再使用這兩個節點了,我們將 n1 和 n2 都設置為None,Python會像往常一樣將每個節點的引用計數減少到1。

### 3.1 在python中的零代(Generation Zero)

Ruby使用一個鏈表(free_list)來持續追蹤未使用的、自由的對象,Python使用一種不同的鏈表來持續追蹤活躍的對象。而不將其稱之為“活躍列表”,Python的內部C代碼將其稱為零代(Generation Zero)。每次當你創建一個對象或其他什麼值的時候,Python會將其加入零代鏈表:

從上邊可以看到當我們創建ABC節點的時候,Python將其加入零代鏈表。請注意到這並不是一個真正的列表,並不能直接在你的代碼中訪問,事實上這個鏈表是一個完全內部的Python運行時。

疑惑1:對於容器對象(比如list、dict、class、instance等等),是在什麼時候綁定GC,放入第0鏈表呢?

相似的,當我們創建DEF節點的時候,Python將其加入同樣的鏈表:

現在零代包含了兩個節點對象。(他還將包含Python創建的每個其他值,與一些Python自己使用的內部值。)

3.2 標記循環引用

當達到某個 閾值之後 解釋器會循環遍歷,循環遍歷零代列表上的每個對象,檢查列表中每個互相引用的對象,根據規則減掉其引用計數。在這個過程中,Python會一個接一個的統計內部引用的數量以防過早地釋放對象。以下例子便於理解:

從上面可以看到 ABC 和 DEF 節點包含的引用數為1.有三個其他的對象同時存在於零代鏈表中,藍色的箭頭指示了有一些對象正在被零代鏈表之外的其他對象所引用。

通過識別內部引用,Python能夠減少許多零代鏈表對象的引用計數。在上圖的第一行中你能夠看見ABC和DEF的引用計數已經變為零了,這意味着收集器可以釋放它們並回收內存空間了。剩下的活躍的對象則被移動到一個新的鏈表:一代鏈表。

疑惑2: 內部如何識別零代的循環引用計數,在什麼閾值下會觸發GC執行?

3.3 在源碼中摸索答案

Python通過PyGC_Head來跟蹤container對象,PyGC_Head信息位於PyObject_HEAD之前,定義在Include/objimpl.h

typedef union _gc_head {
    struct {
        union _gc_head *gc_next;
        union _gc_head *gc_prev;
        Py_ssize_t gc_refs;
    } gc;
    double dummy;  /* force worst-case alignment */
} PyGC_Head;

表頭數據結構

//Include/internal/mem.h
struct gc_generation {
      PyGC_Head head;
      int threshold; /* collection threshold */  // 閾值
      int count; /* count of allocations or collections of younger
                    generations */    // 實時個數
  };

Python中用於分代垃圾收集的三個“代”由_gc_runtime_state.generations數組所表示着:

解答疑惑2,三個代的閾值如下數組

/* If we change this, we need to cbhange the default value in the
   signature of gc.collect. */
#define NUM_GENERATIONS 3

_PyGC_Initialize(struct _gc_runtime_state *state)
{
    state->enabled = 1; /* automatic collection enabled? */

#define _GEN_HEAD(n) (&state->generations[n].head)
    struct gc_generation generations[NUM_GENERATIONS] = {
        /* PyGC_Head,                                 threshold,      count */
        {{{_GEN_HEAD(0), _GEN_HEAD(0), 0}},           700,            0},
        {{{_GEN_HEAD(1), _GEN_HEAD(1), 0}},           10,             0},
        {{{_GEN_HEAD(2), _GEN_HEAD(2), 0}},           10,             0},
    };
    for (int i = 0; i < NUM_GENERATIONS; i++) {
        state->generations[i] = generations[i];
    };
    state->generation0 = GEN_HEAD(0);
    struct gc_generation permanent_generation = {
          {{&state->permanent_generation.head, &state->permanent_generation.head, 0}}, 0, 0
    };
    state->permanent_generation = permanent_generation;
}

**解答疑惑1:那container對象是什麼時候加入第0“代”的container對象鏈表呢?**

對於python內置對象的創建,container對象是通過PyObject_GC_New函數來創建的,而非container對象是通過PyObject_Malloc函數來創建的。

// Include/objimpl.h
#define PyObject_GC_New(type, typeobj) \
                ( (type *) _PyObject_GC_New(typeobj) )


// 調用了Modules/gcmodule.c中的_PyObject_GC_New函數:
PyObject *
_PyObject_GC_New(PyTypeObject *tp)
{
    PyObject *op = _PyObject_GC_Malloc(_PyObject_SIZE(tp));
    if (op != NULL)
        op = PyObject_INIT(op, tp);
    return op;
}

static PyObject *
_PyObject_GC_Alloc(int use_calloc, size_t basicsize)
{
    PyObject *op;
    PyGC_Head *g;
    size_t size;
    if (basicsize > PY_SSIZE_T_MAX - sizeof(PyGC_Head))
        return PyErr_NoMemory();
    size = sizeof(PyGC_Head) + basicsize;
    // [1]  申請PyGC_Head和對象本身的內存
    if (use_calloc)
        g = (PyGC_Head *)PyObject_Calloc(1, size);
    else
        g = (PyGC_Head *)PyObject_Malloc(size);
    if (g == NULL)
        return PyErr_NoMemory();
    // [2] 設置gc_refs的值
    g->gc.gc_refs = 0;
    _PyGCHead_SET_REFS(g, GC_UNTRACKED);
    // [3]
    generations[0].count++; /* number of allocated GC objects */
    if (generations[0].count > generations[0].threshold &&
        enabled &&
        generations[0].threshold &&
        !collecting &&
        !PyErr_Occurred()) {
        collecting = 1;
        collect_generations();
        collecting = 0;
    }
    // [4]  FROM_GC宏定義可以通過PyGC_Head地址轉換PyObject_HEAD地址,逆運算是AS_GC宏定義。
    op = FROM_GC(g);
    return op;
}

PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
    return _PyObject_GC_Alloc(0, basicsize);
}

[4] FROM_GC宏定義可以通過PyGC_Head地址轉換PyObject_HEAD地址,逆運算是AS_GC宏定義。

/* Get an object's GC head */
#define AS_GC(o) ((PyGC_Head *)(o)-1)

/* Get the object given the GC head */
#define FROM_GC(g) ((PyObject *)(((PyGC_Head *)g)+1))

當觸發閾值后,是如何進行GC回收的?

collect是垃圾回收的主入口函數。特別注意 finalizers 與 python 的__del__綁定了

/* This is the main function.  Read this to understand how the
 * collection process works. */
static Py_ssize_t
collect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
        int nofail)
{
    int i;
    Py_ssize_t m = 0; /* # objects collected */
    Py_ssize_t n = 0; /* # unreachable objects that couldn't be collected */
    PyGC_Head *young; /* the generation we are examining */
    PyGC_Head *old; /* next older generation */
    PyGC_Head unreachable; /* non-problematic unreachable trash */
    PyGC_Head finalizers;  /* objects with, & reachable from, __del__ */
    PyGC_Head *gc;
    _PyTime_t t1 = 0;   /* initialize to prevent a compiler warning */

    struct gc_generation_stats *stats = &_PyRuntime.gc.generation_stats[generation];
    
    ...

    // “標記-清除”前的準備
    
    // 垃圾標記

    // 垃圾清除
  
    ...

    /* Update stats */
    if (n_collected)
        *n_collected = m;
    if (n_uncollectable)
        *n_uncollectable = n;
    stats->collections++;
    stats->collected += m;
    stats->uncollectable += n;

    if (PyDTrace_GC_DONE_ENABLED())
        PyDTrace_GC_DONE(n+m);

    return n+m;
}

3.3.1 標記-清除前的準備

    // [1]
    /* update collection and allocation counters */
    if (generation+1 < NUM_GENERATIONS)
        _PyRuntime.gc.generations[generation+1].count += 1;
    for (i = 0; i <= generation; i++)
        _PyRuntime.gc.generations[i].count = 0;

    // [2]
    /* merge younger generations with one we are currently collecting */
    for (i = 0; i < generation; i++) {
        gc_list_merge(GEN_HEAD(i), GEN_HEAD(generation));
    }

    // [3]
    /* handy references */
    young = GEN_HEAD(generation);
    if (generation < NUM_GENERATIONS-1)
        old = GEN_HEAD(generation+1);
    else
        old = young;

    // [4]
    /* Using ob_refcnt and gc_refs, calculate which objects in the
     * container set are reachable from outside the set (i.e., have a
     * refcount greater than 0 when all the references within the
     * set are taken into account).
     */
    update_refs(young);
    subtract_refs(young);

[1] 先更新了將被回收的“代”以及老一“代”的count計數器。
這邊對老一“代”的count計數器增量1就可以看出來在第1“代”和第2“代”的count值其實表示的是該代垃圾回收的次數。
[2] 通過gc_list_merge函數將這些“代”合併成一個鏈表。

/* append list `from` onto list `to`; `from` becomes an empty list */
static void
gc_list_merge(PyGC_Head *from, PyGC_Head *to)
{
    PyGC_Head *tail;
    assert(from != to);
    if (!gc_list_is_empty(from)) {
        tail = to->gc.gc_prev;
        tail->gc.gc_next = from->gc.gc_next;
        tail->gc.gc_next->gc.gc_prev = tail;
        to->gc.gc_prev = from->gc.gc_prev;
        to->gc.gc_prev->gc.gc_next = to;
    }
    gc_list_init(from);
}

static void
gc_list_init(PyGC_Head *list)
{
    list->gc.gc_prev = list;
    list->gc.gc_next = list;
}

gc_list_merge函數將from鏈錶鏈接到to鏈表末尾並把from鏈表置為空鏈表。

[3] 經過合併操作之後,所有需要被進行垃圾回收的對象都鏈接到young“代”(滿足超過閾值的最老“代”),並記錄old“代”,後面需要將不可回收的對象移到old“代”。

鏈表的合併操作:

[4] 尋找root object集合

要對合併的鏈表進行垃圾標記,首先需要尋找root object集合。
所謂的root object即是一些全局引用和函數棧中的引用。這些引用所用的對象是不可被刪除的。

list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
a = list1
del list1
del list2

上面的Python中循環引用的代碼,變量a所指向的對象就是root object。

三色標記模型

3.3.2 垃圾標記

// [1]
/* Leave everything reachable from outside young in young, and move
     * everything else (in young) to unreachable.
     * NOTE:  This used to move the reachable objects into a reachable
     * set instead.  But most things usually turn out to be reachable,
     * so it's more efficient to move the unreachable things.
     */
gc_list_init(&unreachable);
move_unreachable(young, &unreachable);

// [2]
/* Move reachable objects to next generation. */
if (young != old) {
    if (generation == NUM_GENERATIONS - 2) {
        _PyRuntime.gc.long_lived_pending += gc_list_size(young);
    }
    gc_list_merge(young, old);
}
else {
    /* We only untrack dicts in full collections, to avoid quadratic
           dict build-up. See issue #14775. */
    untrack_dicts(young);
    _PyRuntime.gc.long_lived_pending = 0;
    _PyRuntime.gc.long_lived_total = gc_list_size(young);
}

[1] 初始化不可達鏈表,調用move_unreachable函數將循環引用的對象移動到不可達鏈表中:

/* Move the unreachable objects from young to unreachable.  After this,
 * all objects in young have gc_refs = GC_REACHABLE, and all objects in
 * unreachable have gc_refs = GC_TENTATIVELY_UNREACHABLE.  All tracked
 * gc objects not in young or unreachable still have gc_refs = GC_REACHABLE.
 * All objects in young after this are directly or indirectly reachable
 * from outside the original young; and all objects in unreachable are
 * not.
 */
static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
    PyGC_Head *gc = young->gc.gc_next;

    /* Invariants:  all objects "to the left" of us in young have gc_refs
     * = GC_REACHABLE, and are indeed reachable (directly or indirectly)
     * from outside the young list as it was at entry.  All other objects
     * from the original young "to the left" of us are in unreachable now,
     * and have gc_refs = GC_TENTATIVELY_UNREACHABLE.  All objects to the
     * left of us in 'young' now have been scanned, and no objects here
     * or to the right have been scanned yet.
     */

    while (gc != young) {
        PyGC_Head *next;

        if (_PyGCHead_REFS(gc)) {
            /* gc is definitely reachable from outside the
             * original 'young'.  Mark it as such, and traverse
             * its pointers to find any other objects that may
             * be directly reachable from it.  Note that the
             * call to tp_traverse may append objects to young,
             * so we have to wait until it returns to determine
             * the next object to visit.
             */
            PyObject *op = FROM_GC(gc);
            traverseproc traverse = Py_TYPE(op)->tp_traverse;
            assert(_PyGCHead_REFS(gc) > 0);
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
            (void) traverse(op,
                            (visitproc)visit_reachable,
                            (void *)young);
            next = gc->gc.gc_next;
            if (PyTuple_CheckExact(op)) {
                _PyTuple_MaybeUntrack(op);
            }
        }
        else {
            /* This *may* be unreachable.  To make progress,
             * assume it is.  gc isn't directly reachable from
             * any object we've already traversed, but may be
             * reachable from an object we haven't gotten to yet.
             * visit_reachable will eventually move gc back into
             * young if that's so, and we'll see it again.
             */
            next = gc->gc.gc_next;
            gc_list_move(gc, unreachable);
            _PyGCHead_SET_REFS(gc, GC_TENTATIVELY_UNREACHABLE);
        }
        gc = next;
    }
}

這邊遍歷young“代”的container對象鏈表,_PyGCHead_REFS(gc)判斷是不是root object或從某個root object能直接/間接引用的對象,由於root object集合中的對象是不能回收的,因此,被這些對象直接或間接引用的對象也是不能回收的。

_PyGCHead_REFS(gc)為0並不能斷定這個對象是可回收的,但是還是先移動到unreachable鏈表中,設置了GC_TENTATIVELY_UNREACHABLE標誌表示暫且認為是不可達的,如果是存在被root object直接或間接引用的對象,這樣的對象還會被移出unreachable鏈表中。

[2] 將可達的對象移到下一“代”。

3.3.3 垃圾清除

// [1]
    /* All objects in unreachable are trash, but objects reachable from
     * legacy finalizers (e.g. tp_del) can't safely be deleted.
     */
    gc_list_init(&finalizers);
    move_legacy_finalizers(&unreachable, &finalizers);
    /* finalizers contains the unreachable objects with a legacy finalizer;
     * unreachable objects reachable *from* those are also uncollectable,
     * and we move those into the finalizers list too.
     */
    move_legacy_finalizer_reachable(&finalizers);

    // [2]
    /* Collect statistics on collectable objects found and print
     * debugging information.
     */
    for (gc = unreachable.gc.gc_next; gc != &unreachable;
                    gc = gc->gc.gc_next) {
        m++;
    }

    // [3]
    /* Clear weakrefs and invoke callbacks as necessary. */
    m += handle_weakrefs(&unreachable, old);

    // [4]
    /* Call tp_finalize on objects which have one. */
    finalize_garbage(&unreachable);

    // [5]
    if (check_garbage(&unreachable)) {
        revive_garbage(&unreachable);
        gc_list_merge(&unreachable, old);
    }
    else {
        /* Call tp_clear on objects in the unreachable set.  This will cause
         * the reference cycles to be broken.  It may also cause some objects
         * in finalizers to be freed.
         */
        delete_garbage(&unreachable, old);
    }
    
    // [6]
    /* Collect statistics on uncollectable objects found and print
     * debugging information. */
    for (gc = finalizers.gc.gc_next;
         gc != &finalizers;
         gc = gc->gc.gc_next) {
        n++;
    }
    
    ...

    // [7]
    /* Append instances in the uncollectable set to a Python
     * reachable list of garbage.  The programmer has to deal with
     * this if they insist on creating this type of structure.
     */
    (void)handle_legacy_finalizers(&finalizers, old);
    
    /* Clear free list only during the collection of the highest
     * generation */
    if (generation == NUM_GENERATIONS-1) {
        clear_freelists();
    }

[1] 處理unreachable鏈表中有finalizer的對象。即python中 實現了__del__魔法方法的對象

/* Move the objects in unreachable with tp_del slots into `finalizers`.
 * Objects moved into `finalizers` have gc_refs set to GC_REACHABLE; the
 * objects remaining in unreachable are left at GC_TENTATIVELY_UNREACHABLE.
 */
static void
move_legacy_finalizers(PyGC_Head *unreachable, PyGC_Head *finalizers)
{
    PyGC_Head *gc;
    PyGC_Head *next;

    /* March over unreachable.  Move objects with finalizers into
     * `finalizers`.
     */
    for (gc = unreachable->gc.gc_next; gc != unreachable; gc = next) {
        PyObject *op = FROM_GC(gc);

        assert(IS_TENTATIVELY_UNREACHABLE(op));
        next = gc->gc.gc_next;

        if (has_legacy_finalizer(op)) {
            gc_list_move(gc, finalizers);
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
        }
    }
}

遍歷unreachable鏈表,將擁有finalizer的實例對象移到finalizers鏈表中,並標示為GC_REACHABLE

/* Return true if object has a pre-PEP 442 finalization method. */
static int
has_legacy_finalizer(PyObject *op)
{
    return op->ob_type->tp_del != NULL;
}

擁有finalizer的實例對象指的就是實現了tp_del函數的對象。

/* Move objects that are reachable from finalizers, from the unreachable set
 * into finalizers set.
 */
static void
move_legacy_finalizer_reachable(PyGC_Head *finalizers)
{
    traverseproc traverse;
    PyGC_Head *gc = finalizers->gc.gc_next;
    for (; gc != finalizers; gc = gc->gc.gc_next) {
        /* Note that the finalizers list may grow during this. */
        traverse = Py_TYPE(FROM_GC(gc))->tp_traverse;
        (void) traverse(FROM_GC(gc),
                        (visitproc)visit_move,
                        (void *)finalizers);
    }
}

finalizers鏈表中擁有finalizer的實例對象遍歷其引用對象,調用visit_move訪問者,這些被引用的對象也不應該被釋放。

/* A traversal callback for move_legacy_finalizer_reachable. */
static int
visit_move(PyObject *op, PyGC_Head *tolist)
{
    if (PyObject_IS_GC(op)) {
        if (IS_TENTATIVELY_UNREACHABLE(op)) {
            PyGC_Head *gc = AS_GC(op);
            gc_list_move(gc, tolist);
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
        }
    }
    return 0;
}

#define IS_TENTATIVELY_UNREACHABLE(o) ( \
    _PyGC_REFS(o) == GC_TENTATIVELY_UNREACHABLE)

visit_move函數將引用對象還在unreachable鏈表的對象移到finalizers鏈表中。

[2] 統計unreachable鏈表數量。
[3] 處理弱引用。
[4] [5] 開始清除垃圾對象,我們先只看delete_garbage函數:

/* Break reference cycles by clearing the containers involved.  This is
 * tricky business as the lists can be changing and we don't know which
 * objects may be freed.  It is possible I screwed something up here.
 */
static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)
{
    inquiry clear;

    while (!gc_list_is_empty(collectable)) {
        PyGC_Head *gc = collectable->gc.gc_next;
        PyObject *op = FROM_GC(gc);

        if (_PyRuntime.gc.debug & DEBUG_SAVEALL) {
            PyList_Append(_PyRuntime.gc.garbage, op);
        }
        else {
            if ((clear = Py_TYPE(op)->tp_clear) != NULL) {
                Py_INCREF(op);
                clear(op);
                Py_DECREF(op);
            }
        }
        if (collectable->gc.gc_next == gc) {
            /* object is still alive, move it, it may die later */
            gc_list_move(gc, old);
            _PyGCHead_SET_REFS(gc, GC_REACHABLE);
        }
    }
}

遍歷unreachable鏈表中的container對象,調用其類型對象的tp_clear指針指向的函數,我們以list對象為例:

static int
_list_clear(PyListObject *a)
{
    Py_ssize_t i;
    PyObject **item = a->ob_item;
    if (item != NULL) {
        /* Because XDECREF can recursively invoke operations on
           this list, we make it empty first. */
        i = Py_SIZE(a);
        Py_SIZE(a) = 0;
        a->ob_item = NULL;
        a->allocated = 0;
        while (--i >= 0) {
            Py_XDECREF(item[i]);
        }
        PyMem_FREE(item);
    }
    /* Never fails; the return value can be ignored.
       Note that there is no guarantee that the list is actually empty
       at this point, because XDECREF may have populated it again! */
    return 0;
}

_list_clear函數對container對象的每個元素進行引用數減量操作並釋放container對象內存。

delete_garbage在對container對象進行clear操作之後,還會檢查是否成功,如果該container對象沒有從unreachable鏈表上摘除,表示container對象還不能銷毀,需要放回到老一“代”中,並標記GC_REACHABLE

[6] 統計finalizers鏈表數量。
[7] 處理finalizers鏈表的對象。

/* Handle uncollectable garbage (cycles with tp_del slots, and stuff reachable
 * only from such cycles).
 * If DEBUG_SAVEALL, all objects in finalizers are appended to the module
 * garbage list (a Python list), else only the objects in finalizers with
 * __del__ methods are appended to garbage.  All objects in finalizers are
 * merged into the old list regardless.
 * Returns 0 if all OK, <0 on error (out of memory to grow the garbage list).
 * The finalizers list is made empty on a successful return.
 */
static int
handle_legacy_finalizers(PyGC_Head *finalizers, PyGC_Head *old)
{
    PyGC_Head *gc = finalizers->gc.gc_next;

    if (_PyRuntime.gc.garbage == NULL) {
        _PyRuntime.gc.garbage = PyList_New(0);
        if (_PyRuntime.gc.garbage == NULL)
            Py_FatalError("gc couldn't create gc.garbage list");
    }
    for (; gc != finalizers; gc = gc->gc.gc_next) {
        PyObject *op = FROM_GC(gc);

        if ((_PyRuntime.gc.debug & DEBUG_SAVEALL) || has_legacy_finalizer(op)) {
            if (PyList_Append(_PyRuntime.gc.garbage, op) < 0)
                return -1;
        }
    }

    gc_list_merge(finalizers, old);
    return 0;
}

遍歷finalizers鏈表,將擁有finalizer的實例對象放到一個名為garbage的PyListObject對象中,可以通過gc模塊查看。

>>> import gc
>>> gc.garbage

並把finalizers鏈表晉陞到老一“代”。

注意:__del__給gc帶來的影響, gc模塊唯一處理不了的是循環引用的類都有__del__方法,所以項目中要避免定義__del__方法 官方警告

3.4 小結

  1. GC的流程:

    -> 發現超過閾值了
    -> 觸發垃圾回收
    -> 將所有可達對象鏈表放到一起
    -> 遍歷, 計算有效引用計數
    -> 分成 有效引用計數=0 和 有效引用計數 > 0 兩個集合
    -> 大於0的, 放入到更老一代
    -> =0的, 執行回收
    -> 回收遍歷容器內的各個元素, 減掉對應元素引用計數(破掉循環引用)
    -> 執行-1的邏輯, 若發現對象引用計數=0, 觸發內存回收
    -> 由python底層內存管理機制回收內存
    
  2. 觸發GC的條件

    • 主動調用gc.collect(),

    • 當gc模塊的計數器達到閥值的時候

    • 程序退出的時候

4. GC閾值

分代回收 以空間換時間

重要思想:將系統中的所有內存塊根據其存活的時間劃分為不同的集合, 每個集合就成為一個”代”, 垃圾收集的頻率隨着”代”的存活時間的增大而減小(活得越長的對象, 就越不可能是垃圾, 就應該減少去收集的頻率)

弱代假說

分代垃圾回收算法的核心行為:垃圾回收器會更頻繁的處理新對象。一個新的對象即是你的程序剛剛創建的,而一個來的對象則是經過了幾個時間周期之後仍然存在的對象。Python會在當一個對象從零代移動到一代,或是從一代移動到二代的過程中提升(promote)這個對象。

為什麼要這麼做?這種算法的根源來自於弱代假說(weak generational hypothesis)。這個假說由兩個觀點構成:

首先是年親的對象通常死得也快,而老對象則很有可能存活更長的時間。

假定我們創建了一個Python創建:

n1 = Node("ABC")

根據假說,我的代碼很可能僅僅會使用ABC很短的時間。這個對象也許僅僅只是一個方法中的中間結果,並且隨着方法的返回這個對象就將變成垃圾了。大部分的新對象都是如此般地很快變成垃圾。然而,偶爾程序會創建一些很重要的,存活時間比較長的對象-例如web應用中的session變量或是配置項。

通過頻繁的處理零代鏈表中的新對象,Python的垃圾收集器將把時間花在更有意義的地方:它處理那些很快就可能變成垃圾的新對象。同時只在很少的時候,當滿足閾值的條件,收集器才回去處理那些老變量。

5. Python中的gc模塊使用

gc模塊默認是開啟自動回收垃圾的,gc.isenabled()=True

常用函數:

  • gc.set_debug(flags) 設置gc的debug日誌,一般設置為gc.DEBUG_LEAK
"""
DEBUG_STATS - 在垃圾收集過程中打印所有統計信息
DEBUG_COLLECTABLE - 打印發現的可收集對象
DEBUG_UNCOLLECTABLE - 打印unreachable對象(除了uncollectable對象)
DEBUG_SAVEALL - 將對象保存到gc.garbage(一個列表)裏面,而不是釋放它
DEBUG_LEAK - 對內存泄漏的程序進行debug (everything but STATS).
    
"""
  • gc.collect([generation]) 顯式進行垃圾回收,可以輸入參數,0代表只檢查第一代的對象,1代表檢查一,二代的對象,2代表檢查一,二,三代的對象,如果不傳參數,執行一個full collection,也就是等於傳2。 返回不可達(unreachable objects)對象的數目

  • gc.get_threshold() 獲取的gc模塊中自動執行垃圾回收的頻率

  • gc.get_stats()查看每一代的具體信息

  • gc.set_threshold(threshold0[, threshold1[, threshold2]) 設置自動執行垃圾回收的頻率

  • gc.get_count() 獲取當前自動執行垃圾回收的計數器,返回一個長度為3的列表

    例如(488,3,0),其中488是指距離上一次一代垃圾檢查,Python分配內存的數目減去釋放內存的數目,注意是內存分配,而不是引用計數的增加。

    3是指距離上一次二代垃圾檢查,一代垃圾檢查的次數,同理,0是指距離上一次三代垃圾檢查,二代垃圾檢查的次數。

計數器和閾值關係解釋:

當計數器從(699,3,0)增加到(700,3,0),gc模塊就會執行gc.collect(0),即檢查一代對象的垃圾,並重置計數器為(0,4,0)
當計數器從(699,9,0)增加到(700,9,0),gc模塊就會執行gc.collect(1),即檢查一、二代對象的垃圾,並重置計數器為(0,0,1)
當計數器從(699,9,9)增加到(700,9,9),gc模塊就會執行gc.collect(2),即檢查一、二、三代對象的垃圾,並重置計數器為(0,0,0)

6. 工作中如何避免循環引用?

To avoid circular references in your code, you can use weak references, that are implemented in the weakref module. Unlike the usual references, the weakref.ref doesn’t increase the reference count and returns None if an object was destroyed. rushter

import weakref


class Node():
    def __init__(self, value):
        self.value = value
        self._parent = None
        self.children = []

    def __repr__(self):
        return 'Node({!r:})'.format(self.value)

    @property
    def parent(self):
        return None if self._parent is None else self._parent()

    @parent.setter
    def parent(self, node):
        self._parent = weakref.ref(node)

    def add_child(self, child):
        self.children.append(child)
        child.parent = self


if __name__ == '__main__':

    a = Data()
    del a

    a = Node()
    del a

    a = Node()
    a.add_child(Node())
    del a

弱引用消除了引用循環的這個問題,本質來講,弱引用就是一個對象指針,它不會增加它的引用計數

弱引用的主要用途是實現保存大對象的高速緩存或映射,但又並希望大對象僅僅因為它出現在高速緩存或映射中而保持存活

為了訪問弱引用所引用的對象,你可以像函數一樣去調用它即可。如果那個對象還存在就會返回它,否則就返回一個None。 由於原始對象的引用計數沒有增加,那麼就可以去刪除它了

並非所有對象都可以被弱引用;可以被弱引用的對象包括類實例,用 Python(而不是用 C)編寫的函數,實例方法、集合、凍結集合,某些 文件對象,生成器,類型對象,套接字,數組,雙端隊列,正則表達式模式對象以及代碼對象等。

幾個內建類型如 listdict 不直接支持弱引用,但可以通過子類化添加支持:

class Dict(dict):
    pass

obj = Dict(red=1, green=2, blue=3)   # this object is weak referenceable

其他內置類型例如 tupleint 不支持弱引用,即使通過子類化也不支持

python Cookbook 書中推薦弱引用來處理循環引用

假設我們想創建一個類,用它的實例來代表臨時目錄。 當以下事件中的某一個發生時,這個目錄應當與其內容一起被刪除:

  • 對象被作為垃圾回收,
  • 對象的 remove() 方法被調用,或
  • 程序退出。

原本用__del__()方法

class TempDir:
    def __init__(self):
        self.name = tempfile.mkdtemp()
       
   	def __remove(self):
        if self.name is not None:
            shutil.rmtree(self.name)
            self.name = None
    
    @property
    def removed(self):
        return self.name is None
   
	def __del__(self):
        self.__remove()

更健壯的替代方式可以是定義一個終結器,只引用它所需要的特定函數和對象,而不是獲取對整個對象狀態的訪問權:

class TempDir:
    def __init__(self):
        self.name = tempfile.mkdtemp()
        self._finalizer = weakref.finalize(self, shutil.rmtree, self.name)
       
   	def remove(self):
        self._finalizer()
    
    @property
    def removed(self):
        return not self._finalizer.alive

像這樣定義后,我們的終結器將只接受一個對其完成正確清理目錄任務所需細節的引用。 如果對象一直未被作為垃圾回收,終結器仍會在退出時被調用.weakref

參考文章和書籍:

  1. visualizing garbage collection in ruby and python
  2. 膜拜的大佬-Junnplus’blog
  3. wklken前輩
  4. The Garbage Collector
  5. Garbage collection in Python: things you need to know
  6. Python-CookBook-循環引用數據結構的內存管理
  7. 《python源碼剖析》
  8. Python-3.8.3/Modules/gcmodule.c

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

Java併發相關知識點梳理和研究

1. 知識點思維導圖

(圖比較大,可以右鍵在新窗口打開)

2. 經典的wait()/notify()/notifyAll()實現生產者/消費者編程範式深入分析 & synchronized

注:本節代碼和部分分析參考了你真的懂wait、notify和notifyAll嗎。

看下面一段典型的wait()/notify()/notifyAll()代碼,對於值得注意的細節,用註釋標出。

import java.util.ArrayList;
import java.util.List;

public class Something {
    private Buffer mBuf = new Buffer(); // 共享的池子

    public void produce() {
        synchronized (this) { // 注1、注2
            while (mBuf.isFull()) { // 注3
                try {
                    wait(); // 注4
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.add();
            notifyAll();  // 注5、注6
        }
    }

    public void consume() {
        synchronized (this) { // 見注1、注2
            while (mBuf.isEmpty()) { // 注3
                try {
                    wait(); // 注4
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            mBuf.remove();
            notifyAll(); // 注5、注6
        }
    }

    private class Buffer {
        private static final int MAX_CAPACITY = 1;
        private List innerList = new ArrayList<>(MAX_CAPACITY);

        void add() {
            if (isFull()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.add(new Object());
            }
            System.out.println(Thread.currentThread().toString() + " add");

        }

        void remove() {
            if (isEmpty()) {
                throw new IndexOutOfBoundsException();
            } else {
                innerList.remove(MAX_CAPACITY - 1);
            }
            System.out.println(Thread.currentThread().toString() + " remove");
        }

        boolean isEmpty() {
            return innerList.isEmpty();
        }

        boolean isFull() {
            return innerList.size() == MAX_CAPACITY;
        }
    }

    public static void main(String[] args) {
        Something sth = new Something();
        Runnable runProduce = new Runnable() {
            int count = 4;

            @Override
            public void run() {
                while (count-- > 0) {
                    sth.produce();
                }
            }
        };
        Runnable runConsume = new Runnable() {
            int count = 4;

            @Override
            public void run() {
                while (count-- > 0) {
                    sth.consume();
                }
            }
        };
        for (int i = 0; i < 2; i++) {
            new Thread(runConsume).start();
        }
        for (int i = 0; i < 2; i++) {
            new Thread(runProduce).start();
        }
    }
}
  • 注1:wait()/notify()/notifyAll()必須在synchronized塊中使用
  • 注2:使用synchronized(this)的原因是,這段代碼的main(),是通過實例化Something的對象,並使用它的方法來進行生產/消費的,因此是一個指向this的對象鎖。不同的場景,需要注意同步的對象的選擇。
  • 注3:必須使用while循環來包裹wait()。設想一種場景:存在多個生產者或多個消費者消費者,以多個生成者為例,在緩衝區滿的情況下,如果生產者通過notify()喚醒的線程仍是生產者,如果不使用while,那麼獲取鎖的線程無法重新進入睡眠,鎖也不能釋放,造成死鎖。
  • 注4:wait()會釋放鎖
  • 注5:notfiy()、notifyAll()會通知其他在wait的線程來獲取鎖,但是獲取鎖的真正時機是鎖的原先持有者退出synchronized塊的時候。
  • 注6:使用notifyAll()而不是notfiy()的原因是,仍考慮注3的場景,假如生產者喚醒的也是生產者,後者發現緩衝區滿重新進入阻塞,此時沒有辦法再喚醒在等待的消費者線程了,也會造成死鎖。

擴展知識點1:synchronized塊的兩個隊列

synchronized入口是將線程放入同步隊列,wait()是將線程放入阻塞隊列。notify()/notifyAll()實際上是把線程從阻塞隊列放入同步隊列。wait/notify/notifyAll方法需不需要被包含在synchronized塊中,為什麼?

擴展知識點2:synchronized重入原理

synchronized是可重入的,原理是它內部包含了一個計數器,進入時+1,退出時-1。 Java多線程:synchronized的可重入性

擴展知識點3:作用範圍

synchronized支持三種用法:修飾靜態方法、修飾實例方法、修飾代碼塊,前兩種分別鎖類對象、鎖對象實例,最後一種根據傳入的值來決定鎖什麼。
synchronized是基於java的對象頭實現的,從字節碼可以看出包括了一對進入&退出的監視器。
深入理解Java併發之synchronized實現原理

擴展知識點4:分佈式環境synchronized的意義

單看應用所運行的的單個宿主機,仍然可能有多線程的處理模式,在這個前提下使用併發相關技術是必須的。

擴展知識點5:哪些方法釋放資源,釋放鎖

所謂資源,指的是系統資源。

wait(): 線程進入阻塞狀態,釋放資源,釋放鎖,Object類final方法(notify/notifyAll一樣,不可改寫)。
sleep(): 線程進入阻塞態,釋放資源,(如果在synchronized中)不釋放鎖,進入阻塞狀態,喚醒隨機線程,Thread類靜態native方法。
yield(): 線程進入就緒態,釋放資源,(如果在synchronized中)不釋放鎖,進入可執行狀態,選擇優先級高的線程執行,Thread類靜態native方法。
如果線程產生的異常沒有被捕獲,會釋放鎖。
sleep和yield的比較

可以進一步地將阻塞劃分為同步阻塞——進入synchronized時沒獲取到鎖、等待阻塞——wait()、其他阻塞——sleep()/join(),可以參考線程的狀態及sleep、wait等方法的區別

再進一步地,Java線程狀態轉移可以用下圖表示(圖源《Java 併發編程藝術》4.1.4 節)

WAITING狀態的線程是不會消耗CPU資源的。

3. 線程數調優

理論篇

本節參考了《Java併發編程實戰》8.2節,也可以結合面試問我,創建多少個線程合適?我該怎麼說幫助理解,其中的計算題比較有價值。

前置知識

I/O密集型任務:I/O任務執行時CPU空閑。
CPU密集型任務:進行計算
有的任務是二者兼備的。為了便於分析,不考慮。

定性分析

場景:單核單線程/單核多線程/多核多線程。單核多線程+CPU密集型不能提升執行效率,多核+CPU密集型任務可以;單核多線程+I/O密集型可以提升執行效率。
因此,I/O耗時越多,線程也傾向於變多來充分利用IO等待時間。

定量分析

對於CPU密集型,線程數量=CPU 核數(邏輯)即可。特別的,為了防止線程在程序運行異常時不空轉,額外多設一個線程線程數量 = CPU 核數(邏輯)+ 1
對於I/O密集型,最佳線程數 = CPU核數 * (1/CPU利用率) = CPU核數 * (1 + I/O耗時/CPU耗時)
為什麼CPU利用率=1/(1+ I/O耗時/CPU耗時)?簡單推導一下:

1/(1+ I/O耗時/CPU耗時) = 1/((CPU耗時+I/O耗時)/ CPU耗時) = CPU耗時/總耗時 = CPU利用率

如何獲取參數——CPU利用率?

因為利用率不是一成不變的,需要通過全面的系統監控工具(如SkyWalking、CAT、zipkin),並長期進行調整觀測。
可以先取2N即2倍核數,此時即假設I/O耗時/CPU耗時=1:1,再進行調優。

阿姆達爾定律

CPU併發處理時性能提升上限。
S=1/(1-a+a/n)
其中,a為并行計算部分所佔比例,n為并行處理結點個數。
簡單粗暴理解【阿姆達爾定律】

Java線程池篇

基本屬性

/**
 * 使用給定的初始參數和默認線程工廠創建一個新的ThreadPoolExecutor ,並拒絕執行處理程序。 使用Executors工廠方法之一可能更方便,而不是這種通用構造函數。
參數
 *  corePoolSize - 即使空閑時仍保留在池中的線程數,除非設置 allowCoreThreadTimeOut
 *  maximumPoolSize - 池中允許的最大線程數
 *  keepAliveTime - 當線程數大於核心時,這是多餘的空閑線程在終止之前等待新任務的最大時間。
 *  unit - keepAliveTime參數的時間單位
 *  workQueue - 在執行任務之前用於保存任務的隊列。 該隊列將僅保存execute方法提交的Runnable任務。
 * threadFactory - 執行程序創建新線程時使用的工廠
 */
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)

常見線程池

由java.util.concurrent.Executors創建的線程池比較常用,而不是使用ThreadPoolExecutor的構造方法。

名稱 特性
newFixedThreadPool 線程池大小為固定值
newSingleThreadExecutor 線程池大小固定為1
newCachedThreadPool 線程池大小初始為0,默認最大值為MAX INTEGER
newScheduledExecutor 延遲執行任務或按周期重複執行任務

線程工廠的作用

用來創建線程,統一在創建線程時設置一些參數,如是否守護線程。線程一些特性等,如優先級。
可參考004-多線程-JUC線程池-ThreadFactory線程工廠

4. 併發容器相關

併發容器可以說是一個面試時的高頻問題了,網絡上也有很多介紹,這裏就不重複解讀,將相關的知識整理一下,邊看源碼邊讀文章效果會很好。
先提一句,Vector是線程安全的,為啥現在不推薦用呢?看源碼可以知道,它將大部分方法都加了synchronized,犧牲了性能換取線程安全,是不可取的。如果真的有需要線程安全的容器,可以用Collections.synchronizedList()來手動給list加synchronized。
再補充一句,其實Vector和Collections.synchronizedList()使用複合操作或迭代器Iterator時也不是線程安全的,具體解釋會在下一篇博客Java容器中介紹。

ConcurrentHashMap

先重點介紹Map的兩個實現類HashMap和ConcurrentHashMap

  • HashMap和ConcurrentHashMap HashMap?ConcurrentHashMap?相信看完這篇沒人能難住你!
  • HashMap擴容原理:HashMap的擴容機制—resize()
  • 多線程下HashMap擴容resize可能導致鏈表循環
  • 這兩個數據結構在JDK1.7到1.8時,當數目達到一個閾值時,都從鏈表改用了紅黑樹
  • HashMap的node重寫了equals方法來比較節點。Objects.equals會調用Object的equals,對於Object實現類則是實現類自己的equals。
 public final boolean equals(Object o) {
     if (o == this)
         return true;
     if (o instanceof Map.Entry) {
         Map.Entry<?,?> e = (Map.Entry<?,?>)o;
         if (Objects.equals(key, e.getKey()) &&
             Objects.equals(value, e.getValue()))
             return true;
     }
     return false;
 }

ConcurrentLinkedQueue

ConcurrentLinkedQueue使用CAS無鎖操作,保證入隊出隊的線程安全,但不保證遍歷時的線程安全。遍歷要想線程安全需要單獨加鎖。
由於算法的特性,這個容器的尾結點是有延遲的,tail不一定是尾節點,但p.next == null的節點一定是尾結點。
入隊出隊操作很抽象,需要畫圖幫助理解源碼,對應的源碼分析可參考併發容器-ConcurrentLinkedQueue詳解。

5. AQS解讀

抽象隊列同步器AbstractQueuedSynchronizer(AQS)是JUC中很多併發工具類的基礎,用來抽象各種併發控制行為,如ReentranLock、Semaphore。
之前試着直接讀源碼,效果不太好,還是建議結合質量較高的文章來讀,這裏推薦一篇:Java併發之AQS詳解,並且作者還在不斷更新。
這裏簡單記錄一下總結的點。

結構特點

  • volatile int state標記位,標識當前的同步狀態。具體的用法和使用AQS的工具類有關。同時,在做CAS的時候,state的狀態變更是通過計算該變量在對象的偏移量來設置的。
  • CLH隊列。CLH鎖(Craig,Landin andHagersten)是一種在SMP(Symmetric Multi-Processor對稱多處理器)架構下基於單鏈表的高性能的自旋鎖,隊列中每個節點代表一個自旋的線程,每個線程只需在代表前一個線程的節點上的布爾值locked自旋即可,如圖

    圖源和CLH的詳解見算法:CLH鎖的原理及實現

  • exclusiveOwnerThread獨佔模式的擁有者,記錄現在是哪個線程佔用這個AQS。

操作特點

  • 對state使用>0和<0的判斷,初看代碼很難看懂,這麼寫的原因是負值表示結點處於有效等待狀態,而正值表示結點已被取消
  • 大量的CAS:無論是獲取鎖、入隊、獲取鎖失敗后的自旋,全部是依賴CAS實現的。
  • 沒有使用synchronized:不難理解,如果使用了同步塊,那麼其實現ReentranLock就沒有和synchronized比較的價值了。不過這一點很少有文章專門提到。
  • LockSupport類的unpark()/park()方法的使用:回憶上文提到的線程狀態,如果線程獲取不到AQS控制的資源,需要將線程置於waiting,對應可選的方法是wait()/join()/park()。在AQS這個場景下,顯然一沒有synchronized,二沒有顯式的在同一個代碼塊中用join處理多線程(藉助隊列來處理線程,線程相互之間不感知),那麼只有park()才能達到目的。

處理流程

獲取資源acquire(int)

  1. 嘗試獲取資源(改寫state),成功則返回
  2. CAS(失敗則自旋)加入等待隊列隊尾
  3. 在隊列中自旋,嘗試獲取一次資源(前提:隊頭+ tryAcquire()成功),每次失敗都會更改線程狀態為waiting。自旋時會看看前驅有沒有失效的節點(即不再請求資源的),如果有就插隊到最前面並把前面無效節點清理掉便於gc
  4. waiting狀態中不響應中斷,獲取資源后才會補一個自我中斷selfInterrupt (調用Thread.currentThread().interrupt())

釋放資源release(int)

  1. 嘗試釋放,成功則處理後續動作,失敗直接返回false
  2. 喚醒(unpark)等待隊列的下一個線程。如果當前節點沒找到後繼,則從隊尾tail從后往前找。

共享模式獲取資源acquireShared(int)

除了抽象方法tryAcquireShared()以外,基本和acquire(int)一致。
在等待隊列中獲取資源后,會調用獨有的setHeadAndPropagate()方法,將這個節點設為頭結點的同時,檢查後續節點是否可以獲取資源。

共享模式釋放資源releaseShared()

和release(int)區別在於,喚醒後繼時,不要求當前線程節點狀態為0。舉例:當前線程A原先擁有5個資源,釋放1個,後繼的等待線程B剛好需要1個,那麼此時A、B就可以并行了。

未實現的方法

為了便於使用AQS的類更加個性化,AQS有一下方法直接拋UnsupportedOperationException。

  • isHeldExclusively()
  • tryAcquire()
  • tryRelease()
  • tryAcquireShared()
  • tryReleaseShared()
    不寫成abstract方法的原因是,避免強迫不需要對應方法的類實現這些方法。比如要寫一個獨佔的鎖,那麼就不需要實現共享模式的方法。

AQS小結

讀完源碼總結一下,AQS是一個維護資源和請求資源的線程之間的關係的隊列。對於資源(有序或無序的)獲取和釋放已經提取成了線程的出入隊方法,這個隊列同時維護上線程的自旋狀態和管理線程間的睡眠喚醒。

應用

本節可以看作為《JAVA併發變成實戰》14.6的引申。

ReentrantLock

用內部類Sync實現AQS,Sync實現ReentrantLock的行為。Sync又有FairSync和UnfairSync兩種實現。FairSync,lock對應aquire(1);UnfairSync,lock先CAS試着獲取一次,不行再aquire(1)。
實際上,ReentrantLock的公平/非公平鎖只在首次lock時有區別,入隊后喚醒仍是按順序的。可以參考reentrantLock公平鎖和非公平鎖源碼解析
Sync只實現了獨佔模式。

注意:CyclicBarrier直接用了ReentrantLock,沒有直接用AQS。

Semaphore

和ReentrantLock類似,Semaphore也有一個內部類Sync,但相反的是這個Sync只實現了共享模式的acquire()/release()。
Semaphore在acquire()/release()時會計算資源余量並設置,其中unfair模式下的acquire會無條件自旋CAS,fair模式下只有在AQS里不存在排隊中的後繼的情況下才會CAS,否則自旋。

CountDownLatch

同樣有一個內部類Sync,但是不再區分fair/unfair,並且是共享模式的。
await()調用的是acquireSharedInterruptibly(),自然也存在自旋的可能,只是編程時一般不這麼用。countDown()時釋放一個資源繼續在releaseShared()里自旋直到全部釋放。

FutureTask

新版的FutureTask已經重寫,不再使用AQS,這裏就不再提了。

ReentrantReadWriteLock

可重入讀寫鎖,涉及到鎖升級,這裏沒有研究的很透徹,有興趣可以自行了解。
注意到讀鎖和寫鎖是共用同一個Sync的。

6 JMM到底是個啥?

The Java memory model specifies how the Java virtual machine works with the computer’s memory (RAM)。
—— Java Memory Model
雖然被冠以”模型“,JMM實際上是定義JVM如何與計算機內存協同工作的規範,也可以理解為__指令__與其操作的__數據__的行為。這樣,自然而然地引入了指令重排序、變量更改的可見性的探討。
JMM定義了一個偏序關係,稱之為happens-before。不滿足happens-before的兩個操作可以由JVM進行重排序。

6.1 什麼是偏序關係

假設 R 是集合 A 上的關係,如果R是自反的、反對稱的和傳遞的,則稱 R 是 A 上的一個偏序。偏序關係
那麼,自反的、反對稱的和傳遞的,又是什麼?下面粘貼了百度百科相關詞條:

  • 自反關係:設 R是 A上的一個二元關係,若對於 A中的每一個元素 a, (a,a)都屬於 R,則稱 R為自反關係。
  • 反對稱關係:集合 A 上的二元關係 R 是反對稱的,當且僅當對於X里的任意元素a, b,若a R-關係於 b 且 b R-關係於 a,則a=b。
  • 傳遞關係:令R是A上的二元關係,對於A中任意的 ,若 ,且 ,則 ,則稱R具有傳遞性(或稱R是傳遞關係)。

上面的反對稱關係稍微不好理解,轉換成逆否命題就好理解了:若a!=b,那麼R中不能同存在aRb和bRa。

6.2 偏序關係和JMM

將R作為兩個操作間的關係,集合A是所有操作的集合,那麼就可以理解JMM為什麼實際上是一套偏序關係了。

6.3 happens-before規則

這部分的說明很多文章都是有差異,比如鎖原則,JLS(Java Language Specification,Java語言規範)特指的是監視器鎖,只不過顯式鎖和內置鎖有相同的內存語義而已。這裏直接摘錄原文並配上說明。原文見Chapter 17. Threads and Locks

If we have two actions x and y, we write hb(x, y) to indicate that x happens-before y.

If x and y are actions of the same thread and x comes before y in program order, then hb(x, y).

There is a happens-before edge from the end of a constructor of an object to the start of a finalizer (§12.6) for that object.

If an action x synchronizes-with a following action y, then we also have hb(x, y).

If hb(x, y) and hb(y, z), then hb(x, z).

The wait methods of class Object (§17.2.1) have lock and unlock actions associated with them; their happens-before relationships are defined by these associated actions.

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

For example, the write of a default value to every field of an object constructed by a thread need not happen before the beginning of that thread, as long as no read ever observes that fact.

More specifically, if two actions share a happens-before relationship, they do not necessarily have to appear to have happened in that order to any code with which they do not share a happens-before relationship. Writes in one thread that are in a data race with reads in another thread may, for example, appear to occur out of order to those reads.

The happens-before relation defines when data races take place.

A set of synchronization edges, S, is sufficient if it is the minimal set such that the transitive closure of S with the program order determines all of the happens-before edges in the execution. This set is unique.

It follows from the above definitions that:

An unlock on a monitor happens-before every subsequent lock on that monitor.

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

A call to start() on a thread happens-before any actions in the started thread.

All actions in a thread happen-before any other thread successfully returns from a join() on that thread.

The default initialization of any object happens-before any other actions (other than default-writes) of a program.

試着翻譯一下各項規則:
先定義hb(x, y)表示操作x和操作y的happens-before關係。

  1. 同一個線程的操作x, y,代碼中順序為x, y,那麼hb(x, y)
  2. 對象構造方法要早於終結方法完成
  3. 如果x synchronizes-with y那麼hb(x,y)
  4. 傳遞性,hb(x, y) 且hb(y,z)則hb(x,z)
  5. 同一個監視器鎖解鎖需要hb所有加鎖(注:該規則擴展到顯式鎖)
  6. volatile的讀hb所有寫(該規則擴展到原子操作)
  7. 線程start() hb所有它的啟動后的任何動作
  8. 線程中所有操作hb 對它的join()
  9. 對象默認構造器hb對它的讀寫

synchronizes-with又是啥?查閱了一下,表示”這個關係表示一個行為在發生時,它首先把要操作的那些對象同主存同步完畢之後才繼續執行“。參考JMM(Java內存模型)中的核心概念。
JLS上對happens-before的解釋翻譯過來還是不太好理解,《Java併發編程實戰》的解釋和Happens-beofre 先行發生原則(JVM 規範)一樣,可以參考下。

最後可以發現,JMM只是一套規則,並沒有提到具體的實現,程序員知道Java有這一重保證即可。

7. 短篇話題整理總結

7.1 ThreadLocal的用法總結

應用場景:在多線程下替代類的靜態變量(static),在多線程環境進行單個 的數據隔離。

為什麼推薦使用static修飾ThreadLocal?

這時才能保證”一個線程,一個ThreadLocal”,否則便成了“一個線程,(多個對象實例時)多個ThreadLocal”。
可能會有內存泄漏:ThreadLocalMap的key(Thread對象)是弱引用,但value不是,如果key被回收,value還在。解法是手動remove掉。
(本節參考了《Java併發編程實戰》)

7.2 CountDownLatch和CyclicBarrier區別

https://blog.csdn.net/tolcf/article/details/50925145
CountDownLatch的子任務調用countDown後會繼續執行直至該線程結束。
CyclicBarrier的子任務await時會暫停執行;可重複使用,即await的數目達到設置的值時,喚醒所有await的線程進行下一輪。

7.3 ReentrantLock用了CAS但為什麼不是樂觀鎖?

https://blog.csdn.net/qq_35688140/article/details/101223701
我的看法:因為仍有可能造成阻塞,而樂觀鎖更新失敗則會直接返回(CAS允許自旋)。
換一個角度,悲觀鎖是預先做最壞的設想——一定會有其他任務併發,那麼就先佔好坑再更新;樂觀鎖則是認為不一定有併發,更新時判斷再是否有問題。這樣看來ReentrantLock從使用方式上來說是悲觀鎖。

7.4 雙重檢查加鎖

public classDoubleCheckedLocking{ //1
      private static Instance instance; //2
      public staticI nstance getInstance(){ //3
            if(instance==null){ //4:第一次檢查
                  synchronized(DoubleCheckedLocking.class){ //5:加鎖
                        if(instance==null) //6:第二次檢查
                              instance=newInstance(); //7:問題的根源出在這裏
                  } //8
            }//9
            return instance;
      }
}

問題

一個線程看到另一個線程初始化該類的部分構造的對象,即以上代碼註釋第4處這裏讀到非null但未完全初始化

原因

註釋第7處,創建對象實例的三步指令1.分配內存空間2.初始化3.引用指向分配的地址,2和3可能重排序

解決

方案1,給instance加violatile
方案2,使用佔位類,在類初始化時初始化對象,如下

public class InstanceFactory {
      private static class InstanceHolder{
            public static Instance instance= newInstance();
      }
      public static Instance getInstance() {
            return InstanceHolder.instance;  //這裏將導致InstanceHolder類被初始化
      }
}

7.5 FutureTask

FutureTask是Future的實現類,可以使用Future來接收線程池的submit()方法,也可以直接用FutureTask封裝任務,作為submit()的參數。具體的用法可以參考Java併發編程:Callable、Future和FutureTask 。
新版的FutureTask不再使用AQS。
FutureTask設置了當前工作線程,對於其任務維護了一個內部狀態轉換狀態機,通過CAS做狀態判斷和轉換。
當其他線程來get()時,如果任務未完成則放入等待隊列,自旋直到取到結果(for循環+LockSupport.park()),否則直接取結果。
具體實現原理可以參考《線程池系列一》-FutureTask原理講解與源碼剖析。

7.6 JDK1.6鎖優化之輕量級鎖和偏向鎖

實際上二者是有聯繫的,都是基於mark word實現。這個轉換關係可以用《深入理解Java虛擬機》第十三章的插圖表現

但是這個圖沒有體現輕量級鎖釋放后,仍可恢復為可偏向的。

7.7 問題排查三板斧

  1. top查看內存佔用率,-H可以看線程(不會完整展示),-p [pid]看指定進程的線程
    注意:linux線程和進程id都是在pid這一列展示的。
  2. pstack跟蹤進程棧,strace查看進程的系統操作。多次執行pstack來觀察進程是不是總是處於某種上下文中。
  3. jps直接獲取java進程id,jstat看java進程情況。jstate可用不同的參數來查看不同緯度的信息:類加載情況、gc統計、堆內存統計、新生代/老年代內存統計等,具體可以參考【JVM】jstat命令詳解—JVM的統計監測工具
  4. jstack打印java線程堆棧,和pstack展示方式很像,是java緯度的
  5. jmap打印java內存情況,-dump可以生成dump文件
  6. 分析dump文件,如MAT

8. LeetCode多線程習題

原題目和詳解參考Concurrency – 力扣

1114.按序打印

按照指定次序完成一系列動作,可以看做是buffer為1的1對1生產者消費者模型。

1115.交替打印FooBar

交替執行(不完全是生產者-消費者模型)某些動作。
可用的解法:

  • synchronized
  • Semaphore
  • CountDownLatch
  • CyclicBarrier
  • Lock

1116.打印零與奇偶數:0102…

和1114類似

1188. 設計有限阻塞隊列

注意: 使用synchronize解法時,wait()應置於while中循環判斷.
如果只用if,喚醒后不再次判斷dequeue可能NPE
本題可以加深理解為什麼要用while

1195. 交替打印字符串

根據AC的解法推斷, 每個線程只調用對應方法一次,因此需要在方法內部循環
不推薦只用synchronized,四個線程按順序打印, 如果使用單一的鎖很容易飢餓導致超時

推薦解法:
AtomicInteger無鎖解法
CylicBarrier高效解法
Semaphore加鎖

1279. 紅綠燈路口

題目難懂,暗含條件:車來時紅綠燈不是綠的,則強制變綠通過。紅綠燈本身的時間沒有嚴格控制

延伸閱讀

什麼是分佈式鎖
一文了解分佈式鎖

9. 未展開的話題

併發研究之CPU緩存一致性協議(MESI)
線程池原理(四):ScheduledThreadPoolExecutor
一半是天使一半是魔鬼的Unsafe類詳解 —— unsafe類都有什麼?用偏移量直接訪問、線程操作、內存管理和內存屏障、CAS

10. 其他參考

Java併發高頻面試題

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

小師妹學JavaIO之:MappedByteBuffer多大的文件我都裝得下

目錄

  • 簡介
  • 虛擬地址空間
  • 詳解MappedByteBuffer
    • MapMode
  • MappedByteBuffer的最大值
  • MappedByteBuffer的使用
  • MappedByteBuffer要注意的事項
  • 總結

簡介

大大大,我要大!小師妹要讀取的文件越來越大,該怎麼幫幫她,讓程序在性能和速度上面得到平衡呢?快來跟F師兄一起看看吧。

虛擬地址空間

小師妹:F師兄,你有沒有發現,最近硬盤的價格真的是好便宜好便宜,1T的硬盤大概要500塊,平均1M五毛錢。現在下個電影都1G起步,這是不是意味着我們買入了大數據時代?

沒錯,小師妹,硬件技術的進步也帶來了軟件技術的進步,兩者相輔相成,缺一不可。

小師妹:F師兄,如果要是去讀取G級的文件,有沒有什麼快捷簡單的方法?

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

還記得上次我們講的虛擬地址空間嗎?

再把上次講的圖搬過來:

通常來說我們的應用程序調用系統的接口從磁盤空間獲取Buffer數據,我們把自己的應用程序稱之為用戶空間,把系統的底層稱之為系統空間。

傳統的IO操作,是操作系統講磁盤中的文件讀入到系統空間裏面,然後再拷貝到用戶空間中,供用戶使用。

這中間多了一個Buffer拷貝的過程,如果這個量夠大的話,其實還是挺浪費時間的。

於是有人在想了,拷貝太麻煩太耗時了,我們單獨劃出一塊內存區域,讓系統空間和用戶空間同時映射到同一塊地址不就省略了拷貝的步驟嗎?

這個被劃出來的單獨的內存區域叫做虛擬地址空間,而不同空間到虛擬地址的映射就叫做Buffer Map。 Java中是有一個專門的MappedByteBuffer來代表這種操作。

小師妹:F師兄,那這個虛擬地址空間和內存有什麼區別呢?有了內存還要啥虛擬地址空間?

虛擬地址空間有兩個好處。

第一個好處就是虛擬地址空間對於應用程序本身而言是獨立的,從而保證了程序的互相隔離和程序中地址的確定性。比如說一個程序如果運行在虛擬地址空間中,那麼它的空間地址是固定的,不管他運行多少次。如果直接使用內存地址,那麼可能這次運行的時候內存地址可用,下次運行的時候內存地址不可用,就會導致潛在的程序出錯。

第二個好處就是虛擬空間地址可以比真實的內存地址大,這個大其實是對內存的使用做了優化,比如說會把很少使用的內存寫如磁盤,從而釋放出更多的內存來做更有意義的事情,而之前存儲到磁盤的數據,當真正需要的時候,再從磁盤中加載到內存中。

這樣物理內存實際上可以看做虛擬空間地址的緩存。

詳解MappedByteBuffer

小師妹:MappedByteBuffer聽起來好神奇,怎麼使用它呢?

我們先來看看MappedByteBuffer的定義:

public abstract class MappedByteBuffer
    extends ByteBuffer

它實際上是一個抽象類,具體的實現有兩個:

class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer
class DirectByteBufferR extends DirectByteBuffer
implements DirectBuffer

分別是DirectByteBuffer和DirectByteBufferR。

小師妹:F師兄,這兩個ByteBuffer有什麼區別呢?這個R是什麼意思?

R代表的是ReadOnly的意思,可能是因為本身是個類的名字就夠長了,所以搞了個縮寫。但是也不寫個註解,讓人看起來十分費解….

我們可以從RandomAccessFile的FilChannel中調用map方法獲得它的實例。

我們看下map方法的定義:

 public abstract MappedByteBuffer map(MapMode mode, long position, long size)
        throws IOException;

MapMode代表的是映射的模式,position表示是map開始的地址,size表示是ByteBuffer的大小。

MapMode

小師妹:F師兄,文件有隻讀,讀寫兩種模式,是不是MapMode也包含這兩類?

對的,其實NIO中的MapMode除了這兩個之外,還有一些其他很有趣的用法。

  • FileChannel.MapMode.READ_ONLY 表示只讀模式
  • FileChannel.MapMode.READ_WRITE 表示讀寫模式
  • FileChannel.MapMode.PRIVATE 表示copy-on-write模式,這個模式和READ_ONLY有點相似,它的操作是先對原數據進行拷貝,然後可以在拷貝之後的Buffer中進行讀寫。但是這個寫入並不會影響原數據。可以看做是數據的本地拷貝,所以叫做Private。

基本的MapMode就這三種了,其實除了基礎的MapMode,還有兩種擴展的MapMode:

  • ExtendedMapMode.READ_ONLY_SYNC 同步的讀
  • ExtendedMapMode.READ_WRITE_SYNC 同步的讀寫

MappedByteBuffer的最大值

小師妹:F師兄,既然可以映射到虛擬內存空間,那麼這個MappedByteBuffer是不是可以無限大?

當然不是了,首先虛擬地址空間的大小是有限制的,如果是32位的CPU,那麼一個指針佔用的地址就是4個字節,那麼能夠表示的最大值是0xFFFFFFFF,也就是4G。

另外我們看下map方法中size的類型是long,在java中long能夠表示的最大值是0x7fffffff,也就是2147483647字節,換算一下大概是2G。也就是說MappedByteBuffer的最大值是2G,一次最多只能map 2G的數據。

MappedByteBuffer的使用

小師妹,F師兄我們來舉兩個使用MappedByteBuffer讀寫的例子吧。

善!

先看一下怎麼使用MappedByteBuffer來讀數據:

public void readWithMap() throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "r"))
        {
            //get Channel
            FileChannel fileChannel = file.getChannel();
            //get mappedByteBuffer from fileChannel
            MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
            // check buffer
            log.info("is Loaded in physical memory: {}",buffer.isLoaded());  //只是一個提醒而不是guarantee
            log.info("capacity {}",buffer.capacity());
            //read the buffer
            for (int i = 0; i < buffer.limit(); i++)
            {
                log.info("get {}", buffer.get());
            }
        }
    }

然後再看一個使用MappedByteBuffer來寫數據的例子:

public void writeWithMap() throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(new File("src/main/resources/big.www.flydean.com"), "rw"))
        {
            //get Channel
            FileChannel fileChannel = file.getChannel();
            //get mappedByteBuffer from fileChannel
            MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, 4096 * 8 );
            // check buffer
            log.info("is Loaded in physical memory: {}",buffer.isLoaded());  //只是一個提醒而不是guarantee
            log.info("capacity {}",buffer.capacity());
            //write the content
            buffer.put("www.flydean.com".getBytes());
        }
    }

MappedByteBuffer要注意的事項

小師妹:F師兄,MappedByteBuffer因為使用了內存映射,所以讀寫的速度都會有所提升。那麼我們在使用中應該注意哪些問題呢?

MappedByteBuffer是沒有close方法的,即使它的FileChannel被close了,MappedByteBuffer仍然處於打開狀態,只有JVM進行垃圾回收的時候才會被關閉。而這個時間是不確定的。

總結

本文再次介紹了虛擬地址空間和MappedByteBuffer的使用。

本文的例子https://github.com/ddean2009/learn-java-io-nio

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/io-nio-mappedbytebuffer/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

【深度思考】JDK8中日期類型該如何使用?

在JDK8之前,處理日期時間,我們主要使用3個類,DateSimpleDateFormatCalendar

這3個類在使用時都或多或少的存在一些問題,比如SimpleDateFormat不是線程安全的,

比如DateCalendar獲取到的月份是0到11,而不是現實生活中的1到12,關於這一點,《阿里巴巴Java開發手冊》中也有提及,因為很容易犯錯:

不過,JDK8推出了全新的日期時間處理類解決了這些問題,比如InstantLocalDateLocalTimeLocalDateTimeDateTimeFormatter,在《阿里巴巴Java開發手冊》中也推薦使用Instant

LocalDateTimeDateTimeFormatter

但我發現好多項目中其實並沒有使用這些類,使用的還是之前的DateSimpleDateFormatCalendar,所以本篇博客就講解下JDK8新推出的日期時間類,主要是下面幾個:

  1. Instant
  2. LocalDate
  3. LocalTime
  4. LocalDateTime
  5. DateTimeFormatter

1. Instant

1.1 獲取當前時間

既然Instant可以代替Date類,那它肯定可以獲取當前時間:

Instant instant = Instant.now();
System.out.println(instant);

輸出結果:

2020-06-10T08:22:13.759Z

細心的你會發現,這個時間比北京時間少了8個小時,如果要輸出北京時間,可以加上默認時區:

System.out.println(instant.atZone(ZoneId.systemDefault()));

輸出結果:

2020-06-10T16:22:13.759+08:00[Asia/Shanghai]

1.2 獲取時間戳

Instant instant = Instant.now();

// 當前時間戳:單位為秒
System.out.println(instant.getEpochSecond());
// 當前時間戳:單位為毫秒
System.out.println(instant.toEpochMilli());

輸出結果:

1591777752

1591777752613

當然,也可以通過System.currentTimeMillis()獲取當前毫秒數。

1.3 將long轉換為Instant

1)根據秒數時間戳轉換:

Instant instant = Instant.now();
System.out.println(instant);

long epochSecond = instant.getEpochSecond();
System.out.println(Instant.ofEpochSecond(epochSecond));
System.out.println(Instant.ofEpochSecond(epochSecond, instant.getNano()));

輸出結果:

2020-06-10T08:40:54.046Z

2020-06-10T08:40:54Z

2020-06-10T08:40:54.046Z

2)根據毫秒數時間戳轉換:

Instant instant = Instant.now();
System.out.println(instant);

long epochMilli = instant.toEpochMilli();
System.out.println(Instant.ofEpochMilli(epochMilli));

輸出結果:

2020-06-10T08:43:25.607Z

2020-06-10T08:43:25.607Z

1.4 將String轉換為Instant

String text = "2020-06-10T08:46:55.967Z";
Instant parseInstant = Instant.parse(text);
System.out.println("秒時間戳:" + parseInstant.getEpochSecond());
System.out.println("豪秒時間戳:" + parseInstant.toEpochMilli());
System.out.println("納秒:" + parseInstant.getNano());

輸出結果:

秒時間戳:1591778815

豪秒時間戳:1591778815967

納秒:967000000

如果字符串格式不對,比如修改成2020-06-10T08:46:55.967,就會拋出java.time.format.DateTimeParseException異常,如下圖所示:

2. LocalDate

2.1 獲取當前日期

使用LocalDate獲取當前日期非常簡單,如下所示:

LocalDate today = LocalDate.now();
System.out.println("today: " + today);

輸出結果:

today: 2020-06-10

不用任何格式化,輸出結果就非常友好,如果使用Date,輸出這樣的格式,還得配合SimpleDateFormat指定yyyy-MM-dd進行格式化,一不小心還會出個bug,比如去年年底很火的1個bug,我當時還是截了圖的:

這2個好友是2019/12/31關注我的,但我2020年1月2號查看時,卻显示成了2020/12/31,為啥呢?格式化日期時格式寫錯了,應該是yyyy/MM/dd,卻寫成了YYYY/MM/dd,剛好那周跨年,就显示成下一年,也就是2020年了,當時好幾個博主寫過文章解析原因,我這裏就不做過多解釋了。

划重點:都說到這了,給大家安利下我新註冊的公眾號「申城異鄉人」,歡迎大家關注,更多原創文章等着你哦,哈哈。

2.2 獲取年月日

LocalDate today = LocalDate.now();

int year = today.getYear();
int month = today.getMonthValue();
int day = today.getDayOfMonth();

System.out.println("year: " + year);
System.out.println("month: " + month);
System.out.println("day: " + day);

輸出結果:

year: 2020

month: 6

day: 10

獲取月份終於返回1到12了,不像java.util.Calendar獲取月份返回的是0到11,獲取完還得加1。

2.3 指定日期

LocalDate specifiedDate = LocalDate.of(2020, 6, 1);
System.out.println("specifiedDate: " + specifiedDate);

輸出結果:

specifiedDate: 2020-06-01

如果確定月份,推薦使用另一個重載方法,使用枚舉指定月份:

LocalDate specifiedDate = LocalDate.of(2020, Month.JUNE, 1);

2.4 比較日期是否相等

LocalDate localDate1 = LocalDate.now();
LocalDate localDate2 = LocalDate.of(2020, 6, 10);
if (localDate1.equals(localDate2)) {
    System.out.println("localDate1 equals localDate2");
}

輸出結果:

localDate1 equals localDate2

2.5 獲取日期是本周/本月/本年的第幾天

LocalDate today = LocalDate.now();

System.out.println("Today:" + today);
System.out.println("Today is:" + today.getDayOfWeek());
System.out.println("今天是本周的第" + today.getDayOfWeek().getValue() + "天");
System.out.println("今天是本月的第" + today.getDayOfMonth() + "天");
System.out.println("今天是本年的第" + today.getDayOfYear() + "天");

輸出結果:

Today:2020-06-11

Today is:THURSDAY

今天是本周的第4天

今天是本月的第11天

今天是本年的第163天

2.6 判斷是否為閏年

LocalDate today = LocalDate.now();

System.out.println(today.getYear() + " is leap year:" + today.isLeapYear());

輸出結果:

2020 is leap year:true

3. LocalTime

3.1 獲取時分秒

如果使用java.util.Date,那代碼是下面這樣的:

Date date = new Date();

int hour = date.getHours();
int minute = date.getMinutes();
int second = date.getSeconds();

System.out.println("hour: " + hour);
System.out.println("minute: " + minute);
System.out.println("second: " + second);

輸出結果:

注意事項:這幾個方法已經過期了,因此強烈不建議在項目中使用:

如果使用java.util.Calendar,那代碼是下面這樣的:

Calendar calendar = Calendar.getInstance();

// 12小時制
int hourOf12 = calendar.get(Calendar.HOUR);
// 24小時制
int hourOf24 = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
int milliSecond = calendar.get(Calendar.MILLISECOND);

System.out.println("hourOf12: " + hourOf12);
System.out.println("hourOf24: " + hourOf24);
System.out.println("minute: " + minute);
System.out.println("second: " + second);
System.out.println("milliSecond: " + milliSecond);

輸出結果:

注意事項:獲取小時時,有2個選項,1個返回12小時制的小時數,1個返回24小時制的小時數,因為現在是晚上8點,所以calendar.get(Calendar.HOUR)返回8,而calendar.get(Calendar.HOUR_OF_DAY)返回20。

如果使用java.time.LocalTime,那代碼是下面這樣的:

LocalTime localTime = LocalTime.now();
System.out.println("localTime:" + localTime);

int hour = localTime.getHour();
int minute = localTime.getMinute();
int second = localTime.getSecond();

System.out.println("hour: " + hour);
System.out.println("minute: " + minute);
System.out.println("second: " + second);

輸出結果:

可以看出,LocalTime只有時間沒有日期。

4. LocalDateTime

4.1 獲取當前時間

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("localDateTime:" + localDateTime);

輸出結果:

localDateTime: 2020-06-11T11:03:21.376

4.2 獲取年月日時分秒

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("localDateTime: " + localDateTime);

System.out.println("year: " + localDateTime.getYear());
System.out.println("month: " + localDateTime.getMonthValue());
System.out.println("day: " + localDateTime.getDayOfMonth());
System.out.println("hour: " + localDateTime.getHour());
System.out.println("minute: " + localDateTime.getMinute());
System.out.println("second: " + localDateTime.getSecond());

輸出結果:

4.3 增加天數/小時

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("localDateTime: " + localDateTime);

LocalDateTime tomorrow = localDateTime.plusDays(1);
System.out.println("tomorrow: " + tomorrow);

LocalDateTime nextHour = localDateTime.plusHours(1);
System.out.println("nextHour: " + nextHour);

輸出結果:

localDateTime: 2020-06-11T11:13:44.979

tomorrow: 2020-06-12T11:13:44.979

nextHour: 2020-06-11T12:13:44.979

LocalDateTime還提供了添加年、周、分鐘、秒這些方法,這裏就不一一列舉了:

4.4 減少天數/小時

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("localDateTime: " + localDateTime);

LocalDateTime yesterday = localDateTime.minusDays(1);
System.out.println("yesterday: " + yesterday);

LocalDateTime lastHour = localDateTime.minusHours(1);
System.out.println("lastHour: " + lastHour);

輸出結果:

localDateTime: 2020-06-11T11:20:38.896

yesterday: 2020-06-10T11:20:38.896

lastHour: 2020-06-11T10:20:38.896

類似的,LocalDateTime還提供了減少年、周、分鐘、秒這些方法,這裏就不一一列舉了:

4.5 獲取時間是本周/本年的第幾天

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println("localDateTime: " + localDateTime);

System.out.println("DayOfWeek: " + localDateTime.getDayOfWeek().getValue());
System.out.println("DayOfYear: " + localDateTime.getDayOfYear());

輸出結果:

localDateTime: 2020-06-11T11:32:31.731

DayOfWeek: 4

DayOfYear: 163

5. DateTimeFormatter

JDK8中推出了java.time.format.DateTimeFormatter來處理日期格式化問題,《阿里巴巴Java開發手冊》中也是建議使用DateTimeFormatter代替SimpleDateFormat

5.1 格式化LocalDate

LocalDate localDate = LocalDate.now();

System.out.println("ISO_DATE: " + localDate.format(DateTimeFormatter.ISO_DATE));
System.out.println("BASIC_ISO_DATE: " + localDate.format(DateTimeFormatter.BASIC_ISO_DATE));
System.out.println("ISO_WEEK_DATE: " + localDate.format(DateTimeFormatter.ISO_WEEK_DATE));
System.out.println("ISO_ORDINAL_DATE: " + localDate.format(DateTimeFormatter.ISO_ORDINAL_DATE));

輸出結果:

如果提供的格式無法滿足你的需求,你還可以像以前一樣自定義格式:

LocalDate localDate = LocalDate.now();

System.out.println("yyyy/MM/dd: " + localDate.format(DateTimeFormatter.ofPattern("yyyy/MM/dd")));

輸出結果:

yyyy/MM/dd: 2020/06/11

5.2 格式化LocalTime

LocalTime localTime = LocalTime.now();
System.out.println(localTime);
System.out.println("ISO_TIME: " + localTime.format(DateTimeFormatter.ISO_TIME));
System.out.println("HH:mm:ss: " + localTime.format(DateTimeFormatter.ofPattern("HH:mm:ss")));

輸出結果:

14:28:35.230

ISO_TIME: 14:28:35.23

HH:mm:ss: 14:28:35

5.3 格式化LocalDateTime

LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);
System.out.println("ISO_DATE_TIME: " + localDateTime.format(DateTimeFormatter.ISO_DATE_TIME));
System.out.println("ISO_DATE: " + localDateTime.format(DateTimeFormatter.ISO_DATE));

輸出結果:

2020-06-11T14:33:18.303

ISO_DATE_TIME: 2020-06-11T14:33:18.303

ISO_DATE: 2020-06-11

6. 類型相互轉換

6.1 Instant轉Date

JDK8中,Date新增了from()方法,將Instant轉換為Date,代碼如下所示:

Instant instant = Instant.now();
System.out.println(instant);

Date dateFromInstant = Date.from(instant);
System.out.println(dateFromInstant);

輸出結果:

2020-06-11T06:39:34.979Z

Thu Jun 11 14:39:34 CST 2020

6.2 Date轉Instant

JDK8中,Date新增了toInstant方法,將Date轉換為Instant,代碼如下所示:

Date date = new Date();
Instant dateToInstant = date.toInstant();
System.out.println(date);
System.out.println(dateToInstant);

輸出結果:

Thu Jun 11 14:46:12 CST 2020

2020-06-11T06:46:12.112Z

6.3 Date轉LocalDateTime

Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
System.out.println(date);
System.out.println(localDateTimeOfInstant);

輸出結果:

Thu Jun 11 14:51:07 CST 2020

2020-06-11T14:51:07.904

6.4 Date轉LocalDate

Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
LocalDate localDate = localDateTimeOfInstant.toLocalDate();
System.out.println(date);
System.out.println(localDate);

輸出結果:

Thu Jun 11 14:59:38 CST 2020

2020-06-11

可以看出,Date是先轉換為Instant,再轉換為LocalDateTime,然後通過LocalDateTime獲取LocalDate

6.5 Date轉LocalTime

Date date = new Date();
Instant instant = date.toInstant();
LocalDateTime localDateTimeOfInstant = LocalDateTime.ofInstant(instant, ZoneId.systemDefault());
LocalTime toLocalTime = localDateTimeOfInstant.toLocalTime();
System.out.println(date);
System.out.println(toLocalTime);

輸出結果:

Thu Jun 11 15:06:14 CST 2020

15:06:14.531

可以看出,Date是先轉換為Instant,再轉換為LocalDateTime,然後通過LocalDateTime獲取LocalTime

6.6 LocalDateTime轉Date

LocalDateTime localDateTime = LocalDateTime.now();

Instant toInstant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date dateFromInstant = Date.from(toInstant);
System.out.println(localDateTime);
System.out.println(dateFromInstant);

輸出結果:

2020-06-11T15:12:11.600

Thu Jun 11 15:12:11 CST 2020

6.7 LocalDate轉Date

LocalDate today = LocalDate.now();

LocalDateTime localDateTime = localDate.atStartOfDay();
Instant toInstant = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date dateFromLocalDate = Date.from(toInstant);
System.out.println(dateFromLocalDate);

輸出結果:

Thu Jun 11 00:00:00 CST 2020

6.8 LocalTime轉Date

LocalDate localDate = LocalDate.now();
LocalTime localTime = LocalTime.now();

LocalDateTime localDateTime = LocalDateTime.of(localDate, localTime);
Instant instantFromLocalTime = localDateTime.atZone(ZoneId.systemDefault()).toInstant();
Date dateFromLocalTime = Date.from(instantFromLocalTime);

System.out.println(dateFromLocalTime);

輸出結果:

Thu Jun 11 15:24:18 CST 2020

7. 總結

JDK8推出了全新的日期時間類,如InstantLocaleDateLocalTimeLocalDateTimeDateTimeFormatter,設計比之前更合理,也是線程安全的。

《阿里巴巴Java開發規範》中也推薦使用Instant代替DateLocalDateTime 代替 CalendarDateTimeFormatter 代替 SimpleDateFormat

因此,如果條件允許,建議在項目中使用,沒有使用的,可以考慮升級下。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

車主們說這款有着跑車外觀的車 油耗出奇地低

只是高速時的風噪略大、儲物空間比較少,應該在四個門板多做一些儲物格。關於空間,我的身高176cm,把駕駛位置調好后,我在後排還有一拳多的腿部空間。而我的車型是2。0L的平均百公里油耗是7。8L。車主:Bestss購買車型:三廂 1。

10多萬的合資A級車選擇非常多,要論最個性、最具動感外觀的車型,馬自達3昂克賽拉絕對是最具實力的車型之一。下面我們就來看看這款車的車主們都對它有哪些評價。

長安馬自達-馬自達3 Axela昂克賽拉

指導價:11.49-15.99萬

車主:人稱啊明

購買車型:三廂 1.5L自動豪華型

裸車價格:12.89萬

每一個男人都有一個跑車夢,昂克賽拉的外觀很像跑車,還有聰明的變速箱、讓人滿意的低油耗、起步快等優點,所以我最終選擇了這款車。

它的操控真的不錯,給人的感覺是穩、實、准,轉向手感不錯,採用了四輪獨立懸架,支撐性好,在同級車型中性價比很高。

目前我的車行駛了快8000公里了,平均百公里油耗只有7.1L!創馳藍天技術真不是蓋的。

車主:佛山小偉

購買車型:三廂 2.0L自動旗艦型

裸車價格:14.99萬

我對“魂動”的設計外觀和駕駛體驗最滿意,它指向精準、換擋果斷、動力也充沛。只是高速時的風噪略大、儲物空間比較少,應該在四個門板多做一些儲物格。

關於空間,我的身高176cm,把駕駛位置調好后,我在後排還有一拳多的腿部空間。而我的車型是2.0L的平均百公里油耗是7.8L。

車主:Bestss

購買車型:三廂 1.5L手動豪華型

裸車價格:12.29萬

外觀和內飾就不用我多說了,大多數買昂克賽拉的人都是奔着漂亮的外觀去的!它的1.5L缸內直噴發動機動力夠用,2000轉以後的動力有比較大的爆發。

行駛起來胎噪有些大,可能是輪胎側重抓地力的原因,還有就是車漆有些薄。

目前我的車行駛了15000公里了,我對它比較滿意,目前的平均油耗是7.2L,油耗不算高!

編者點評:

昂克賽拉是一款性格鮮明的車型,它堅持採用四輪獨立懸挂、偏向性能的輪胎、AT變速箱等,讓它的操控性出色,如果你喜歡駕駛感受好的A級車,昂克賽拉絕對是一個不錯的選擇。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

為什麼朋友說他夢想的車是艾母雞?看完本文後終於懂了

說到奔馳許多人最初的印象都是寬、大、貴外加後排坐着一個人肥,牙黃,地中海的老闆似乎奔馳一向跟年輕、運動、操控這些詞扯不上什麼關係所以坊間也流傳這麼一個說法——甚至有一些“自信狂人”遇上大奔時會抑制不住腎上腺

說到奔馳

許多人最初的印象都是

寬、大、貴

外加後排坐着一個

人肥,牙黃,地中海的老闆

似乎奔馳一向跟

年輕、運動、操控這些詞

扯不上什麼關係

所以坊間也流傳這麼一個說法——

甚至有一些“自信狂人”

遇上大奔時

會抑制不住腎上腺本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

我很驚訝!十幾萬的國產SUV竟有人幹得過眾泰?

對於我們汽車媒體來說,這是個很方面的功能。榮威RX5配備了車身穩定系統、定速巡航、一鍵啟動、電動駐車、前撞預警系統、陡坡緩降、電動後備廂、車道保持、道路限速識別、四驅系統鎖止等豐富配置。動力系統方面,RX5將搭載1。

國內緊湊型SUV的熱度只增不減,2016年,榮威帶來了他們潛心打造,並得到馬雲的阿里技術支持的榮威RX5,主打互聯網概念。

榮威RX5外觀走大氣沉穩范,中規中矩但又精緻的造型給人不錯的印象。前臉“展翼格柵”將兩側前大燈融貫一體,前大燈為矩陣式LED大燈,其由24顆LED光源組成,燈組內部還集成了“如意形”LED日間行車燈。矩陣式全LED大燈更顯科技感。

一進車內最吸睛的當屬中控10.4英寸大屏。由於大屏幕的使用,一些傳統按鍵被取消,整體設計簡潔時尚,僅在屏幕下方保留了五個實體按鍵。同時RX5在內飾用料上也多處使用了軟質皮革包裹,凸顯質感。

RX5配備了榮威和阿里聯合開發的Yun OS,功能較為強大,可以通過大數據為用戶提供個性化服務。還可以支持綁定航拍機和運動相機,旅途中拍攝的畫面可以呈現在中控的大屏幕上。對於我們汽車媒體來說,這是個很方面的功能。

榮威RX5配備了車身穩定系統、定速巡航、一鍵啟動、電動駐車、前撞預警系統、陡坡緩降、電動後備廂、車道保持、道路限速識別、四驅系統鎖止等豐富配置。

動力系統方面,RX5將搭載1.5T和2.0T兩款渦輪增壓發動機,其中1.5T發動機最大功率為169馬,峰值扭矩250N·m,與之匹配的是手動/TST 7速雙離合變速箱;2.0T發動機最大功率為220馬力,峰值扭矩350N·m,與之匹配的是TST 6速濕式雙離合變速箱。

整車開起來動力表現不錯,車輛的轉向是比較精準的,底盤偏向柔軟和舒適,但是又保留了一些路感。很符合家用SUV得定位。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

機器學習——手把手教你用Python實現回歸樹模型

本文始發於個人公眾號:TechFlow,原創不易,求個關注

今天這篇是機器學習專題的第24篇文章,我們來聊聊回歸樹模型。

所謂的回歸樹模型其實就是用樹形模型來解決回歸問題,樹模型當中最經典的自然還是決策樹模型,它也是幾乎所有樹模型的基礎。雖然基本結構都是使用決策樹,但是根據預測方法的不同也可以分為兩種。第一種,樹上的恭弘=叶 恭弘子節點就對應一個預測值和分類樹對應,這一種方法稱為回歸樹。第二種,樹上的恭弘=叶 恭弘子節點對應一個線性模型,最後的結果由線性模型給出。這一種方法稱為模型樹。

今天我們先來看看其中的回歸樹。

回歸樹模型

回歸樹模型的核心算法,也就是構建決策樹的算法,就是我們上篇文章所講的CART算法。如果有生疏或者是遺漏的同學,可以通過下方傳送門回顧一下:

機器學習——十大數據挖掘之一的決策樹CART算法

CART算法的核心精髓就是我們每次選擇特徵對數據進行拆分的時候,永遠對數據集進行二分。無論是離散特徵還是連續性特徵,一視同仁。CART還有一個特點是使用GINI指數而不是信息增益或者是信息增益比來選擇拆分的特徵,但是在回歸問題當中用不到這個。因為回歸問題的損失函數是均方差,而不是交叉熵,很難用熵來衡量連續值的準確度。

在分類樹當中,我們一個恭弘=叶 恭弘子節點代表一個類別的預測值,這個類別的值是落到這個恭弘=叶 恭弘子節點當中訓練樣本的類別的眾數,也就是出現頻率最高的類別。在回歸樹當中,恭弘=叶 恭弘子節點對應的自然就是一個連續值。這個連續值是落到這個節點的訓練樣本的均值,它的誤差就是這些樣本的均方差。

另外,之前我們在選擇特徵的劃分閾值的時候,對閾值的選擇進行了優化,只選擇了那些會引起預測類別變化的閾值。但是在回歸問題當中,由於預測值是一個浮點數,所以這個優化也不存在了。整體上來說,其實回歸樹的實現難度比分類樹是更低的。

實戰

我們首先來加載數據,我們這次使用的是scikit-learn庫當中經典的波士頓房價預測的數據。關於房價預測,kaggle當中也有一個類似的比賽,叫做:house-prices-advanced-regression-techniques。不過給出的特徵更多,並且存在缺失等情況,需要我們進行大量的特徵工程。感興趣的同學可以自行研究一下。

首先,我們來獲取數據,由於sklearn庫當中已經有數據了,我們可以直接調用api獲取,非常簡單:

import numpy as np
import pandas as pd
from sklearn.datasets import load_boston
boston = load_boston()

X, y = boston.data, boston.target

我們輸出前幾條數據查看一下:

這個數據質量很高,sklearn庫已經替我們做完了數據篩選與特徵工程,直接拿來用就可以了。為了方便我們傳遞數據,我們將X和y合併在一起。由於y是一維的數組形式是不能和二維的X合併的,所以我們需要先對y進行reshape之後再進行合併。

y = y.reshape(-1, 1)
X = np.hstack((X, y))

hstack函數可以將兩個np的array橫向拼接,與之對應的是vstack,是將兩個array縱向拼接,這個也是常規操作。合併之後,y作為新的一列添加在了X的後面。數據搞定了,接下來就要輪到實現模型了。

在實現決策樹的主體部分之前,我們先來實現兩個輔助函數。第一個輔助函數是計算一批樣本的方差和,第二個輔助函數是獲取樣本的均值,也就是子節點的預測值。

def node_mean(X):
    return np.mean(X[:, -1])


def node_variance(X):
    return np.var(X[:, -1]) * X.shape[0]

這個搞定了之後,我們繼續實現根據閾值拆分數據的函數。這個也可以復用之前的代碼:

from collections import defaultdict
def split_dataset(X, idx, thred):
    split_data = defaultdict(list)
    for x in X:
        split_data[x[idx] < thred].append(x)
    return list(split_data.values()), list(split_data.keys())

接下來是兩個很重要的函數,分別是get_thresholds和split_variance。顧名思義,第一個函數用來獲取閾值,前面說了由於我們做的是回歸模型,所以理論上來說特徵的每一個取值都可以作為切分的依據。但是也不排除可能會存在多條數據的特徵值相同的情況,所以我們對它進行去重。第二個函數是根據閾值對數據進行拆分,返回拆分之後的方差和。

def get_thresholds(X, i):
    return set(X[:, i].tolist())

# 每次迭代方差優化的底線
MINIMUM_IMPROVE = 2.0
# 每個恭弘=叶 恭弘子節點最少樣本數
MINIMUM_SAMPLES = 10

def split_variance(dataset, idx, threshold):
    left, right = [], []
    n = dataset.shape[0]
    for data in dataset:
        if data[idx] < threshold:
            left.append(data)
        else:
            right.append(data)
    left, right = np.array(left), np.array(right)
    # 預剪枝
    # 如果拆分結果有一邊過少,則返回None,防止過擬合
    if len(left) < MINIMUM_SAMPLES or len(right) < MINIMUM_SAMPLES:
        return None
    # 拆分之後的方差和等於左子樹的方差和加上右子樹的方差和
    # 因為是方差和而不是均方差,所以可以累加
    return node_variance(left) + node_variance(right)

這裏我們用到了MINIMUM_SAMPLES這個參數,它是用來預剪枝用的。由於我們是回歸模型,如果不對決策樹的生長加以限制,那麼很有可能得到的決策樹的恭弘=叶 恭弘子節點和訓練樣本的數量一樣多。這顯然就陷入了過擬合了,對於模型的效果是有害無益的。所以我們要限制每個節點的樣本數量,這個是一個參數,我們可以根據需要自行調整。

接下來,就是特徵和閾值篩選的函數了。我們需要開發一個函數來遍歷所有可以拆分的特徵和閾值,對數據進行拆分,從所有特徵當中找到最佳的拆分可能。

def choose_feature_to_split(dataset):
    n = len(dataset[0])-1
    m = len(dataset)
    # 記錄最佳方差,特徵和閾值
    var_ = node_variance(dataset)
    bestVar = float('inf')
    feature = -1
    thred = None
    for i in range(n):
        threds = get_thresholds(dataset, i)
        for t in threds:
            # 遍歷所有的閾值,計算每個閾值的variance
            v = split_variance(dataset, i, t)
            # 如果v等於None,說明拆分過擬合了,跳過
            if v is None:
                continue
            if v  < bestVar:
                bestVar, feature, thred = v, i, t
    # 如果最好的拆分效果達不到要求,那麼就不拆分,控制子樹的數量
    if var_ - bestVar < MINIMUM_IMPROVE:
        return None, None
    return feature, thred

和上面一樣,這個函數當中也用到了一個預剪枝的參數MINIMUM_IMPROVE,它衡量的是每一次生成子樹帶來的收益。當某一次生成子樹帶來的收益小於某個值的時候,說明收益很小,並不划算,所以我們就放棄這次子樹的生成。這也是預剪枝的一種。

這些都搞定了之後,就可以來建樹了。建樹的過程和之前類似,只是我們這一次的數據當中沒有特徵的name,所以我們去掉特徵名稱的相關邏輯。

def create_decision_tree(dataset):
    dataset = np.array(dataset)
    
    # 如果當前數量小於10,那麼就不再繼續劃分了
    if dataset.shape[0] < MINIMUM_SAMPLES:
        return node_mean(dataset)
    
    # 記錄最佳拆分的特徵和閾值
    fidx, th = choose_feature_to_split(dataset)
    if fidx is None:
        return th
    
    node = {}
    node['feature'] = fidx
    node['threshold'] = th
    
    # 遞歸建樹
    split_data, vals = split_dataset(dataset, fidx, th)
    for data, val in zip(split_data, vals):
        node[val] = create_decision_tree(data)
    return node

我們來完整測試一下建樹,首先我們需要對原始數據進行拆分。將原始數據拆分成訓練數據和測試數據,由於我們的場景比較簡單,就不設置驗證數據了。

拆分數據不用我們自己實現,sklearn當中提供了相應的工具,我們直接調用即可:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=23)

我們一般用到的參數就兩個,一個是test_size,它可以是一個整數也可以是一個浮點數。如果是整數,代表的是測試集的樣本數量。如果是一個0-1.0的浮點數,則代表測試集的佔比。random_state是生成隨機數的時候用到的隨機種子。

我們輸出一下生成的樹,由於數據量比較大,可以看到一顆龐大的樹結構。建樹的部分實現了之後,最後剩下的就是預測的部分了。

預測部分的代碼和之前分類樹相差不大,整體的邏輯完全一樣,只是去掉了feature_names的相關邏輯。

def classify(node, data):
    key = node['feature']
    pred = None
    thred = node['threshold']

    if isinstance(node[data[key] < thred], dict):
        pred = classify(node[data[key] < thred], data)
    else:
        pred = node[data[key] < thred]
            
    # 放置pred為空,挑選一個恭弘=叶 恭弘子節點作為替補
    if pred is None:
        for key in node:
            if not isinstance(node[key], dict):
                pred = node[key]
                break
    return pred

由於這個函數一次只能接受一條數據,如果我們想要批量預測的話還不行,所以最好的話再實現一個批量預測的predict函數比較好。

def predict(node, X):
    y_pred = []
    for x in X:
        y = classify(node, x)
        y_pred.append(y)
    return np.array(y_pred)

后剪枝

后剪枝的英文原文是post-prune,但是翻譯成事後剪枝也有點奇怪。anyway,我們就用后剪枝這個詞好了。

在回歸樹當中,我們利用的思想非常樸素,在建樹的時候建立一棵盡量複雜龐大的樹。然後在通過測試集對這棵樹進行修剪,修剪的邏輯也非常簡單,我們判斷一棵子樹存在分叉和沒有分叉單獨成為恭弘=叶 恭弘子節點時的誤差,如果修剪之後誤差更小,那麼我們就減去這棵子樹。

整個剪枝的過程和建樹的過程一樣,從上到下,遞歸執行。

整個邏輯很好理解,我們直接來看代碼:

def is_dict(node):
    return isinstance(node, dict)


def prune(node, testData):
    testData = np.array(testData)
    if testData.shape[0] == 0:
        return node
 
    # 拆分數據
    split_data, _ = split_dataset(testData, node['feature'], node['threshold'])
    # 對左右子樹遞歸修剪
    if is_dict(node[0]):
        node[0] = prune(node[0], split_data[0])
    if is_dict(node[1]) and len(split_data) > 1:
        node[1] = prune(node[1], split_data[1])

    # 如果左右都是恭弘=叶 恭弘子節點,那麼判斷當前子樹是否需要修剪
    if len(split_data) > 1 and not is_dict(node[0]) and not is_dict(node[1]):
        # 計算修剪前的方差和
        baseError = np.sum(np.power(np.array(split_data[0])[:, -1] - node[0], 2)) + np.sum(np.power(np.array(split_data[1])[:, -1] - node[1], 2))
        # 計算修剪后的方差和
        meanVal = (node[0] + node[1]) / 2
        mergeError = np.sum(np.power(meanVal - testData[:, -1], 2))
        if mergeError < baseError:
            return meanVal
        else:
            return node
    return node

最後,我們對修剪之後的效果做一下驗證:

從圖中可以看到,修剪之前我們在測試數據上的均方差是19.65,而修剪之後降低到了19.48。從數值上來看是有效果的,只是由於我們的訓練數據比較少,同時進行了預剪枝,影響了后剪枝的效果。但是對於實際的機器學習工程來說,一個方法只要是有明確效果的,在代價可以承受的範圍內,它就是有價值的,千萬不能覺得提升不明顯,而隨便否定一個方法。

這裏計算均方差的時候用到了sklearn當中的一個庫函數mean_square_error,從名字當中我們也可以看得出來它的用途,它可以對兩個Numpy的array計算均方差

總結

關於回歸樹模型的相關內容到這裏就結束了,我們不僅親手實現了模型,而且還在真實的數據集上做了實驗。如果你是親手實現的模型的代碼,相信你一定會有很多收穫。

雖然從實際運用來說我們幾乎不會使用樹模型來做回歸任務,但是回歸樹模型本身是非常有意義的。因為在它的基礎上我們發展出了很多效果更好的模型,比如大名鼎鼎的GBDT。因此理解回歸樹對於我們後續進階的學習是非常重要的。在深度學習普及之前,其實大多數高效果的模型都是以樹模型為基礎的,比如隨機森林、GBDT、Adaboost等等。可以說樹模型撐起了機器學習的半個時代,這麼說相信大家應該都能理解它的重要性了吧。

今天的文章就到這裏,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

本文使用 mdnice 排版

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

【其他文章推薦】

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

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

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

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

※回頭車貨運收費標準

沒有國產主機,怎麼開發:交叉編譯和QEMU虛擬機

1. 背景

近期國產化的趨勢越來越濃,包括國產操作系統、國產CPU等。時隔十多年,QQ for Linux也更新了。做為軟件開發人員,“有幸”也需要適配國產化。至於國產化的意義等就不在此討論。

本文提到的國產主機主要是指使用國產CPU和操作系統的計算機,比如:操作系統是銀河麒麟,CPU是飛騰FT2000。如果需要做適配開發,起碼需要一台對應的主機吧。據說在國產化早期,有錢都難買到機器,需要特殊渠道申請購買。不過,現在購買還是比較方便的。

通過客戶提供的正規正統的廠家詢價,着實嚇一跳,一台居然要一萬多!!而同等性能配置的windows-x86普通台式主機,才兩三千塊左右,相差有點大呀。本着能省就省的原則,上萬能的某寶看能不能淘一個。真得感謝馬爸爸和深圳華強北,5千多塊,突然感覺肉沒那麼痛了。

其實完全可以理解,國產的批量肯定很小很小,價格必然是高的。對於不專門開發“國產軟件”的公司來說,買一台使用率比較低的機器不太值得。後面將介紹在沒有國產主機情況下,進行軟件開發的兩種替代方法:交叉編譯和QEMU虛擬機。

2. 銀河麒麟是什麼

銀河麒麟操作系統有服務器版本和桌面版本,本文使用的是桌面版本。具體細節看官方的介紹即可,就不做搬運工了。官方說的自主研發、安全可控都不是我們所關心的,我們只需要關心它的內核是什麼,會不會如網上所說根本就是個Ubutun,改個皮膚而已?!。

先用VMware安裝個虛擬機試試吧,網上找了一個只有X86架構的鏡像包Kylin-4.0.2-desktop-sp2_Community-20171127-x86_64.iso,安裝過程略過,使用命令“uname -a”查一下。

Linux wrgz-Lenovo 4.11.0-14-generic #20~16.04.1kord0k1-Ubuntu SMP Wed Oct 18 00:56:13 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

看到Ubuntu就放心了,就當它是個Ubuntu Linux就行了。

3. 飛騰FT2000又是什麼

通俗點講它就是個CPU,再看看飛騰的官網上的描述。FT-2000系列芯片是基於飛騰片上并行系統(PSoC)體繫結構設計的通用微處理器,兼容ARMv8指令集,兼容支持ARM64和ARM32兩種執行模式。哦嚯,划個重點,簡單點看它就是一個ARMv8的64位CPU。

划個不考試的重點:對於應用軟件開發者,簡單理解為是在ARMv8架構上的Ubuntu Linux上進行開發軟件;對於普通辦公者,則理解為是仿Windows的Linux系統。

4. 交叉編譯

本文提到的軟件開發,是使用C/C++開發無界面的應用軟件,實際上開發和測試都有是可以在Ubuntu上進行。但發布軟件則需要真機編譯或者交叉編譯才能運行。

很幸運,在上飛騰官網時,發現了飛騰FT2000的技術文檔FT-2000+64Sv1.1.pdf,裏面有介紹到交叉編譯環境。

  • 安裝Ubuntu16.04(可安裝在虛擬機上或 X86電腦裸機上)
  • 安裝成功后,虛擬機 apt 源修改 修改/etc/apt/source.list 內容為如下:
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial main restricted
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates main restricted
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial universe
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates universe
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-backports main restricted universe > multiverse
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security main restricted
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security universe
deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security multiverse
  • 運行 apt-get update,再運行apt-get install gcc-aarch64-linux-gnu安裝
  • 使用命令aarch64-linux-gnu-gcc –v可以看到gcc版本號為gcc version 5.4.0 20160609

有了交叉編譯器,編譯是很輕鬆的事。經後續測試,交叉編譯出來的程序,可以在國產真機上運行。

5. QEMU虛擬機是什麼

我們經常使用的虛擬機軟件是VMware,擺着這麼好的不用,為什麼選擇QEMU呢。這得從他們的區別說起。

VMware重點於在一個硬件平台下運行多個操作系統,虛擬硬件平台與宿主硬件架構一致,也就是說虛擬機程序中的指令一般就是宿主CPU指令集,可以直接執行,因此一般速度上也就比較快。

QEMU的特點是可以虛擬不同的硬件平台架構,比如在X86機器上虛擬出ARM架構的機器。許多基於ARM指令集的Android手機模擬器是基於Qemu的,很適合無真機情況下進行Android開發。當然執行ARM指令,需要轉換成X86指令才能在宿主機器上運行,這樣速度一般會慢點。

由於本文提到的國產主機就是ARM架構的,VMware並不適用,而QEMU則符合要求。還有一個原因是QEMU支持Windows,只需要一個安裝包,安裝過程簡單,太香了。

6. QEMU安裝銀河麒麟操作系統

無獨有偶,鯤鵬處理器也是ARMv8指令集,在華為官網看到詳細的安裝過程,安裝細節可參考https://www.huaweicloud.com/kunpeng/software/qemu.html。

下面只針對一些重點關注點做些說明。

  • 需要下載一個Arm64架構的麒麟桌面操作系統鏡像包,名字類似Kylin-4.0.2-desktop-sp3-xxxxxxx-arm64.iso。之所以重點提這點,是因為這種鏡像包在網上很難找。有想到用Arm64架構的Ubuntu鏡像包代替,才發現原來官方並沒有提供ARM桌面版的鏡像包(有ARM服務器版)。
  • 原來華為提供的安裝參數有些問題,包括網絡、鼠標、鍵盤參數。這些參數配置不對,會直接影響使用。

QEMU有一個不太人性化的特點,就是沒有提供類似VMware的界面操作,只能通過命令操作,參數還特別多,網上的資料不多,官方文檔都有是英文的。下面給出三個重要的QEMU命令:創建、安裝、啟動。

創建
這個步驟就是創建一個預分配一個大文件,做為虛擬機的磁盤,我比較任性地分配了40G。

c:\qemu\qemu-img.exe create D:\qemu\vm\kylin\hdd01.img 40G

安裝

c:\qemu\qemu-system-aarch64.exe -m 4096 -cpu cortex-a72 -smp 2,cores=2,threads=1,sockets=1 -M virt -bios D:\qemu\bios\QEMU_EFI.fd -net nic,model=pcnet -device nec-usb-xhci -device usb-kbd -device usb-mouse -device VGA -drive if=none,file=D:\software\kylin\Kylin-4.0.2-desktop-sp3-19122616.Z1-arm64.iso,id=cdrom,media=cdrom -device virtio-scsi-device -device scsi-cd,drive=cdrom -drive if=none,file=D:\qemu\vm\kylin\hdd01.img,id=hd0 -device virtio-blk-device,drive=hd0

啟動

c:\qemu\qemu-system-aarch64.exe -m 4096 -cpu cortex-a72 -smp 2,cores=2,threads=1,sockets=1 -M virt -bios D:\qemu\bios\QEMU_EFI.fd -net nic -net tap,ifname=tap0 -device nec-usb-xhci -device usb-kbd -device usb-mouse -device VGA -device virtio-scsi-device -drive if=none,file=D:\qemu\vm\kylin\hdd01.img,id=hd0 -device virtio-blk-device,drive=hd0

安裝和啟動的命令參數差不多,統一說明它們的含義:

參數 說明
qemu-system-aarch64.exe 二進制文件,提供模擬aarch64架構的虛擬機進程
-m 2048 分配2048MB內存
-M virt 模擬成什麼服務器,我們一般選擇virt就可以了,他會自動選擇最高版本的virt
-cpu cortex-a72 模擬成什麼CPU,其中cortex-a53\a57\a72都是ARMv8指令集的
-smp 2,cores=2,threads=1,sockets=1 2個vCPU,這2個vCPU由qemu模擬出的一個插槽(socket)中的2個核心,每個核心支持一個超線程構成
-bios xxx 指定bios bin所在的路徑
-device xxx 添加一個設備,參數可重複
-drive 添加一個驅動器,參數可重複
-net 添加網絡設備

QEMU虛擬機怎麼連網
在Windows上使用qemu虛擬機,使虛擬機能連網,配置方法如下:

  • 在Windows主機上安裝TAP網卡驅動:可下載openvpn客戶端軟件,只安裝其中的TAP驅動;在網絡連接中,會看到一個新的虛擬網卡,屬性類似於TAP-Windows Adapter V9,將其名稱修改為tap0
  • 將虛擬網卡和Windows上真實網卡橋接:選中這兩塊網卡,右鍵,橋接。此時,Windows主機將不能連接互聯網,需要在網橋上配置IP地址和域名等信息,才能使Windows主機連接互聯網。
  • QEMU參數配置:在虛擬機啟動命令行添加以下參數–net nic -net tap,ifname=tap0;tap0為的虛擬網卡名。

7. 總結

國產操作系統的使用體驗已經好了很多,輕度辦公室還是可行的,但想替換Windows,太難了。
QEMU可以虛擬不同的硬件平台架構,是個不錯的虛擬機軟件,而且開源,但在使用體驗方面還是差了一些。

歡迎關注我的公眾號【林哥哥的編程札記】,謝謝!

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

寫了個全局變量的bug,被同事們打臉!!!

話說棧長前陣子寫了一個功能,測試 0 bug 就上線了,上線后也運行好好的,好多天都沒有人反饋bug,超爽。。

不出問題還好,出問題就是大問題。。

最近有個客戶反饋某些數據混亂問題,看代碼死活看不出什麼問題,很詭異,再仔細看代碼,原來是一個全局變量的問題,導致在併發情況下出現了線程不安全的問題,事後被同事們打臉!!!

慎用全局變量,我在公司一直在強調,沒想到這麼低級的問題居然發生在自己身上,說起來真的慚愧啊。。

最開始使用的是 Spring 注入對象的方式:

@Autowired
private Object object;

因為 Spring 默認是單例,所以這樣寫是沒有問題的,後來隨着業務的發展,需要多個不同的業務實例,我改成了這種方式:

@Setter
private Object object;

這個 @Setter 是 Lombok 的註解,用來生成 setters 方法,現在想起來,真是低級啊,同時操作的情況下,這個對象肯定會出現覆蓋的情況,從而導致上面說的問題。

寫了一個這麼低級bug,我也不怕不好意思發出來,大家都謹記一下吧。

另外,我再總結幾個慎用全局變量的場景:

1、SimpleDateFormat

SimpleDateFormat 禁止定義成 static 變量或者全局共享變量,因為它是線程不安全的,都被寫進阿里巴巴的《Java開發手冊》里了:

為什麼說 SimpleDateFormat 不是線程安全的呢?

來看下它的 format 方法源碼:

可以看到 calendar 變量居然也是全局變量,多線程情況下就會存在設置臟變量的情況。

所以,如果要用 SimpleDateFormat,就在每次用的時候都創建一個 SimpleDateFormat 對象,做到線程間隔離。

2、資源連接

資源連接包括數據庫連接、FTP連接、Redis連接等,這種也要慎用全局變量,一量使用全局變量,就會遇到以下問題:

1)關閉連接的時候,就可能把別人正在操作的連接給關了,導致其他線程的業務中斷;

2)因為是全局變量,創建的時候可能會創建多個實例,在關閉連接的時候,就可能只關閉了一個對象的連接,造成其他連接沒有被關閉,最後導致連接耗光系統不可用;

3、数字運算

這也是個很經典的問題了,如果要用多線程對一個数字進行累加等其他運算處理,千萬不要用全局基礎類型的變量,如下所示:

private long count;

多線程情況下,某個線程獲取到的值可能已經被其他線程修改了,最後得到的值就不準確了。

當然,上面的示例可以通過加鎖的方式來解決,也可以使用全局的原子類(java.util.concurrent.atomic.Atom*)進行處理,比如:

private AtomicInteger count = new AtomicInteger();

注意,這種原子類使用全局變量就沒有線程安全的問題,它使用了 CAS 算法保證了數據一致性。

不過,阿里推薦使用LongAdder,因為性能更好:

java.util.concurrent.atomic.LongAdder

4、全局session

來看下面的例子:

@Autowired
protected HttpSession session;

全局注入一個 Session 對象,在 Spring 中,這樣全局注入使用上面是默認沒問題的,包括 request, response 對象,都可以通過全局注入來獲取。

這樣會存在線程安全性嗎?

不會!

使用這種方式,當 Bean 初始化時,Spring 並沒有注入真實對象,而是注入了一個代理對象,真正使用的時候通過該代理對象獲取真正的對象。

並且,在注入此類對象時,Spring使用了線程局部變量(ThreadLocal),這就保證了 request/response/session 對象的線程安全性了。

具體就不展開了,詳細的介紹及測試大家可以點擊這個鏈接查看這篇文章。

既然是線程安全,但也得小心,如果我在方法中主動使 session 對象失效並重建了:

session.invalidate();
session = request.getSession();

這樣,session對象就變成了真實對象了,不再是代理對象,就變成了文章最開始的時候我說的那種多線程安全問題了,如果線上出現 session 會話混亂,用戶 A 就可能看到用戶 B 的數據,你想想可不可怕?

所以,即使可以這樣使用,也得千萬小心謹慎,最好是在方法級別使用這些對象。

總結

今天,棧長總結了一下我是怎麼寫出這個全局變量的低級 bug,也總結了下慎用全局變量的 4 種情況,相信大家多多少都遇到過類似的問題,希望能幫助大家少踩坑。

全局變量雖好,但我們也得謹慎使用啊,一定要考慮是否引起多線程安全問題,不然會引起重大問題。

你還遇到過哪些全局變量的問題,歡迎留言分享哦!

推薦去我的博客閱讀更多:

1.Java JVM、集合、多線程、新特性系列教程

2.Spring MVC、Spring Boot、Spring Cloud 系列教程

3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

4.Java、後端、架構、阿里巴巴等大廠最新面試題

覺得不錯,別忘了點贊+轉發哦!

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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