月份: 2020 年 3 月

  • 【集合系列】- 深入淺出的分析TreeMap

    【集合系列】- 深入淺出的分析TreeMap

    一、摘要

    在集合系列的第一章,咱們了解到,Map的實現類有HashMap、LinkedHashMap、TreeMap、IdentityHashMap、WeakHashMap、Hashtable、Properties等等。

    本文主要從數據結構和算法層面,探討TreeMap的實現。

    二、簡介

    Java TreeMap實現了SortedMap接口,也就是說會按照key的大小順序對Map中的元素進行排序,key大小的評判可以通過其本身的自然順序(natural ordering),也可以通過構造時傳入的比較器(Comparator)。

    TreeMap底層通過紅黑樹(Red-Black tree)實現,所以要了解TreeMap就必須對紅黑樹有一定的了解,在《集合系列》文章中,如果你已經讀過紅黑樹的講解,其實本文要講解的TreeMap,跟其大同小異。

    紅黑樹又稱紅-黑二叉樹,它首先是一顆二叉樹,它具有二叉樹所有的特性。同時紅黑樹更是一顆自平衡的排序二叉樹。

    對於一棵有效的紅黑樹二叉樹,主要有以下規則:

    • 1、每個節點要麼是紅色,要麼是黑色,但根節點永遠是黑色的;
    • 2、每個紅色節點的兩個子節點一定都是黑色;
    • 3、紅色節點不能連續(也即是,紅色節點的孩子和父親都不能是紅色);
    • 4、從任一節點到其子樹中每個恭弘=叶 恭弘子節點的路徑都包含相同數量的黑色節點;
    • 5、所有的恭弘=叶 恭弘節點都是是黑色的(注意這裏說恭弘=叶 恭弘子節點其實是上圖中的 NIL 節點);

    這些約束強制了紅黑樹的關鍵性質:從根到恭弘=叶 恭弘子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這棵樹大致上是平衡的。因為操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查找樹。所以紅黑樹它是複雜而高效的,其檢索效率O(log n)。下圖為一顆典型的紅黑二叉樹。

    在樹的結構發生改變時(插入或者刪除操作),往往會破壞上述條件3或條件4,需要通過調整使得查找樹重新滿足紅黑樹的條件。

    調整方式主要有:左旋、右旋和顏色轉換!

    2.1、左旋

    左旋的過程是將x的右子樹繞x逆時針旋轉,使得x的右子樹成為x的父親,同時修改相關節點的引用。旋轉之後,二叉查找樹的屬性仍然滿足。

    2.2、右旋

    右旋的過程是將x的左子樹繞x順時針旋轉,使得x的左子樹成為x的父親,同時修改相關節點的引用。旋轉之後,二叉查找樹的屬性仍然滿足。

    2.3、顏色轉換

    顏色轉換的過程是將紅色節點轉換為黑色節點,或者將黑色節點轉換為紅色節點,以滿足紅黑樹二叉樹的規則!

    三、常用方法介紹

    3.1、get方法

    get方法根據指定的key值返回對應的value,該方法調用了getEntry(Object key)得到相應的entry,然後返回entry.value

    算法思想是根據key的自然順序(或者比較器順序)對二叉查找樹進行查找,直到找到滿足k.compareTo(p.key) == 0entry

    源碼如下:

    final Entry<K,V> getEntry(Object key) {
            //如果傳入比較器,通過getEntryUsingComparator方法獲取元素
            if (comparator != null)
                return getEntryUsingComparator(key);
            //不允許key值為null
            if (key == null)
                throw new NullPointerException();
            //使用元素的自然順序
                Comparable<? super K> k = (Comparable<? super K>) key;
            Entry<K,V> p = root;
            while (p != null) {
                int cmp = k.compareTo(p.key);
                if (cmp < 0)
                    //向左找
                    p = p.left;
                else if (cmp > 0)
                    //向右找
                    p = p.right;
                else
                    return p;
            }
            return null;
    }

    如果傳入比較器,通過getEntryUsingComparator方法獲取元素

    final Entry<K,V> getEntryUsingComparator(Object key) {
                K k = (K) key;
            Comparator<? super K> cpr = comparator;
            if (cpr != null) {
                Entry<K,V> p = root;
                while (p != null) {
                    //通過比較器順序,獲取元素
                    int cmp = cpr.compare(k, p.key);
                    if (cmp < 0)
                        p = p.left;
                    else if (cmp > 0)
                        p = p.right;
                    else
                        return p;
                }
            }
            return null;
    }

    測試用例:

    public static void main(String[] args) {
            Map initMap = new TreeMap();
            initMap.put("4", "d");
            initMap.put("3", "c");
            initMap.put("1", "a");
            initMap.put("2", "b");
            //默認自然排序,key為升序
            System.out.println("默認 排序結果:" + initMap.toString());
    
            //自定義排序,在TreeMap初始化階段傳入Comparator 內部對象
            Map comparatorMap = new TreeMap<String, String>(new Comparator<String>() {
    
                @Override
                public int compare(String o1, String o2){
                    //根據key比較大小,採用倒敘,以大到小排序
                    return o2.compareTo(o1);
                }
            });
            comparatorMap.put("4", "d");
            comparatorMap.put("3", "c");
            comparatorMap.put("1", "a");
            comparatorMap.put("2", "b");
    
            System.out.println("自定義 排序結果:" + comparatorMap.toString());
    }

    輸出結果:

    默認 排序結果:{1=a, 2=b, 3=c, 4=d}
    自定義 排序結果:{4=d, 3=c, 2=b, 1=a}

    3.2、put方法

    put方法是將指定的key, value對添加到map里。該方法首先會對map做一次查找,看是否包含該元組,如果已經包含則直接返回,查找過程類似於getEntry()方法;如果沒有找到則會在紅黑樹中插入新的entry,如果插入之後破壞了紅黑樹的約束,還需要進行調整(旋轉,改變某些節點的顏色)。

    源碼如下:

    public V put(K key, V value) {
            Entry<K,V> t = root;
            //如果紅黑樹根部為空,直接插入
            if (t == null) {
                compare(key, key); // type (and possibly null) check
    
                root = new Entry<>(key, value, null);
                size = 1;
                modCount++;
                return null;
            }
            int cmp;
            Entry<K,V> parent;
            // split comparator and comparable paths
            Comparator<? super K> cpr = comparator;
            //如果比較器,通過比較器順序,找到插入位置
            if (cpr != null) {
                do {
                    parent = t;
                    cmp = cpr.compare(key, t.key);
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else
                        return t.setValue(value);
                } while (t != null);
            }
            else {
                //通過自然順序,找到插入位置
                if (key == null)
                    throw new NullPointerException();
                    Comparable<? super K> k = (Comparable<? super K>) key;
                do {
                    parent = t;
                    cmp = k.compareTo(t.key);
                    if (cmp < 0)
                        t = t.left;
                    else if (cmp > 0)
                        t = t.right;
                    else
                        return t.setValue(value);
                } while (t != null);
            }
            //創建並插入新的entry
            Entry<K,V> e = new Entry<>(key, value, parent);
            if (cmp < 0)
                parent.left = e;
            else
                parent.right = e;
            //紅黑樹調整函數
            fixAfterInsertion(e);
            size++;
            modCount++;
            return null;
    }

    紅黑樹調整函數fixAfterInsertion(Entry<K,V> x)

    private void fixAfterInsertion(Entry<K,V> x) {
        x.color = RED;
        while (x != null && x != root && x.parent.color == RED) {
            //判斷x是否在樹的左邊,還是右邊
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                //如果x的父親的父親的右子樹是紅色,違反了紅色節點不能連續
                if (colorOf(y) == RED) {
                    //進行顏色調整,以滿足紅色節點不能連續規則
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK); 
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //如果x的父親的右子樹等於自己,那麼需要進行左旋轉
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);  
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                //跟上面的流程正好相反
                //獲取x的父親的父親的左子樹節點
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                //如果y是紅色節點,違反了紅色節點不能連續
                if (colorOf(y) == RED) {
                    //進行顏色轉換
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //如果x的父親的左子樹等於自己,那麼需要進行右旋轉
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        //根節點一定為黑色
        root.color = BLACK;
    }

    上述代碼的插入流程:

    • 1、首先在紅黑樹上找到合適的位置;
    • 2、然後創建新的entry並插入;
    • 3、通過函數fixAfterInsertion(),對某些節點進行旋轉、改變某些節點的顏色,進行調整;

    調整圖解:

    3.3、remove方法

    remove的作用是刪除key值對應的entry,該方法首先通過上文中提到的getEntry(Object key)方法找到 key 值對應的 entry,然後調用deleteEntry(Entry<K,V> entry)刪除對應的 entry。由於刪除操作會改變紅黑樹的結構,有可能破壞紅黑樹的約束,因此有可能要進行調整。

    源碼如下:

    public V remove(Object key) {
            Entry<K,V> p = getEntry(key);
            if (p == null)
                return null;
    
            V oldValue = p.value;
            deleteEntry(p);
            return oldValue;
    }

    刪除函數 deleteEntry()

    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;
        if (p.left != null && p.right != null) {// 刪除點p的左右子樹都非空。
            Entry<K,V> s = successor(p);// 後繼
            p.key = s.key;
            p.value = s.value;
            p = s;
        }
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);
        if (replacement != null) {// 刪除點p只有一棵子樹非空。
            replacement.parent = p.parent;
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;
            p.left = p.right = p.parent = null;
            if (p.color == BLACK)
                fixAfterDeletion(replacement);// 調整
        } else if (p.parent == null) {
            root = null;
        } else { //刪除點p的左右子樹都為空
            if (p.color == BLACK)
                fixAfterDeletion(p);// 調整
            if (p.parent != null) {
                if (p == p.parent.left)
                    p.parent.left = null;
                else if (p == p.parent.right)
                    p.parent.right = null;
                p.parent = null;
            }
        }
    }

    刪除后調整函數fixAfterDeletion()的具體代碼如下:

    private void fixAfterDeletion(Entry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {
            //判斷當前刪除的元素,是在x父親的左邊還是右邊
            if (x == leftOf(parentOf(x))) {
                Entry<K,V> sib = rightOf(parentOf(x));
                //判斷x的父親的右子樹,是紅色還是黑色節點
                if (colorOf(sib) == RED) {
                    //進行顏色轉換
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateLeft(parentOf(x));
                    sib = rightOf(parentOf(x));
                }
                //x的父親的右子樹的左邊是黑色節點,右邊也是黑色節點
                if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    //設置x的父親的右子樹為紅色節點,將x的父親賦值給x
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    //x的父親的右子樹的左邊是紅色節點,右邊也是黑色節點
                    if (colorOf(rightOf(sib)) == BLACK) {
                        //x的父親的右子樹的左邊進行顏色調整,右旋調整
                        setColor(leftOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateRight(sib);
                        sib = rightOf(parentOf(x));
                    }
                    //對x進行左旋,顏色調整
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(rightOf(sib), BLACK);
                    rotateLeft(parentOf(x));
                    x = root;
                }
            } else { // 跟前四種情況對稱
                Entry<K,V> sib = leftOf(parentOf(x));
                if (colorOf(sib) == RED) {
                    setColor(sib, BLACK);
                    setColor(parentOf(x), RED);
                    rotateRight(parentOf(x));
                    sib = leftOf(parentOf(x));
                }
                if (colorOf(rightOf(sib)) == BLACK &&
                    colorOf(leftOf(sib)) == BLACK) {
                    setColor(sib, RED);
                    x = parentOf(x);
                } else {
                    if (colorOf(leftOf(sib)) == BLACK) {
                        setColor(rightOf(sib), BLACK);
                        setColor(sib, RED);
                        rotateLeft(sib);
                        sib = leftOf(parentOf(x));
                    }
                    setColor(sib, colorOf(parentOf(x)));
                    setColor(parentOf(x), BLACK);
                    setColor(leftOf(sib), BLACK);
                    rotateRight(parentOf(x));
                    x = root;
                }
            }
        }
        setColor(x, BLACK);
    }

    上述代碼的刪除流程:

    • 1、首先在紅黑樹上找到合適的位置;
    • 2、然後刪除entry;
    • 3、通過函數fixAfterDeletion(),對某些節點進行旋轉、改變某些節點的顏色,進行調整;

    四、總結

    TreeMap 默認是按鍵值的升序排序,如果需要自定義排序,可以通過new Comparator構造參數,重寫compare方法,進行自定義比較。

    以上,主要是對 java 集合中的 TreeMap 做了寫講解,如果有理解不當之處,歡迎指正。

    五、參考

    1、JDK1.7&JDK1.8 源碼
    2、
    2、

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

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

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

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

  • 高質量App的架構設計與思考!

    高質量App的架構設計與思考!

    最近在做一功能不大、業務也不複雜的小眾App,以往做App是發現自己從來沒有考慮過一些架構方面的問題,只是按照自己以往的習慣去寫代碼,忽略了App的設計。本次分享主要包含一些開發App的小經驗和技巧,來一次App開發與設計的分享。

    先和分享下一下實體類的設計與組織形式

    實體類的組織

    在做App開發的時候有很多的實體類,項目越複雜實體類就會越多,經過我的一番思考大致這可以將實體分為以下幾大數:

    • 面向數據庫的
    • 服務端返回的數據實體
    • 用於渲染View的實體(使用Databinding)

    一般情況下實體類的操作會經過以下步驟:

    1. App請求服務器獲取數據
    2. 將數據存入數據庫(可選)
    3. 渲染頁面展示數據

    現在的實體的產生只用在請求服務器數據的時候才需要新建,後續的數據庫、頁面渲染其實是可以使用一套實體:

    先不說這樣做的行不行,首先三個地方使用同一實體就會引起字段歧義比如服務器數據有Id、本地數據也有Id,那兩個id字段就有衝突了不得不改字段名。

    另一種情況渲染和數據本身並不會一一對應,有時候後端數據給的是一個純数字而前端頁面显示的是字符串兩個都對應不上,強行放在一起會起來更多的問題。

    所為實體類的的正確組織形式應該是:相互隔離、互不干擾

    數據實體的在渲染之前都需要準備好,比如在ViewModel中將int型的數據轉換成文本型的數據然後再使用Databinding+頁面渲染實體來渲染頁面。

    優雅的處理網絡數據

    現在Android開發使用的網絡庫大部分都是Okhttp + Retrofit,使用Retrofit網絡交互變的非常簡單一個Service接口就能搞定一切,美茲茲~~,現在大部分後端返回的數據都會是以下形式:

    {
        "code":0,
        "data": {},
        "msg": ""
    }

    雖然不能涵蓋所有,但還是可以非常贊的數據、消息、成功與否啥都有!對於前面主要是關注data字段,其他msgcode等都屬於輔助字段。前端對應的實體對象應該是這樣的(假代碼):

    public class ApiResponse<T> {
        private int code;
        private T data;
        private String msg;
    }

    對應的Service那就得定義成這樣(使用了RxJava):

    public intface UserService {
        @GET("/xx/{id}")
        Single<ApiResponse<UserInfo> getUserInfoById(@Path("id") Long userId);
    }

    從接口中可以看出來,方法的返回值就包了幾層,如果要拿data字段需要經過:ApiResponse -> UserInfo,而且在拿之前還要判斷code字段:

    
    ...
    
    if(ApiResponse.code == 0){
        UserInfo info = ApiResponse.getData();
    }
    
    ...
    

    為了消除這些冗餘的代碼可以使用CallAdapter來使Service方法返回的數據直接就是實體類:

    public intface UserService {
        @GET("/xx/{id}")
        Single<UserInfo> getUserInfoById(@Path("id") Long userId);
    }

    CallAdapter的代碼就不貼了,可以自行查找。這樣做帶來的另外一個問題就是業務代碼如何判斷接口是否成功或失敗,前端必需友好的把錯誤提示給用戶而不是一直搞個Loading在那裡瞎轉~~。現階段最方便的的錯誤傳遞方式是使用Java異常,前端可以定義業務異常網絡異常

    public class BizException extends RuntimeException {
        ...
    }

    CallAdapter中檢查ApiResponse的返回值是否成功:

    
    if(!ApiResponse != 0){
        throw new BizExcepiton(ApiResponse);
    }
    

    如果後端返回業務異常那前端就對應拋出一個BizExcepiton,如果是http錯誤如:404、400那可以拋出HttpException。除了BizExcepitonHttpException外還可使用特定的異常比如後端返回密碼錯誤異常:

    public class InvalidPasswordException extends BizException {
        ...
    }

    如需特殊處理,也可以滿足要求。

    健壯的數據層

    現在很多應用都開發使用MVVM開發模式數據層都使用Repository來表示,面向數據驅動的開發模式,頁面變化都需要隨着數據變更而更新,數據發生變化然後頁面再做出響應。Repository的拆分要細一點,不建議簡單的弄個UserRepository包含登陸、註冊、更新密碼等等操作,設計Repository的一些想法:

    1. 面向接口編程
    2. 保持單一原則
    3. 功能邊界要清晰(如:登陸、註冊可以分開)
    4. 業務邏輯盡可能的少(複雜的業務考慮Presenter)

    一個判斷是否是好的設計的辦法可以這樣:一個登陸頁面從Activive/Fragment到ViewModel再到Repository,有沒有多餘的代碼。比如上面說的UserRepository包含登陸、註冊但是在一個登陸頁面就不需要有註冊功能,從登陸頁面上來看註冊的代碼就是多餘的(有些App登陸/註冊在一個頁面的~~)。

    一個包含登陸、註冊的UserRepository簡單圖:

    另外一點是盡量將repository使用到的一些東西集中管理,可引入一個基礎的repository:

    public class SimpleRepository {
        
        protected final  <T> T getService(Class<T> clz){
            return Services.getService(clz);
        }
    }
    

    做為SimpleRepository的子類,就不需要考慮從哪裡獲取service的問題。

    簡潔的UI層

    UI層面可以分為ViewModel和View(Activity/Fragment), View的職責應當只有二點:

    1. 展示業務數據
    2. 收集業務數據

    例如一些數據的組織、判斷都不應該出現在View中比如:

     if (Strings.isNullOrEmpty(phone)) {
           ...
            return;
     }
    
     if (Strings.isNullOrEmpty(pwd)) {
            ...
            return;
      }

    像上面這類的代碼都不應該出現在View中,而在放置在ViewModel裏面,View只收集用戶數據傳遞給ViewModel由它來進行數據校驗。再比如像這樣的if/else代碼也應該放置在ViewModel中:

     int age = 10;
     String desc = "";
     if(age < 18){
        desc = "青年";
     }else if(age < 29){
        desc = "中年";
     }

    如果數據的显示和數據的收集過多,建議使用Databinding來進行雙向綁定數據。再搭配LiveData使View作為觀察者實時監聽數據變化:

    registerViewModel.getRegistryResult().observe(this, new SimpleObserver<RegistryInfo>(this));

    一旦數據發生變化LiveData就會通知Observer更新,通過DataBinding更新各個頁面數據。

    再說ViewModel應該只包含一些簡單的判斷、檢查、打通數據的代碼,如果業務過於複雜可以考慮加Presetner,如果真的超級複雜那可以反思下這個複雜的邏輯應不應該放在前端,能不能放在後端呢?

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

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • Fork/Join框架詳解

    Fork/Join框架詳解

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

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

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

    ForkJoinTask

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

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

    ForkJoinPool

    ForkJoinTask需要通過ForkJoinPool來執行。

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

    Fork/Join框架的實現原理

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

    ForkJoinTask的fork方法的實現原理

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

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

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

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

    ForkJoinTask的join方法的實現原理

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

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

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

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

    doExec() :

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

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

    Fork/Join框架的異常處理

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

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

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

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

    DEMO

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

    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.ForkJoinPool;
    import java.util.concurrent.Future;
    import java.util.concurrent.RecursiveTask;
    
    /**
     *
     * @author aikq
     * @date 2018年11月21日 20:37
     */
    public class ForkJoinTaskDemo {
    
        public static void main(String[] args) {
            ForkJoinPool pool = new ForkJoinPool();
            CountTask task = new CountTask(1,4);
            Future<Integer> result = pool.submit(task);
            try {
                System.out.println("計算結果=" + result.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    }
    
    class CountTask extends RecursiveTask<Integer>{
        private static final long serialVersionUID = -7524245439872879478L;
    
        private static final int THREAD_HOLD = 2;
    
        private int start;
        private int end;
    
        public CountTask(int start,int end){
            this.start = start;
            this.end = end;
        }
    
        @Override
        protected Integer compute() {
            int sum = 0;
            //如果任務足夠小就計算
            boolean canCompute = (end - start) <= THREAD_HOLD;
            if(canCompute){
                for(int i=start;i<=end;i++){
                    sum += i;
                }
            }else{
                int middle = (start + end) / 2;
                CountTask left = new CountTask(start,middle);
                CountTask right = new CountTask(middle+1,end);
                //執行子任務
                left.fork();
                right.fork();
                //獲取子任務結果
                int lResult = left.join();
                int rResult = right.join();
                sum = lResult + rResult;
            }
            return sum;
        }
    }

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

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

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

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

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

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

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

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

  • paper sharing :學習特徵演化的數據流

    paper sharing :學習特徵演化的數據流

    特徵演化的數據流

        數據流學習是近年來機器學習與數據挖掘領域的一個熱門的研究方向,數據流的場景和靜態數據集的場景最大的一個特點就是數據會發生演化,關於演化數據流的研究大多集中於概念漂移檢測(有監督學習),概念/聚類演化分析(無監督學習),然而,人們往往忽略了一個經常出現的演化場景:特徵演化。大多數研究都考慮數據流的特徵空間是固定的,然而,在很多場景下這一假設並不成立:例如,當有限壽命傳感器收集的數據被新的傳感器替代時,這些傳感器對應的特徵將發生變化。

        今天要分享的文章出自周志華的實驗室《Learning with Feature Evolvable Streams》(NIPS 2017),它提出了一個新的場景,即在數據流中會有特徵消亡也會有新特徵出現。當出現新的特徵空間時,我們並不直接拋棄之前學到的模型並在新的數據上重新創建模型,而是嘗試恢復消失的特徵來提升模型的表現。具體來說,通過從恢復的特徵和新的特徵空間中分別學習兩個模型。為了從恢復的特徵中獲得提升,論文中提出了兩種集成策略:第一種方法是合併兩個模型的預測結果;第二種是選擇最佳的預測模型。下面我們具體來理解特徵演化數據流以及論文中提出的一些有趣的方法吧~

    paper link:

     

    什麼是特徵演化數據流?

        在很多現實的任務中,數據都是源源不斷收集的,關於數據流學習的研究近年來受到越來越多的關注,雖然已經有很多有效的算法針對特定的場景對數據流進行挖掘,但是它們都基於一個假設就是數據流中數據的特徵空間是穩定的。不幸的是,這一假設在很多場景下都不滿足。針對特徵演化的場景,最直接的想法就是利用新的特徵空間的數據學習一個新的模型,但是這一方法有很多問題:首先,當新的特徵剛出現的時候,只有很少的數據樣本來描述這些信息,訓練樣本並不足夠去學習一個新的模型;其次,包含消失特徵的舊模型被直接丟棄了,其中可能包含對當前數據有用的信息。論文中定義了一種特徵演化數據流的場景:一般情況下,特徵不會任意改變,而在一些重疊時期,新特徵和舊特徵都存在,如下圖所示:

        其中,T1階段,原始特徵集都是有效的,B1階段出現了新的特徵集,T2階段原始特徵集消失,只有新的特徵集。

        論文提出的方法是通過使用重疊(B1)階段來發現新舊特徵之間的關係,嘗試學習新特徵到舊特徵的一個映射,這樣就可以通過重構舊特徵並使用舊模型對新數據進行預測

    問題描述

        論文中着重解決的是分類和回歸任務,在每一輪學習過程中,對每一個實例進行預測,結合它的真實標籤會得到一個loss(反映預測和真實標籤的差異),我們將上面提到的T1+B1+T的過程稱為一個周期,每個周期中只包含兩個特徵空間,所以,之後的研究主要關注一個周期內的模型的學習,而且,我們假設一個周期內的舊特徵會同時消失。定義Ω1和Ω2分別表示兩個特徵空間S1和S2上的線性模型,並定義映射,定義第i維特徵在第t輪的預測函數為線性模型,。損失函數是凸的,最直接的方式是使用在線梯度下降來求解w,但是在數據流上不適用。

     

    方法介紹

        上文提到的基本算法的主要限制是在第1,…T1輪學習的模型在T1+1,…T1+T2時候被忽略了,這是因為T1之後數據的特徵空間改變了,我們無法直接應用原來的模型。為了解決這一問題,我們假設新舊特徵空間之間有一種特定的關係:,我們嘗試通過重疊階段B1來學習這種關係。學習兩組特徵之間的關係的方法很多,如多元回歸,數據流多標籤學習等。但是在當前的場景下,由於重疊階段特別短,學習一個複雜的關係模型是不現實的。所以我們採用線性映射來近似。定義線性映射的係數矩陣為M,那麼在B1階段,M的估計可以基於如下的目標方程:

    M的最優解可以解得:

        然後,當我觀測到S2空間得數據,就可以通過M將其轉化到S1空間,並應用舊模型對其進行預測。

    除了學習這個關係映射之外,我們得算法主要包括兩個部分:

    1. 在T1-B1+1,…T1階段,我們學習兩個特徵空間之間得關係;

    2. 在T1之後,我們使用新特徵空間的數據轉化后的原特徵空間數據,持續更新舊模型以提升它的預測效果,然後集成兩個模型進行預測。

     

    預測結果集成

        論文中提出兩種集成方法,第一種是加權組合,即將兩個模型的預測結果求加權平均,權重是基於exponential of the cumulative loss。

    其中

        這種權重的更新規則表明,如果上一輪模型的損失較大,下一輪模型的權值將以指數速度下降,這是合理的,可以得到很好的理論結果。

        第二種集成方法是動態選擇。

        上面提到的組合的方法結合了幾個模型來提升整體性能,通常來說,組合多個分類器的表現會比單分類器的效果要好,但是,這基於一個重要的假設就是每個基分類器的表現不能太差(如,在Adaboost中,基分類器的預測精度不應低於0.5)。然而在這個問題中,由於新特徵空間剛出現的時候訓練集較小,訓練的模型不好,因此可能並不適合用組合的方法來預測,相反,用動態選擇最優模型的方法反而能獲得好的效果。

    有趣的靈魂在等你長按二維碼識別

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

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

  • Go 多變量賦值時注意事項

    說到多變量賦值時,先計算所有相關值,然後再從左到右依次賦值,但是這個規則不適用於python
    我們來看一例:

    package main
    
    import "fmt"
    
    func main() {
        data, i := [3]string{"喬幫主","慕容復","鳩摩智"}, 0
        i, data[i] = 2, "枯榮大師"
        fmt.Println(i, data)
    }

    輸出結果:

    2 [枯榮大師 慕容復 鳩摩智]  

    有的朋友會認為,結果不應該是這樣么?(但是python下輸出的結果卻是下面的)?

    2 [喬幫主 慕容復 枯榮大師]

    事實並如此,我們來看賦值順序這段的理解:

    1     data, i := [3]string{"喬幫主","慕容復","鳩摩智"}, 0
    2     i, data[i] = 2, "枯榮大師" //注意原則:先計算所有相關值,然後再從左到右依次賦值
    3     // 這裏變量i 的順序其實是(i = 0,因為上一行的變量i是0) -> (然後 i = 2), (data[i] 此時取的值是data[0],而不是data[2],也就是data[0] = 枯榮大師)
    4     fmt.Println(i, data) //所以這裏最終 輸出 i=2,[枯榮大師 慕容復 鳩摩智]

    同樣的多變量賦值卻不適用於python.

    data,i=["喬幫主", "慕容復", "鳩摩智"],0
    i, data[i] = 2, "枯榮大師" # 注意這裏data[i] 已經是 data[2]了,即data[2]="枯榮大師"
    print(i,data) # 輸出 2 ['喬幫主', '慕容復', '枯榮大師']

    另外:我們要注意重新賦值與定義新同名變量的區別:再看一例:

    package main
    
    func main() {
        name := "喬幫主"
        println(&name)
        name, age := "鳩摩智", 30 // 重新賦值: 與前 name 在同層次的代碼塊中,且有新的變量被定義。
        println(&name, age)    // 通常函數多返回值 err 會被重複使用。
        {
            name, weight := "清風揚", 50 // 定義新同名變量: 不在同層次代碼塊。
            println(&name, weight)
        }
    }

    輸出:

    0xc00002bf78
    0xc00002bf78 30
    0xc00002bf68 50

    注意:因個人機器不同,大家返回的內存引用地址可能和我的不一樣,但是 這步是重點。重點在這裏:
    同層級相同變量的賦值,內存地址並不會改變。不同層級相同變量的賦值,其實是定義了一個新同名變量,也就是大家看到的第三行內存地址變了。
    接着我們再看有點意思的一段代碼(大家來找茬):

    package main
    
    func main() {
        name := "喬幫主"
        println(&name)
        name, age := "鳩摩智", 30 // 重新賦值: 與前 name 在同 層次的代碼塊中,且有新的變量被定義。
        println(&name, age)    // 通常函數多返回值 err 會被重複使用。
    
        name, weight := 100, 50 // 定義新同名變量: 不在同 層次代碼塊。
        println(&name, weight, age)
    
    }

    輸出:

    cannot use 100 (type int) as type string in assignment

    原因很明顯,因為上面:name := “喬幫主” 已經隱試滴申明了name 是字符串,等同於 var name string. 同層級再次賦值100為整形。這是不允許滴,

    但是:重點來了,我們稍改下:

    package main
    
    func main() {
        name := "喬幫主"
        println(&name)
        name, age := "鳩摩智", 30 // 重新賦值: 與前 name 在同 層次的代碼塊中,且有新的變量被定義。
        println(&name, age)    // 通常函數多返回值 err 會被重複使用。
        {
            name, weight := 100, 50 // 定義新同名變量: 不在同層次代碼塊。
            println(&name, weight, age)
        }
    }

    區別就是層級發生了變化,因為{}裏面的name已經是新的變量了。
    好啦,到此介紹結束了。博友們有關golang變量使用中遇到的各種奇怪的“坑”,請留下寶貴滴足跡,歡迎拍磚留言.

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

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

    ※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

    ※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

  • 理解MySQL數據庫事務-隔離性

    理解MySQL數據庫事務-隔離性

    Transaction事務是指一個邏輯單元,執行一系列操作的SQL語句。

    事務中一組的SQL語句,要麼全部執行,要麼全部回退。在Oracle數據庫中有個名字,叫做transaction ID

    在關係型數據庫中,事務必須ACID的特性。

    • 原子性,事務中的操作,要不全部執行,要不都不執行
    • 一致性,事務完成前後,數據的必須保持一致。
    • 隔離性,多個用戶併發訪問數據庫時,每一個用戶開啟的事務,相互隔離,不被其他事務的操作所干擾。
    • 持久性,事務一旦commit,它對數據庫的改變是持久性的。

    目前重點討論隔離性。數據庫一共有四個隔離級別

    • 未提交讀(RU,Read Uncommitted)。它能讀到一個事物的中間狀態,不符合業務中安全性的保證,違背 了ACID特性,存在臟讀的問題,基本不會用到,可以忽略

    • 提交讀(RC,Read Committed)。顧名思義,事務提交之後,那麼我們可以看到。這是一種最普遍的適用的事務級別。我們生產環境常用的使用級別。

    • 可重複讀(RR,Repeatable Read)。是目前被使用得最多的一種級別。其特點是有GAP鎖,目前還是默認級別,這個級別下會經常發生死鎖,低併發等問題。

    • 可串行化,這種實現方式,其實已經是不是多版本了,而是單版本的狀態,因為它所有的實現都是通過鎖來實現的。

    因此目前數據庫主流常用的是RCRR隔離級別。

    隔離性的實現方式,我們通常用Read View表示一個事務的可見性。

    RC級別,事務可見性比較高,它可以看到已提交的事務的所有修改。因此在提交讀(RC,Read Committed)隔離級別下,每一次select語句,都會獲取一次Read View,得到數據庫最新的事務提交狀態。因此對於數據庫,併發性能也最好。

    RR級別,則不是。它為了避免幻讀和不可重複讀。保證在一個事務內前後數據讀取的一致。其可見性視圖Read View只有在自己當前事務提交之後,才會更新。

    那如何保證數據的一致性?其核心是通過redo logundo log來保證的。

    而在數據庫中,為了實現這種高併發訪問,就需要對數據庫進行多版本控制,通過事務的可見性來保證事務看到自己想看到的那個數據版本(或者是最新的Read View亦或者是老的Read View)。這種技術叫做MVCC

    多版本是如何實現的?通過undo日誌來保證。每一次數據庫的修改,undo日誌會存儲之前的修改記錄值。如果事務未提交,會回滾至老版本的數據。其MVCC的核心原理,以後詳談

    舉例論證:

    ##  開啟事務
    MariaDB [scott]> begin;                   
    Query OK, 0 rows affected (0.000 sec)
    
    ##查看當前的數據
    MariaDB [scott]>  select * from dept;
    +--------+------------+----------+
    | deptno | dname      | loc      |
    +--------+------------+----------+
    |     10 | ACCOUNTING | beijing  |
    |     20 | RESEARCH   | DALLAS   |
    |     30 | SALES      | CHICAGO  |
    |     40 | OPERATIONS | beijing  |
    |     50 | security   | beijing  |
    |     60 | security   | nanchang |
    +--------+------------+----------+
    6 rows in set (0.001 sec)
    
    ##更新數據
    MariaDB [scott]> update dept set loc ='beijing' where deptno = 20;
    Query OK, 1 row affected (0.001 sec)
    
    ## 其行記錄| 20 | RESEARCH | DALLAS |已經被放置在undo日誌中,目前最新的記錄被改為'beijing':
    MariaDB [scott]> select * from dept;
    +--------+------------+----------+
    | deptno | dname      | loc      |
    +--------+------------+----------+
    |     10 | ACCOUNTING | beijing  |
    |     20 | RESEARCH   | beijing  |
    |     30 | SALES      | CHICAGO  |
    |     40 | OPERATIONS | beijing  |
    |     50 | security   | beijing  |
    |     60 | security   | nanchang |
    +--------+------------+----------+
    
    ##事務不提交,回滾。數據回滾至老版本的數據。
    MariaDB [scott]> rollback;
    Query OK, 0 rows affected (0.004 sec)
    
    MariaDB [scott]> select * from dept;
    +--------+------------+----------+
    | deptno | dname      | loc      |
    +--------+------------+----------+
    |     10 | ACCOUNTING | beijing  |
    |     20 | RESEARCH   | DALLAS   |
    |     30 | SALES      | CHICAGO  |
    |     40 | OPERATIONS | beijing  |
    |     50 | security   | beijing  |
    |     60 | security   | nanchang |
    +--------+------------+----------+
    6 rows in set (0.000 sec)

    因為MVCC,讓數據庫有了很強的併發能力。隨着數據庫併發事務處理能力大大增強,從而提高了數據庫系統的事務吞吐量,可以支持更多的用戶併發訪問。但併發訪問,會出現帶來一系列問題。如下:

    數據庫併發帶來的問題 概述解釋
    臟讀(Dirty Reads) 當一個事務A正在訪問數據,並且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務B也訪問這同一個數據,如不控制,事務B會讀取這些”臟”數據,並可能做進一步的處理。這種現象被稱為”臟讀”(Dirty Reads)
    不可重複讀(Non-Repeatable Reads) 指在一個事務A內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務B也訪問該同一數據。那麼,在事務A的兩次讀數據之間,由於第二個事務B的修改,那麼第一個事務兩次讀到的的數據可能是不一樣的 。出現了”不可重複讀”(Non-Repeatable Reads)的現象
    幻讀(Phantom Reads) 指在一個事務A內,按相同的查詢條件重新檢索以前檢索過的數據,同時發現有其他事務插入了數據,其插入的數據滿足事務A的查詢條件。因此查詢出了新的數據,這種現象就稱為”幻讀”(Phantom Reads)

    隔離級別和上述現象之間的聯繫。

    隔離級別有:未提交讀(RU,Read Uncommitted),提交讀(RC,Read Committed),可重複讀(RR,Repeatable Read),可串行化(Serializable)

    隔離級別 臟讀 不可重複讀 幻讀
    未提交讀(RU,Read Uncommitted) 可能 可能 可能
    提交讀(RC,Read Committed) 不可能 可能 可能
    可重複讀(RR,Repeatable Read) 不可能 不可能 可能
    (間隙鎖解決)
    可串行化(Serializable) 不可能 不可能 不可能

    實驗環節

    舉例在隔離級別RRRC下,說明“不可重複讀”問題。

    MySQL的默認級別是Repeatable Read,如下:

    MariaDB [(none)]> select @@global.tx_isolation;
    +-----------------------+
    | @@global.tx_isolation |
    +-----------------------+
    | REPEATABLE-READ       |
    +-----------------------+
    1 row in set (0.000 sec)

    這裏修改當前會話級別為Read Committed

    MariaDB [scott]> set session transaction isolation level read committed;
    Query OK, 0 rows affected (0.001 sec)
    
    MariaDB [scott]> select @@tx_isolation;
    +----------------+
    | @@tx_isolation |
    +----------------+
    | READ-COMMITTED |
    +----------------+
    1 row in set (0.000 sec)

    在隔離級別已提交讀(RC,Read Committed)下,出現了不可重複讀的現象。在事務A中可以讀取事務B中的數據。

    在隔離級別可重複讀(RR,Repeatable Read),不會出現不可重複讀現象,舉例如下:

    舉例說明“幻讀”的現象。

    行鎖可以防止不同事務版本的數據在修改(update)提交時造成數據衝突的問題。但是插入數據如何避免呢?

    在RC隔離級別下,其他事務的插入數據,會出現幻讀(Phantom Reads)的現象。

    而在RR隔離級別下,會通過Gap鎖,鎖住其他事務的insert操作,避免”幻讀”的發生。

    因此,在MySQL事務中,鎖的實現方式與隔離級別有關,如上述實驗所示。在RR隔離級別下,MySQL為了解決幻讀的問題,已犧牲并行度為代價,通過Gap鎖來防止數據的寫入。這種鎖,并行度差,衝突多。容易引發死鎖。

    目前流行的Row模式可以避免很多衝突和死鎖問題,因此建議數據庫使用ROW+RC(Read Committed)模式隔離級別,很大程度上提高數據庫的讀寫并行度,提高數據庫的性能。

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

    台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

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

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

  • 應屆畢業生工作7個月小結

    應屆畢業生工作7個月小結

    前言: 不知不覺已經工作了快 7 個月了,去年這個時候還躋身在考研的大軍中,不禁有些感慨… 結合這 7 個月發生的一些事情,簡單做一下總結吧…

    為獲得更好的閱讀體驗,請訪問原文地址:

    一、那時候剛入職

    不同於其他同學忙於畢設的 4 月,提早安排趁寒假已經完成畢設的我,已經開始撲在了「找工作」這件事上,有了去年「秋招」打下的基礎,複習起來快了很多,沒過多久就開始投簡歷面試了,面試也總體比較順利,剛面沒幾家就迅速和一家自己看好的初創公司簽下了。

    公司使用的技術棧區別於自己熟悉的 Java/ MySQL 這一套,而是主要使用的 Rails/ MongoDB,所以剛入職的一段時間,基本上都是在自己熟悉技術棧,也趁着閑暇的時間,把自己入門時候的一些學習心得寫成了文章發表:

    • MongoDB【快速入門】:
    • Java轉Ruby【快速入門】:

    對於職場小白來說,所謂「職場」還是顯得有些陌生,剛來的時候,雖然跟周圍的同事都稀鬆平常地打了一圈兒招呼,坐下之後,隨着他們又埋頭噼里啪啦敲打鍵盤工作的深入,又頓覺周圍一片陌生,還挺奇妙的,在第一周完的周報裏面我寫道:

    剛來公司有些迷茫,只是看着CheckList對照着熟悉一些技術,也不了解自己應該要熟悉到哪種程度,就希望自己能再主動些,不管是技術問題還是其他問題多請教,然後儘快跟其他成員熟悉起來。

    剛開始上手的時候也有好多問題不懂,我都習慣性的選擇自己研究一陣兒,因為自己有寫博客的一些經歷,被問過好多一搜索 or 自己一嘗試就能解決的問題,所以比較克制,但是後來「入職 1v1」溝通的時候被說到有問題別自己死磕,半個小時沒解決盡量就找一下旁邊的同事。摁?我一下子就把我的「主動性」發揮了出來。

    不過好記性也不如爛筆頭,找了一些工具記錄把這些「問題的答案」都記錄了下來,方便之後再查找,當時對於 Git 都不是很熟悉,也記錄了很多常用的命令在裏面,還有一些問題的反饋,甚至知道了月會要自我介紹,也打了一遍草稿記錄在了這裏:(那段時間真的問了好多問題,周報里也手動感謝了坐我旁邊的兩位大佬..)

    入職兩周的時候,雖然已經開始上手做一些簡單的埋點工作,但自己對於 Ruby 還是不是特別了解和熟悉,趁着某一個雙休,抓着一本《Effetive-Ruby》啃了两天,也把自己的學習輸出了一下:

    • 《Effective-Ruby》讀書筆記:

    二、逐漸能夠上手

    就這樣一邊熟悉,一邊開始接一些小需求,我記得我寫下的第一個 BUG,就報出了 6K 條記錄.. 慌慌張張在修復之後我不禁感嘆:「不要太相信用戶的任何數據」。(包括 equal 反寫也是之後在錯誤之中學習到的..)

    剛上手沒有一段時間,就接到了一個新項目的需求,跟着一位大佬開發一個新功能,大佬負責搭建基礎代碼和設計,我負責完成其餘的功能代碼,沒敢一絲懈怠,下班回家之後也對照着別人寫的代碼敲敲敲,時間和完成度上倒是沒有一絲耽擱,只是現在回過頭一想,當時沒有什麼單元測試的概念和意識,就自己在本地 Post-Man 測試完就完,所幸比較簡單 + 自己測試得比較仔細,到現在也沒有出現過什麼問題。

    工作對我這樣的小白另一個好處就是:「見識和增加技術的廣度」。公司所使用技術棧不論是廣度還是深度,都是自己在大學本科的學習中不可企及的程度,Jekins?Docker?K8S?跳板機?一下子冒出來好多新鮮陌生的名詞,懷着好奇心也嘗試了解了一些:

    • 了解【Docker】從這裏開始:
    • 「消息隊列」看過來!:
    • Kafka【入門】就這一篇!:

    也隨着公司的逐漸壯大,各模塊的耦合也越發嚴重,各條業務線之間的協作溝通成本越來越大,逐漸開始提出「微服務」這樣的概念,具體怎麼樣理解就不作討論了,總之就是期望通過梳理/ 重構/ 拆服務的方式來解決「協作」問題,所以期間也開始了解學習一些這方面的東西:

    • 什麼是微服務?:
    • 《重構:改善既有代碼的設計》讀書筆記:

    甚至期間還做了一些「微服務」的調研,我們選用什麼樣的姿勢和技術棧更加合適,所以也輸出了一些關於「Spring Cloud」的東西,但是最終駁回的原因是待我們整個容器化之後 k8s 平台自帶了這麼一套東西,業務同學只需要關心業務代碼就行了,也就沒有繼續深入了:

    • 你想了解的「Spring Cloud」都在這裏:

    然後我們在拆解的過程中,也借鑒到一些「DDD」的思想,也嘗試進行了一波學習:

    • 【吐血推薦】領域驅動設計學習輸出:

    總之,這一段時間我一邊通過各種小需求,接觸和了解了公司的系統的大半,一邊學習和了解着各種不同的技術,增加了技術上的廣度。

    三、開始負責一些項目

    為了加速服務化的推進工作和驗證「DDD」的一些東西,部門老大把一個邊界足夠清晰,也足夠小的一個模塊單獨交給我,期望我快速上線,不過最終交付已經逾期快大半個月了.. 雖然從最終的結果來看,順利交付完成了拆解任務並從 MongoDB 數據庫轉變成了 MySQL.. 但期間也踩過好些坑,當然也學習到一些東西..

    例如我真實地意識到「完美」這個詞的理想化。就拿設計 API 來說吧.. 自己就基於 RESTful 風格設計了好幾版.. 左想右想都覺得差一些,有一些接口覺得怎麼設計都不優雅.. 後來糾結一陣子也就放棄了.. 再例如寫代碼這件事情吧,好的代碼整潔的代碼是一次一次迭代和重構中出來的,如果一開始就想着寫出「完美」的代碼,那麼最終的結果可能就是寫不出來代碼。

    另外一個小插曲是,在做數據遷移的時候,我差點把線上服務器搞掛了.. 我在測試環境驗證了一把之後,就直接在線上進行操作了,因為當時對於數據庫的操作管控還沒有那麼嚴格,加上自己對於線上環境的複雜程度認識不足,我就起了 50 個線程,去分批量地讀取 MongoDB 的數據遷移到 MySQL,造成了線上庫的性能報警,就趕緊停了.. 緊接着就被一群大佬抓進了一個會議室做事件的復盤..

    說實話,我緊張壞了,第一次經歷這樣的算是「事故」的情況吧,差一點線上就被我搞掛啦,一時間不知所措… 讓人感到溫暖的是部門老大隨即丟來的消息:

    那天還有一些相關的同事都陪我寫復盤郵件到了晚上 10:30,現在想來都十分感謝他們。後來回到家我還打電話給我媽,我說我在工作中犯錯了,我做了xxxx這些動作,你覺得我做的怎麼樣呢,老媽的回復也讓人安心,只是現在想來,一些後續的動作可以做得更好的…

    因為「埋點」這件事涉及到系統的方方面面,我也藉此了解了很多不同的模塊,也是拜這一點所賜吧,後來我被派到各種各樣的支援任務中,同樣也因為對不同模塊都還不算陌生,都還算完成得不錯吧…

    時間一晃,在公司就四個月過去了,也在這個過程中從各個大佬那兒都學到了一些東西,在 8 月底發的周報裏面我寫下了以下的總結:

    之後也跟着大佬碰了一些公司的核心模塊,期間也沒有停止在工作中不斷地做學習輸出:

    • Git 原理入門解析:
    • Java計時新姿勢√:
    • Java8流操作-基本使用&性能測試:
    • 《代碼整潔之道》讀書筆記:
    • React 入門學習:
    • 談一談依賴倒置原則:

    四、回顧做的不好的部分

    • 對代碼還沒有保持足夠的敬畏之心。

    特別是一開始上手的時候,有時候甚至是在線上環境搞測試,後來越來越注重 codereview 和單元測試好了很多。

    • 溝通還不夠深入/ 到位

    有一次是臨時接到一個需求,因為「通用語言」沒有達成一致,導致最終交付的結果不符合產品的期望,最終我們所有相關人員在一起開了一個會,統一了「通用語言」,造成了額外的工作和負擔,拿到需求就應該確認好相關事宜的,越底層越細節越好,這方面的能力我仍然欠缺,但我已經持續在注意當中。

    另一次也是因為這一點,我需要幫助 A 系統擁有某一項功能,之前 A 系統已經介入了 B 系統完成了部分功能,我因為沒有進一步地確認 B 系統的現狀,就去接入了有完整功能的 C 系統,但其實 B 系統已經在上一周和開發 C 系統和 A 系統的同學對接好了,並完成了相關功能的接入,少了這一部分的溝通,就造成了不少額外的工作量.. 所以「溝通」還是非常重要的,也只能說持續進步吧…

    • 缺少一些主動性

    當我頭上掛着一些事情的時候,還是能夠保持着效率的,只是當我做完了,就時常缺乏一些主動地思考了,通常都是被動地去詢問同小組的同事有什麼事情是需要幫忙的.. 雖然也积極地參与到自己感興趣的那些技術評審之類的事情之中,但似乎效果都不佳.. 也沒有什麼實際好的輸出..

    • 接了一些私活兒黑活兒(沒有充分考慮團隊之間的配合)

    因為「埋點」會接觸各個平台的童鞋,並且時常變化和有一些新的需求,有時候直接繞過了一些環節,直接找上我了,我心想直接自己弄弄改改就可以了,也就沒多想… 但是現在想來,這樣跨團隊的事情,不能越過「頂頭上司」私自進行,一方面經常我的 BOSS 不知道我接了活兒,另一方面這樣的私自對接就會造成一些信息的流失,對於團隊之內還是團隊之間都會造成影響…

    五、回顧做得好的部分

    • 養成了閱讀的習慣

    公司買書是免費的,也有自己的圖書館,同事也不乏喜歡閱讀學習的,所以跟着跟着就養成了閱讀的習慣,期間也學習到了一些方法論的東西,貼一下入職以來讀過的那些書吧:(技術類的就沒有囊括了)

    其實每天閱讀的時間也不長,想我大學總共捧起的那麼些課外書,不禁有些唏噓…

    • 早睡早起 + 晨間日記

    早睡早起,從步入職場以來,就發現這樣的習慣會帶來一些額外的價值,例如一些閱讀我會放在早上,後來還加入了「晨間日記」,用來「回顧前一天的事情」和提前部署「今天的任務」,這不禁讓我多了一份清醒,也讓現在不怎麼鍛煉的我每一天精力更加好一些:(目前正在從印象筆記往 Notion 逐步遷移的過程中)

    • 學習撰寫 Commit Message && 遵守一些 Git 規範

    起初使用 Git 十分不規範,後來向大佬那兒學習到了如何標準地提交 Commit,包括 Commit Message 應該怎麼寫,我覺得這是一個很好的習慣,每一個 Commit 都有上下文,並且還帶上了 JIRA 號,任務也很好跟蹤,雖然公司並沒有大範圍地盛行起來,但我覺得這樣好習慣應該堅持下來:

    • 任務進度及時反饋給相關人員

    自己比較注意這一點,因為不這樣做會讓別人感受不怎麼好.. 光是自己心裏清楚是不行的.. 要保持信息的通暢才行,及時反饋是很重要的一步..

    • 自己先 review 一遍代碼

    犯過一些白痴錯誤之後,就有些擔心,逐步養成了自己先 review 一遍代碼的習慣..

    六、小結 && 展望

    總的來說,看着自己這樣一步一步成長過來,沒有很懈怠,自己就算比較滿意了,在工作中學習了很多東西,不管是技術上的硬技能,還是溝通中的軟技能,也認識到了很多厲害的大佬和有趣的小夥伴們..

    感恩在路上相遇,有幸共同行走過一段已然算是幸運,突然翻看起自己的朋友圈有一句話說得好:「成長從來都不是告別過去,成長是更加堅定的看向未來!」

    期待一路同行的大家,都能夠 Be Better!

    按照慣例黏一個尾巴:

    歡迎轉載,轉載請註明出處!
    獨立域名博客:wmyskxz.com
    簡書ID:
    github:
    歡迎關注公眾微信號:wmyskxz
    分享自己的學習 & 學習資料 & 生活
    想要交流的朋友也可以加qq群:3382693

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

    ※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

    ※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

    ※帶您來看台北網站建置台北網頁設計,各種案例分享

  • ThreadLocal深度解析和應用示例

    ThreadLocal深度解析和應用示例

    開篇明意

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

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

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

    源碼解析

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

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

    解析:

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

      2.核心方法值 get()

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

    解析:

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

    核心方法之  remove()

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

    解析:

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

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

     

     

     

    靈魂提問

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

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

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

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

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

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

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


     

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

    總結

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

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

    拓展:

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

      

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

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

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

     

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

     

      參考鏈接:

     

    在線程池中使用ThreadLocal

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

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

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

     

      解決方案:

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

        

        

     

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

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

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

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

  • 【併發編程】Java中的原子操作

    【併發編程】Java中的原子操作

    什麼是原子操作

    原子操作是指一個或者多個不可再分割的操作。這些操作的執行順序不能被打亂,這些步驟也不可以被切割而只執行其中的一部分(不可中斷性)。舉個列子:

    //就是一個原子操作
    int i = 1;
    
    //非原子操作,i++是一個多步操作,而且是可以被中斷的。
    //i++可以被分割成3步,第一步讀取i的值,第二步計算i+1;第三部將最終值賦值給i
    i++;

    Java中的原子操作

    在Java中,我們可以通過同步鎖或者CAS操作來實現原子操作。

    CAS操作

    CAS是Compare and swap的簡稱,這個操作是硬件級別的操作,在硬件層面保證了操作的原子性。CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什麼都不做。Java中的sun.misc.Unsafe類提供了compareAndSwapIntcompareAndSwapLong等幾個方法實現CAS。

    另外,在jdk的atomic包下面提供了很多基於CAS實現的原子操作類,見下圖:

    下面我們就使用其中的AtomicInteger來看看怎麼使用這些原子操作類。

    package com.csx.demo.spring.boot.concurrent.atomic;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.atomic.AtomicInteger;
    
    public class AtomicIntegerDemo {
    
        private static int THREAD_COUNT = 100;
    
        public static void main(String[] args) throws InterruptedException {
    
    
            NormalCounter normalCounter = new NormalCounter("normalCounter",0);
            SafeCounter safeCounter = new SafeCounter("safeCounter",0);
            List<Thread> threadList = new ArrayList<>();
    
            for (int i = 0; i < THREAD_COUNT ; i++) {
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int j = 0; j < 10000; j++) {
                            normalCounter.add(1);
                            safeCounter.add(1);
                        }
                    }
                });
                threadList.add(thread);
            }
    
            for (Thread thread : threadList) {
                thread.start();
            }
            for (Thread thread : threadList) {
                thread.join();
            }
            System.out.println("normalCounter:"+normalCounter.getCount());
            System.out.println("safeCounter:"+safeCounter.getCount());
        }
    
    
        public static class NormalCounter{
            private String name;
            private Integer count;
    
            public NormalCounter(String name, Integer count) {
                this.name = name;
                this.count = count;
            }
    
            public void add(int delta){
                this.count = count+delta;
            }
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public Integer getCount() {
                return count;
            }
    
            public void setCount(Integer count) {
                this.count = count;
            }
        }
    
        public static class SafeCounter{
            private String name;
            private AtomicInteger count;
    
            public SafeCounter(String name, Integer count) {
                this.name = name;
                this.count = new AtomicInteger(count);
            }
    
            public void add(int delta){
                count.addAndGet(delta);
            }
    
            public String getName() {
                return name;
            }
    
            public void setName(String name) {
                this.name = name;
            }
    
            public int getCount() {
                return count.get();
            }
    
            public void setCount(Integer count) {
                this.count.set(count);
            }
        }
    
    }

    上面的代碼中,我們分別創建了一個普通的計數器和一個原子操作的計數器(使用AtomicInteger進行計數)。然後創建了100個線程,每個線程進行10000次計數。理論上線程執行完之後,計數器的值都是1000000,但是結果如下:

    normalCounter:496527
    safeCounter:1000000

    每次執行,普通計數器的值都是不一樣的,而使用AtomicInteger進行計數的計數器都是1000000。

    CAS操作存在的問題

    • ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。

    從Java1.5開始JDK的atomic包里提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置為給定的更新值。

    • 循環時間長開銷大:自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支持處理器提供的pause指令那麼效率會有一定的提升,pause指令有兩個作用,第一它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零。第二它可以避免在退出循環的時候因內存順序衝突(memory order violation)而引起CPU流水線被清空(CPU pipeline flush),從而提高CPU的執行效率。

    • 只能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖,或者有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操作。比如有兩個共享變量i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java1.5開始JDK提供了AtomicReference類來保證引用對象之間的原子性,你可以把多個變量放在一個對象里來進行CAS操作。

    使用鎖來保證原子操作

    還是以上面的列子為列,普通的計數器我們只需要在計數方法上加鎖就行了:

    public synchronized void  add(int delta){
      this.count = count+delta;
    }

    執行結果如下:

    normalCounter:1000000
    safeCounter:1000000

    兩個計數器都能拿到正確的結果

    CPU是怎麼實現原子操作的

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

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

    網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

    ※想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師”嚨底家”!!

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

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

    Java內存模型

    (圖源: )

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

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

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

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

    垃圾回收器總結

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

    分代

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

    分代的原因

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

    回收算法

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

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

    GC種類

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

    垃圾回收器小結

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

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

    理解GC日誌

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

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

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

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

    可借鑒的編程模式

    對象分配的併發控制

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

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

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

    對象訪問的定位方式

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

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

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

    方式2: 使用句柄池

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

    參考資料

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

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

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

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

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

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

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