巴哥食慾減弱呈陽性反應恐為全美首隻確診寵物犬

摘錄自2020年4月29日自由時報報導

美國北卡羅萊納州有一隻巴哥犬被檢測出對武漢肺炎病毒呈陽性反應,恐為美國第一隻寵物犬確診案例。

《NBC》報導,該隻名叫溫斯頓(Winston)的巴哥其主人家庭有多人確診,男女主人和兒子均呈陽性反應,女兒、另一隻狗以及寵物貓則呈陰性反應。女主人麥可萊恩(Heather McLean)表示,溫斯頓有輕微症狀,早上沒有食慾。報導指出,該隻巴哥的家庭成員還透露,狗狗會舔遍所有的餐盤,然後跟主人一起睡覺。

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

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

曹工說JDK源碼(2)–ConcurrentHashMap的多線程擴容,說白了,就是分段取任務

前言

先預先說明,我這邊jdk的代碼版本為1.8.0_11,同時,因為我直接在本地jdk源碼上進行了部分修改、調試,所以,導致大家看到的我這邊貼的代碼,和大家的不太一樣。

不過,我對源碼進行修改、重構時,會保證和原始代碼的功能、邏輯嚴格一致,更多時候,可能只是修改變量名,方便理解。

大家也知道,jdk代碼寫得實在是比較深奧,變量名經常都是單字符,i,j,k啥的,實在是很難理解,所以,我一般會根據自己的理解,去重命名,為了減輕我們的頭腦負擔。

至於怎麼去修改代碼並調試,可以參考我之前的文章:

曹工力薦:調試 jdk 中 rt.jar 包部分的源碼(可自由增加註釋,修改代碼並debug)

文章中,我改過的代碼放在:

https://gitee.com/ckl111/jdk-debug

sizeCtl field的初始化

大家知道,concurrentHashMap底層是數組+鏈表+紅黑樹,數組的長度假設為n,在hashmap初始化的時候,這個n除了作為數組長度,還會作為另一個關鍵field的值。

    /**
     * Table initialization and resizing control.  When negative, the
     * table is being initialized or resized: -1 for initialization,
     * else -(1 + the number of active resizing threads).  Otherwise,
     * when table is null, holds the initial table size to use upon
     * creation, or 0 for default. After initialization, holds the
     * next element count value upon which to resize the table.
     */
    private transient volatile int sizeCtl;

該字段非常關鍵,根據取值不同,有不同的功能。

使用默認構造函數時

    public ConcurrentHashMap() {
    }

此時,sizeCtl被初始化為0.

使用帶初始容量的構造函數時

此時,sizeCtl也是32,和容量一致。

使用另一個map來初始化時

    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }

此時,sizeCtl,直接使用了默認值,16.

使用初始容量、負載因子來初始化時

    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }

這裏重載了:

這裏,我們傳入的負載因子為0.75,這也是默認的負載因子,傳入的初始容量為14.

這裏面會根據: 1 + 14/0.75 = 19,拿到真正的size,然後根據size,獲取到第一個大於19的2的n次方,即32,來作為數組容量,然後sizeCtl也被設置為32.

initTable時,對sizeCtl field的修改

實際上,new一個hashmap的時候,我們並沒有創建支撐數組,那,什麼時候創建數組呢?是在真正往裡面放數據的時候,比如put的時候。

/** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());

        int binCount = 0;
        ConcurrentHashMapPutResultVO vo = new ConcurrentHashMapPutResultVO();
        vo.setBinCount(0);
        for (Node<K,V>[] tab = table;;) {
            int tableLength;
            // 1
            if (tab == null) {
                tab = initTable();
                continue;
            }
            ...
        }

1處,即會去初始化table。

/**
     * Initializes table, using the size recorded in sizeCtl.
     * 初始化hashmap,使用sizeCtl作為容量
     */
    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            sc = sizeCtl;
            if (sc < 0){
                Thread.yield(); // lost initialization race; just spin
                continue;
            }

            /**
             * 走到這裏,說明sizeCtl大於0,大於0,代表什麼,可以去看下其構造函數,此時,sizeCtl表示
             * capacity的大小。
             * {@link #ConcurrentHashMap(int)}
             *
             * cas修改為-1,如果成功修改為-1,則表示搶到了鎖,可以進行初始化
             *
             */
            // 1
            boolean bGotChanceToInit = U.compareAndSwapInt(this, SIZECTL, sc, -1);
            if (bGotChanceToInit) {
                try {
                    tab = table;
                    /**
                     * 如果當前表為空,尚未初始化,則進行初始化,分配空間
                     */
                    if (tab == null || tab.length == 0) {
                        /**
                         * sc大於0,則以sc為準,否則使用默認的容量
                         */
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

                        Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
                        table = tab = nt;
                        /**
                         * n >>> 2,無符號右移2位,則是n的四分之一。
                         * n- n/4,結果為3/4 * n
                         * 則,這裏修改sc為 3/4 * n
                         * 比如,默認容量為16,則修改sc為12
                         */
                        // 2
                        sc = n - (n >>> 2);
                    }
                } finally {
                    /**
                     * 修改sizeCtl到field
                     */
                    // 3
                    sizeCtl = sc;
                }
                break;
            }
        }

        return tab;
    }
  • 1處,cas修改sizeCtl為-1,成功了的,獲得初始化table的權利
  • 2處,修改局部變量sc為: n – (n >>> 2),也就是修改為 0.75n,假設此時的數組容量為16,那麼sc就是16 * 0.75 = 12.
  • 3處,將sc賦值到field: sizeCtl

經過上面的分析,initTable時,這個字段可能有兩種取值:

  • -1,有線程正在對該table進行初始化
  • 0.75*數組長度,此時,已經初始化完成

上面說的是,在put的時候去initTable,實際上,這個initTable,也會在以下函數中被調用,其共同點就是,都是往裡面放數據的操作:

擴容時機

上面說了很多,目前,我們知道的是,在initTable后,sizeCtl的值,是舊的數組的長度 * 0.75。

接下來,我們看看擴容時機,在put時,會調用putVal,這個函數的大體步驟:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 1
    int hash = spread(key.hashCode());

    int binCount = 0;
    System.out.println("binCount:" + binCount);
    // 2
    ConcurrentHashMapPutResultVO vo = new ConcurrentHashMapPutResultVO();
    vo.setBinCount(0);
    for (Node<K,V>[] tab = table;;) {
        int tableLength;
        // 3
        if (tab == null) {
            tab = initTable();
            continue;
        }
        
        tableLength = tab.length;
        if (tableLength == 0) {
            tab = initTable();
            continue;
        }

        int entryNodeHashCode;
		
        // 4
        int entryNodeIndex = (tableLength - 1) & hash;
        Node<K,V> entryNode = tabAt(tab,entryNodeIndex);

        /**
         * 5 如果我們要放的桶,還是個空的,則直接cas放進去
         */
        if (entryNode == null) {
            Node<K, V> node = new Node<>(hash, key, value, null);

            // no lock when adding to empty bin
            boolean bSuccess = casTabAt(tab, entryNodeIndex, null, node);
            if (bSuccess) {
                break;
            } else {
                /**
                 * 如果沒成功,則繼續下一輪循環
                 */
                continue;
            }
        }
		
        entryNodeHashCode = entryNode.hash;
        /**
         * 6 如果要放的這個桶,正在遷移,則幫助遷移
         */
        if (entryNodeHashCode == MOVED){
            tab = helpTransfer(tab, entryNode);
            continue;
        }


        /**
         * 7 對entryNode加鎖
         */
        V oldVal = null;
        System.out.println("sync");
        synchronized (entryNode) {
            /**
             * 這一行是判斷,在我們執行前面的一堆方法的時候,看看entryNodeIndex處的node是否變化
             */
            if (tabAt(tab, entryNodeIndex) != entryNode) {
                continue;
            }

            /**
             * 8 hashCode大於0,說明不是處於遷移狀態
             */
            if (entryNodeHashCode >= 0) {
                /**
                 * 9 鏈表中找到合適的位置並放入
                 */
                findPositionAndPut(key, value, onlyIfAbsent, hash, vo, entryNode);
                binCount = vo.getBinCount();
                oldVal = (V) vo.getOldValue();
            }
            else if (entryNode instanceof TreeBin) {
                ...
            }
        }
		
        System.out.println("binCount:" + binCount);
        // 10
        if (binCount != 0) {
            if (binCount >= TREEIFY_THRESHOLD)
                treeifyBin(tab, entryNodeIndex);
            if (oldVal != null)
                return oldVal;
            break;
        }
    }
    // 11
    addCount(1L, binCount);
    return null;
}
  • 1處,計算key的hashcode

  • 2處,我這邊new了一個對象,裏面兩個字段:

    public class ConcurrentHashMapPutResultVO<V> {
        int binCount;
    
        V oldValue;
    }
    

    其中,oldValue用來存放,如果put進去的key/value,其中key已經存在的話,一般會直接覆蓋之前的舊值,這裏主要存放之前的舊值,因為我們需要返回舊值。

    binCount,則存放:在找到對應的hash桶之後,在鏈表中,遍歷了多少個元素,該值後面會使用,作為一個標誌,當該標誌大於0的時候,才去進一步檢查,看看是否擴容。

  • 3處,如果table為null,說明table里沒有任何一個鍵值對,數組也還沒創建,則初始化table

  • 4處,根據hashcode,和(數組長度 – 1)相與,計算出應該存放的哈希桶在數組中的索引

  • 5處,如果要放的哈希桶,還是空的,則直接cas設置進去,成功則跳出循環,否則重試

  • 6處,如果要放的這個桶,該節點的hashcode為MOVED(一個常量,值為-1),說明有其他線程正在擴容該hashmap,則幫助擴容

  • 7處,對要存放的hash桶的頭節點加鎖

  • 8處,如果頭節點的hashcode大於0,說明是拉了一條鏈表,則調用子方法(我這邊自己抽的),去找到合適的位置並插入到鏈表

  • 9處,findPositionAndPut,在鏈表中,找到合適的位置,並插入

  • 10處,在findPositionAndPut函數中,會返回:為了找到合適的位置,遍歷了多少個元素,這個值,就是binCount。

    如果這個binCount大於8,則說明遍歷了8個元素,則需要轉紅黑樹了。

  • 11處,因為我們新增了一個元素,總數自然要加1,這裏面會去增加總數,和檢查是否需要擴容。

其中,第9步,因為是自己抽的函數,所以這裏貼出來給大家看下:

/**
     * 遍歷鏈表,找到應該放的位置;如果遍歷完了還沒找到,則放到最後
     * @param key
     * @param value
     * @param onlyIfAbsent
     * @param hash
     * @param vo
     * @param entryNode
     */
    private void findPositionAndPut(K key, V value, boolean onlyIfAbsent, int hash, ConcurrentHashMapPutResultVO vo, Node<K, V> entryNode) {
        vo.setBinCount(1);

        for (Node<K,V> currentIterateNode = entryNode;
                ;
             vo.setBinCount(vo.getBinCount() + 1)) {


            /**
             * 如果當前遍歷指向的節點的hash值,與參數中的key的hash值相等,則,
             * 繼續判斷
             */
            K currentIterateNodeKey = currentIterateNode.key;
            boolean bKeyEqualOrNot = Objects.equals(currentIterateNodeKey, key);
            /**
             * key的hash值相等,且equals比較也相等,則就是我們要找的
             */
            if (currentIterateNode.hash == hash && bKeyEqualOrNot) {
                /**
                 * 獲取舊的值
                 */
                vo.setOldValue(currentIterateNode.val);

                /**
                 * 覆蓋舊的node的val
                 */
                if (!onlyIfAbsent)
                    currentIterateNode.val = value;
                // 這裏直接break跳出循環
                break;
            }

            /**
             * 把當前節點保存起來
             */
            Node<K,V> pred = currentIterateNode;
            /**
             * 獲取下一個節點
             */
            currentIterateNode = currentIterateNode.next;
            /**
             * 如果下一個節點為null,說明當前已經是鏈表的最後一個node了
             */
            if ( currentIterateNode  == null) {
                /**
                 * 則在當前節點後面,掛上新的節點
                 */
                pred.next = new Node<K,V>(hash, key,
                        value, null);
                break;
            }
        }

    }

第11步,也是我們要看的重點:

private final void addCount(long delta, int check) {
        CounterCell[] counterCellsArray = counterCells;
		// 1
        long b = baseCount;
    	// 2
        long newBaseCount = b + delta;

        /**
         * 3 直接cas在baseCount上增加
         */
        boolean bSuccess = U.compareAndSwapLong(this, BASECOUNT, b, newBaseCount);
        if ( counterCellsArray != null ||  !bSuccess) {
			...
            newBaseCount = sumCount();
        }
		
    	// 4
        if (check >= 0) {
            while (true) {

                Node<K,V>[] tab = table;
                Node<K,V>[] nt;
                int n = 0;
                // 5
                int sc =  sizeCtl;
                // 6
                boolean bSumExteedSizeControl = newBaseCount >= (long) sc;
				// 7
                boolean bContinue = bSumExteedSizeControl && tab != null && (n = tab.length) < MAXIMUM_CAPACITY;
                if (bContinue) {
                    int rs = resizeStamp(n);
                    if (sc < 0) {
                        if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                                sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                                transferIndex <= 0)
                            break;
                        if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                            transfer(tab, nt);
                    } else if (U.compareAndSwapInt(this, SIZECTL, sc,
                            (rs << RESIZE_STAMP_SHIFT) + 2))
                        // 8
                        transfer(tab, null);
                    newBaseCount = sumCount();
                } else {
                    break;
                }
            }

        }
    }
  • 1處,baseCount是一個field,存儲當前hashmap中,有多少個鍵值對,你put一次,就一個;remove一次,就減一個。

  • 2處,b + delta,其中,b就是baseCount,是舊的數量;dalta,我們傳入的是1,就是要增加的元素數量

    所以,b + delta,得到的,就是經過這次put后,預期的數量

  • 3處,直接cas,修改baseCount這個field為 新值,也就是第二步拿到的值。

  • 4處,這裏檢查check是否大於0,check,是第二個形參;這個參數,我們外邊怎麼傳的?

    addCount(1L, binCount);

    不就是bincount嗎,也就是說,這裏檢查:我們在put過程中,在鏈表中遍歷了幾個元素,如果遍歷了至少1個元素,這裏要進入下面的邏輯:檢查是否要擴容,因為,你binCount大於0,說明可能已經開始出現哈希衝突了。

  • 5處,取field:sizeCtl的值,給局部變量sc

  • 6處,判斷當前的新的鍵值對總數,是否大於sc了;比如容量是16,那麼sizeCtl是12,如果此時,hashmap中存放的鍵值對已經大於等於12了,則要檢查是否擴容了

  • 7處,幾個組合條件,查看是否要擴容,其中,主要的條件就是第6步的那個。

  • 8處,調用transfer,進行擴容

總結一下,經過前面的第6處,我們知道,如果存放的鍵值對總數,已經大於等於0.75*哈希桶(也就是底層數組的長度)的數量了,那麼,就基本要擴容了。

擴容的大體過程

擴容也是一個相對複雜的過程,這裏只說大概,詳細的放下講。

假設,現在底層數組長度,128,也就是128個哈希桶,當存放的鍵值對數量,大於等於 128 * 0.75的時候,就會開始擴容,擴容的過程,大概是:

  • 申請一個256(容量翻倍)的數組
  • 現在有128個桶,相當於,需要對128個桶進行遍歷,遍歷每個桶拉出去的鏈表或紅黑樹,查看每個鍵值對,是需要放到新數組的什麼位置

這個過程,昨天的博文,畫了個圖,這裏再貼一下。

擴容后:

可是,如果我們要一個個去遍歷所有哈希桶,然後遍歷對應的鏈表/紅黑樹,會不會太慢了?完全是單線程工作啊。

換個思路,我們能不能加快點呢?比如,線程1可以去處理數組的 0 -15這16個桶,16- 31這16個桶,完全可以讓線程2去做啊,這樣的話,不就多線程了嗎,不是就快了嗎?

沒錯,jdk就是這麼乾的。

jdk維護了一個field,這個field,專門用來存當前可以獲取的任務的索引,舉個例子:

大家看上圖就懂了,一開始,這裏假設我們有128個桶,每次每個線程,去拿16個桶來處理。

剛開始的時候,field:transferIndex就等於127,也就是最後一個桶的位置,然後我們要從后往前取,那麼,127 到112,剛好就是16個桶,所以,申請任務的時候,就會用cas去更新field為112,則表示,自己取到了112 到127這一個區間的hash桶遷移任務。

如果自始至終,只有一個線程呢,它處理完了112 – 127這一批hash桶后,會繼續取下一波任務,96 – 112;以此類推。

如果多線程的話呢,也是類似的,反正都是去嘗試cas更新transferIndex的值為任務區間的開始下標的值,成功了,就算任務認領成功了。

多線程,怎麼知道需要去幫助擴容呢? 發起擴容的線程,在處理完bucket[k]時,會把老的table中的對應的bucket[k]的頭節點,修改為下面這種類型的節點:

    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
    }

其他線程,在put或者其他操作時,發現頭結點變成了這個,就會去協助擴容了。

多線程擴容,和分段取任務的差別?

我個人感覺,差別不大,多線程擴容,就是多線程去獲取自己的那一段任務,然後來完成。我這邊寫了簡單的demo,不過感覺還是很有用的,可以幫助我們理解。

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.*;
import java.util.concurrent.locks.LockSupport;

public class ConcurrentTaskFetch {

    /**
     * 空閑任務索引,獲取任務時,從該下標開始,往前獲取。
     * 比如當前下標為10,表示tasks數組中,0-10這個區間的任務,沒人領取
     */
    // 0
    private  volatile int freeTaskIndexForFetch;
	
    // 1
    private static final int TASK_COUNT_PER_FETCH = 16;
	
    // 2
    private String[] tasks = new String[128];

    public static void main(String[] args) {
        ConcurrentTaskFetch fetch = new ConcurrentTaskFetch();
        // 3
        fetch.init();

        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(100));
        executor.prestartAllCoreThreads();

        CyclicBarrier cyclicBarrier = new CyclicBarrier(10);
		
        // 4
        for (int i = 0; i < 10; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        cyclicBarrier.await();
                    } catch (InterruptedException | BrokenBarrierException e) {
                        e.printStackTrace();
                    }
					
                    // 5
                    FetchedTaskInfo fetchedTaskInfo = fetch.fetchTask();
                    if (fetchedTaskInfo != null) {
                        System.out.println("thread:" + Thread.currentThread().getName() + ",get task success:" + fetchedTaskInfo);
                        try {
                            TimeUnit.SECONDS.sleep(3);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        System.out.println("thread:" + Thread.currentThread().getName()  +  ", process task finished");
                    }
                }
            });
        }


        LockSupport.park();
    }

    public void init() {
        for (int i = 0; i < 128; i++) {
            tasks[i] = "task" + i;
        }
        freeTaskIndexForFetch = tasks.length;
    }

	// 6
    public FetchedTaskInfo fetchTask() {
        System.out.println("Thread start fetch task:"+Thread.currentThread().getName()+",time: "+System.currentTimeMillis());

        while (true){
			// 6.1
            if (freeTaskIndexForFetch == 0) {
                System.out.println("thread:" + Thread.currentThread().getName() + ",get task failed,there is no task");
                return null;
            }

            /**
             * 6.2 獲取當前任務的集合的上界
             */
            int subTaskListEndIndex = this.freeTaskIndexForFetch;

            /**
             * 6.3 獲取當前任務的集合的下界
             */
            int subTaskListStartIndex = subTaskListEndIndex > TASK_COUNT_PER_FETCH ?
                    subTaskListEndIndex - TASK_COUNT_PER_FETCH : 0;

            /**
             * 6.4
             * 現在,我們拿到了集合的上下界,即[subTaskListStartIndex,subTaskListEndIndex)
             * 該區間為前開后閉,所以,實際的區間為:
             * [subTaskListStartIndex,subTaskListEndIndex - 1]
             */

            /**
             * 6.5 使用cas,嘗試更新{@link freeTaskIndexForFetch} 為 subTaskListStartIndex
             */
            if (U.compareAndSwapInt(this, FREE_TASK_INDEX_FOR_FETCH, subTaskListEndIndex, subTaskListStartIndex)) {
                // 6.6 
                FetchedTaskInfo info = new FetchedTaskInfo();
                info.setStartIndex(subTaskListStartIndex);
                info.setEndIndex(subTaskListEndIndex - 1);


                return info;
            }
        }

    }



    // Unsafe mechanics
    private static final sun.misc.Unsafe U;

    private static final long FREE_TASK_INDEX_FOR_FETCH;

    static {
        try {
//            U = sun.misc.Unsafe.getUnsafe();
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            U = (Unsafe) f.get(null);
            Class<?> k = ConcurrentTaskFetch.class;
            FREE_TASK_INDEX_FOR_FETCH = U.objectFieldOffset
                    (k.getDeclaredField("freeTaskIndexForFetch"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }


    static class FetchedTaskInfo{
        int startIndex;
        int endIndex;

        public int getStartIndex() {
            return startIndex;
        }

        public void setStartIndex(int startIndex) {
            this.startIndex = startIndex;
        }

        public int getEndIndex() {
            return endIndex;
        }

        public void setEndIndex(int endIndex) {
            this.endIndex = endIndex;
        }

        @Override
        public String toString() {
            return "FetchedTaskInfo{" +
                    "startIndex=" + startIndex +
                    ", endIndex=" + endIndex +
                    '}';
        }
    }
}

  • 0處,定義了一個field,類似於前面的transferIndex

        /**
         * 空閑任務索引,獲取任務時,從該下標開始,往前獲取。
         * 比如當前下標為10,表示tasks數組中,0-10這個區間的任務,沒人領取
         */
        // 0
        private  volatile int freeTaskIndexForFetch;
    
  • 1,定義了每次取多少個任務,這裏也是16個

    private static final int TASK_COUNT_PER_FETCH = 16;
    
  • 2,定義任務列表,共128個任務

  • 3,main函數中,進行任務初始化

    public void init() {
        for (int i = 0; i < 128; i++) {
            tasks[i] = "task" + i;
        }
        freeTaskIndexForFetch = tasks.length;
    }
    

    主要初始化任務列表,其次,將freeTaskIndexForFetch 賦值為128,後續取任務,從這個下標開始

  • 4處,啟動10個線程,每個線程去執行取任務,按理說,我們128個任務,每個線程取16個,只能有8個線程取到任務,2個線程取不到

  • 5處,線程邏輯里,去獲取任務

  • 6處,獲取任務的方法定義

  • 6.1 ,如果可獲取的任務索引為0了,說明沒任務了,直接返回

  • 6.2,獲取當前任務的集合的上界

  • 6.3,獲取當前任務的集合的下界,減去16就行了

  • 6.4,拿到了集合的上下界,即[subTaskListStartIndex,subTaskListEndIndex)

  • 6.5, 使用cas,更新field為:6.4中的任務下界。

執行效果演示:

可以看到,8個線程取到任務,2個線程沒取到。

該思想在內存分配時的應用

其實jvm內存分配時,也是類似的思路,比如,設置堆內存為200m,那這200m是啟動時立馬從操作系統分配了的。

接下來,就是每次new對象的時候,去這個大內存里,找個小空間,這個過程,也是需要cas去競爭的,比如肯定也有個全局的字段,來表示當前可用內存的索引,比如該索引為100,表示,第100個字節后的空間是可以用的,那我要new個對象,這個對象有3個字段,需要大概30個字節,那我是不是需要把這個索引更新為130。

這中間是多線程的,所以也是要cas操作。

道理都是類似的。

總結

時間倉促,有問題在所難免,歡迎及時指出或加群討論。

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

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

java作品集:企業信息門戶webtap

作品背景

隨着企業應用的軟件越來越多,並且信息軟件基本以B/S為主了,很多時候各種軟件的地址,讓大家記的頭昏腦脹,並且一堆密碼要記,而且大部分系統之間無法互通,雖然市面上有各種集成方案,但無法做到簡單有效,都是大型軟件廠商的PPT解決方案加一堆開發工作和大量的成本支出,最重要的是大部分都是體驗極差、毫無美感的東西。

解決方案

基於上述背景,個人利用業餘時間在持續完善做一款小作品,或多或少的解決一點問題,雖然目前還沒成熟,但是考慮再三,先開源出來,希望有志同道合的人一起完善。

作品說明

1.首頁

首頁主要功能有

  • app显示
  • 文件夾分類
  • 應用搜索
  • 登錄
  • 登錄后快捷新增應用
  • 背景自動每天同步bing搜索引擎的的壁紙
  • 應用和新聞站點鏈接(未完成)

通過點擊應用上的鎖 icon即可查看應用的賬號和密碼,在沒有單點登錄的功能情況下這個功能非常有用

2.系統登錄

點擊首頁右上角的 sigin 到登錄頁面

3.應用列表

後台管理 主功能只有新建應用、應用列表、系統設置,極其簡約,好不好看只是個人風格,默認登錄進來及显示應用列表。

4、新增應用

添加應用除了常規功能還增加了敏感信息輸入,敏感信息只能登錄后才能查看;
查看密碼功能考慮到很多時候連接地址需要密碼才能訪問;
查看權限目前只實現了登錄可見以及自己可見(權限功能還需要繼續完善);

5、系統設置

系統設置里可以進行基本信息維護,個人登錄信息維護,用戶管理,app分類管理,及多組織管理,主要介紹以下2重點功能。

常規設置

基本設置里為當前組織的組織名稱,訪問短鏈接地址(多組織情況下),以及組織的logo

多組織管理

技術架構

技術棧

  • springboot
  • mysql5.7
  • gradle
  • thymeleaf
  • vue2.0

代碼結構

數據庫結構

源碼地址

https://github.com/robotbird/webtap

https://gitee.com/robotbird/webtap

使用方法

  • 1、mysql 新建webtap數據庫
  • 2、導入工程目錄下doc/db/webtap.sql
  • 3、設置好application-dev.properties 里的數據庫密碼,默認root/root
  • 4、打個war包放到tomcat下即可運行,這地方沒有用jar包的原因是考慮上傳目錄採用jar包不知道什麼樣的方式合適。
  • 5、登錄管理員默認賬號robotbird@qq.com,密碼123456(暫時只支持郵箱登錄)

在線體驗

體驗地址:http://webtap.cn/
由於服務器在國外,訪問時候還請耐心等候。

總結

作品當前還未實現的功能,企業內部信息搜索集成、單點登錄集成、權限管理,以及後續考慮的小程序功能,但是依然放出來,激勵自己繼續完善下去。
此作品完全個人原創,開源遵從GNU General Public License v3.0,版權所屬個人所有,如果有同學對這個作品比較感興趣可以微信聯繫robotbird798

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

基於Azure IoT開發.NET物聯網應用系列-全新的Azure IoT架構

物聯網技術已經火了很多年了,業界各大廠商都有各自成熟的解決方案。我們公司主要搞新能源汽車充電,充電樁就是物聯網技術的最大應用,車聯網、物聯網、互聯網三網合一。2017年的時候重點研究過Azure IoT技術架構和使用,

Azure IoT 技術研究系列1-入門篇

隨着業界技術的發展,近期又重新關注並研究了最新的Azure  IoT架構,現在將結合著.NET Core技術和Azure IoT 做一些物聯網應用,將研究的成果分享給大家。

關於IoT的一些基本概念,重新梳理一下,分享給大家:

  • IoT:Internet of Things,即萬物互聯。
  • IoT Devices:物聯網設備。
  • IoT Edge Devices:物聯網邊緣計算設備。
  • IoT Gateway:IoT網關,負責IoT物聯網設備的接入、管理和控制、通訊(上行和下行)
  • 通訊協議:TCP、MQTT、AMQP、HTTPS、zgebee等等
  • Azure IoT Central。 IoT Central 是完全託管的 IoT物聯網 SaaS(軟件即服務)服務
  • 目前Azure仍然提供了Azure IoT Hub:直譯為Azure的物聯網中心
  • Azure IoT Hub為物聯網設備提供註冊、管理、溝通交互的雲服務。可用於管理數十億物聯網設備,提供可靠和安全的雲端與設備之間的雙向通信支持,每月可處理數以萬億計消息,並簡化了與其他Azure服務之間的集成,包括Azure機器學習以及
  • Azure流分析等。它是微軟Azure IoT Suite的重要組成部分,也是微軟物聯網戰略的重要基礎。

接下來,我們看一下Azure IoT最新的技術架構:

   

   下面,我們詳細介紹一下這個架構組成:

   一、 Things(物聯網設備側)

  1. IoT devices:前面已經介紹過了,泛指各類物聯網設備。設備可以安全地註冊到雲中,並且可以連接到雲之後,發送和接收數據。

  2. IoT edge devices:物聯網邊緣計算設備,某些設備可能會是在設備本身上或在現場網關中執行一些數據處理的邊緣設備。舉個大家平時常見的設備:充電樁,作為IoT邊緣計算設備,其自身有嵌入式操作系統、AI智能芯片,可以實現一些簡單的邊緣計算場景

  3. Cloud Gateway:雲網關,雲網關提供一個雲中心,以便設備安全地連接到雲併發送數據。 它還提供設備管理功能,包括設備的命令和控制。

      對於雲網關,Azure 建議使用Azure  IoT 中心。Azure IoT 中心是從設備引入事件的託管雲服務,充當設備與後端服務之間的消息代理。 同時提供安全連接、事件引入、雙向通信和設備管理。

      當然,我們也可以自建雲網關,支持各類物聯網設備的接入、管理和控制。

  4. Bulk devices provisioning:設備批量設置,統一管理設置海量設備。 對於註冊和連接許多組設備。可以使用 IoT 中心設備預配服務 (DPS)。 DPS 可用於大規模分配設備並將設備註冊到特定 Azure IoT 中心終結點。

二、Insights(洞察、洞見,可以理解為設備接入管理、數據處理、數據持久化、數據分析、可視化)

  1. Streaming Processing:流式數據處理

  Azure提供了專門的流分析服務。 流分析可以使用時間開窗函數、流聚合和外部數據源聯接大規模執行複雜分析。假如說我們自建系統做物聯網數據流式分析的話,可以使用Kafka、Flink、Spark等主流的大數據流式分析技術。

  2. Data transformation:數據轉換操作或聚合遙測數據流。

  常見的場景包括通訊協議轉換,例如,將二進制數據轉換為 JSON,或者合併數據點。 如果數據在到達 IoT 中心之前必須轉換,可以使用協議網關(一個可以轉換數據的網關)。 同時,數據可以在到達 IoT 中心後轉換。

  在這種情況下,可以使用 Azure Functions 函數計算,Azure Functions內置了與 IoT 中心、Cosmos DB 和 Blob 存儲的集成。

  3. Warm path store:熱存儲

  熱存儲,存儲實時物聯網設備上傳下發的數據,這些數據必須可按設備實時查詢,以用於報告和可視化。舉個實際的業務場景:充電樁實時上傳的電壓、電流、SOC等實時設備數據,這些數據的實時性要求高,可以存儲在熱存儲中。

  4. Cold path store:冷存儲

  如果所有的物聯網設備數據全部存儲在熱存儲中,其硬件成本會很高。數據具備一定的時效性,因為,當數據失去了一定的時效性要求后,可以存儲在冷存儲中,降低存儲的成本。

  這些數據會保留較長時間,用於批處理。 對於冷路徑存儲,可以使用 Azure Blob 存儲。 數據可無限期地以較低成本在 Blob 存儲中存檔,並且可以輕鬆訪問以進行批處理。

  5. UI Reporting and tools:可視化展現

  可視化展現方面,通常包含:IoT設備管理UI、設備控制UI、趨勢圖、連接狀態圖表、數據分析圖表等等,這個地方可以使用各類UI展現技術實現了。

三、 Action(運維管理、操作)

  1. Machine Learning:機器學習

   大家會問,用機器學習干什麼?通過歷史遙測數據執行模型訓練,實現IoT設備的預測性維護,同時還能做什麼?還可以對上報的數據建立不同的模型,實時進行訓練,智能控制設備。比如說充電樁的例子,動態調控充電功率,實現最大充電效率。

  2. Business integration:業務流程集成

   業務流程集成根據來自設備數據執行各類後續操作。 可以包括:存儲實時消息、引發警報、發送电子郵件或短信,或者與 CRM 集成。舉個實際的業務場景:當需要設備運維時,發出一個運維工單到產品運維部門,實現IoT設備的智能運維和派單處理。

  3. User Management:用戶管理

   用戶管理限制哪些用戶或組可以在設備上執行操作,例如升級固件。 它還定義應用程序中的用戶功能。

  綜上是Azure IoT架構的詳細介紹和說明,比2017年時,產品更加SaaS化,更加AI智能、更加體系。分享給大家。

 

 

周國慶

2020/6/7

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

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

GitHub 熱點速覽 Vol.23:前後端最佳實踐

作者:HelloGitHub-小魚乾

摘要:最佳實踐,又名 best-practices,是 GitHub 常見的項目名,也是本周 Trending 關鍵詞。25 年 Python 開發經驗的 David Beazley 撰寫的 practical-python 開局並獲得了超 1k 的 star,而老項目 Node.js 最佳實踐在六月也開啟了更新模式,持續更新 Node.js 性能實踐篇。卡內基梅隆大學開源的 Penrose 一個可將複雜的數學符號轉換為各種風格的簡單圖表的項目無疑是數據圖表的最佳實踐…

以下內容摘錄自微博@HelloGitHub 的 GitHub Trending,選項標準:新發布 | 實用 | 有趣,根據項目 release 時間分類,發布時間不超過 7 day 的項目會標註 New,無該標誌則說明項目 release 超過一周。由於本文篇幅有限,還有部分項目未能在本文展示,望周知

  • 本文目錄
      1. 本周特推
      • 1.1 青春回憶:CnC_Remastered_Collection
      • 1.2 實用指南:practical-python
      1. GitHub Trending 周榜
      • 2.1 跨平台自動化:robotgo
      • 2.2 Docker 上的 Mac:Docker-OSX
      • 2.3 數學圖表生成工具:Penrose
      • 2.4 換臉技術:Deepfakes
      • 2.5 面經:go-interview
      • 2.6 高顏值播放器:ZY-Player
      • 2.7 最佳實踐:nodebestpractices
      • 2.8 蘋果開源密碼管理器:password-manager-resources
      1. 本周 GitHub Trending #Python 開發小工具# 主題的主力軍
      • 3.1 內存佔用分析:Memory Profiler
      • 3.2 異常監控:Sentry
      • 3.3 內存分析:scalene
      1. 推薦閱讀

1. 本周特推

1.1 青春回憶:CnC_Remastered_Collection

本周 star 增長數:7550+

New CnC_Remastered_Collection 收錄了遊戲公司 EA 發布的《命令與征服》和《紅色警戒》原作源代碼。開源的代碼不涉及遊戲引擎和遊戲素材,只包括 TiberianDawn.dll 和 RedAlert.dll 的源代碼,開源的 DLL 可幫助玩家設計地圖、創建自定義單位、替換藝術作品,以及更改遊戲邏輯和編輯數據。

GitHub 地址→https://github.com/electronicarts/CnC_Remastered_Collection

1.2 實用指南:practical-python

本周 star 增長數:1050+

Newpractical-python 是一名有 25 年 Python 開發經驗的程序員撰寫的實用 Python 指南。無需任何 Python 開發經驗,非 Python 程序員也可以學習下該項目。

GitHub 地址→https://github.com/dabeaz-course/practical-python

2. GitHub Trending 周榜

2.1 跨平台自動化:robotgo

本周 star 增長數:1700+

robotgo 是 Golang 跨平台自動化系統,控制鍵盤鼠標位圖和讀取屏幕,窗口句柄以及全局事件監聽。支持 Windows、Linux、macOS。

GitHub 地址→https://github.com/go-vgo/robotgo

2.2 Docker 上的 Mac:Docker-OSX

本周 star 增長數:3700+

New Docker-OSX 是一個能讓你在 Docker 上跑 Mac 的項目,它支持近乎原生的 OSX-KVM。

GitHub 地址→https://github.com/sickcodes/Docker-OSX

2.3 數學圖表生成工具:Penrose

本周 star 增長數:1550+

Penrose 一個只需在純文本中輸入數學符號就可以創建漂亮的圖表的工具。這樣做的目的是為了方便非專家專心研究更有有挑戰性的技術,而非花精力研究如何創建和探索高質量圖上。

GitHub 地址→https://github.com/penrose/penrose

2.4 換臉技術:Deepfakes

本周 star 增長數:1000+

Deepfakes 是一種利用機器學習中的深度學習實現深度視頻換臉的技術。這種技術在特定的場合下可以做出非常逼真自然的換臉視頻。

GitHub 地址→https://github.com/iperov/DeepFaceLab

2.5 面經:go-interview

本周 star 增長數:500+

go-interview 收錄了用 Go 解決技術面試的方法。

GitHub 地址→https://github.com/public-apis/public-apis

2.6 高顏值播放器:ZY-Player

本周 star 增長數:1300+

ZY-Player 是一個跨平台桌面端視頻資源播放器,簡潔無廣告且顏值高。特性:

  • 全平台支持:Windows、Mac、Linux
  • 12 個視頻源
  • 支持歷史播放記錄,並記錄播放進度
  • 支持分享功能,可一鍵分享海報圖片
  • 精簡模式支持修改透明度
  • 收藏夾同步更新視頻追劇
  • 支持演員名稱搜索
  • 後台自動更新
  • 全新布局配色
  • 多語言
  • 全局快捷鍵
  • 支持下載

GitHub 地址→https://github.com/Hunlongyu/ZY-Player

2.7 最佳實踐:nodebestpractices

本周 star 增長數:1100+

nodebestpractices 是一個 Node.js 最佳實踐列表,收錄 5 篇項目結構實踐、11 篇錯誤處理實踐、12 篇代碼風格實踐、13 篇測試和整體質量實踐、19 篇生產實踐、25 篇安全實踐及 2 篇性能實踐,項目持續更新中,如果你對 Node.js 最佳實踐用有心得不妨和項目作者交流下。

GitHub 地址→https://github.com/goldbergyoni/nodebestpractices

2.8 蘋果開源密碼管理器:password-manager-resources

本周 star 增長數:800+

New蘋果推出全新開源項目——Password Manager Resources,它集成蘋果 iCloud Keychain Password Manager,可以讓密碼管理 App 開發者為特定網站創建可以兼容的強密碼,這個機制與 iCloud 鑰匙串密碼管理器相同。

GitHub 地址→https://github.com/apple/password-manager-resources

3. 本周 GitHub Trending #Python 開發小工具#主題的主力軍

在本期主題模塊,小魚乾這裏選取了 3 個 Python 性能相關的小工具,希望能提高你的開發效率。

3.1 內存佔用分析:Memory Profiler

Memory Profiler 一聽名字就是一個 Python 程序內存佔用分析工具,它可以監視一個進程的內存消耗,甚至可以一行一行的分析 Python 程序的內存消耗。Memory Profiler 由 Python 實現,用戶可選 psutil 模塊(強烈推薦)作為依賴,會分析得更快。

GitHub 地址→https://github.com/pythonprofilers/memory_profiler

3.2 異常監控:Sentry

Sentry,一款免費開源的 Python 實時異常監控平台。Sentry 採用 C/S 模式,服務器端通過 Python 實現,同時提供 web 管理頁面,支持從任何語言、任何應用程序發送事件。一個成熟的服務必要的一環就是異常告警,Sentry 可以幫你及時知道服務非預期的異常。

GitHub 地址→https://github.com/getsentry/sentry

3.3 內存分析:scalene

scalene 一個 Python 的高性能 CPU 和內存分析器。Scalene 很快、佔用資源少、展示信息全面,可用來排查、優化 Python 程序佔用資源過多等問題。

GitHub 地址→https://github.com/emeryberger/scalene

推薦閱讀

  • GitHub 熱點速覽 Vol.22:如何打造超級技術棧
  • GitHub 熱點速覽 Vol.21:Go 新手起手式,學就完事兒了
  • GitHub 熱點速覽 Vol.20:VSCode 插件全家桶新增畫圖小能手

以上為 2020 年第 23 個工作周的 GitHub Trending 如果你 Pick 其他好玩、實用的 GitHub 項目,記得來 HelloGitHub issue 區和我們分享下喲

HelloGitHub 交流群現已全面開放,添加微信號:HelloGitHub 為好友入群,可同前端、Java、Go 等各界大佬談笑風生、切磋技術~

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

實至名歸 COC廈門站眾泰T600六連冠

運動版全身大幅採用超高張力鋼板,並在車身關鍵部位進行了強化,安全性進一步提升。在主動安全性方面,T600運動版更將安全防護展現得淋漓盡致,安全配一應俱全。ESC車身穩定系統、HAC上坡輔助系統、前後倒車雷達及360°可視倒車影像等安全配置,與6方位安全氣囊、盲點信息系統、紅外夜視系統、TpMS智能胎壓監測、可選裝的HUD抬頭显示系統等尖端科技配備聯合上演重重壁壘,出色安全,呼之欲出,滿滿自信應對挑戰,盡享出行便利。

12月6日,中國汽車場地越野錦標賽(COC)廈門站比賽圓滿結束,也是最後一場分站賽,在各組別激烈的決賽搶分大戰中,眾泰T600越野車隊在汽油廠商組中奪得頭籌,車手鹿丙龍奪取該組冠軍,並與隊友徐瑩一起為車隊捧回了廠商杯冠軍殊榮,從而擴大了在年度積分方面的領先優勢,眾泰已經在今年分站比賽中已獲得六連冠,高歌猛進,一步步接近年度總冠軍。

作為本年度分站賽的最後一站,各個車隊之間的競爭日趨白熱化,尤其是之前比賽積分接近的車隊及隊員,比賽前已經是“劍拔弩張”,力爭本站取得更好排名和積分。眾泰車隊隊長楊逵如是向記者說道:“相對來說,我們在汽油改裝組的優勢要大一些,汽油廠商組和奇瑞車隊比較接近,由於總決賽採用雙倍積分的賽制,能否最終獲得全年總冠軍,廈門站比賽顯得尤為重要”。

【場地航拍圖】

【車隊大營】

【眾泰T600戰車】

汽油廠商組

本次比賽最大的變化就是之前因嚴重違規被禁賽的長安CS75車隊,重新回到了比賽。針對COC廈門站比賽形勢的變化,眾泰T600越野車隊對參賽陣容也進行了微調,喬旭與刁志剛攜手出擊汽油改裝組,鹿丙龍回歸汽油廠商組,和徐瑩搭檔。

在6圈的第一輪預賽中,車手們都拿出渾身解數,以求跑出好成績,從而得到決賽中最好的發車位置。眾泰車隊的鹿丙龍和徐瑩不負眾望,以小組第三、第四的成績闖進決賽,一起進入決賽的還有長安CS75車隊的文凡和孟斌。

“我們自身和車輛都調整到了最好狀態,對下午進行的決賽充滿信心”,眾泰T600越野車隊的車手鹿丙龍在決賽前向記者如是說。決賽中,鹿丙龍的表現堪稱“完美”,以絕對優勢力壓長安CS75車隊的孟斌和文凡獲得本組冠軍,其隊友徐瑩獲得本組季軍,獲得本組亞軍的是來自長安CS75車隊的孟斌,同時,鹿丙龍和徐瑩為眾泰T600越野車隊爭得了汽油廠商組的車隊團體冠軍獎盃。

汽油改裝組

眾泰T600越野車隊的喬旭在第一輪預賽中並不順利,他在第三圈的時候賽車出現失誤,賽車在幾處急彎都發生失控打橫,這極大地影響了喬旭的成績,儘管第二輪成績出色,但仍與決賽失之交臂,其隊友刁志剛以小組第三的成績征戰第二天進行的決賽。

6日下午的決賽中,車手刁志剛一人獨自面對其他三位車手的多面夾擊,面對發車位置不力的劣勢,刁志剛仍然奮起直追,最後以微小差距獲得了本小組的季軍,獲得本組冠亞軍的是來自另外兩支車隊的趙向前和童振榮。

作為“主流價值SUV”的眾泰T600,同眾泰車隊一樣,已然成為乘用車銷售市場上的佼佼者,早已進入月銷量“萬台俱樂部”,2016年1-10月份更是實現了94371台的銷量,以月均近萬台的銷量位居自主品牌中型SUV銷量榜首。

而且2016年眾泰汽車推出了更為年輕時尚的眾泰T600運動版,作為在眾泰T600優勢平台上推出的車型,眾泰T600運動版同樣以其年輕時尚又不乏沉穩的外觀、越級的配置在整個市場中還是有着普遍好評,銷量也是芝麻開花節節高。

眾泰T600運動版全系標配10寸中控彩色大屏,內容豐富。而Tye-net智控系統的優勢融入,實現手機操遠程控愛車,娛樂隨行,舒心便利。

此外,眾泰T600運動版還配備了一鍵啟動/無鑰匙進入、紅外夜視系統、腳步感應式電動尾門等尖端科技配備,讓駕乘人員充分享受科技智能帶來的便捷體驗。電動全景天窗、电子駐車系統、前排座椅分級加熱、雙區獨立自動恆溫空調、手機無線充電、方向盤/座椅/后視鏡三項聯動記憶功能、全液晶儀錶盤、定速巡航等帶來更加細緻入微的貼心關懷,讓出行一路無虞。

安全配置方面,眾泰T600運動版同級領先的安全性讓駕乘者無需前瞻後顧,無憂外出。運動版全身大幅採用超高張力鋼板,並在車身關鍵部位進行了強化,安全性進一步提升。在主動安全性方面,T600運動版更將安全防護展現得淋漓盡致,安全配一應俱全。ESC車身穩定系統、HAC上坡輔助系統、前後倒車雷達及360°可視倒車影像等安全配置,與6方位安全氣囊、盲點信息系統、紅外夜視系統、TpMS智能胎壓監測、可選裝的HUD抬頭显示系統等尖端科技配備聯合上演重重壁壘,出色安全,呼之欲出,滿滿自信應對挑戰,盡享出行便利。

而眾泰T600運動版不只是在外觀上吸引目光,在內飾的色彩搭配上,更是可圈可點,整個車內空間看起來既神秘又科技時尚。

眾泰T600運動版擁有的2807mm的傲人軸距,有效保證了車輛的駕乘空間。車內豐富的儲物空間為日常儲物提供了便利,而且後排座椅放倒後進一步拓展了後備箱空間,可以盡情享受眾泰T600運動版帶來的寬適空間。

動力方面,T600運動版提供1.5T及2.0T兩種發動機車型,1.5T渦輪增壓發動機與5速手動變速器搭配出黃金動力組合,最大功率達119KW,最大扭矩達215N·m。更加值得期待的是其2.0T車型,搭配使用旋鈕換擋式6速雙離合或5速手動變速器,最大功率140KW,最大扭矩250N·m,百公里加速只需9.26秒,充分提高了燃油的利用率,更加的節能環保,同時降低了用車成本。眾泰T600運動版,就是這樣讓你既有“面子”,又有“裡子”。

還有值得一說的是,眾泰T600在2015年J.D.power亞太公司發布的中國新車質量研究(IQS)報告,眾泰T600在中型SUV中pp100(每百車問題數)為100,優於中型SUV平均水平(pp100:106),全國綜合排名第13位,位列中型SUV中國品牌第二名。

2016年度COC總決賽將於12月中旬在廣西柳州打響,總決賽將實行雙倍積分制,各組別總決賽冠軍將收穫50分,這也讓之前積分落後並不太多的車手擁有了翻身逆轉的機會,那眾泰T600能否攜勢而來,獲得全年比賽的總冠軍,讓我們拭目以待!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

.NET Web應用中為什麼要使用async/await異步編程

前言

  1. 什麼是async/await?
    await和async是.NET Framework4.5框架、C#5.0語法裏面出現的技術,目的是用於簡化異步編程模型。

  2. async和await的關係?
    async和await是成對出現的。
    async出現在方法的聲明裡,用於批註一個異步方法。光有async是沒有意義的。
    await出現在方法內部,Task前面。只能在使用async關鍵字批註的方法中使用await關鍵字。

        private async Task DoSomething()
        {
            await Task.Delay(TimeSpan.FromSeconds(10));
        }
  1. async/await會創建新的線程嗎?
    不會。async/await關鍵字本身是不會創建新的線程的,但是被await的方法內部一般會創建新的線程。

  2. asp.net mvc/webapi action中使用async/await會提高請求的響應速度嗎?
    不會。

正題

我們都知道web應用不同於winform、wpf等客戶端應用,客戶端應用為了保證UI渲染的一致性往往都是採用單線程模式,這個UI線程稱為主線程,如果在主線程做耗時操作就會導致程序界面假死,所以客戶端開發中使用多線程異步編程非常必要。
可web應用本身就是多線程模式,服務器會為每個請求分配工作線程。
既然async/await不能創建新線程,又不能使提高請求的響應速度,那.NET Web應用中為什麼要使用async/await異步編程呢?

在 web 服務器上,.NET Framework 維護用於處理 ASP.NET 請求的線程池。 當請求到達時,將調度池中的線程以處理該請求。 如果以同步方式處理請求,則處理請求的線程將在處理請求時處於繁忙狀態,並且該線程無法處理其他請求。

在啟動時看到大量併發請求的 web 應用中,或具有突發負載(其中併發增長突然增加)時,使 web 服務調用異步會提高應用程序的響應能力。 異步請求與同步請求所需的處理時間相同。 如果請求發出需要兩秒鐘時間才能完成的 web 服務調用,則該請求將需要兩秒鐘,無論是同步執行還是異步執行。 但是,在異步調用期間,線程在等待第一個請求完成時不會被阻止響應其他請求。 因此,當有多個併發請求調用長時間運行的操作時,異步請求會阻止請求隊列和線程池的增長。

下面用代碼來實際測試一下:

  • 先是同步的方式,代碼很簡單,就是輸出一下請求開始和結束的時間和線程ID:
        public ActionResult Index()
        {
            DateTime startTime = DateTime.Now;//進入DoSomething方法前的時間
            var startThreadId = Thread.CurrentThread.ManagedThreadId;//進入DoSomething方法前的線程ID

            DoSomething();//耗時操作

            DateTime endTime = DateTime.Now;//完成DoSomething方法的時間
            var endThreadId = Thread.CurrentThread.ManagedThreadId;//完成DoSomething方法后的線程ID
            return Content($"startTime:{ startTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } startThreadId:{ startThreadId }<br/>endTime:{ endTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } endThreadId:{ endThreadId }<br/><br/>");
        }

        /// <summary>
        /// 耗時操作
        /// </summary>
        /// <returns></returns>
        private void DoSomething()
        {
            Thread.Sleep(10000);
        }

使用瀏覽器開3個標籤頁進行測試(因為瀏覽器對同一域名下的連接數有限制,一般是6個左右,所以就弄3個吧):

可以看到耗時都是10秒,開始和結束的線程ID一致。

  • 下面改造成異步的:
        public async Task<ActionResult> Index()
        {
            DateTime startTime = DateTime.Now;//進入DoSomething方法前的時間
            var startThreadId = Thread.CurrentThread.ManagedThreadId;//進入DoSomething方法前的線程ID

            await DoSomething();//耗時操作

            DateTime endTime = DateTime.Now;//完成DoSomething方法的時間
            var endThreadId = Thread.CurrentThread.ManagedThreadId;//完成DoSomething方法后的線程ID
            return Content($"startTime:{ startTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } startThreadId:{ startThreadId }<br/>endTime:{ endTime.ToString("yyyy-MM-dd HH:mm:ss:fff") } endThreadId:{ endThreadId }<br/><br/>");
        }

        /// <summary>
        /// 耗時操作
        /// </summary>
        /// <returns></returns>
        private async Task DoSomething()
        {
            await Task.Run(() => Thread.Sleep(10000));
        }

結果:

可以看到3次請求中,雖然耗時都是10秒,但是出現了開始和結束的線程ID不一致的情況,ID為22的這個線程工作了多次,這意味着使用異步方式在同一時間可以處理更多的請求!(這句話不太對,3個同步的併發請求必然會分配3個工作線程,但是使用異步的話,同一個線程可以被多個請求重複利用。因為線程池的線程數量是有上限的,所以在相同數量的線程下,使用異步方式能處理更多的請求。)
IIS默認隊列長度:

await關鍵字不會阻塞線程直到任務完成。 它將方法的其餘部分註冊為任務的回調,並立即返回。 當await的任務最終完成時,它將調用該回調,並因此在其中斷時繼續執行方法。

簡單來說:就是使用同步方法時,線程會被耗時操作一直佔有,直到耗時操作完成。而使用異步方法,程序走到await關鍵字時會立即return,釋放線程,餘下的代碼會放進一個回調中(Task.GetAwaiter()的UnsafeOnCompleted(Action)回調),耗時操作完成時才會回調執行,所以async/await是語法糖,其本質是一個狀態機。

那是不是所有的action都要用async/await呢?
不是。一般的磁盤IO或者網絡請求等耗時操作才考慮使用異步,不要為了異步而異步,異步也是需要消耗性能的,使用不合理會適得其反。

結論

async/await異步編程不能提升響應速度,但是可以提升響應能力(吞吐量)。異步和同步各有優劣,要合理選擇,不要為了異步而異步。

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

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

MySQL查詢優化利刃-EXPLAIN

有一個 ?

遇到這樣一個疑問:當where查詢中In一個索引字段作為條件,那麼在查詢中還會使用到索引嗎?

SELECT * FROM table_name WHERE column_index in (expr)

上面的sql語句檢索會使用到索引嗎?帶着這個問題,在網上查找了很多文章,但是有的說 in 會導致放棄索引,全表掃描;有的說Mysql5.5之前的版本不會走,之後的innodb版本會走索引…

越看越迷糊,那答案到底是怎樣的呢?

唯有實踐是檢驗真理的唯一方式!

拿出我們的利刃——EXPLAIN,去剖析 SELECT 語句,一探究竟!

EXPLAIN 的用法

在 SELECT 語句前加上 EXPLAIN 就可以了 ,例如:

EXPLAIN SELECT * FROM table_name [WHERE Clause]

EXPLAIN 的輸出

EXPLAIN 命令的輸出內容為一個表格形式,表的每一個字段含義如下:

列名 解釋
id SELECT 查詢的標識符. 每個 SELECT 都會自動分配一個唯一的標識符
select_type SELECT 查詢的類型
table 查詢的是哪個表
partitions 匹配的分區
type join 類型
possible_keys 此次查詢中可能選用的索引
key 此次查詢中確切使用到的索引
ref 哪個字段或常數與 key 一起被使用;與索引比較的列
rows 显示此查詢一共掃描了多少行, 這個是一個估計值
filtered 表示此查詢條件所過濾的數據的百分比
extra 額外的信息
select_type
查詢類型 解釋
SIMPLE 表示此查詢不包含 UNION 查詢或子查詢
PRIMARY 表示此查詢是最外層的查詢
UNION 表示此查詢是 UNION 的第二或隨後的查詢
DEPENDENT UNION UNION 中的第二個或後面的查詢語句, 取決於外面的查詢
UNION RESULT UNION 的結果
SUBQUERY 子查詢中的第一個 SELECT
DEPENDENT SUBQUERY 子查詢中的第一個 SELECT,取決於外面的查詢。子查詢依賴於外層查詢的結果
MATERIALIZED Materialized subquery
table

表示查詢涉及的表或衍生表 。 這也可以是以下值之一:

  • <unionM,N>:該行指的是具有和id值的行 的 M並集 N。
  • :該行是指用於與該行的派生表結果id的值 N。派生表可能來自FROM子句中的子查詢 。
  • :該行是指該行的物化子查詢的結果,其id 值為N。
partitions

查詢將匹配記錄的分區。該值適用NULL於未分區的表。

type

聯接類型。 提供了判斷查詢是否高效的重要依據依據。通過 type 字段,我們判斷此次查詢是全表掃描還是索引掃描等。 從最佳類型到最差類型:

  • system: 該表只有一行(=系統表)。這是const聯接類型的特例 。

  • const: 針對主鍵或唯一索引的等值查詢掃描,最多只返回一行數據。const 查詢速度非常快,因為它僅僅讀取一次即可 。

    SELECT * FROM tbl_name WHERE primary_key=1;
    
    SELECT * FROM tbl_name
      WHERE primary_key_part1=1 AND primary_key_part2=2;
    
  • eq_ref: 此類型通常出現在多表的 join 查詢,表示對於前表的每一個結果,都只能匹配到后表的一行結果。並且查詢的比較操作通常是 =,查詢效率較高

    SELECT * FROM ref_table,other_table
      WHERE ref_table.key_column=other_table.column;
    
    SELECT * FROM ref_table,other_table
      WHERE ref_table.key_column_part1=other_table.column
      AND ref_table.key_column_part2=1;
    
  • ref : 此類型通常出現在多表的 join 查詢,針對於非唯一或非主鍵索引,或者是使用了最左前綴規則索引的查詢。ref可以用於使用=或<=> 運算符進行比較的索引列。

    SELECT * FROM ref_table WHERE key_column=expr;
    
    SELECT * FROM ref_table,other_table
      WHERE ref_table.key_column=other_table.column;
    
    SELECT * FROM ref_table,other_table
      WHERE ref_table.key_column_part1=other_table.column
      AND ref_table.key_column_part2=1;
    
  • ref_or_null: 這種連接類型類似於 ref,但是除了MySQL會額外搜索包含NULL值的行。此聯接類型優化最常用於解析子查詢。

    SELECT * FROM ref_table
      WHERE key_column=expr OR key_column IS NULL;
    
  • unique_subquery: 只是一個索引查找函數,它完全替代了子查詢以提高效率。

    value IN (SELECT primary_key FROM single_table WHERE some_expr)
    
  • index_subquery:此連接類型類似於 unique_subquery。它代替IN子查詢,但適用於以下形式的子查詢中的非唯一索引。

  • range: 表示使用索引範圍查詢, 通過索引字段範圍獲取表中部分數據記錄。這個類型通常出現在 =,<>,>,>=,<,<=,IS NULL,<=>,BETWEEN,IN() 操作中。

    當 type 是 range 時,那麼 EXPLAIN 輸出的 ref 字段為 NULL,並且 key_len 字段是此次查詢中使用到的索引的最長的那個 。

    SELECT * FROM tbl_name
      WHERE key_column = 10;
    
    SELECT * FROM tbl_name
      WHERE key_column BETWEEN 10 and 20;
    
    SELECT * FROM tbl_name
      WHERE key_column IN (10,20,30);
    
    SELECT * FROM tbl_name
      WHERE key_part1 = 10 AND key_part2 IN (10,20,30);
    
  • index: 表示全索引掃描(full index scan)和 ALL 類型類似,只不過 ALL 類型是全表掃描,而 index 類型則僅僅掃描所有的索引,而不掃描數據。

    index 類型通常出現在: 所要查詢的數據直接在索引樹中就可以獲取到,而不需要掃描數據。當是這種情況時,Extra 字段 會显示 Using index

  • ALL: 表示全表掃描,這個類型的查詢是性能最差的查詢之一。

    我們的查詢不應該出現 ALL 類型的查詢,因為這樣的查詢在數據量大的情況下,對數據庫的性能是巨大的災難。如一個查詢是 ALL 類型查詢,那麼一般來說可以對相應的字段添加索引來避免 。

possible_keys

表示 MySQL 在查詢時,能夠使用到的索引。

即使有些索引在 possible_keys 中出現,但是並不表示此索引會真正地被 MySQL 使用到。MySQL 在查詢時具體使用了哪些索引,由 key 字段決定。

key

是 MySQL 在當前查詢時所真正使用到的索引。

key_len

表示查詢優化器使用了索引的字節數。

這個字段可以評估組合索引是否完全被使用,或只有最左部分字段被使用到。key_len 的計算規則如下:

  • 字符串
    • char(n): n 字節長度
    • varchar(n): 如果是 utf8 編碼, 則是 3n + 2字節; 如果是 utf8mb4 編碼, 則是 4n + 2 字節
  • 數值類型
  • TINYINT: 1字節
  • SMALLINT: 2字節
  • MEDIUMINT: 3字節
  • INT: 4字節
  • BIGINT: 8字節
  • 時間類型
  • DATE: 3字節
  • TIMESTAMP: 4字節
  • DATETIME: 8字節
  • 字段屬性: NULL 屬性 佔用一個字節。如果一個字段是 NOT NULL 的, 則沒有此屬性
rows

查詢優化器根據統計信息,估算 SQL 要查找到結果集需要掃描讀取的數據行數。這個值非常直觀显示 SQL 的效率好壞,原則上 rows 越少越好。

這個 rows 就是 mysql 認為必須要逐行去檢查和判斷的記錄的條數。舉個例子來說,假如有一個語句 select * from t where column_a = 1 and column_b = 2; 全表假設有 100 條記錄,column_a 字段有索引(非聯合索引),column_b沒有索引。column_a = 1 的記錄有 20 條, column_a = 1 and column_b = 2 的記錄有 5 條。

Extra

EXplain 中的很多額外的信息會在 Extra 字段显示,常見的有以下幾種內容:

  • Using filesort:當 Extra 中有 Using filesort 時,表示 MySQL 需額外的排序操作,不能通過索引順序達到排序效果。一般有 Using filesort,都建議優化去掉,因為這樣的查詢 CPU 資源消耗大。
  • Using index:”覆蓋索引掃描”,表示查詢在索引樹中就可查找所需數據,不用掃描表數據文件,往往說明性能不錯
  • Using temporary:查詢有使用臨時表,一般出現於排序,分組和多表 join 的情況,查詢效率不高,建議優化
  • Using where: WHERE子句用於限制哪些行與下一個表匹配或發送給客戶端 。

得出結論

說到最後,那 WHERE column_index in (expr) 到底走不走索引呢? 答案是不確定的。

走不走索引是由 expr 來決定的,不是一概而論走還是不走。

SELECT * FROM a WHERE id in (1,23,456,7,8)
-- id 是主鍵,查詢是走索引的。type = range,key = PRIMARY
SELECT * FROM a WHERE id in (SELECT b.a_id FROM b WHERE some_expr)
-- id 是主鍵,如果 some_expr 是一個索引查詢,那麼 select a 將走索引;
-- some_expr 不是索引查詢,那麼 select a 將全表掃描;

上面是兩個通用案例,但到底對不對了,還是自己去實踐最好了,拿起EXPLAIN去剖析吧~

參考文章: https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain

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

【其他文章推薦】

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

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

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

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

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

※回頭車貨運收費標準

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

由美國普渡大學開發 友善環境的稀土開採技術獲專利

環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:ENS

美國普渡大學開發出更環境友善且具商業可行性的稀土開採技術。新技術已獲專利,有機會改寫產業生態,幫助美國創造更穩定的本土供給。

舉凡電腦、手機、DVD、充電電池、觸媒轉換器、磁鐵、風力渦輪機和日光燈、雷射測距儀、導航系統和精準武器都需要稀土金屬,但是在自然界中往往濃度不高,很難進行商業開採。

此外,目前以酸為基礎的稀土金屬分離和純化技術不利環境,世界各地的大多數公司無法進入市場。

碳酸鑭的結晶體,鑭是稀土之一。照片來源:ZEISS Microscopy(CC BY-NC-ND 2.0)

中國是全球稀土主要生產者。17種稀土金屬的全球儲藏量,有36%掌握在中國手上。目前中國已不再像1980至1990年代,以低於生產成本的價格出售稀土,其他國家也有機會成為稀土生產者。

但是當中國在2010年降低稀土金屬的出口配額時,一台風力渦輪機的稀土磁體成本從8萬美元飆升至50萬美元。18個月後中國放鬆了出口限制,價格又回到了低於2010年的水準。

中國目前仍是本土和出口市場中,用來生產電子產品的稀土金屬的主要消費國,緊接著是日本和美國。

幾乎60%的稀土金屬都用於生產磁鐵。具有永磁性的稀土磁鐵幾乎日常生活無所不在,不論是電子產品、飛機、油電混合車或風機。開發出新技術的普渡大學化學教授王念華(音譯)說:「目前稀土只有一個海外供給,如果因故供給受限,將嚴重影響人們的生活。美國也有這種資源,但需要一種更好、更乾淨的方式來處理這些稀土金屬。」

新的專利提取和純化技術使用配體輔助層析法,經證實可以安全有效地從煤灰、回收磁鐵和原礦石中分離出稀土金屬,對環境幾乎沒有不利影響。

鎦是一種銀白色金屬,稀土金屬之一。照片來源:
維基百科/Alchemist-hp(CC BY-NC-ND 3.0)

稀土金屬的生產每年全球有40億美元的市場。隨著新的電子產品、飛機、軍艦、電動汽車、磁鐵和其他需要稀土金屬的重要產品的開發,這個市場繼續成長中。每年靠稀土金屬作用的產品價值超過4兆美元。

王念華解釋:「傳統生產高純度稀土元素方法採用兩階段液相萃取法,這需要用到數千個串聯或併聯的混合沈降槽,產生大量有毒廢物。我們的兩區域配體輔助置換層析系統使用一種新的分區方法,可生產高純度(> 99%)金屬並達到高產率(> 99%)。」

王念華的配體輔助方法有機會從廢磁鐵和礦石回收高效、環保地純化稀土金屬,並有助使稀土加工成為循環永續的過程。

普渡大學化學工程教授喬.佩克尼(Joe Pekny)說,王念華的創新技術使美國能以環境友善、安全和永續的方式重新進入稀土金屬市場。佩克尼說:「美國的稀土金屬可以滿足美國和全球其他市場不斷成長的需求,減少對外國資源的依賴。」

這項研究部分由美國國防部(Department of Defense﹐DoD)資助。

現在美國國防部正在與美國稀土礦供應鏈簽訂新合約,加州沙漠中一度停產的鈾礦礦場,也是北美唯一的稀土礦開採和加工基地可望重新啟用。

Greener Process Grows U.S. Supply of Rare Earth Metals WEST LAFAYETTE, Indiana, May 11, 2020(ENS)

Mining for rare earth metals is about to become more environmentally and economically feasible though a process newly developed and patented at Purdue University.

These new environmentally-friendly technologies promise to be game-changers in this field and could enable the United States to create a more stable and reliable domestic source of these essential metals.

Used in computers, cell phones, DVDs, rechargeable batteries, catalytic converters, magnets, wind turbines, and fluorescent lights, and for defense in laser range-finders, guidance systems, and precision-guided weapons, these metals are difficult to mine because it is unusual to find them in concentrations high enough for economical extraction.

In addition, the detrimental environmental impact of current acid-based separation and purification of rare earth metals prohibits most companies everywhere in the world from entering the market.

China is currently the world leader in rare earth production, although it controls just 36 percent of the world’s reserves of these 17 metals. This provides an opportunity for other countries to become producers now that China is not selling rare earth materials below the cost of production as it did in the 1980s and ’90s.

But when China reduced the export quotas for rare earth metals in 2010, the costs of rare earth magnets for one wind turbine soared from $80,000 to $500,000. After China relaxed the export restrictions 18 months later, prices returned to lower levels than in 2010.

China is also the dominant consumer of rare earth metals for manufacturing electronics products for domestic and export markets. Today Japan and the United States are the second and third largest consumers of rare earth materials.

“About 60 percent of rare earth metals are used in magnets that are needed in almost everyone’s daily lives. These metals are used in electronics, airplanes, hybrid cars and even windmills,” said Nien-Hwa Linda Wang, the Purdue professor of chemistry who developed the new processes.

“We currently have one dominant foreign source for these metals and if the supply were to be limited for any reason, it would be devastating to people’s lives,” Wang said. “It’s not that the resource isn’t available in the U.S., but that we need a better, cleaner way to process these rare earth metals.”

The new patented extraction and purifying processes use ligand-assisted chromatography, a separation method that has been shown to remove and purify rare earth metals from coal ash, recycled magnets, and raw ore safely, efficiently and with virtually no detrimental environmental impact.

The production of rare earth metals is a global US$4 billion annual market that continues to grow as new electronics, computerized engines for aircraft, warships, electric automobiles, magnets, and other critical products are developed that require rare earth metals to perform. The value of the products using rare earth metals to function is valued at more than $4 trillion per year.

“Conventional methods for producing high-purity rare earth elements employ two-phase liquid–liquid extraction methods, which require thousands of mixer-settler units in series or in parallel and generate large amounts of toxic waste,” Wang said.

“We use a two-zone ligand-assisted displacement chromatography system with a new zone-splitting method that is producing high-purity (>99%) metals with high yields (>99%).”

Wang’s ligand assisted method has the potential for efficient and environmentally friendly purification of the rare earth metals from all sources of recyclates, such as waste magnets and ore-based sources and helps transform rare earth processing to a circular, sustainable process.

Joe Pekny, a Purdue professor of chemical engineering, said Wang’s innovation enables the United States to reenter the rare earth metals market in an earth-friendly, safe and sustainable way. “What’s exciting is that the U.S. has the rare earth metals to meet the growing demands of the U.S. market and other markets around the globe and reduces our dependence on foreign sources,” Pekny said.

This research was funded in part by the U.S. Department of Defense, DoD.

Now, the Defense Department is supporting the U.S. rare earth supply chain with a new contract to a once defunct uranium mine in the California desert that is now the only rare earth mining and processing site in North America.

※ 全文及圖片詳見:ENS

稀土金屬
開採
專利
友善環境
礦業
國際新聞
美國
生活環境
環境經濟
循環經濟

作者

姜唯

如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

林大利

於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

延伸閱讀

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

【其他文章推薦】

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

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

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

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

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

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

一文講透Java序列化

 

本文目錄

  • 一、序列化是什麼
  • 二、為什麼需要序列化
  • 三、序列化怎麼用
  • 四、序列化深度探秘
    • 4.1 為什麼必須實現Serializable接口
    • 4.2 被序列化對象的字段是引用時該怎麼辦 
    • 4.3 同一個對象會被序列化多次嗎
    • 4.4 只想序列化對象的部分字段該怎麼辦
    • 4.5 被序列化對象具有繼承關係該怎麼辦
  • 五、serialVersionUID的作用及自動生成
  • 六、序列化的缺點
  • 七、參考文獻

 

前言

 

Oracle 公司計劃廢除 Java 中的古董:序列化技術,因為它帶來了許多嚴重的安全問題(如序列化存儲安全、反序列化安全、傳輸安全等),據統計,至少有3分之1的漏洞是序列化帶來的,這也是 1997 年誕生序列化技術的一個巨大錯誤。但是,序列化技術現在在 Java 應用中無處不在,特別是現在的持久化框架和分佈式技術中,都需要利用序列化來傳輸對象,如:Hibernate、Mybatis、Java RMI、Dubbo等,即對象要存儲或者傳輸都不可避免要用到序列化技術,所以刪除序列化技術將是一個長期的計劃。

 

你在實際工作中可能會很難有機會真正用到Java自帶的序列化技術了,工業界一般也會選擇一些更安全的對象編解碼方案例如Google的Protobuf等。所以,對於Java序列化,我們不必再投入過多的精力學習,你花20分鐘讀完本文所掌握的知識,對於應付日常源碼閱讀中遇到的遺留的Java序列化技術應該是足夠了。

 

一、序列化是什麼

 

序列化機制允許將實現序列化的Java對象轉換成字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以備以後重新恢復成原來的對象。序列化機制使得對象可以脫離程序的運行而獨立存在。

  • 序列化:將一個Java對象寫入IO流中
  • 反序列化:從IO流中恢復該Java對象

 

本文中用序列化來簡稱整個序列化和反序列化機制。 

 

二、為什麼需要序列化

 

所有可能在網絡上傳輸的對象的類都應該是可序列化的,否則程序將會出現異常,比如RMI(Remote Method Invoke,即遠程方法調用,是JavaEE的基礎)過程中的參數和返回值;所有需要保存到磁盤裡的對象的類都必須可序列化,比如Web應用中需要保存到HttpSession或ServletContext屬性的Java對象。

 

因為序列化是RMI過程的參數和返回值都必須實現的機制,而RMI又是Java EE技術的基礎——所有的分佈式應用常常需要跨平台、跨網絡,所以要求所有傳遞的參數、返回值必須實現序列化。因此序列化機制是Java EE平台的基礎。通常建議:程序創建的每個JavaBean類都實現Serializable。

 

三、序列化怎麼用

 

如果一個類的對象需要序列化,那麼在Java語法層面,這個類需要:

  • 實現Serializable接口
  • 使用ObjectOutputStream將對象輸出到流,實現對象的序列化;使用ObjectInputStream從流中讀取對象,實現對象的反序列化

 

下面我們通過代碼示例來看看序列化最基本的用法。我們創建了Person類,其擁有兩個基本類型的屬性,並實現了Serializable接口。testSerialize方法用來測試序列化,testDeserialize方法用來測試反序列化。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 148.2);
10         Person two = new Person(35, 177.8);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable {
40     int age;
41     double height;
42 
43     public Person(int age, double height) {
44         this.age = age;
45         this.height = height;
46     }
47 
48     @Override
49     public String toString() {
50         return "Person{" +
51                 "age=" + age +
52                 ", height=" + height +
53                 '}';
54     }
55 }

 

四、序列化深度探秘

4.1 為什麼必須實現Serializable接口

如果某個類需要支持序列化功能,那麼它必須實現Serializable接口,否則會報 java.io.NotSerializableException。Serializable接口是一個標誌性接口(Marker Interface),也就是說,該接口並不包含任何具體的方法,是一個空接口,僅僅用來判斷該類是否能夠序列化。JDK8中Serializable接口的源碼如下:

1 package java.io;
2 
3 public interface Serializable {
4 }

 

在 ObjectOutputStream.java 的 writeObject0 方法中,我們確實可以看到對對象是否實現了 Serializable接口進行了驗證(第15行),否則會拋出 NotSerializableException 異常(第22行)。

 1     private void writeObject0(Object obj, boolean unshared)
 2         throws IOException
 3     {
 4         boolean oldMode = bout.setBlockDataMode(false);
 5         depth++;
 6         try {
 7             ...
 8             // remaining cases
 9             if (obj instanceof String) {
10                 writeString((String) obj, unshared);
11             } else if (cl.isArray()) {
12                 writeArray(obj, desc, unshared);
13             } else if (obj instanceof Enum) {
14                 writeEnum((Enum<?>) obj, desc, unshared);
15             } else if (obj instanceof Serializable) {
16                 writeOrdinaryObject(obj, desc, unshared);
17             } else {
18                 if (extendedDebugInfo) {
19                     throw new NotSerializableException(
20                         cl.getName() + "\n" + debugInfoStack.toString());
21                 } else {
22                     throw new NotSerializableException(cl.getName());
23                 }
24             }
25         } finally {
26             depth--;
27             bout.setBlockDataMode(oldMode);
28         }
29     }

 

4.2 被序列化對象的字段是引用時該怎麼辦

在第三部分“序列化怎麼用”部分的示例中,Person類的字段全都是基本類型,我們知道基本類型其地址中直接存放的就是它的值,那如果是引用類型呢?引用類型其地址中存放的是指向堆內存中的一個地址,難道序列化時就是將這個地址進行了保存嗎?顯然,這是說不通的,因為對象的內存地址是可變的,在同一系統的不同運行時刻或者是不同系統中,對象的地址肯定是不同的,因此,序列化內存地址沒有意義。

 

如果被序列化對象的字段是引用,那麼要求該引用的類型也是可序列化實現了Serializable接口的,否則無法序列化。當對某個對象進行序列化時,系統會自動把該對象的所有Field依次進行序列化,如果某個Field引用到另一個對象,則被引用的對象也會被序列化;如果被引用的對象的Field也引用了其他對象,則被引用的對象也會被序列化,這種情況被稱為遞歸序列化。

 

4.3 同一個對象會被序列化多次嗎

如果對象A和對象B同時引用了對象C,那麼,當序列化對象A和對象B時,對象C會被序列化兩次嗎?答案顯然是不會

 

要解釋這個問題,就不得不說一下Java序列化的基本算法了:

  • 所有序列化到二進制流的對象都有一個序列化編號
  • 當程序試圖序列化一個對象時,程序將先檢查該對象是否已經被序列化過,只有該對象從未(在本次虛擬機中)被序列化過,系統才會將該對象轉換成字節序列並賦予一個唯一的編號
  • 如果某個對象已經序列化過,程序將只是直接輸出其序列化編號,而不是再次重新序列化該對象

 

4.4 只想序列化對象的部分字段該怎麼辦

 在一些特殊的場景下,如果一個類里包含的某些Field值是敏感信息,例如銀行賬戶信息等,這時不希望系統將該Field值進行序列化;或者某個Field的類型是不可序列化的,因此不希望對該Field進行遞歸序列化,以避免引發java.io.NotSerializableException異常。

 

此時,我們就需要自定義序列化了。自定義序列化的常用方式有兩種:

  • 使用transient關鍵字
  • 重寫writeObject與readObject方法

 

我們先看第一種方式,使用transient關鍵字。transient關鍵字只能用於修飾Field,不可修飾Java程序中的其他成分。使用transient修飾的屬性,java序列化時,會忽略掉此字段,所以反序列化出的對象,被transient修飾的屬性是默認值。對於引用類型,值是null;基本類型,值是0;boolean類型,值是false。

 

下列代碼中,我們把People的height字段設置為transient,在反序列化時,可觀察到輸出為默認值0.0。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 156.6);
10         Person two = new Person(16, 177.7);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable{
40     protected int age;
41     protected transient double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     @Override
52     public String toString() {
53         return "Person{" +
54                 "age=" + age +
55                 ", height=" + height +
56                 '}';
57     }
58 }

 

 程序輸出:

Person{age=12, height=0.0}
Person{age=16, height=0.0}

Process finished with exit code 0

 

使用transient關鍵字修飾Field雖然簡單、方便,但被transient修飾的Field將被完全隔離在序列化機制之外,這樣導致在反序列化恢復Java對象時無法取得該Field值。Java還提供了一種自定義序列化機制,通過這種自定義序列化機制可以讓程序控制如何序列化各Field,甚至完全不序列化某些Field(與使用transient關鍵字的效果相同)。在序列化和反序列化過程中需要特殊處理的類應該提供如下特殊簽名的方法,這些特殊的方法用以實現自定義序列化。

 private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;

 

  • writeObject()方法負責寫入特定類的實例狀態,以便相應的readObject()方法可以恢復它。通過重寫該方法,程序員可以完全獲得對序列化機制的控制,可以自主決定哪些Field需要序列化,需要怎樣序列化。在默認情況下,該方法會調用out.defaultWriteObject來保存Java對象的各Field,從而可以實現序列化Java對象狀態的目的。
  • readObject()方法負責從流中讀取並恢復對象Field,通過重寫該方法,程序員可以完全獲得對反序列化機制的控制,可以自主決定需要反序列化哪些Field,以及如何進行反序列化。在默認情況下,該方法會調用in.defaultReadObject來恢復Java對象的非靜態和非瞬態Field。在通常情況下,readObject()方法與writeObject()方法對應,如果writeObject()方法中對Java對象的Field進行了一些處理,則應該在readObject()方法中對其Field進行相應的反處理,以便正確恢復該對象。
  • 當序列化流不完整時,readObjectNoData()方法可以用來正確地初始化反序列化的對象。例如,接收方使用的反序列化類的版本不同於發送方,或者接收方版本擴展的類不是發送方版本擴展的類,或者序列化流被篡改時,系統都會調用readObjectNoData()方法來初始化反序列化的對象。

下面的示例代碼中,我們在writeObject方法中對Person的字段進行了簡單的加密處理,在readObject方法中對其進行了相應的解密。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Person one = new Person(12, 156.6);
10         Person two = new Person(16, 177.7);
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Person.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Person.txt"))) {
26             Person one = (Person) input.readObject();
27             Person two = (Person) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person implements Serializable{
40     protected int age;
41     protected double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     private void writeObject(java.io.ObjectOutputStream out)
52             throws IOException {
53         System.out.println("Encryption!");
54         out.writeInt(age + 1);
55         out.writeDouble(height - 1);
56     }
57     private void readObject(java.io.ObjectInputStream in)
58             throws IOException, ClassNotFoundException {
59         System.out.println("Decryption!");
60         this.age = in.readInt() - 1;
61         this.height = in.readDouble() + 1;
62     }
63 
64     @Override
65     public String toString() {
66         return "Person{" +
67                 "age=" + age +
68                 ", height=" + height +
69                 '}';
70     }
71 }

 

4.5 被序列化對象具有繼承關係該怎麼辦

被序列化對象具有繼承關係時無非就兩種情況,第一,該類具有子類,第二,該類具有父類。

 

當該類實現了Serializable接口且具有子類時,根據官方文檔中的說明,其子類天然具有可被序列化的屬性,不需要顯式實現Serializable接口;。

 All subtypes of a serializable class are themselves serializable. 

 

當該類實現了Serializable接口且具有父類時,,該類的父類需要實現Serializable接口嗎?在JDK8中Serializable接口的官方文檔中有這樣一段話:

 1 /**
 2  * ......
 3  *
 4  * To allow subtypes of non-serializable classes to be serialized, the
 5  * subtype may assume responsibility for saving and restoring the
 6  * state of the supertype's public, protected, and (if accessible)
 7  * package fields.  The subtype may assume this responsibility only if
 8  * the class it extends has an accessible no-arg constructor to
 9  * initialize the class's state.  It is an error to declare a class
10  * Serializable if this is not the case.  The error will be detected at
11  * runtime. 
12  *
13  * During deserialization, the fields of non-serializable classes will
14  * be initialized using the public or protected no-arg constructor of
15  * the class.  A no-arg constructor must be accessible to the subclass
16  * that is serializable.  The fields of serializable subclasses will
17  * be restored from the stream. 
18  */

 

閱讀文檔我們得知,為了使得不可序列化類的子類能夠序列化,其子類必須擔負起保存和恢復其超類的public、protected 和 package(if accessible)實例域的責任,且要求其父類必須有一個可訪問的無參構造函數以使得在反序列化時能夠初始化實例域。

 

我們寫代碼驗證一下,如果父類中沒有可訪問的無參構造函數會發生什麼,注意Person類中沒有無參構造函數。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Student one = new Student(12, 156.6, "1234");
10         Student two = new Student(16, 177.7, "5678");
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Student.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Student.txt"))) {
26             Student one = (Student) input.readObject();
27             Student two = (Student) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person{
40     protected int age;
41     protected double height;
42     
43     public Person(int age, double height) {
44         this.age = age;
45         this.height = height;
46     }
47 
48     @Override
49     public String toString() {
50         return "Person{" +
51                 "age=" + age +
52                 ", height=" + height +
53                 '}';
54     }
55 }
56 
57 class Student extends Person implements Serializable{
58     private String id;
59 
60     public Student(int age, double height, String id) {
61         super(age, height);
62         this.id = id;
63     }
64 
65     @Override
66     public String toString() {
67         return "Student{" +
68                 "age=" + age +
69                 ", height=" + height +
70                 ", id='" + id + '\'' +
71                 '}';
72     }
73 }

 

程序輸出產生異常:

java.io.InvalidClassException: Student; no valid constructor
    at java.io.ObjectStreamClass$ExceptionInfo.newInvalidClassException(ObjectStreamClass.java:150)
    at java.io.ObjectStreamClass.checkDeserialize(ObjectStreamClass.java:768)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1775)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:371)
    at SerializableTest.testDeserialize(SerializableTest.java:26)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    ...

Process finished with exit code 0

 

當我們為Person類添加默認構造函數時:

 1 class Person{
 2     protected int age;
 3     protected double height;
 4 
 5     public Person() {
 6     }
 7 
 8     public Person(int age, double height) {
 9         this.age = age;
10         this.height = height;
11     }
12 
13     @Override
14     public String toString() {
15         return "Person{" +
16                 "age=" + age +
17                 ", height=" + height +
18                 '}';
19     }
20 }

 

程序輸出如下,我們可觀察到,父類中的字段都是默認值,只有子類中的字段得到了正確的序列化。出現這種情況的原因是子類並沒有擔負起序列化父類中字段的責任。

Student{age=0, height=0.0, id='1234'}
Student{age=0, height=0.0, id='5678'}

Process finished with exit code 0

 

為了解決上述問題,我們需要藉助上一節中學到的知識,使用自定義的序列化方法writeObject和readObject來主動將父類中的字段進行序列化。

 1 import org.junit.Test;
 2 
 3 import java.io.*;
 4 
 5 public class SerializableTest {
 6 
 7     @Test
 8     public void testSerialize() {
 9         Student one = new Student(12, 156.6, "1234");
10         Student two = new Student(16, 177.7, "5678");
11 
12         try (ObjectOutputStream output =
13                      new ObjectOutputStream(new FileOutputStream("Studnet.txt"))) {
14             output.writeObject(one);
15             output.writeObject(two);
16         } catch (IOException e) {
17             e.printStackTrace();
18         }
19     }
20 
21     @Test
22     public void testDeserialize() {
23 
24         try (ObjectInputStream input =
25                      new ObjectInputStream(new FileInputStream("Studnet.txt"))) {
26             Student one = (Student) input.readObject();
27             Student two = (Student) input.readObject();
28 
29             System.out.println(one);
30             System.out.println(two);
31         } catch (IOException e) {
32             e.printStackTrace();
33         } catch (ClassNotFoundException e) {
34             e.printStackTrace();
35         }
36     }
37 }
38 
39 class Person{
40     protected int age;
41     protected double height;
42 
43     public Person() {
44     }
45 
46     public Person(int age, double height) {
47         this.age = age;
48         this.height = height;
49     }
50 
51     @Override
52     public String toString() {
53         return "Person{" +
54                 "age=" + age +
55                 ", height=" + height +
56                 '}';
57     }
58 }
59 
60 class Student extends Person implements Serializable{
61     private String id;
62 
63     public Student(int age, double height, String id) {
64         super(age, height);
65         this.id = id;
66     }
67 
68     private void writeObject(java.io.ObjectOutputStream out)
69             throws IOException {
70         out.defaultWriteObject();
71         out.writeInt(age);
72         out.writeDouble(height);
73     }
74     
75     private void readObject(java.io.ObjectInputStream in)
76             throws IOException, ClassNotFoundException {
77         in.defaultReadObject();
78         this.age = in.readInt();
79         this.height = in.readDouble();
80     }
81 
82     @Override
83     public String toString() {
84         return "Student{" +
85                 "age=" + age +
86                 ", height=" + height +
87                 ", id='" + id + '\'' +
88                 '}';
89     }
90 }

 

程序輸出如下,可以看到完全正確。

Student{age=12, height=156.6, id='1234'}
Student{age=16, height=177.7, id='5678'}

Process finished with exit code 0

 

五、serialVersionUID的作用及自動生成

 

我們知道,反序列化必須擁有class文件,但隨着項目的升級,class文件也會升級,序列化怎麼保證升級前後的兼容性呢?

 

java序列化提供了一個private static final long serialVersionUID 的序列化版本號,只有版本號相同,即使更改了序列化屬性,對象也可以正確被反序列化回來。如果反序列化使用的class的版本號與序列化時使用的不一致,反序列化會報InvalidClassException異常。下面是JDK 8中ArrayList的源碼中的serialVersionUID。

 

 1 public class ArrayList<E> extends AbstractList<E>
 2         implements List<E>, RandomAccess, Cloneable, java.io.Serializable
 3 {
 4     private static final long serialVersionUID = 8683452581122892189L;
 5 
 6     /**
 7      * Default initial capacity.
 8      */
 9     private static final int DEFAULT_CAPACITY = 10;
10     ...  
11 }

 

序列化版本號可自由指定,如果不指定,JVM會根據類信息自己計算一個版本號,這樣隨着class的升級,就無法正確反序列化;不指定版本號另一個明顯隱患是,不利於jvm間的移植,可能class文件沒有更改,但不同jvm可能計算的規則不一樣,這樣也會導致無法反序列化。

 

什麼情況下需要修改serialVersionUID呢?分三種情況。

  • 如果只是修改了方法,反序列化不容影響,則無需修改版本號
  • 如果只是修改了靜態Field或瞬態Field,則反序列化不受任何影響
  •  如果修改類時修改了非靜態Field、非瞬態Field,則可能導致序列化版本不兼容。如果對象流中的對象和新類中包含同名的Field,而Field類型不同,則反序列化失敗,類定義應該更新serialVersionUID Field值。如果只是新增了實例變量,則反序列化回來新增的是默認值;如果減少了實例變量,反序列化時會忽略掉減少的實例變量。

 

我們在日常編程實踐中,一般會選擇使用IDE來自動生成serialVersionUID,這樣可以最大化地減少重複的可能性。對於IntelliJ IDEA,自動生成serialVersionUID有三步:

  • 修改IDEA配置:File->Setting->Editor->Inspections->Serialization issues->Serializable class without ’serialVersionUID’
  • 類實現Serializable接口
  • 在類名上執行Alt+Enter,然後選擇生成serialVersionUID即可

 

六、序列化的缺點

 

Java序列化存在四個致命缺點,導致其不適用於網絡傳輸:

  • 無法跨語言:在網絡傳輸中,經常會有異構語言的進程的交互,但Java序列化技術是Java語言內部的私有協議,其他語言無法進行反序列化。目前所有流行的RPC框架都沒有使用Java序列化作為編解碼框架。
  • 潛在風險高:不可信流的反序列化可能導致遠程代碼執行(RCE)、拒絕服務(DoS)和一系列其他攻擊。
  • 序列化后的碼流太大
  • 序列化的性能較低

 

在真正的生產環境中,一般會選擇其它編解碼框架,領先的跨平台結構化數據表示是 JSON 和 Protocol Buffers,也稱為 protobuf。JSON 由 Douglas Crockford 設計用於瀏覽器與服務器通信,Protocol Buffers 由谷歌設計用於在其服務器之間存儲和交換結構化數據。JSON 和 protobuf 之間最顯著的區別是 JSON 是基於文本的,並且是人類可讀的,而 protobuf 是二進制的,但效率更高。

 

 七、參考文獻

 

  1. 《瘋狂Java講義》第2版,李剛著,电子工業出版社
  2. 《Java核心技術》第10版,霍斯特曼等著,机械工業出版本
  3. 《Netty權威指南》第2版,李林鋒著,电子工業出版社
  4. 《Effective Java》第2版,Joshua Bloch著,机械工業出版社

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價