7. SOFAJRaft源碼分析—如何實現一個輕量級的對象池?

前言

我在看SOFAJRaft的源碼的時候看到了使用了對象池的技術,看了一下感覺要吃透的話還是要新開一篇文章來講,內容也比較充實,大家也可以學到之後運用到實際的項目中去。

這裏我使用RecyclableByteBufferList來作為講解的例子:

RecyclableByteBufferList

public final class RecyclableByteBufferList extends ArrayList<ByteBuffer> implements Recyclable {

    private transient final Recyclers.Handle handle;

    private static final Recyclers<RecyclableByteBufferList> recyclers = new Recyclers<RecyclableByteBufferList>(512) {

        @Override
        protected RecyclableByteBufferList newObject(final Handle handle) {
            return new RecyclableByteBufferList(
                    handle);
        }
    };

      //獲取一個RecyclableByteBufferList實例
    public static RecyclableByteBufferList newInstance(final int minCapacity) {
        final RecyclableByteBufferList ret = recyclers.get();
        //容量不夠的話,進行擴容
        ret.ensureCapacity(minCapacity);
        return ret;
    }
      //回收RecyclableByteBufferList對象
    @Override
    public boolean recycle() {
        clear();
        this.capacity = 0;
        return recyclers.recycle(this, handle);
    }
}

我在上面將RecyclableByteBufferList獲取對象的方法和回收對象的方法給列舉出來了,獲取實例的時候會通過recyclers的get方法去獲取,回收對象的時候會去調用list的clear方法清空list裏面的內容之後再去調用recyclers的recycle方法進行回收。
如果recyclers裏面沒有對象可以獲取,那麼會調用newObject方法創建一個對象,然後將handle對象傳入構造器中進行實例化。

對象池Recyclers

數據結構

  1. 每一個 Recyclers 對象包含一個 ThreadLocal<Stack<T>> threadLocal實例;
    每一個線程包含一個 Stack 對象,該 Stack 對象包含一個 DefaultHandle[],而 DefaultHandle 中有一個屬性 T value,用於存儲真實對象。也就是說,每一個被回收的對象都會被包裝成一個 DefaultHandle 對象
  2. 每一個 Recyclers 對象包含一個ThreadLocal<Map<Stack<?>, WeakOrderQueue>> delayedRecycled實例;
    每一個線程對象包含一個 Map<Stack<?>, WeakOrderQueue>,存儲着為其他線程創建的 WeakOrderQueue 對象,WeakOrderQueue 對象中存儲一個以 Head 為首的 Link 數組,每個 Link 對象中存儲一個 DefaultHandle[] 數組,用於存放回收對象。

假設線程A創建的對象

  1. 線程A回收RecyclableByteBufferList時,直接將RecyclableByteBufferList的DefaultHandle 對象壓入 Stack 的 DefaultHandle[] 中;
  2. 線程B回收RecyclableByteBufferList時,會首先從其 Map<Stack<?>, WeakOrderQueue> 對象中獲取 key=線程A的Stack 對象的 WeakOrderQueue,然後直接將RecyclableByteBufferList的DefaultHandle 對象(內部包含RecyclableByteBufferList對象)壓入該 WeakOrderQueue 中的 Link 鏈表中的尾部 Link 的 DefaultHandle[]中,同時,這個 WeakOrderQueue 會與線程 A 的 Stack 中的 head 屬性進行關聯,用於後續對象的 pop 操作;
  3. 當線程 A 從對象池獲取對象時,如果線程 A 的 Stack 中有對象,則直接彈出;如果沒有對象,則先從其 head 屬性所指向的 WeakorderQueue 開始遍歷 queue 鏈表,將 RecyclableByteBufferList 對象從其他線程的 WeakOrderQueue 中轉移到線程 A 的 Stack 中(一次 pop 操作只轉移一個包含了元素的 Link),再彈出。

Recyclers靜態代碼塊

private static final int DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD = 4 * 1024; // Use 4k instances as default.
private static final int DEFAULT_MAX_CAPACITY_PER_THREAD;
private static final int INITIAL_CAPACITY;

static {
    // 每個線程的最大對象池容量
    int maxCapacityPerThread = SystemPropertyUtil.getInt("jraft.recyclers.maxCapacityPerThread", DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD);
    if (maxCapacityPerThread < 0) {
        maxCapacityPerThread = DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD;
    }

    DEFAULT_MAX_CAPACITY_PER_THREAD = maxCapacityPerThread;
    if (LOG.isDebugEnabled()) {
        if (DEFAULT_MAX_CAPACITY_PER_THREAD == 0) {
            LOG.debug("-Djraft.recyclers.maxCapacityPerThread: disabled");
        } else {
            LOG.debug("-Djraft.recyclers.maxCapacityPerThread: {}", DEFAULT_MAX_CAPACITY_PER_THREAD);
        }
    }
    // 設置初始化容量信息
    INITIAL_CAPACITY = Math.min(DEFAULT_MAX_CAPACITY_PER_THREAD, 256);
}

 public static final Handle NOOP_HANDLE = new Handle() {};

Recyclers會在靜態代碼塊中做一些對象池容量初始化的工作,初始化了最大對象池容量和初始化容量信息。

從對象池中獲取對象

Recyclers#get

// 線程變量,保存每個線程的對象池信息,通過 ThreadLocal 的使用,避免了不同線程之間的競爭情況
private final ThreadLocal<Stack<T>> threadLocal = new ThreadLocal<Stack<T>>() {

    @Override
    protected Stack<T> initialValue() {
        return new Stack<>(Recyclers.this, Thread.currentThread(), maxCapacityPerThread);
    }
};

public final T get() {
    if (maxCapacityPerThread == 0) {
        return newObject(NOOP_HANDLE);
    }
    //從threadLocal中獲取一個棧對象
    Stack<T> stack = threadLocal.get();
    //拿出棧頂元素
    DefaultHandle handle = stack.pop();
    //如果棧裏面沒有元素,那麼就實例化一個
    if (handle == null) {
        handle = stack.newHandle();
        handle.value = newObject(handle);
    }
    return (T) handle.value;
}

Get方法會從threadLocal中去獲取數據,如果獲取不到,那麼會初始化一個Stack,並傳入當前Recyclers實例,當前線程,與最大容量。然後從stack中pop拿出棧頂元素,如果獲取的元素為空,那麼直接調用newHandle新建一個DefaultHandle實例,並調用Recyclers實現類的newObject獲取實現類的實例。也就是說DefaultHandle是用來封裝真正的對象的實例。

從stack中申請一個對象

Stack(Recyclers<T> parent, Thread thread, int maxCapacity) {
    this.parent = parent;
    this.thread = thread;
    this.maxCapacity = maxCapacity;
    elements = new DefaultHandle[Math.min(INITIAL_CAPACITY, maxCapacity)];
}

DefaultHandle pop() {
    int size = this.size;
    if (size == 0) {
        if (!scavenge()) {
            return null;
        }
        size = this.size;
    }
    //size表示整個stack中的大小
    size--;
    //獲取最後一個元素
    DefaultHandle ret = elements[size];
    if (ret.lastRecycledId != ret.recycleId) {
        throw new IllegalStateException("recycled multiple times");
    }
    // 清空回收信息,以便判斷是否重複回收
    ret.recycleId = 0;
    ret.lastRecycledId = 0;
    this.size = size;
    return ret;
}

獲取對象的邏輯也比較簡單,當 Stack 中的 DefaultHandle[] 的 size 為 0 時,需要從其他線程的 WeakOrderQueue 中轉移數據到 Stack 中的 DefaultHandle[],即 scavenge方法,該方法下面再聊。當 Stack 中的 DefaultHandle[] 中最終有了數據時,直接獲取最後一個元素

對象池回收對象

我們再來看看RecyclableByteBufferList是怎麼回收對象的。
RecyclableByteBufferList#recycle

public boolean recycle() {
    clear();
    this.capacity = 0;
    return recyclers.recycle(this, handle);
}

RecyclableByteBufferList回收對象的時候首先會調用clear方法清空屬性,然後調用recyclers的recycle方法進行對象回收。

Recyclers#recycle

public final boolean recycle(T o, Handle handle) {
    if (handle == NOOP_HANDLE) {
        return false;
    }

    DefaultHandle h = (DefaultHandle) handle;
    //stack在實例化的時候會在構造器中傳入一個Recyclers作為parent
    //所以這裡是校驗一下,如果不是當前線程的, 直接不回收了
    if (h.stack.parent != this) {
        return false;
    }
    if (o != h.value) {
        throw new IllegalArgumentException("o does not belong to handle");
    }
    h.recycle();
    return true;
}

這裡會接着調用DefaultHandle的recycle方法進行回收

DefaultHandle

static final class DefaultHandle implements Handle {
    //在WeakOrderQueue的add方法中會設置成ID
    //在push方法中設置成為OWN_THREAD_ID
    //在pop方法中設置為0
    private int lastRecycledId;
    //只有在push方法中才會設置OWN_THREAD_ID
    //在pop方法中設置為0
    private int recycleId;
    //當前的DefaultHandle對象所屬的Stack
    private Stack<?> stack;
    private Object value;

    DefaultHandle(Stack<?> stack) {
        this.stack = stack;
    }

    public void recycle() {
        Thread thread = Thread.currentThread();
        //如果當前線程正好等於stack所對應的線程,那麼直接push進去
        if (thread == stack.thread) {
            stack.push(this);
            return;
        }
        // we don't want to have a ref to the queue as the value in our weak map
        // so we null it out; to ensure there are no races with restoring it later
        // we impose a memory ordering here (no-op on x86)
        // 如果不是當前線程,則需要延遲回收,獲取當前線程存儲的延遲回收WeakHashMap
        Map<Stack<?>, WeakOrderQueue> delayedRecycled = Recyclers.delayedRecycled.get();
        // 當前 handler 所在的 stack 是否已經在延遲回收的任務隊列中
        // 並且 WeakOrderQueue是一個多線程間可以共享的Queue
        WeakOrderQueue queue = delayedRecycled.get(stack);
        if (queue == null) {
            delayedRecycled.put(stack, queue = new WeakOrderQueue(stack, thread));
        }
        queue.add(this);
    }
}

DefaultHandle在實例化的時候會傳入一個stack實例,代表當前實例是屬於這個stack的。
所以在調用recycle方法的時候,會判斷一下,當前的線程是不是stack所屬的線程,如果是那麼直接push到stack裏面就好了,不是則調用延遲隊列delayedRecycled;
從delayedRecycled隊列中獲取Map<Stack<?>, WeakOrderQueue> delayedRecycled ,根據stack作為key來獲取WeakOrderQueue,然後將當前的DefaultHandle實例放入到WeakOrderQueue中。

同線程回收對象

Stack#push

void push(DefaultHandle item) {
    // (item.recycleId | item.lastRecycleId) != 0 等價於 item.recycleId!=0 && item.lastRecycleId!=0
    // 當item開始創建時item.recycleId==0 && item.lastRecycleId==0
    // 當item被recycle時,item.recycleId==x,item.lastRecycleId==y 進行賦值
    // 當item被pop之後, item.recycleId = item.lastRecycleId = 0
    // 所以當item.recycleId 和 item.lastRecycleId 任何一個不為0,則表示回收過
    if ((item.recycleId | item.lastRecycledId) != 0) {
        throw new IllegalStateException("recycled already");
    }
    // 設置對象的回收id為線程id信息,標記自己的被回收的線程信息
    item.recycleId = item.lastRecycledId = OWN_THREAD_ID;

    int size = this.size;
    if (size >= maxCapacity) {
        // Hit the maximum capacity - drop the possibly youngest object.
        return;
    }
    // stack中的elements擴容兩倍,複製元素,將新數組賦值給stack.elements
    if (size == elements.length) {
        elements = Arrays.copyOf(elements, Math.min(size << 1, maxCapacity));
    }

    elements[size] = item;
    this.size = size + 1;
}

同線程回收對象 DefaultHandle#recycle 步驟:

  1. stack 先檢測當前的線程是否是創建 stack 的線程,如果不是,則走異線程回收邏輯;如果是,則首先判斷是否重複回收,然後判斷 stack 的 DefaultHandle[] 中的元素個數是否已經超過最大容量(4k),如果是,直接返回;
  2. 判斷當前的 DefaultHandle[] 是否還有空位,如果沒有,以 maxCapacity 為最大邊界擴容 2 倍,之後拷貝舊數組的元素到新數組,然後將當前的 DefaultHandle 對象放置到 DefaultHandle[] 中
  3. 最後重置 stack.size 屬性

異線程回收對象

WeakOrderQueue

static final class Stack<T> {
    //使用volatile可以立即讀取到該queue
      private volatile WeakOrderQueue head;
}
WeakOrderQueue(Stack<?> stack, Thread thread) {
    head = tail = new Link();
    //使用的是WeakReference ,作用是在poll的時候,如果owner不存在了
    // 則需要將該線程所包含的WeakOrderQueue的元素釋放,然後從鏈表中刪除該Queue。
    owner = new WeakReference<>(thread);
    //假設線程B和線程C同時回收線程A的對象時,有可能會同時創建一個WeakOrderQueue,就坑同時設置head,所以這裏需要加鎖
    synchronized (stackLock(stack)) {
        next = stack.head;
        stack.head = this;
    }
}

創建WeakOrderQueue對象的時候會初始化一個WeakReference的owner,作用是在poll的時候,如果owner不存在了, 則需要將該線程所包含的WeakOrderQueue的元素釋放,然後從鏈表中刪除該Queue。

然後給stack加鎖,假設線程B和線程C同時回收線程A的對象時,有可能會同時創建一個WeakOrderQueue,就坑同時設置head,所以這裏需要加鎖。

以head==null的時候為例
加鎖:
線程B先執行,則head = 線程B的queue;之後線程C執行,此時將當前的head也就是線程B的queue作為線程C的queue的next,組成鏈表,之後設置head為線程C的queue
不加鎖:
線程B先執行 next = stack.head此時線程B的queue.next=null->線程C執行next = stack.head;線程C的queue.next=null-> 線程B執行stack.head = this;設置head為線程B的queue -> 線程C執行stack.head = this;設置head為線程C的queue,此時線程B和線程C的queue沒有連起來。

WeakOrderQueue#add

void add(DefaultHandle handle) {
    // 設置handler的最近一次回收的id信息,標記此時暫存的handler是被誰回收的
    handle.lastRecycledId = id;

    Link tail = this.tail;
    int writeIndex;
    // 判斷一個Link對象是否已經滿了:
    // 如果沒滿,直接添加;
    // 如果已經滿了,創建一個新的Link對象,之後重組Link鏈表,然後添加元素的末尾的Link(除了這個Link,前邊的Link全部已經滿了)
    if ((writeIndex = tail.get()) == LINK_CAPACITY) {
        this.tail = tail = tail.next = new Link();
        writeIndex = tail.get();
    }
    tail.elements[writeIndex] = handle;
    // 如果使用者在將DefaultHandle對象壓入隊列后,將Stack設置為null
    // 但是此處的DefaultHandle是持有stack的強引用的,則Stack對象無法回收;
    //而且由於此處DefaultHandle是持有stack的強引用,WeakHashMap中對應stack的WeakOrderQueue也無法被回收掉了,導致內存泄漏
    handle.stack = null;
    // we lazy set to ensure that setting stack to null appears before we unnull it in the owning thread;
    // this also means we guarantee visibility of an element in the queue if we see the index updated
    // tail本身繼承於AtomicInteger,所以此處直接對tail進行+1操作
    tail.lazySet(writeIndex + 1);
}

Stack異線程push對象流程

  1. 首先獲取當前線程的 Map<Stack<?>, WeakOrderQueue> 對象,如果沒有就創建一個空 map;
  2. 然後從 map 對象中獲取 key 為當前的 Stack 對象的 WeakOrderQueue;
  3. 如果獲取的WeakOrderQueue對象為null,那麼創建一個WeakOrderQueue對象,並將對象放入到map中,最後調用WeakOrderQueue#add添加對象

WeakOrderQueue 的創建流程:

  1. 創建一個Link對象,將head和tail的引用都設置為此對象
  2. 創建一個WeakReference指向owner對象,設置當前的 WeakOrderQueue 所屬的線程為當前線程。
  3. 先將原本的 stack.head 賦值給剛剛創建的 WeakOrderQueue 的 next 節點,之後將剛剛創建的 WeakOrderQueue 設置為 stack.head(這一步非常重要:假設線程 A 創建對象,此處是線程 C 回收對象,則線程 C 先獲取其 Map<Stack<?>, WeakOrderQueue> 對象中 key=線程A的stack對象的 WeakOrderQueue,然後將該 Queue 賦值給線程 A 的 stack.head,後續的 pop 操作打基礎),形成 WeakOrderQueue 的鏈表結構。

WeakOrderQueue#add添加對象流程

  1. 首先設置 item.lastRecycledId = 當前 WeakOrderQueue 的 id
  2. 然後看當前的 WeakOrderQueue 中的 Link 節點鏈表中的尾部 Link 節點的 DefaultHandle[] 中的元素個數是否已經達到 LINK_CAPACITY(16)
  3. 如果不是,則直接將當前的 DefaultHandle 元素插入尾部 Link 節點的 DefaultHandle[] 中,之後置空當前的 DefaultHandle 元素的 stack 屬性,最後記錄當前的 DefaultHandle[] 中的元素數量;
  4. 如果是,則新建一個 Link,並且放在當前的 Link 鏈表中的尾部節點處,與之前的 tail 節點連起來(鏈表),之後進行第三步的操作。

從異線程獲取對象

我再把pop方法搬下來一次:

DefaultHandle pop() {
    int size = this.size;
    // size=0 則說明本線程的Stack沒有可用的對象,先從其它線程中獲取。
    if (size == 0) {
        // 當 Stack<T> 此時的容量為 0 時,去 WeakOrder 中轉移部分對象到 Stack 中
        if (!scavenge()) {
            return null;
        }
        //由於在transfer(Stack<?> dst)的過程中,可能會將其他線程的WeakOrderQueue中的DefaultHandle對象傳遞到當前的Stack,
        //所以size發生了變化,需要重新賦值
        size = this.size;
    }
    //size表示整個stack中的大小
    size--;
    //獲取最後一個元素
    DefaultHandle ret = elements[size];
    if (ret.lastRecycledId != ret.recycleId) {
        throw new IllegalStateException("recycled multiple times");
    }
    // 清空回收信息,以便判斷是否重複回收
    ret.recycleId = 0;
    ret.lastRecycledId = 0;
    this.size = size;
    return ret;
}
  1. 首先獲取當前的 Stack 中的 DefaultHandle 對象中的元素個數。
  2. 如果為 0,則從其他線程的與當前的 Stack 對象關聯的 WeakOrderQueue 中獲取元素,並轉移到 Stack 的 DefaultHandle[] 中(每一次 pop 只轉移一個有元素的 Link),如果轉移不成功,說明沒有元素可用,直接返回 null;
  3. 如果轉移成功,則重置 size屬性 = 轉移后的 Stack 的 DefaultHandle[] 的 size,之後直接獲取 Stack 對象中 DefaultHandle[] 的最後一位元素,之後做防護性檢測,最後重置當前的 stack 對象的 size 屬性以及獲取到的 DefaultHandle 對象的 recycledId 和 lastRecycledId 回收標記,返回 DefaultHandle 對象。

scavenge轉移

Stack#scavenge

boolean scavenge() {
    // continue an existing scavenge, if any
    // 掃描判斷是否存在可轉移的 Handler
    if (scavengeSome()) {
        return true;
    }
    
    // reset our scavenge cursor
    prev = null;
    cursor = head;
    return false;
}

調用scavengeSome掃描判斷是否存在可轉移的 Handler,如果沒有,那麼就返回false,表示沒有可用對象

Stack#scavengeSome

boolean scavengeSome() {
    WeakOrderQueue cursor = this.cursor;
    if (cursor == null) {
        cursor = head;
        // 如果head==null,表示當前的Stack對象沒有WeakOrderQueue,直接返回
        if (cursor == null) {
            return false;
        }
    }

    boolean success = false;
    WeakOrderQueue prev = this.prev;
    do {
        // 從當前的WeakOrderQueue節點進行 handler 的轉移
        if (cursor.transfer(this)) {
            success = true;
            break;
        }
        // 遍歷下一個WeakOrderQueue
        WeakOrderQueue next = cursor.next;
        // 如果 WeakOrderQueue 的實際持有線程因GC回收了
        if (cursor.owner.get() == null) {
            // If the thread associated with the queue is gone, unlink it, after
            // performing a volatile read to confirm there is no data left to collect.
            // We never unlink the first queue, as we don't want to synchronize on updating the head.
            // 如果當前的WeakOrderQueue的線程已經不可達了
            //如果該WeakOrderQueue中有數據,則將其中的數據全部轉移到當前Stack中
            if (cursor.hasFinalData()) {
                for (;;) {
                    if (cursor.transfer(this)) {
                        success = true;
                    } else {
                        break;
                    }
                }
            }
            //將當前的WeakOrderQueue的前一個節點prev指向當前的WeakOrderQueue的下一個節點,
            // 即將當前的WeakOrderQueue從Queue鏈表中移除。方便後續GC
            if (prev != null) {
                prev.next = next;
            }
        } else {
            prev = cursor;
        }

        cursor = next;

    } while (cursor != null && !success);

    this.prev = prev;
    this.cursor = cursor;
    return success;
}
  1. 首先設置當前操作的 WeakOrderQueue cursor,如果為 null,則賦值為 stack.head 節點,如果 stack.head 為 null,則表明外部線程沒有回收過當前線程創建的 對象,外部線程在回收對象的時候會創建一個WeakOrderQueue,並將stack.head 指向新創建的WeakOrderQueue對象,則直接返回 false;如果不為 null,則繼續向下執行;
  2. 首先對當前的 cursor 進行元素的轉移,如果轉移成功,則跳出循環,設置 prev 和 cursor 屬性;
  3. 如果轉移不成功,獲取下一個線程 Y 中的與當前線程的 Stack 對象關聯的 WeakOrderQueue,如果該 queue 所屬的線程 Y 還可達,則直接設置 cursor 為該 queue,進行下一輪循環;如果該 queue 所屬的線程 Y 不可達了,則判斷其內是否還有元素,如果有,全部轉移到當前線程的 Stack 中,之後將線程 Y 的 queue 從查詢 queue 鏈表中移除。

transfer轉移

    boolean transfer(Stack<?> dst) {
        //尋找第一個Link
        Link head = this.head;
        // head == null,沒有存儲數據的節點,直接返回
        if (head == null) {
            return false;
        }
        // 讀指針的位置已經到達了每個 Node 的存儲容量,如果還有下一個節點,進行節點轉移
        if (head.readIndex == LINK_CAPACITY) {
            //判斷當前的Link節點的下一個節點是否為null,如果為null,說明已經達到了Link鏈表尾部,直接返回,
            if (head.next == null) {
                return false;
            }
            // 否則,將當前的Link節點的下一個Link節點賦值給head和this.head.link,進而對下一個Link節點進行操作
            this.head = head = head.next;
        }
        // 獲取Link節點的readIndex,即當前的Link節點的第一個有效元素的位置
        final int srcStart = head.readIndex;
        // 獲取Link節點的writeIndex,即當前的Link節點的最後一個有效元素的位置
        int srcEnd = head.get();
        // 本次可轉移的對象數量(寫指針減去讀指針)
        final int srcSize = srcEnd - srcStart;
        if (srcSize == 0) {
            return false;
        }
        // 獲取轉移元素的目的地Stack中當前的元素個數
        final int dstSize = dst.size;
        // 計算期盼的容量
        final int expectedCapacity = dstSize + srcSize;
        // 期望的容量大小與實際 Stack 所能承載的容量大小進行比對,取最小值
        if (expectedCapacity > dst.elements.length) {
            final int actualCapacity = dst.increaseCapacity(expectedCapacity);
            srcEnd = Math.min(srcStart + actualCapacity - dstSize, srcEnd);
        }

        if (srcStart != srcEnd) {
            // 獲取Link節點的DefaultHandle[]
            final DefaultHandle[] srcElems = head.elements;
            // 獲取目的地Stack的DefaultHandle[]
            final DefaultHandle[] dstElems = dst.elements;
            // dst數組的大小,會隨着元素的遷入而增加,如果最後發現沒有增加,那麼表示沒有遷移成功任何一個元素
            int newDstSize = dstSize;
            //// 進行對象轉移
            for (int i = srcStart; i < srcEnd; i++) {
                DefaultHandle element = srcElems[i];
                // 表明自己還沒有被任何一個 Stack 所回收
                if (element.recycleId == 0) {
                    element.recycleId = element.lastRecycledId;
                //  避免對象重複回收
                } else if (element.recycleId != element.lastRecycledId) {
                    throw new IllegalStateException("recycled already");
                }
                // 將可轉移成功的DefaultHandle元素的stack屬性設置為目的地Stack
                element.stack = dst;
                // 將DefaultHandle元素轉移到目的地Stack的DefaultHandle[newDstSize ++]中
                dstElems[newDstSize++] = element;
                // 設置為null,清楚暫存的handler信息,同時幫助 GC
                srcElems[i] = null;
            }
            // 將新的newDstSize賦值給目的地Stack的size
            dst.size = newDstSize;

            if (srcEnd == LINK_CAPACITY && head.next != null) {
                // 將Head指向下一個Link,也就是將當前的Link給回收掉了
                // 假設之前為Head -> Link1 -> Link2,回收之後為Head -> Link2
                this.head = head.next;
            }
            // 設置讀指針位置
            head.readIndex = srcEnd;
            return true;
        } else {
            // The destination stack is full already.
            return false;
        }
    }
}
  1. 尋找 cursor 節點中的第一個 Link如果為 null,則表示沒有數據,直接返回;
  2. 如果第一個 Link 節點的 readIndex 索引已經到達該 Link 對象的 DefaultHandle[] 的尾部,則判斷當前的 Link 節點的下一個節點是否為 null,如果為 null,說明已經達到了 Link 鏈表尾部,直接返回,否則,將當前的 Link 節點的下一個 Link 節點賦值給 head ,進而對下一個 Link 節點進行操作;
  3. 獲取 Link 節點的 readIndex,即當前的 Link 節點的第一個有效元素的位置
  4. 獲取 Link 節點的 writeIndex,即當前的 Link 節點的最後一個有效元素的位置
  5. 計算 Link 節點中可以被轉移的元素個數,如果為 0,表示沒有可轉移的元素,直接返回
  6. 獲取轉移元素的目標 Stack 中當前的元素個數(dstSize)並計算期盼的容量 expectedCapacity,如果 expectedCapacity 大於目標Stack 的長度(dst.elements.length),則先對目的地 Stack 進行擴容,計算 Link 中最終的可轉移的最後一個元素的下標;
  7. 如果發現目的地 Stack 已經滿了( srcStart != srcEnd為false),則直接返回 false
  8. 獲取 Link 節點的 DefaultHandle[] (srcElems)和目標 Stack 的 DefaultHandle[](dstElems)
  9. 根據可轉移的起始位置和結束位置對 Link 節點的 DefaultHandle[] 進行循環操作
  10. 將可轉移成功的 DefaultHandle 元素的stack屬性設置為目標 Stack(element.stack = dst),將 DefaultHandle 元素轉移到目的地 Stack 的 DefaultHandle[newDstSize++] 中,最後置空 Link 節點的 DefaultHandle[i]
  11. 如果當前被遍歷的 Link 節點的 DefaultHandle[] 已經被掏空了(srcEnd == LINK_CAPACITY),並且該 Link 節點還有下一個 Link 節點
  12. 重置當前 Link 的 readIndex

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

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

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

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

程序員一般通過什麼途徑接私活?

二哥,你好,我想知道一般程序猿都如何接私活,我也想接,能告訴我一些方法嗎?

上面是一個讀者“煩不煩”問我的一個問題。其實不止是“煩不煩”,還有很多讀者問過我類似這樣的問題。

我接的私活不算多,掙到的錢也沒有多少,加起來不到 20W。說實話,這個數目說出來我是有點心虛的,畢竟太少了,大家輕噴。但我想,恰好配得上“一般程序員”這個稱號啊。畢竟蒼蠅再小也是肉,我也算是有經驗的人了。

唾棄接私活、做外包的程序員有很多很多,曾經高傲的我也嫌棄過。但沒辦法,為了掙點零花錢,我垂下了高昂的頭。記得有位朋友曾說過,當年沈從文為了生計,寫了很多稱不上他自己喜歡的文字給報刊。

聽朋友這麼一說,我也不再覺得“接私活”是多麼一件值得羞愧的事情了。人首先要活着,才有體力講情懷啊。好了,言歸正傳,我來替“煩不煩”同學介紹幾個容易上手的操作。

01、朋友介紹

大體上,天底下做生意都只有一條捷徑:從熟人下手。

“哥們,聽說你有個朋友是做程序員的,我這有台電腦不知道為啥黑屏了,能問問他知道什麼原因嗎?要是能修好,保准請你吃頓大餐。”

“老弟啊,我有一個朋友說最近流行炒鞋,我想你不是程序員嘛,找你最合適了,要不我把他推薦給你,談成的話給我發個紅包就行了。”

我的第一個私活,就是之前在蘇州的一個同事介紹的。不過最後黃了。我搞了兩周時間(技術框架用的 JEPF),同事說甲方換方案了,沒把我氣壞。

同事礙於情面,說有機會請我吃頓飯。這一等就是 3 年,3 年過去了,飯還是沒有吃到。主要是因為我這位朋友在蘇州,我在洛陽,吃飯是沒辦法遠程完成啊。

第二個私活,是之前在蘇州的一個領導介紹的。由同事升級為領導,多少靠譜了點。這次做的是蘇州相城區的一個电子商務網站。前後做了三個多月,最後拿到手的錢也就不到一萬塊錢。

現在感覺自己當時是在出售廉價勞動力,何止是廉價,簡直是公益事業。不過,第一次接私活,拿到錢買了個華為的 MateBook,真香。

第二個私活做完后,領導可能覺得虧待了我,良心難安,就介紹了第三個私活給我。這次蠻輕鬆的,一個月搞定,還不累,兩萬塊到手。

既然是私活,當然都是利用業餘時間做的。這個投入的成本和實際得到的回報是一定要考慮的

我第一個私活打了水漂,辛苦了兩周,零回報。不過,這也是接私活常有的事,需要用平常心來對待。

第二個私活說實話非常辛苦,有幾次熬到半夜兩三點,當時覺得太不划算了。但當初自己接了,就只能忍着拼到底。畢竟咱是敬業愛崗的好同志。

第三個私活就相對輕鬆多了,單位時間內的收益非常高,算下來一個小時有 500 的工時費吧,就彷彿是對前兩個的補償。

總結一下,朋友介紹的項目相對來說還是比較靠譜的,前提條件是要有一定的“人情世故”原始成本積累。如果我當時在蘇州表現得不夠優異,和同事、領導的關係相處的不夠融洽,那自然他們也不會時隔多年後再找到我。

記住一點,做事的同時要好好的做人。當你既有能力,又值得信任的時候,私活就會找上門來

02、個人品牌

既然是朋友,自然就不會有很多。也就意味着,單純依賴朋友介紹的私活來源是有限度的。那如果想接更多的私活,該怎麼辦呢?

這就需要個人品牌了。

我平常不是喜歡寫作嘛,分享了很多技術文章在各大平台上,瀏覽量還算不錯。博客園上的排名和瀏覽量都能拿得出手。

博客地址:

當你做了一件事,並且一直在堅持,況且還做出了一定的成績,自然就會有生意主動找上門來——花香蜂自來嘛

寫博客的好處有很多,比如說吸引一批忠實的讀者,他們追隨你的文字,喜歡你的風格;再比如說勾引一些出版社,他們欣賞你的文字,願意合作互利共贏。

最後,還會有一些做私活的甲方。以前,我總覺得這是不可能發生的事情,他們是怎麼找到我的?很不可思議,但互聯網就是這麼神奇,你覺得不可能,它卻悄悄地發生着。

第一個通過這個途徑找到我的甲方,姓康。康哥找到我后,一上來就對我一頓吹捧(甭管是真是假)。信任建立起來后,他就說自己在醞釀一個很牛逼的項目,看我有沒有意向一起做。

然後呢,承諾項目成功后,再給我一定數額的獎勵金,並且寫到了合同里。吃完他這個大餅,我很飽,忍不住打了好幾個嗝。

再然後,我們就開始整理需求,然後我出報價,他再砍價;他再提需求,我再加價。最後呢,項目總款談到 7.5 萬,兩個多月的工期。合同的細節也敲定的差不多了。

結果,黃了。和我合夥的一個開發人員小何覺得甲方新提的需求需要再追加 600 塊,甲方覺得這點錢擱不住再追加了。總之呢,7.5W 的項目就因為這個細節黃了,很遺憾。

第二個通過這個途徑找到我的甲方,叫鵬哥。開發一個網站,總價一萬多,吃了上次的虧后,我自己就不想參与了,就找了一個讀者(小李)做。

結果這個項目爛尾了。小李交付的產物我自己都覺得不好意思,bug 非常多。在我看來,既然項目的訂金已經收了,作為開發人員,至少應該交付一個說得過去的產物——負責任吧。

很遺憾,個人品牌招攬來的前兩個私活最後都搞砸了。這裡有必要總結一下:作為程序員,既然打定主意要接私活,那麼接到的時候一定要珍惜。如果一開始覺得價錢低,就趁早拒絕,免得因為需求變動等等原因砸了招牌

當然了,通過這個途徑也做成了四單,每單的價格差不多兩萬。這裏就不再詳談了。

個人品牌的確可以引流來更多的私活,但與此同時,也會浪費很多時間。

像這種泛泛之談的意向客戶有很多。話說,我啥時候變成“社會王”了,我特么是正兒八經的“王老師”好不好?

03、外包平台

外包平台有很多,我就不再一一列舉了。只說幾個我認為還不錯的平台,也不打算細說,免得有些讀者“誇我”良苦用心地在打廣告。

04、一點忠告

在我寫這篇文章的時候,突然收到朋友的一條信息,說她們公司剛剛辭退了一位員工,還通報批評了,就因為接私活被舉報了——她們公司一般不辭退員工,這下子相當於鐵飯碗丟了。

所以說呢,接私活是有風險的。並且在我看來,如果主業沒有遇到瓶頸,強烈不建議接私活。就好比一個小孩子走路還不會,就要求他要跑起來。

時間對於一個程序員來說很寶貴,尤其是一個正在成長中的程序員。

如果你確實急用錢,價格又合適,那就去做。如果不怎麼缺錢,我再強調一次,別去接私活。私活的錢不好掙是一個方面,更重要的是如果你把做私活的時間花在提升自己上,產生的價值就要大得多。等你提升了自己,提升了固定薪水,遠比拿的這點私活的錢划算。千萬不要“撿了芝麻丟了西瓜”。

如果你像我,主業上遇到了瓶頸,平時的時間比較充分,想有一些額外的收入,同時為了保持技術的熟練度,這種情況下,是可以考慮接一些私活的。對於那種投入時間巨大,回報很可憐的項目,千萬不要接!

另外呢,如果甲方只提供幾個簡單的想法,甚至幾張圖片,更或者發一個參照的效果網站,就可以直接忽視了,這類通通不靠譜!

最後呢,還要說一句,如果訂金都收了,自己就算是覺得吃了虧,也應該有點職業素質,把像樣的產品交付,千萬別應付。

謝謝大家的閱讀,原創不易,喜歡就隨手點個贊,這將是我最強的寫作動力。

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

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

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

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

SpringSecurity退出功能實現的正確方式

本文將介紹在Spring Security框架下如何實現用戶的”退出”logout的功能。其實這是一個非常簡單的功能,我見過很多的程序員在使用了Spring Security之後,仍然去自己寫controller方法實現logout功能,這種做法就好像耕地,你有机械設備你不用,你非要用牛。

一、logout最簡及最佳實踐

其實使用Spring Security進行logout非常簡單,只需要在spring Security配置類配置項上加上這樣一行代碼:http.logout()。關於spring Security配置類的其他很多實現、如:HttpBasic模式、formLogin模式、自定義登錄驗證結果、使用權限表達式、session會話管理,在本號的之前的文章已經都寫過了。本節的核心內容就是在原有配置的基礎上,加上這樣一行代碼:http.logout()。

@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.logout();
   }

}

加上logout配置之後,在你的“退出”按鈕上使用/logtou作為請求登出的路徑。

<a href="/logout" >退出</a>

logout功能我們就完成了。實際上的核心代碼只有兩行。

二、默認的logout做了什麼?

雖然我們簡簡單單的實現了logout功能,是不是還不足夠放心?我們下面就來看一下Spring Security默認在logout過程中幫我們做了哪些動作。

  • 當前session失效,即:logout的核心需求,session失效就是訪問權限的回收。
  • 刪除當前用戶的 remember-me“記住我”功能信息
  • clear清除當前的 SecurityContext
  • 重定向到登錄頁面,loginPage配置項指定的頁面

通常對於一個應用來講,以上動作就是logout功能所需要具備的功能了。

三、個性化配置

雖然Spring Security默認使用了/logout作為退出處理請求路徑,登錄頁面作為退出之後的跳轉頁面。這符合絕大多數的應用的開發邏輯,但有的時候我們需要一些個性化設置,如下:

 http.logout()
     .logoutUrl("/signout")
     .logoutSuccessUrl("/aftersignout.html")
     .deleteCookies("JSESSIONID")
  • 通過指定logoutUrl配置改變退出請求的默認路徑,當然html退出按鈕的請求url也要修改
  • 通過指定logoutSuccessUrl配置,來顯式指定退出之後的跳轉頁面
  • 還可以使用deleteCookies刪除指定的cookie,參數為cookie的名稱

四、LogoutSuccessHandler

如果上面的個性化配置,仍然滿足不了您的應用需求。可能您的應用需要在logout的時候,做一些特殊動作,比如登錄時長計算,清理業務相關的數據等等。你可以通過實現LogoutSuccessHandler 接口來實現你的業務邏輯。

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    
    @Override
    public void onLogoutSuccess(HttpServletRequest request, 
                                HttpServletResponse response, 
                                Authentication authentication) 
                                throws IOException, ServletException {
        //這裏書寫你自己的退出業務邏輯
        
        // 重定向到登錄頁
        response.sendRedirect("/login.html");
    }
}

然後進行配置使其生效,核心代碼就是一行logoutSuccessHandler。注意logoutSuccessUrl不要與logoutSuccessHandler一起使用,否則logoutSuccessHandler將失效。

@Configuration
@EnableWebSecurity
public class SecSecurityConfig extends WebSecurityConfigurerAdapter {
    
@Autowired
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
         http.logout()
             .logoutUrl("/signout")
             //.logoutSuccessUrl(``"/aftersignout.html"``)
             .deleteCookies("JSESSIONID")
              //自定義logoutSuccessHandler
             .logoutSuccessHandler(myLogoutSuccessHandler);   
   }
}

期待您的關注

  • 博主最近新寫了一本書:
  • 本文轉載註明出處(必須帶連接,不能只轉文字):。

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

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

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

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

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

就該這樣理解 OSI 七層參考模型、淺談不同局域網之間的通信

簡介

說到OSI參考模型,理解網絡與網絡之間的關係,不說太深入難以理解的東西,只求能最大程度上理解與使用。

參考模型是國際標準化組織(ISO)制定的一個用於計算機或通信系統間互聯的標準體系,一般稱為OSI參考模型或七層模型。

概念性的東西,先知道這些就夠了,我們先來聊一聊一個常見的一個模型。

 

局域網與互聯網

互聯網就是許許多多個局域網組成的,從我們最簡單的一個局域網入手,開始理解

這裏舉例兩個不同的局域網,計算機用網線接入交換機、交換機連接網關路由器,另一處

也是通過相同的方式進行連接。先來了解一下OSI參考模型是如何定義這七層的

OSI 參考模型

參考:

這裏定義的七層只是為了方便我們去理解,實際上是不存在的。

簡單的了解一下這七層是如何定義的,具體的功能還是得舉例子來理解說明。

從最上層的應用層開始說起:如何一步步的封裝數據,到最後進行發送。

應用層

應用層是直接面向用戶的最高層,但它卻不是應用程序,它只是為引用程序提供服務的

就好比,我們用的電腦版微信吧!它就是一個實實在在的應用程序,假設要與一個遠方的小姐姐進行聊天會話,這個時候呢,發送一個Hello給遠方的小姐姐。

當你點擊發送的時候,其實做了很多事情,我們就來梳理一下。

需要發送的數據就是:Hello ,當然,應用層首先給這個數據拼接一個AH,這裏就是應用層的報頭,就好比是微信的一個特有的數據,就這樣先理解。

 

表示層

當然,我們總不能發送明文吧,將發送的文本數據進行編碼,平常我們計算機使用的萬國碼UTF-8,肯定要進行一下加密吧

表示層更關心的是所傳送數據的語法和語義,主要包括數據格式變化、與解密、與解壓等

 

會話層

字面意思,就可以理解出這一層表示的意思,建立一個會話,就好比使用Http訪問web的時候,都會存在一個Session 作為標識

讓服務器來區別訪問的計算機。主要功能是負責維護兩個節點之間的傳輸聯接,確保點到點傳輸不中斷,以及管理數據交換等功能。

會話層在應用進程中建立、管理和終止會話。會話層還可以通過對話控制來決定使用何種通信方式,通信或通信。會話層通過自身協議對請求與應答進行協調

 

傳輸層 端到端

傳輸層作為OSI參考模型中,最重要的一層,這裏主要是以端口到端口來區分。這裏涉及到兩個特別重要的協議TCP 以及UDP

一太計算機上同時運行着QQ、微信、以及瀏覽器等。發送數據報,這個數據包到底是哪個程序發出去的呢?當然要從指定的一個端口發出去

計算機的端口範圍 0-65535

0-1023 就是1024個端口為系統佔用端口

了解到這些,就該先來說說UDP協議

UDP 協議

UDP協議定義了端口,同一個主機上的每個應用程序都需要指定唯一的端口號,並且規定網絡中傳輸的數據包必須加上端口信息,當數據包到達主機以後,就可以根據端口號找到對應的應用程序了。UDP協議比較簡單,實現容易,但它沒有確認機制,數據包一旦發出,無法知道對方是否收到,因此可靠性較差,為了解決這個問題,提高網絡可靠性,TCP協議就誕生了。

 

參考:

一個UDP報文包含首部與數據部分,UDP首部佔用8個字節,數據部分最長長度為65535B(字節) 即 64KB

UDP協議是無連接,不保證穩定傳輸的協議,但處理速度較快,通常的音頻、視頻在傳送時候使用UDP較多。

我們這裏的例子是微信,微信能保證數據百分百到達,所以我們採用TCP來具體說明數據的封裝

 

TCP 協議

TCP即傳輸控制協議,是一種面向連接的、可靠的、基於字節流的通信協議。簡單來說TCP就是有確認機制的UDP協議,每發出一個數據包都要求確認,如果有一個數據包丟失,就收不到確認,發送方就必須重發這個數據包。為了保證傳輸的可靠性,TCP協議在UDP基礎之上建立了三次對話的確認機制,即在正式收發數據前,必須和對方建立可靠的連接。TCP數據包和UDP一樣,都是由首部和數據兩部分組成,唯一不同的是,TCP數據包沒有長度限制,理論上可以無限長,但是為了保證網絡的效率,通常TCP數據包的長度不會超過IP數據包的長度,以確保單個TCP數據包不必再分割

 

參考:

 這裏只需要了解的是TCP的基本封裝過程,這裏只涉及到源端口以及目的端口,還未涉及到IP相關的內容。它和UDP協議一樣。就好像是一個改進版的

UDP協議,它能保證數據的可靠傳輸,這個特點記住即可。這裏模擬一下,我們數據的封裝過程。假設微信使用的端口是6666,目標端口就是遠方小姐姐微信

的端口,當然也是一樣的。這裏我為了理解只做簡寫

 

網絡層

從上面幾層來看,我們已經將微信的數據封裝成來一個TCP數據報,裡面包含來微信的端口 假設是6666,當然,就好比寫信一樣,我的信封

已經準備好勒,裏面要發送的內容我也已經準備好了,接下來就是地址了。肯定要指定這個報文我要發送到哪裡去。所以呢IP 網際協議,就誕生了。

IP 網際協議

網絡層引入了IP協議,制定了一套新地址,使得我們能夠區分兩台主機是否同屬一個網絡,這套地址就是網絡地址,也就是所謂的IP地址。IP協議將這個32位的地址分為兩部分,前面部分代表網絡地址,後面部分表示該主機在局域網中的地址。如果兩個IP地址在同一個子網內,則網絡地址一定相同。為了判斷IP地址中的網絡地址,IP協議還引入了子網掩碼,IP地址和子網掩碼通過按位與運算后就可以得到網絡地址

IP地址在這裏我們就比較好理解了。我們平時的生活中都會涉及到。一個IP指向的就是互聯網當中的一台機器或者就是一台路由器了。

我們來封裝數據。再把上面的圖拿下來,說明一下,我們要給E電腦的小姐姐發送消息。比如我是A電腦,小姐姐在另外一個網關下的E電腦

 

 

比較重要的兩個參數:

源地址:192.168.0.120

目標地址:192.168.1.135

進行封裝后的數據,這裏將源地址,告訴路由器(郵局) 發件人 就是源地址,以及收件人 也就是目標地址

 

ARP 協議

這裏暫時不細說這個ARP協議的內容。我們只需要知道 ARP協議是用來拿IP換MAC地址的,上面的IP協議也已經提過了,通過子網掩碼和IP地址的換算,可以得到

網絡號,網絡號就可以區別這兩個IP是否在同一個局域網內。 參考這個秒懂:

數據鏈路層

到這一層,就已經到網卡、網絡設備(交換機)的範疇了。數據鏈路層最重要的協議是以太網協議,數據鏈路層最重要的一點就是數據成幀。

以太網協議

接入以太網的設備必須包含一塊以太網網卡,也就是我們常用的網卡,一組電信號稱作是一個數據幀 、或者叫做一個數據包

網卡都包含一個全球唯一的MAC地址,發送端的和接收端的地址便是指網卡的地址,即Mac地址。 

每塊網卡出廠時都被燒錄上一個實際上唯一的Mac地址,長度為48位2進制,通常由12位16進制數表示,(前六位是廠商編碼,后六位是流水線號)

 進行數據鏈路層的封裝,將本機的MAC(源MAC地址) 和目標MAC地址封裝在頭部,在尾部加入DT報尾,這樣 一個數據幀算是封裝完成了!

等等。我們好像還不知道小姐姐那邊的目標MAC地址,這時候就需要用ARP協議了。我們知道ARP協議就是用來用IP來換MAC地址的。

 

ARP協議

上面已經簡單的了解過了,我們要和在B局域網下的E電腦進行通信,但是我們不知道它的MAC地址,於是我們發送一個ARP請求,來獲取目標的MAC地址

目標MAC 為FF:FF:FF:FF:FF:FF 表示的是廣播地址,這個數據包發出去后,所有的子網機器都會收到,收到的機器判斷目標MAC是否是自己,若不是,則直接丟棄

若是,收到報文的主機會通過單播的形式,將MAC地址回傳給我們。

通過路由協議我們可以得知,若不在一個一個子網內,則會交給路由器

 路由器返回的包裏面,目標MAC就會變成路由器的MAC地址,我們拿路由器的MAC地址組裝數據鏈路層報文即可。

物理層 

經過以上的每一層的層層包裝,這時候,我們已經包裝好了一個以太網數據幀,包含源MAC,目標MAC,源IP,目標IP等等一系列數據。

物理層就是將這個數據通過電信號、光信號的方式傳遞過去的,物理層一般都是我么所說的光纜以及網線這些硬件設備。

 

 

 不同子網間的通信

通過上面的知識,我們已經了解到如何封裝成一個數據幀,以及一些協議的相關內容。那麼這裏就會有一個問題,同一子網、

不同子網、以及相隔很遠的兩個子網是如何進行通信的呢?以及我們撥號上網后,公網IP與內網IP是怎麼一回事呢?

 

同一子網通信

我們先來看一個圖,計算機A要與計算機B進行通信,這時候他們是同處於一個子網內的,這個時候就很簡單了。

按照上面的七層進行封裝數據,這裏的具體參數需要說明一下:

源IP: 0.120(簡寫)

目標IP:0.113

源MAC : A電腦的MAC

目標MAC:B電腦MAC(這裏若不知道就先發送ARP請求)

 

 

A將數據報發送出去后,交換機直接查詢目標MAC所轉發的端口,將這個數據報準確的推送到B電腦連接的那個端口即可。

不同子網通信

A電腦需要與E電腦進行通信,這時候發現A與E不在一個子網內,這時候呢,就需要路由器來協助了

源IP: A的IP

目標IP: E的IP

源MAC:A的 MAC

目標MAC: 路由器C的MAC

 

 

因為不在一個子網內,需要路由器來進行路由這個數據包,送至D路由器后,D路由器拿出數據報中目標的IP,發送ARP請求,

請求E的MAC地址,知道后,將數據報裏面的目標MAC進行替換,然後發送給E即可。

 

公網IP與內網IP通信的方式理解

我們在使用路由器上網后,運營商就會給我們分配一個公網IP,按照圖上的指示,C路由器在進行撥號后,就會給C路由器分配一個公網IP

我這裏假設有這樣兩個。這時候需要封裝數據,該如何封裝呢,還是以A電腦與E電腦進行通信,大家肯定會很迷惑。

這裏就需要了解一個協議:網路地址轉換協議

以下簡稱NAT,NAT 在IPV4 之前起到很大的作用,我們現在也在用,因為IPV4 IP數量的限制,但接入互聯網的電腦又那麼多

該怎麼辦呢。就是給一個路由下分配一個公網IP,路由器下面的IP與公網IP進行一個轉換,這裏面說的轉換就是:NAT

圖中黑色的就是轉換部分,通過端口的轉換,將多個子網IP映射到公網的一個IP上面

 

網絡地址端口轉換(NAPT)

這種方式支持端口的映射,並允許多台主機共享一個公網IP地址。 支持端口轉換的NAT又可以分為兩類:源地址轉換和目的地址轉換。前一種情形下發起連接的計算機的IP地址將會被重寫,使得內網主機發出的數據包能夠到達外網主機。后一種情況下被連接計算機的IP地址將被重寫,使得外網主機發出的數據包能夠到達內網主機。實際上,以上兩種方式通常會一起被使用以支持雙向通信。 還是舉例,這時候,我們的A電腦需要與E電腦進行通信,E電腦在廣東省,他們撥號后,都會分配一個公網IP,並且已經在路由器裏面完成了NAT映射,
源IP: A電腦IP
目標IP: E電腦映射后的公網IP
源MAC :A電腦MAC
目標MAC :  本地路由器MAC地址   封裝完成后,將數據報送到C路由器,路由器通過映射表,將源IP進行一個替換

 

替換后,交給互聯網上的路由器進行數據報的轉發,這就好像發快遞時候一樣,經過一系列的中轉站,到達目的路由。

到達D路由后,D路由將數據報中的目標地址也進行一個轉換,這個地址是可以相互轉的。現在就是公網映射轉到本機IP

 

轉換后就輕鬆了。按照ARP請求到E機器的MAC地址,然後發報即可。

 

小結

以上內容皆是自己查看一些博主的總結,通過學習后,能夠加深自己對OIS模型、以及TCP、IP、ARP

這些非常重要的協議的一個認識。以及了解到不同層級下面。兩台電腦如何完成一個通行。這裏講的比較淺,

互聯網的奧妙不是那麼容易就可以理解透的。還是那句,不要停止學習的腳步。就好

 

參考:

  • ARP請求 
  • 不同子網內兩台機器的通信方式 
  • OSI 參考模型 
  • 內網端口與外網端口的理解 
  • 網絡地址轉換協議:

 

 

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

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

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

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

中高級前端面試秘籍,助你直通大廠(一)

引言

又是一年寒冬季,只身前往沿海工作,也是我第一次感受到沿海城市冬天的寒冷。剛過完金九銀十,經過一場慘烈的江湖廝殺后,相信有很多小夥伴兒已經找到了自己心儀的工作,也有的正在找工作的途中。考慮到年後必定又是一場不可避免的廝殺,這裏提前記錄一下自己平時遇到和總結的一些知識點,自己鞏固複習加強基礎的同時也希望能在你的江湖路上對你有所幫助。筆者在入職最近這家公司之前也曾有過長達3個月的閉關修鍊期,期間查閱資料無數,閱讀過很多文章,但總結下來真正讓你印象深刻的,不是那些前沿充滿神秘感的新技術,也不是為了提升代碼逼格的奇淫巧技,而是那些我們經常由於項目周期緊而容易忽略的基礎知識。所謂萬丈高樓平地起,只有你的地基打得足夠牢固,你才有搭建萬丈高樓的底氣,你才能在你的前端人生路上越走越遠

這篇主要是先總結一下CSS相關的知識點,可能某些部分不會涉及到太多具體的細節,主要是對知識點做一下匯總,如果有興趣或者有疑惑的話可以自行百度查閱下相關資料或者在下方評論區留言討論,後續文章再繼續總結JS和其他方面相關的知識點,如有不對的地方還請指出。

1. CSS盒模型

CSS盒模型就是在網頁設計中經常用到的CSS技術所使用的一種思維模型。CSS 假定所有的HTML 文檔元素都生成了一個描述該元素在HTML文檔布局中所佔空間的矩形元素框,可以形象地將其看作是一個盒子。CSS 圍繞這些盒子產生了一種“盒子模型”概念,通過定義一系列與盒子相關的屬性,可以極大地豐富和促進各個盒子乃至整個HTML文檔的表現效果和布局結構。

CSS盒模型可以看成是由從內到外的四個部分構成,即內容區(content)、內邊距(padding)、邊框(border)和外邊距(margin)。內容區是盒子模型的中心,呈現盒子的主要信息內容;內邊距是內容區和邊框之間的空間;邊框是環繞內容區和內邊距的邊界;外邊距位於盒子的最外圍,是添加在邊框外周圍的空間。

根據計算寬高的區域我們可以將其分為IE盒模型W3C標準盒模型,可以通過box-sizing來進行設置:

  • content-box:W3C標準盒模型
  • border-box:IE盒模型

區別:
W3C標準盒模型:width(寬度) = content(內容寬度)
IE盒模型:width(寬度) = content(內容寬度) + padding(內邊距) + border(邊框)

2. BFC

BFC即Block Fromatting Context(塊級格式化上下文),它是頁面中的一塊獨立的渲染區域,並且有一套渲染規則,它決定了其子元素將如何定位,以及和其他元素的關係和相互作用。具有BFC特性的元素可以看成是一個隔離的獨立容器,讓處於BFC內部的元素與外部的元素相互隔離,使內外元素的定位不會相互影響。

IE瀏覽器下為hasLayout,一般可以通過zoom:(除normal外任意值)來觸發,hasLayout是IE瀏覽器渲染引擎的一個內部組成部分。在IE瀏覽器中,一個元素要麼自己對自身的內容進行計算大小和組織,要麼依賴於父元素來計算尺寸和和組織內容。為了調節這兩個不同的概念,渲染引擎採用了hasLayout的屬性,屬性值可以為true或false。當一個元素的hasLayout屬性為true時,我們就說這個元素有一個布局(Layout)。當擁有布局后,它會負責對自己和可能的子孫元素進行尺寸計算和定位,而不是依賴於祖先元素來完成這些工作。

2.1 觸發條件

  • 根元素(<html>)
  • 浮動元素(元素的float不是none)
  • 絕對定位元素(元素的positionabsolutefixed)
  • 行內塊元素(元素的displayinline-block)
  • 表格單元格(元素的displaytable-cell,HTML表格單元格默認為該值)
  • 表格標題(元素的displaytable-caption,HTML表格標題默認為該值)
  • display值為flow-root的元素
  • overflow屬性的值不為visible
  • 彈性元素(displayflexinline-flex元素的直接子元素)
  • 網格元素(displaygrid或者inline-grid元素的直接子元素)

    2.2 布局規則

    普通文檔流布局規則

  • 浮動的元素是不會被父級計算高度的
  • 非浮動元素會覆蓋浮動元素的位置
  • margin會傳遞給父級
  • 兩個相鄰元素上下margin會發生重疊

BFC布局規則

  • 浮動的元素會被父級計算高度(父級觸發了BFC)
  • 非浮動元素不會覆蓋浮動元素的位置(非浮動元素觸發了BFC)
  • margin不會傳遞給父級(父級觸發了BFC)
  • 兩個相鄰元素上下margin不會發生重疊(給其中一個元素增加一個父級,並讓它的父級觸發BFC)

    2.3 應用

  • 防止margin重疊
  • 清除內部浮動(原理是父級計算高度時,浮動的子元素也會參与計算)
  • 自適應兩欄布局
  • 防止元素被浮動元素所覆蓋

    3. 層疊上下文

    層疊上下文(stacking context),是HTML中一個三維的概念。在CSS2.1規範中,每個盒模型的位置都是三維的,分別是平面畫布上的X軸Y軸以及表示層疊的Z軸。一般情況下,元素在頁面上沿X軸Y軸平鋪,我們察覺不到它們在Z軸上的層疊關係。而一旦元素髮生堆疊,這時就能發現某個元素可能覆蓋了另一個元素或者被另一個元素覆蓋。

如果一個元素含有層疊上下文,我們就可以理解為這個元素在Z軸上就”高人一等”,最終表現就是它離屏幕觀察者更近。

你可以把層疊上下文理解為該元素當了官,而其他非層疊上下文元素則可以理解為普通群眾。凡是”當了官的元素”就比普通元素等級要高,也就是說元素在Z軸上更靠上,更靠近觀察者。

3.1 觸發條件

  • 根層疊上下文(<html>)
  • position屬性為非static值並設置z-index為具體數值
  • CSS3中的屬性也可以產生層疊上下文
    • flex
    • transform
    • opacity
    • filter
    • will-change
    • -webkit-overflow-scrolling

      3.2 層疊等級

      層疊等級(stacking level),又叫”層疊級別”或者”層疊水平”。

  • 在同一個層疊上下文中,它描述定義的是該層疊上下文中的層疊上下文元素在Z軸上的上下順序
  • 在其他普通元素中,它描述定義的是這些普通元素在Z軸上的上下順序

    注意:

    1. 普通元素的層疊等級優先由其所在的層疊上下文決定。
    2. 層疊等級的比較只有在當前層疊上下文元素中才有意義,不同層疊上下文中比較層疊等級是沒有意義的。

根據以上的層疊等級圖,我們在比較層疊等級時可以按照以下的思路來順序比較:

  • 首先判定兩個要比較的元素是否處於同一個層疊上下文中
  • 如果處於同一個層疊上下文中,則誰的層疊等級大,誰最靠上
  • 如果處於不同的層疊上下文中,則先比較他們所處的層疊上下文的層疊等級
  • 當兩個元素層疊等級相同,層疊順序相同時,在DOM結構中後面的元素層疊等級在前面元素之上

4. CSS3中新增的選擇器以及屬性

  • 屬性選擇器:
屬性選擇器 含義描述
E[attr^=”val”] 屬性attr的值以”val”開頭的元素
E[attr$=”val”] 屬性attr的值以”val”結尾的元素
E[attr*=”val”] 屬性attr的值包含“val”子字符串的元素
  • 結構偽類選擇器
選擇器 含義描述
E:root 匹配元素所在文檔的根元素,對於HTML文檔,根元素始終是<html>
E:nth-child(n) 匹配其父元素的第n個子元素,第一個編號為1
E:nth-last-child(n) 匹配其父元素的倒數第n個子元素,第一個編號為1
E:nth-of-type(n) 與:nth-child()作用類似,但是僅匹配使用同種標籤的元素
E:nth-last-of-type(n) 與:nth-last-child() 作用類似,但是僅匹配使用同種標籤的元素
E:last-child 匹配父元素的最後一個子元素,等同於:nth-last-child(1)
E:first-of-type 匹配父元素下使用同種標籤的第一個子元素,等同於:nth-of-type(1)
E:last-of-type 匹配父元素下使用同種標籤的最後一個子元素,等同於:nth-last-of-type(1)
E:only-child 匹配父元素下僅有的一個子元素,等同於:first-child:last-child或 :nth-child(1):nth-last-child(1)
E:only-of-type 匹配父元素下使用同種標籤的唯一一個子元素,等同於:first-of-type:last-of-type或 :nth-of-type(1):nth-last-of-type(1)
E:empty 匹配一個不包含任何子元素的元素,文本節點也被看作子元素
E:not(selector) 匹配不符合當前選擇器的任何元素
  • CSS3新增屬性
屬性 含義描述
transition 過渡效果
transform 變換效果(移動(translate)、縮放(scale)、旋轉(rotate)、傾斜(skew))
transform-origin 設置旋轉元素的基點位置
animation 動畫效果
border-color 為邊框設置多種顏色
border-radius 圓角邊框
box-shadow 邊框陰影
border-image 邊框圖片
background-size 規定背景圖片的尺寸
background-origin 規定背景圖片的定位區域
background-clip 規定背景圖片從什麼位置開始裁切
text-shadow 文本陰影
text-overflow 文本截斷
word-wrap 對長單詞進行拆分,並換行到下一行
opacity 不透明度
box-sizing 控制盒模型的組成模式
rgba 基於r,g,b三個顏色通道來設置顏色值,通過a來設置透明度

5. CSS3中transition和animation的屬性

1) transition(過渡動畫)

用法:transition: property duration timing-function delay
| 屬性 | 含義描述 |
| —- | —- |
| transition-property | 指定哪個CSS屬性需要應用到transition效果 |
| transition-duration | 指定transition效果的持續時間 |
| transition-timing-function | 指定transition效果的速度曲線 |
| transition-delay | 指定transition效果的延遲時間 |

2) animation(關鍵幀動畫)

用法:animation: name duration timing-function delay iteration-count direction fill-mode play-state
| 屬性 | 含義描述 |
| —- | —- |
| animation-name | 指定要綁定到選擇器的關鍵幀的名稱 |
| animation-duration | 指定動畫的持續時間 |
| animation-timing-function | 指定動畫的速度曲線 |
| animation-delay | 指定動畫的延遲時間 |
| animation-iteration-count | 指定動畫的播放次數 |
| animation-direction | 指定是否應該輪流反向播放動畫 |
| animation-fill-mode | 規定當動畫不播放時(當動畫完成時,或當動畫有一個延遲未開始播放時),要應用到元素的樣式 |
| animation-play-state | 指定動畫是否正在運行或已暫停 |

6. 清除浮動的方式以及各自的優缺點

  • 額外標籤法(在最後一個浮動元素的後面新加一個標籤如<div class="clear"></div>,並在其CSS樣式中設置clear: both;)

    優點:簡單,通俗易懂,寫少量代碼,兼容性好
    缺點:額外增加無語義html元素,代碼語義化差,後期維護成本大

  • 給父級設置高度

    優點:簡單,寫少量代碼,容易掌握
    缺點:不夠靈活,只適用於高度固定的布局

  • 觸發父級BFC(如給父元素設置overflow:hidden,特別注意的是:在IE6中還需要觸發hasLayout,例如給父元素設置zoom:1。原理是觸發父級BFC后,父元素在計算高度時,浮動的子元素也會參与計算)

    優點:簡單,代碼簡潔
    缺點:設置overflow:hidden容易造成不會自動換行導致超出的尺寸被隱藏掉,無法显示要溢出的元素

  • 使用after偽元素,常見的寫法如下:
 .clearfix::after {
    content: ".";
    display: block;
    height: 0;
    line-height: 0;
    clear: both;
    visibility:hidden;
    font-size: 0;
 }
 
 .clearfix {
    // 注意此處是為了兼容IE6和IE7瀏覽器,即觸發hasLayout
    zoom: 1;
 }

優點:符合閉合浮動思想,結構語義化正確
缺點:代碼量多,因為IE6-7下不支持after偽元素,需要額外寫zoom:1來觸發hasLayout

7. 居中布局的方式

水平居中

  • 若是行內元素,則直接給其父元素設置text-align: center即可
  • 若是塊級元素,則直接給該元素設置margin: 0 auto即可
  • 若子元素包含浮動元素,則給父元素設置width:fit-content並且配合margin
.parent {
    width: -webkit-fit-content;
    width: -moz-fit-content;
    width: fit-content;
    margin: 0 auto;
}
  • 使用flex布局的方式,可以輕鬆實現水平居中,即使子元素中存在浮動元素也同樣適用
// flex 2012年版本寫法
.parent {
    display: flex;
    flex-direction: row;
    justify-content: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-orient: horizontal;
    box-pack: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    left: 50%;
    transform: translate(-50%, 0);
}
  • 使用絕對定位的方式,再配合負值的margin-left(此方法需要固定寬度)
.child {
    position: absolute;
    left: 50%;
    width: 200px; // 假定寬度為200px
    margin-left: -100px; // 負值的絕對值為寬度的一半
}
  • 使用絕對定位的方式,再配合left:0;right:0;margin:0 auto;(此方法需要固定寬度)
.child {
    position: absolute;
    left: 0;
    right: 0;
    margin: 0 auto;
    width: 200px; // 假定寬度為200px
}

垂直居中

  • 若元素是單行文本,則直接給該元素設置line-height等於其父元素的高度
  • 若元素是行內塊級元素,可以配合使用display:inline-block;vertical-align:middle和一個偽元素來讓內容塊居中
.parent::after, .child {
    display: inline-block;
    vertical-align: middle;
}

.parent::after {
    content: "";
    height: 100%;
}
  • 使用vertical-align屬性並且配合使用display:tabledisplay:table-cell來讓內容塊居中
.parent {
    display: table;
}

.child {
    display: table-cell;
    vertical-align: middle;
}
  • 使用flex布局的方式,可以輕鬆實現垂直居中,即使子元素中存在浮動元素也同樣適用
// flex 2012年版本寫法
.parent {
    display: flex;
    align-items: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-orient: vertical;
    box-pack: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    top: 50%;
    transform: translate(0, -50%);
}
  • 使用絕對定位的方式,再配合負值的margin-top(此方法需要固定高度)
.child {
    position: absolute;
    top: 50%;
    height: 200px; // 假定高度為200px
    margin-top: -100px; // 負值的絕對值為高度的一半
}
  • 使用絕對定位的方式,再配合top:0;bottom:0;margin:auto 0;(此方法需要固定高度)
.child {
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    height: 200px; // 假定高度為200px
}

水平垂直居中

  • 使用flex布局的方式同樣可以輕鬆實現水平垂直居中
// flex 2012年版本寫法
.parent {
    display: flex;
    justify-content: center;
    align-items: center;
}

// flex 2009年版本寫法
.parent {
    display: box;
    box-pack: center;
    box-align: center;
}
  • 使用絕對定位的方式,再配合CSS3新增的transform屬性
.child {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
}
  • 使用絕對定位的方式,再配合使用負值的margin-top和負值的margin-left(此方法需要同時固定寬度和高度)
.child {
    position: absolute;
    left: 50%;
    top: 50%;
    margin-top: -50px; // 負值的絕對值為高度的一半
    margin-left: -100px; // 負值的絕對值為寬度的一半
    width: 200px; // 假定寬度為200px
    height: 100px; // 假定高度為100px
}

8. CSS的優先級和權重

選擇器(優先級從高到低) 示例 特殊性值
!important(重要性標識) div { color: #fff !important; } 無,但為了方便記憶,可將其表示為1,0,0,0,0
行內樣式 <div style="color: #fff;"></div> 1,0,0,0
id選擇器 #id 0,1,0,0
類,偽類和屬性選擇器 .content, :first-child, [type="text"] 0,0,1,0
標籤和偽元素選擇器 h1, ::after 0,0,0,1
通配符、子選擇器、相鄰選擇器 *, div > p, p + p 0,0,0,0
繼承 span { color: inherit; }
瀏覽器默認值 瀏覽器開發者工具右側的Styles面板中會显示user agent stylesheet字樣

9. 移動端1px物理像素邊框

我們知道,在移動端存在物理像素(physical pixel)設備獨立像素(density-independent pixel)的概念。物理像素也稱為設備像素,它是显示設備中一個最微小的物理部件,每個像素可以根據操作系統設置自己的顏色和亮度。設備獨立像素也稱為密度無關像素,可以認為是計算機坐標系統中的一個點,這個點代表一個可以由程序使用的虛擬像素(比如CSS像素),然後由相關係統轉換為物理像素。根據物理像素和設備獨立像素也衍生出了設備像素比(device pixel ratio)的概念,簡稱為dpr,其定義了物理像素和設備獨立像素的對應關係,其計算公式為設備像素比 = 物理像素 / 設備獨立像素。因為視網膜(Retina)屏幕的出現,使得一個物理像素並不能和一個設備獨立像素完全對等,如下圖所示:

在上圖中,在普通屏幕下1個CSS像素對應1個物理像素,而在Retina屏幕下,1個CSS像素卻對應4個物理像素,即在Retina屏幕下會有不同的dpr值。為了追求在移動端網頁中更好的显示質量,因此我們需要做各種各樣的適配處理,最經典的莫過於1px物理像素邊框問題,我們需要根據移動端不同的dpr值來對邊框進行處理。在JavaScript中,可以通過window.devicePixelRatio來獲取當前設備的dpr,在CSS中,可以通過-webkit-device-pixel-ratio,-webkit-min-device-pixel-ratio和-webkit-max-device-pixel-ratio來進行媒體查詢,從而針對不同的設備,來做一些樣式適配。這裏對於1px像素的邊框問題,給出一種最常見的寫法:

.border-1px {
    position: relative;
}

.border-1px::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: 0;
    width: 100%;
    height: 1px;
    background-color: #000;
    -webkit-transform: scaleY(.5);
    transform: scaleY(.5);
}

@media only screen and (-webkit-min-device-pixel-ratio: 2.0), (min-device-pixel-ratio: 2.0) {
    .border-1px::after {
        -webkit-transform: scaleY(.5);
        transform: scaleY(.5);
    }
}

@media only screen and (-webkit-min-device-pixel-ratio: 3.0), (min-device-pixel-ratio: 3.0) {
    .border-1px::after {
        -webkit-transform: scaleY(.33);
        transform: scaleY(.33);
    }
}

10. 實現三欄布局的方式有哪些

三欄布局,顧名思義就是分為左中右三個模塊進行布局,並且左右兩邊固定,中間模塊根據瀏覽器的窗口變化進行自適應,效果圖如下:

這裏給出四種實現三欄布局的方式:

  • 使用絕對定位的方式
.container {
    position: relative;
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.left {
    position: absolute;
    left: 0;
    top: 0;
    width: 150px;
    background: red;
}

.main {
    margin-left: 160px;
    margin-right: 110px;
    background: green;
}

.right {
    position: absolute;
    right: 0;
    top: 0;
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="left">左</div>
    <div class="main">中</div>
    <div class="right">右</div>
</div>

優點:方便快捷,簡單實用,不容易出現問題,而且還可以將<div class="main"></div>元素放到最前面,使得主要內容被優先加載。
缺點:元素脫離了文檔流,可能會造成元素的重疊。

  • 使用flex布局的方式
.container {
    display: flex;      
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.left {
    width: 150px;
    background: red;
}

.main {
    margin: 0 10px;
    flex: 1;
    background: green;
}

.right {
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="left">左</div>
    <div class="main">中</div>
    <div class="right">右</div>
</div>

優點:簡單實用,是現在比較流行的方案,特別是在移動端,大多數布局都採用的這種方式,是目前比較完美的一個。
缺點:需要考慮到瀏覽器的兼容性,根據不同的瀏覽器廠商需要添加相應的前綴。

  • 雙飛翼布局
.content {
    float: left;
    width: 100%;
}

.main,
.left,
.right {
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;
}

.main {
    margin-left: 160px;
    margin-right: 110px;
    background: green;
}

.left {
    float: left;
    margin-left: -100%;
    width: 150px;
    background: red;
}

.right {
    float: right;
    margin-left: -100px;
    width: 100px;
    background: blue;
}

<div class="content">
    <div class="main">中</div>
</div>
<div class="left">左</div>
<div class="right">右</div>

優點:比較經典的一種方式,通用性強,沒有兼容性問題,而且支持主要內容優先加載。
缺點:元素脫離了文檔流,要注意清除浮動,防止高度塌陷,同時額外增加了一層DOM結構,即增加了渲染樹生成的計算量。

  • 聖杯布局
.container {
    margin-left: 160px;
    margin-right: 110px;
}

.left,
.main,
.right {
    height: 200px;
    line-height: 200px;
    text-align: center;
    font-size: 20px;
    color: #fff;    
}

.main {
    float: left;
    width: 100%;
    background: green;      
}

.left {
    position: relative;
    left: -160px;
    margin-left:  -100%;
    float: left;
    width: 150px;
    background: red;
}

.right {
    position: relative;
    right: -110px;
    margin-left:  -100px;
    float: left;
    width: 100px;
    background: blue;
}

<div class="container">
    <div class="main">中</div>
    <div class="left">左</div>
    <div class="right">右</div>
</div>

優點:相比於雙飛翼布局,結構更加簡單,沒有多餘的DOM結構層,同樣支持主要內容優先加載。
缺點:元素同樣脫離了文檔流,要注意清除浮動,防止高度塌陷。

11. 實現等高布局的方式有哪些

等高布局,顧名思義就是在同一個父容器中,子元素高度相等的布局。從等高布局的實現方式來說,可以分為兩種,分別是偽等高真等高偽等高是指子元素的高度差依然存在,只是視覺上給人的感覺就是等高,真等高是指子元素的高度真實相等。效果圖如下:

這裏給出五種實現等高布局的方式:

偽等高

  • 使用padding-bottom和負的margin-bottom來實現
.container {
    position: relative;
    overflow: hidden;
}
    
.left,
.main,
.right {
    padding-bottom: 100%;
    margin-bottom: -100%;
    float: left;
    color: #fff;
}

.left {
    width: 20%;
    background: red;
}

.main {
    width: 60%;
    background: green;
}

.right {
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>

真等高

  • 使用flex布局的方式
.container {
    display: flex;
}

.left,
.main,
.right {
    flex: 1;
    color: #fff;
}

.left {
    background: red;
}

.main {
    background: green;
}

.right {
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用絕對定位的方式
.container {
  position: relative;
  height: 200px;
}

.left,
.main,
.right {
    position: absolute;
    top: 0;
    bottom: 0;
    color: #fff;
}

.left {
    left: 0;
    width: 20%;
    background: red;
}

.main {
    left: 20%;
    right: 20%;
    background: green;
}

.right {
    right: 0;
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用table布局的方式
.container {
    width: 100%;
    display: table;
}

.left,
.main,
.right {
    display: table-cell;
    color: #fff;
}

.left {
    width: 20%;
    background: red;
}

.main {
    width: 60%;
    background: green;
}

.right {
    width: 20%;
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>
  • 使用grid網格布局的方式
.container {
    display: grid;
    width: 100%;
    grid-template-columns: 1fr 1fr 1fr;
    color: #fff;
}

.left {
    background: red;
}

.main {
    background: green;
}

.right {
    background: blue;
}

<div class="container">
    <div class="left">左側內容</div>
    <div class="main">
        <p>中間內容</p>
        <p>中間內容</p>
        <p>中間內容</p>
    </div>
    <div class="right">右側內容</div>
</div>

12. CSS實現三角形的原理

工作中我們經常會遇到需要三角形圖標的應用場景,例如內容展開收起、左右箭頭點擊切換輪播,點擊某條列表數據查看詳情等。三角形圖標的應用範圍之廣,使得我們有必要了解一下它的實現原理。
1) 首先我們來實現一個最基礎的邊框效果

.content {
    width: 50px;
    height: 50px;
    border: 2px solid;
    border-color:#ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

2) 然後我們嘗試將border值放大10倍

.content {
    width: 50px;
    height: 50px;
    border: 20px solid;
    border-color: #ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

上圖中我們可以很清楚地看到,在繪製border的時候並不是矩形區域,而是梯形區域,那麼此時如果我們將widthheight值設置為0,看會發生什麼:

.content {
    width: 0;
    height: 0;
    border: 20px solid;
    border-color: #ff9600 #3366ff #12ad2a #f0eb7a;
}

效果如下:

此時會看到一個由四個三角形拼裝而成的矩形區域,即由上下左右四個邊框組合而成。因此不難想象,如果我們想得到某一個方向的三角形,我們只需要讓其他方向的邊框不可見就行了,例如我們想得到一個朝左的三角形:

.content {
    width: 0;
    height: 0;
    border: 20px solid;
    border-color: transparent #3366ff transparent transparent;
}

效果如下:

這樣就得到了一個很完美的三角形圖標,是不是很簡單?

13. link與@import的區別

  • 從屬關係區別

    @import是CSS提供的語法規則,只有導入樣式表的作用;link是HTML提供的標籤,不僅可以加載 CSS 文件,還可以定義 RSS,Rel連接屬性,設置瀏覽器資源提示符preload、prefetch等。

  • 加載順序區別

    HTML文檔在解析的過程當中,如果遇到link標籤,則會立即發起獲取CSS文件資源的請求;@import引入的CSS將在頁面加載完畢后才會被加載。

  • 兼容性區別

    @import是CSS2.1才有的語法,因此需要IE5以上才能識別;link標籤作為HTML元素,不存在兼容性問題。

  • DOM可控性區別

    link標籤可以通過JS來動態引入,而@import無法通過JS來插入樣式

const loadStyle = (url) => {
    const link = document.createElement('link');
    link.setAttribute('type', 'text/css');
    link.setAttribute('rel', 'stylesheet');
    link.setAttribute('href', url);
    
    document.head.appendChild(link);
}

14. 瀏覽器是怎樣解析CSS選擇器的

CSS選擇器的解析是從右向左解析的。若從左向右地匹配,發現不符合規則,需要進行回溯,會損失很多性能。若從右向左匹配,先找到所有的最右節點,對於每一個節點,向上尋找其父節點直到找到根元素或滿足條件的匹配規則,則結束這個分支的遍歷。兩種匹配規則的性能差別很大,是因為從右向左的匹配在第一步就篩選掉了大量的不符合條件的最右節點(恭弘=叶 恭弘子節點),而從左向右的匹配規則的性能都浪費在了失敗的查找上面。而在CSS解析完畢后,需要將解析的結果與DOM Tree的內容一起進行分析建立一棵 Render Tree,最終用來進行繪圖。在建立Render Tree時瀏覽器就要為每個DOM Tree中的元素根據CSS的解析結果(Style Rules)來確定生成怎樣的Render Tree。

15. CSS的性能優化方案

  • 層級盡量扁平,避免嵌套過多層級的選擇器;
  • 使用特定的選擇器,避免解析器過多層級的查找;
  • 減少使用通配符與屬性選擇器;
  • 減少不必要的多餘屬性;
  • 避免使用!important標識,可以選擇其他選擇器;
  • 實現動畫時優先使用CSS3的動畫屬性,動畫時脫離文檔流,開啟硬件加速;
  • 使用link標籤代替@import;
  • 將渲染首屏內容所需的關鍵CSS內聯到HTML中;
  • 使用資源預加載指令preload讓瀏覽器提前加載CSS資源並緩存;
  • 使用Gulp,Webpack等構建工具對CSS文件進行壓縮處理;

推薦閱讀

交流

終於接近尾聲了,居然花費掉了我一整個周末的時間,不過這篇主要是先總結一下CSS相關的知識點,當然還有很多地方沒有總結到,只是列出了個人覺得比較容易考察的點,如果你有其他補充的,歡迎在下方留言區討論哦,也歡迎關注我的公眾號[前端之境],關注后我可以拉你加入微信前端交流群,我們一起互相交流學習,共同進步。
後續會陸續總結出JS方面、瀏覽器視角、算法基礎和框架方面的內容,希望你能夠喜歡!

文章已同步更新至,若覺文章尚可,歡迎前往star!

你的一個點贊,值得讓我付出更多的努力!

逆境中成長,只有不斷地學習,才能成為更好的自己,與君共勉!

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

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

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

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

Rio手把手教學:如何打造容器化應用程序的一站式部署體驗

11月19日,業界應用最為廣泛的Kubernetes管理平台創建者Rancher Labs(以下簡稱Rancher)宣布Rio發布了beta版本,這是基於Kubernetes的應用程序部署引擎。它於今年5月份推出,現在最新的版本是v0.6.0。Rio結合了多種雲原生技術,從而簡化了將代碼從測試環境發布到生產環境的流程,同時保證了強大而安全的代碼體驗。

什麼是Rio?

下圖是Rio的架構:

Rio採用了諸如Kubernetes、knative、linkerd、cert-manager、buildkit以及gloo等技術,並將它們結合起來為用戶提供一個完整的應用程序部署環境。

Rio具有以下功能:

  1. 從源代碼構建代碼,並將其部署到Kubernetes集群

  2. 自動為應用程序創建DNS記錄,並使用Let’s Encrypt的TLS證書保護這些端點

  3. 基於QPS以及工作負載的指標自動擴縮容

  4. 支持金絲雀發布、藍綠髮布以及A/B部署

  5. 支持通過服務網格路由流量

  6. 支持縮容至零的serverless工作負載

  7. Git觸發的部署

Rancher的產品生態

Rio屬於Rancher整套產品生態的一部分,這些產品支持從操作系統到應用程序的應用程序部署和容器運維。當Rio和諸如Rancher 2.3、k3s和RKE等產品結合使用時,企業可以獲得完整的部署和管理應用程序及容器的體驗。

深入了解Rio

要了解Rio如何實現上述功能,我們來深入了解一些概念以及工作原理。

安裝Rio

前期準備

  • Kubernetes版本在1.15以上的Kubernetes集群

  • 為集群配置的kubeconfig(即上下文是你希望將Rio安裝到的集群)

  • 在$PATH中安裝的Rio CLI工具,可參閱以下鏈接,了解如何安裝CLI:
    https://github.com/rancher/rio/blob/master/README.md

安裝

使用安裝好的Rio CLI工具,調用rio install。你可能需要考慮以下情況:

ip-address:節點的IP地址的逗號分隔列表。你可以在以下情況使用:

  • 你不使用(或不能使用)layer-4的負載均衡器

  • 你的節點IP不是你希望流量到達的IP地址(例如,你使用有公共IP的EC2實例)

服 務

在Rio中,service是一個基本的執行單位。從Git倉庫或容器鏡像實例化之後,一個service由單個容器以及服務網格的關聯sidecar組成(默認啟用)。例如,運行使用Golang構建的一個簡單的“hello world”應用程序。

rio run https://github.com/ebauman/rio-demo

或者運行容器鏡像版本:

rio run ebauman/demo-rio:v1

還有其他選項也可以傳遞給rio run,如需要公開的任意端口(-p 80:8080/http),或者自動擴縮的配置(--scale 1-10)。你可以通過這一命令rio help run,查看所有可傳遞的選項。

想要查看你正在運行的服務,請執行rio ps

$ rio ps
NAME            IMAGE                               ENDPOINT
demo-service    default-demo-service-4dqdw:61825    https://demo-service...

每次你運行一個新的服務,Rio將會為這一服務生成一個全局性的端點:

$ rio endpoints
NAME           ENDPOINTS
demo-service   https://demo-service-default.op0kj0.on-rio.io:30282

請注意,此端點不包括版本——它指向由一個common name標識的服務,並且流量根據服務的權重進行路由。

自動DNS&TLS

默認情況下,所有Rio集群都將為自己創建一個on-rio.io主機名,並以隨機字符串開頭(如lkjsdf.on-rio.io)。該域名成為通配符域名,它的記錄解析到集群的網關。如果使用NodePort服務,則該網關可以是layer-4負載均衡器,或者是節點本身。

除了創建這個通配符域名,Rio還會使用Let’s Encrypt為這個域名生成一個通配符證書。這會允許自動加密任何HTTP工作負載,而無需用戶進行配置。要啟動此功能,請傳遞-p參數,將http指定為協議,例如:

rio run -p 80:8080/http ...

自動擴縮容

Rio可以根據每秒所查詢到的指標自動擴縮服務。為了啟用這一特性,傳遞--scale 1-10作為參數到rio run,例如:

rio run -p 80:8080/http -n demo-service --scale 1-10 ebauman/rio-demo:v1

執行這個命令將會構建ebauman/rio-demo並且部署它。如果我們使用一個工具來添加負載到端點,我們就能夠觀察到自動擴縮容。為了證明這一點,我們需要使用HTTP端點(而不是HTTPS),因為我們使用的工具不支持TLS:

$ rio inspect demo-service
<snipped>
endpoints:
- https://demo-service-v0-default.op0kj0.on-rio.io:30282
- http://demo-service-v0-default.op0kj0.on-rio.io:31976
<snipped>

rio inspect除了端點之外還會显示其他信息,但我們目前所需要的是端點信息。使用HTTP端點以及HTTP基準測試工具rakyll / hey,我們可以添加綜合負載:

hey -n 10000 http://demo-service-v0-default.op0kj0.on-rio.io:31976

這將會發送10000個請求到HTTP端點,Rio將會提高QPS並適當擴大規模,執行另一個rio ps將會展示已經擴大的規模:

$ rio ps
NAME            ...     SCALE       WEIGHT
demo-service    ...     2/5 (40%)   100%

分階段發布、金絲雀部署以及權重

注意

對於每個服務,都會創建一個全局端點,該端點將根據基礎服務的權重路由流量。

Rio可以先交付新的服務版本,然後再推廣到生產環境。分階段發布一個新的版本十分簡單:

rio stage --image ebauman/rio-demo:v2 demo-service v2

這一命令使用版本v2,分階段發布demo-service的新版本,並且使用容器鏡像ebauman/rio-demo:v2。我們通過執行rio ps這一命令,可以看到新階段的發布:

$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  0%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  100%

請注意,新服務的端點具有v2的新增功能,因此即使權重設置為0%,訪問此端點仍將帶你進入服務的v2。這可以讓你能夠在向其發送流量之前驗證服務的運行情況。

說到發送流量:

$ rio weight demo-service@v2=5%
$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  5%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  95%

使用rio weight命令,我們現在將發送我們5%的流量(從全局的服務端點)到新版本。當我們覺得demo-service的v2性能感到滿意之後,我們可以將其提升到100%:

$ rio promote --duration 60s demo-service@v2
demo-service@v2 promoted

超過60秒之後,我們的demo-service@v2服務將會逐漸提升到接收100%的流量。在這一過程中任意端點上,我們可以執行rio ps,並且查看進程:

$ rio ps
NAME                IMAGE                   ENDPOINT                    WEIGHT
demo-service@v2     ebauman/rio-demo:v2     https://demo-service-v2...  34%
demo-service        ebauman/rio-demo:v1     https://demo-service-v0...  66%

路由(Routing)

Rio可以根據主機名、路徑、方法、標頭和cookie的任意組合將流量路由到端點。Rio還支持鏡像流量、注入故障,配置retry邏輯和超時。

創建一個路由器

為了開始制定路由決策,我們必須首先創建一個路由器。路由器代表一個主機名和一組規則,這些規則確定發送到主機名的流量如何在Rio集群內進行路由。你想要要定義路由器,需要執行rio router add。例如,要創建一個在默認測試時接收流量並將其發送到demo-service的路由器,請使用以下命令:

rio route add testing to demo-service

這將創建以下路由器:

$ rio routers
NAME             URL                            OPTS    ACTION      TARGET
router/testing   https://testing-default.0pjk...        to          demo-service,port=80

發送到https://testing-default…的流量將通過端口80轉發到demo-service。

請注意,此處創建的路由為testing-default. 。Rio將始終使用命名空間資源,因此在這種情況下,主機名測試已在默認命名空間中進行了命名。要在其他命名空間中創建路由器,請將 -n <namespace>傳遞給rio命令:

rio -n <namespace> route add ...

基於路徑的路由

為了定義一個基於路徑的路由,當調用rio route add時,指定一個主機名加上一個路徑。這可以是新路由器,也可以是現有路由器。

$ rio route add testing/old to demo-service@v1

以上命令可以創建一個基於路徑的路由,它會在https://testing-default. /old接收流量,並且轉發流量到 demo-service@v1服務。

標頭和基於方法的路由

Rio支持基於HTTP標頭和HTTP verbs的值做出的路由策略。如果你想要創建基於特定標頭路由的規則,請在rio route add命令中指定標頭:

$ rio route add --header X-Header=SomeValue testing to demo-service

以上命令將創建一個路由規則,它可以使用一個X-Header的HTTP標頭和SomeValue的值將流量轉發到demo-service。類似地,你可以為HTTP方法定義規則:

$ rio route add --method POST testing to demo-service

故障注入

Rio路由有一項有趣的功能是能夠將故障注入響應中。通過定義故障路由規則,你可以設置具有指定延遲和HTTP代碼的失敗流量百分比:

$ rio route add --fault-httpcode 502 --fault-delay-milli-seconds 1000 --fault-percentage 75 testing to demo-service

其他路由選項

Rio支持按照權重分配流量、為失敗的請求重試邏輯、重定向到其他服務、定義超時以及添加重寫規則。要查看這些選項,請參閱以下鏈接:

https://github.com/rancher/rio

自動構建

將git倉庫傳遞給rio run將指示Rio在提交到受監控的branch(默認值:master)之後構建代碼。對於Github倉庫,你可以通過Github webhooks啟動此功能。對於任何其他git repo,或者你不想使用webhooks,Rio都會提供一項“gitwatcher”服務,該服務會定期檢查您的倉庫中是否有更改。

Rio還可以根據受監控的branch的拉取請求構建代碼。如果你想要進行配置,請將--build-pr傳遞到rio run。還有其他配置這一功能的選項,包括傳遞Dockerfile的名稱、自定義構建的鏡像名稱以及將鏡像推送到指定的鏡像倉庫。

堆棧和Riofile

Rio使用稱為Riofile的docker-compose-style manifest定義資源

configs:
  conf:
    index.html: |-
      <!DOCTYPE html>
      <html>
      <body>

      <h1>Hello World</h1>

      </body>
      </html>
services:
  nginx:
    image: nginx
    ports:
    - 80/http
    configs:
    - conf/index.html:/usr/share/nginx/html/index.html

Riofile定義了一個簡單的nginx Hello World網頁所有必要的組件。通過rio up部署它,會創建一個Stack(堆棧),它是Riofile定義的資源的集合。

Riofile具有許多功能,例如觀察Git庫中的更改以及使用Golang模板進行模板化。

其他Rio組件

Rio還有許多功能,例如configs、secrets以及基於角色訪問控制(RBAC)。詳情可參閱:

https://rio.io/

Rio可視化

Rio Dashboard

Rio的beta版本包括了一個全新的儀錶盤,使得Rio組件可視化。要訪問此儀錶盤,請執行命令:rio dashboard。在有GUI和默認瀏覽器的操作系統上,Rio將自動打開瀏覽器並加載儀錶盤。

你可以使用儀錶盤來創建和編輯堆棧、服務、路由等。此外,可以直接查看和編輯用於各種組件技術(Linkerd、gloo等)的對象,儘管不建議這樣做。儀錶盤目前處於開發的早期階段,因此某些功能的可視化(如自動縮放和服務網格)尚不可用。

Linkerd

作為Rio的默認服務網格,Linked附帶了一個儀錶盤作為產品的一部分。該儀錶盤可以通過執行rio linkerd來使用,它將代理本地本地主機流量到linkerd儀錶盤(不會在外部公開)。與Rio儀錶盤類似,有GUI和默認瀏覽器的操作系統上,Rio將自動打開瀏覽器並加載儀錶盤:

Linkerd儀錶盤显示了Rio集群的網格配置、流量和網格組件。Linkerd提供了Rio路由的某些功能組件,因此這些配置可能會显示在此儀錶盤上。還有一些工具可用於測試和調試網格配置和流量。

結 論

Rio為用戶提供許多功能,是一款強大的應用程序部署引擎。這些組件可以在部署應用程序時為開發人員提供強大的功能,使流程穩定而安全,同時輕鬆又有趣。在Rancher產品生態中,Rio提供了企業部署和管理應用程序和容器的強大功能。

如果你想了解Rio的更多信息,歡迎訪問Rio主頁或Github主頁:

https://rio.io

https://github.com/rancher/rio

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

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

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

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

從壹開始 [ Design Pattern ] 之二 ║ 單例模式 與 Singleton

前言

這一篇來源我的公眾號,如果你沒看過,正好直接看看,如果看過了也可以再看看,我稍微修改了一些內容,今天講解的內容如下

 

 

 

 

 

 

 

一、什麼是單例模式

 

【單例模式】,英文名稱:Singleton Pattern,這個模式很簡單,一個類型只需要一個實例,他是屬於創建類型的一種常用的軟件設計模式。通過單例模式的方法創建的類在當前進程中只有一個實例(根據需要,也有可能一個線程中屬於單例,如:僅線程上下文內使用同一個實例)。

1、單例類只能有一個實例。

2、單例類必須自己創建自己的唯一實例。

3、單例類必須給所有其他對象提供這一實例。

 

那咱們大概知道了,其實說白了,就是我們整個項目周期內,只會有一個實例,當項目停止的時候,實例銷毀,當重新啟動的時候,我們的實例又會產品。

上文中說到了一個名詞【創建類型】的設計模式,那什麼是創建類型的設計模式呢?

創建型(Creational)模式:負責對象創建,我們使用這個模式,就是為了創建我們需要的對象實例的。

 

那除了創建型還有其他兩種類型的模式:

結構型(Structural)模式:處理類與對象間的組合

行為型(Behavioral)模式:類與對象交互中的職責分

這兩種設計模式,以後會慢慢說到,這裏先按下不表。

咱們就重點從0開始分析分析如何創建一個單例模式的對象實例。

 

二、如何創建單例模式

 

實現單例模式有很多方法:從“懶漢式”到“餓漢式”,最後“雙檢鎖”模式,這裏咱們就慢慢的,從一步一步的開始講解如何創建單例。

 

1、正常的思考邏輯順序

 

既然要創建單一的實例,那我們首先需要學會如何去創建一個實例,這個很簡單,相信每個人都會創建實例,就比如說這樣的:

/// <summary>
/// 定義一個天氣類
/// </summary>
public class WeatherForecast
{
    public WeatherForecast()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}


 [HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     WeatherForecast weather = new WeatherForecast();
     return weather;
 }

 

我們每次訪問的時候,時間都是會變化,所以我們的實例也是一直在創建,在變化:

 

 

相信每個人都能看到這個代碼是什麼意思,不多說,直接往下走,我們知道,單例模式的核心目的就是:

必須保證這個實例在整個系統的運行周期內是唯一的,這樣可以保證中間不會出現問題。

 

那好,我們改進改進,不是說要唯一一個么,好說!我直接返回不就行了:

 

 /// <summary>
 /// 定義一個天氣類
 /// </summary>
 public class WeatherForecast
 {
     // 定義一個靜態變量來保存類的唯一實例
     private static WeatherForecast uniqueInstance;

     // 定義私有構造函數,使外界不能創建該類實例
     private WeatherForecast()
     {
         Date = DateTime.Now;
     }
     /// <summary>
     /// 靜態方法,來返回唯一實例
     /// 如果存在,則返回
     /// </summary>
     /// <returns></returns>
     public static WeatherForecast GetInstance()
     {
         // 如果類的實例不存在則創建,否則直接返回
         // 其實嚴格意義上來說,這個不屬於【單例】
         if (uniqueInstance == null)
         {
             uniqueInstance = new WeatherForecast();
         }
         return uniqueInstance;
     }
     public DateTime Date { get; set; }public int TemperatureC { get; set; }
     public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
     public string Summary { get; set; }
 }

 

 

然後我們修改一下調用方法,因為我們的默認構造函數已經私有化了,不允許再創建實例了,所以我們直接這麼調用:

[HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     WeatherForecast weather = WeatherForecast.GetInstance();
     return weather;
 }

 

最後來看看效果:

 

 

這個時候,我們可以看到,時間已經不發生變化了,也就是說我們的實例是唯一的了,大功告成!是不是很開心!

 

但是,別著急,問題來了,我們目前是單線程的,所以只有一個,那如果多線程呢,如果多個線程同時訪問,會不會也會正常呢?

這裏我們做一個測試,我們在項目啟動的時候,用多線程去調用:

 

 [HttpGet]
 public WeatherForecast Get()
 {
     // 實例化一個對象實例
     //WeatherForecast weather = WeatherForecast.GetInstance();

     // 多線程去調用
     for (int i = 0; i < 3; i++)
     {
         var th = new Thread(
         new ParameterizedThreadStart((state) =>
         {
             WeatherForecast.GetInstance();
         })
         );
         th.Start(i);
     }
     return null;
 }

 

然後我們看看效果是怎樣的,按照我們的思路,應該是只會走一遍構造函數,其實不是:

 

 

 

 

 

 

3個線程在第一次訪問GetInstance方法時,同時判斷(uniqueInstance ==null)這個條件時都返回真,然後都去創建了實例,這個肯定是不對的。那怎麼辦呢,只要讓GetInstance方法只運行一個線程運行就好了,我們可以加一個鎖來控制他,代碼如下:

public class WeatherForecast
{
    // 定義一個靜態變量來保存類的唯一實例
    private static WeatherForecast uniqueInstance;
    // 定義一個鎖,防止多線程
    private static readonly object locker = new object();

    // 定義私有構造函數,使外界不能創建該類實例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 靜態方法,來返回唯一實例
    /// 如果存在,則返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 當第一個線程執行的時候,會對locker對象 "加鎖",
        // 當其他線程執行的時候,會等待 locker 執行完解鎖
        lock (locker)
        {
            // 如果類的實例不存在則創建,否則直接返回
            if (uniqueInstance == null)
            {
                uniqueInstance = new WeatherForecast();
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

    public string Summary { get; set; }
}

 

這個時候,我們再併發測試,發現已經都一樣了,這樣就達到了我們想要的效果,但是這樣真的是最完美的么,其實不是的,因為我們加鎖,只是第一次判斷是否為空,如果創建好了以後,以後就不用去管這個 lock 鎖了,我們只關心的是 uniqueInstance 是否為空,那我們再完善一下:

 

/// <summary>
/// 定義一個天氣類
/// </summary>
public class WeatherForecast
{
    // 定義一個靜態變量來保存類的唯一實例
    private static WeatherForecast uniqueInstance;
    // 定義一個鎖,防止多線程
    private static readonly object locker = new object();

    // 定義私有構造函數,使外界不能創建該類實例
    private WeatherForecast()
    {
        Date = DateTime.Now;
    }
    /// <summary>
    /// 靜態方法,來返回唯一實例
    /// 如果存在,則返回
    /// </summary>
    /// <returns></returns>
    public static WeatherForecast GetInstance()
    {
        // 當第一個線程執行的時候,會對locker對象 "加鎖",
        // 當其他線程執行的時候,會等待 locker 執行完解鎖
        if (uniqueInstance == null)
        {
            lock (locker)
            {
                // 如果類的實例不存在則創建,否則直接返回
                if (uniqueInstance == null)
                {
                    uniqueInstance = new WeatherForecast();
                }
            }
        }

        return uniqueInstance;
    }
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string Summary { get; set; }
}

 

這樣才最終的完美實現我們的單例模式!搞定。

 

2、幽靈事件:指令重排

當然,如果你看完了上邊的那四步已經可以出師了,平時我們就是這麼使用的,也是這麼想的,但是真的就是萬無一失么,有一個 JAVA 的朋友提出了這個問題,C# 中我沒有聽說過,是我孤陋寡聞了么:

單例模式的幽靈事件,時令重排會偶爾導致單例模式失效。

 

是不是聽起來感覺很高大上,而不知所云,沒關係,咱們平時用不到,但是可以了解了解:

為何要指令重排?       

指令重排是指的 volatile,現在的CPU一般採用流水線來執行指令。一個指令的執行被分成:取指、譯碼、訪存、執行、寫回、等若干個階段。然後,多條指令可以同時存在於流水線中,同時被執行。
指令流水線並不是串行的,並不會因為一個耗時很長的指令在“執行”階段呆很長時間,而導致後續的指令都卡在“執行”之前的階段上。
相反,流水線是并行的,多個指令可以同時處於同一個階段,只要CPU內部相應的處理部件未被佔滿即可。比如說CPU有一個加法器和一個除法器,那麼一條加法指令和一條除法指令就可能同時處於“執行”階段, 而兩條加法指令在“執行”階段就只能串行工作。
相比於串行+阻塞的方式,流水線像這樣并行的工作,效率是非常高的。

然而,這樣一來,亂序可能就產生了。比如一條加法指令原本出現在一條除法指令的後面,但是由於除法的執行時間很長,在它執行完之前,加法可能先執行完了。再比如兩條訪存指令,可能由於第二條指令命中了cache而導致它先於第一條指令完成。
一般情況下,指令亂序並不是CPU在執行指令之前刻意去調整順序。CPU總是順序的去內存裏面取指令,然後將其順序的放入指令流水線。但是指令執行時的各種條件,指令與指令之間的相互影響,可能導致順序放入流水線的指令,最終亂序執行完成。這就是所謂的“順序流入,亂序流出”。

 

這個是從網上摘錄的,大概意思看看就行,理解雙檢鎖失效原因有兩個重點

1、編譯器的寫操作重排問題.
例 : B b = new B();

上面這一句並不是原子性的操作,一部分是new一個B對象,一部分是將new出來的對象賦值給b.

直覺來說我們可能認為是先構造對象再賦值.但是很遺憾,這個順序並不是固定的.再編譯器的重排作用下,可能會出現先賦值再構造對象的情況.

2、結合上下文,結合使用情景.

理解了1中的寫操作重排以後,我卡住了一下.因為我真不知道這種重排到底會帶來什麼影響.實際上是因為我看代碼看的不夠仔細,沒有意識到使用場景.雙檢鎖的一種常見使用場景就是在單例模式下初始化一個單例並返回,然後調用初始化方法的方法體內使用初始化完成的單例對象.

 

三、Singleton = 單例 ?

 上邊我們說了很多,也介紹了很多單例的原理和步驟,那這裏問題來了,我們在學習依賴注入的時候,用到的 Singleton 的單例注入,是不是和上邊說的一回事兒呢,這裏咱們直接多多線程測試一下就行:

 

/// <summary>
/// 定義一個心情類
/// </summary>
public class Feeling
{
    public Feeling()
    {
        Date = DateTime.Now;
    }
    public DateTime Date { get; set; }
}


 // 單例注入
 services.AddSingleton<Feeling>();


[HttpGet]
public WeatherForecast Get()
{

    // 多線程去調用
    for (int i = 0; i < 3; i++)
    {
        var th = new Thread(
        new ParameterizedThreadStart((state) =>
        {
            //WeatherForecast.GetInstance();
            
            // 此刻的心情
            Feeling feeling = new Feeling();
            Console.WriteLine(feeling.Date);
        })
        );
        th.Start(i);
    }
    return null;
}

 

測試的結果,情理之中,也是意料之外:

 

 

竟然和我們上邊說的是一樣的, 
Singleton是一種懶漢模式 的單例, 因為結論可以看出,有時候我們使用單例模式,並不是寫一個 Sigleton 就能滿足的。    

四、單例模式的優缺點

 

        【優】、單例模式的優點:

             (1)、保證唯一性:防止其他對象實例化,保證實例的唯一性;

             (2)、全局性:定義好數據后,可以再整個項目種的任何地方使用當前實例,以及數據;

        【劣】、單例模式的缺點: 

             (1)、內存常駐:因為單例的生命周期最長,存在整個開發系統內,如果一直添加數據,或者是常駐的話,會造成一定的內存消耗。

 

以下內容來自百度百科:

優點

一、實例控制 單例模式會阻止其他對象實例化其自己的單例對象的副本,從而確保所有對象都訪問唯一實例。
二、靈活性 因為類控制了實例化過程,所以類可以靈活更改實例化過程。  

缺點

一、開銷 雖然數量很少,但如果每次對象請求引用時都要檢查是否存在類的實例,將仍然需要一些開銷。可以通過使用靜態初始化解決此問題。
二、可能的開發混淆 使用單例對象(尤其在類庫中定義的對象)時,開發人員必須記住自己不能使用
new關鍵字實例化對象。因為可能無法訪問庫源代碼,因此應用程序開發人員可能會意外發現自己無法直接實例化此類。
三、對象生存期 不能解決刪除單個對象的問題。在提供內存管理的語言中(例如基於.NET Framework的語言),只有單例類能夠導致實例被取消分配,因為它包含對該實例的私有引用。在某些語言中(如 C++),其他類可以刪除對象實例,但這樣會導致單例類中出現懸浮引用。

 

五、示例代碼

 

 

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

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

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

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

如何打造一款m3u8視頻爬蟲

0.前言

m3u8是一種很常見的網頁視頻播放器的視頻源,比如說中國大學MOOC中課程就是使用了該種視頻格式。

隨便打開一門課程,就可以發現在網絡請求中存在一個m3u8的文件,在preview中預覽,它並不像我們想象中是亂碼的視頻流。

裏面是一個列表,有一堆ts結尾的文件名,每個下面還跟了一個EXTINF的字段,好像是時間,在我們播放視頻時,網絡請求中會不斷出現請求ts的內容。

隨便打開一個ts文件,它的內容卻是如圖視頻流一般亂碼的。

說到這裏,你可能有猜測了,m3u8並不是視頻流的文件,而有可能是組織ts文件的規範,EXTINF代表播放每多少秒去請求下一片ts流。

這種邊看邊加載的方法無疑可以減少我們的網絡負荷。

要用爬蟲爬取這類視頻的方法也很簡單,我們只需要獲得m3u8文件,就可以得到視頻的ts地址了,將所有ts請求下來之後進行合併,就可以得到視頻文件了。

不過要提的一點是,很多視頻網站會對他們的ts進行加密,我們下載下來合併之後可能視頻能看,但是播放器放着放着就卡住了,然後之後黑屏畫面。

1.編碼部分

我們先根據m3u8來判斷一下創建咋樣一個代表M3U8視頻對象的類。

我們首先需要定義一個list,來存放這個m3u8視頻下所有的ts文件,也就是後面說到的TS類。

這裏提一點,m3u8裏面的ts的路徑一般對路徑,會和m3u8在同一文件夾,我們代碼中也是這麼認為了,但是難免有些網站會單獨存放m3u8和ts文件,如果遇到這種情況,修改一下代碼即可。

有了ts的名稱,我們還需要URL的前綴,也就是圖中紫色劃線部分,也就是basepath。

此外,我們還需要一個TS對象。

這個對象中存儲TS文件名稱以及時間EXTINF。

定義完實體類,就需要編寫下載視頻的過程了。

首先需要請求到m3u8的文件,此處使用Java的HttpURLConnection來請求獲取,其它語言類似,只需要請求到文件即可。

請求到了m3u8的文本內容,我們還需要解析它 ,從中得到ts的名稱。

得到了M3U8視頻對象之後,我們就可以遍歷請求它的list中TS對象的名稱屬性來下載ts文件了。

這麼多ts文件如果我們在單線程中遍歷請求,會很耗費時間,Java給我們提供了Stream,其中parallel可以讓我們併發去遍歷集合,效率會提升不少。

依舊是使用HttpURLConnection來做請求,不過最好本次設置超時時間。

這樣就可以請求到所有ts文件了。

最後要做的就是合併這些ts文件成為一個MP4文件。

對於未加密的正常ts文件,我們只需要按照編號順序直接拼接即可。

這樣就算是完成了M3U8視頻抓取了。

2.打包使用

下載地址:

在命令行中java -jar m3u8-down.jar [m3u8地址],會显示報錯信息。

也可以直接m3u8-down.jar [m3u8地址],不會显示保存信息,會在後台執行。

最終會在同目錄下生成一個output.mp4的文件,temp文件可以刪除。

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

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

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

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

.NET Core 3.0中用 Code-First 方式創建 gRPC 服務與客戶端

.NET Core love gRPC

千呼萬喚的 .NET Core 3.0 終於在 9 月份正式發布,在它的眾多新特性中,除了性能得到了大大提高,比較受關注的應該是 ASP.NET Core 3.0 對 gRPC 的集成了。
它的源碼託管在 grpc-dotnet 這個 Github 庫中,由微軟 .NET 團隊與谷歌 gRPC 團隊共同維護.

.NET Core 對 gRPC 的支持在 grpc 官方倉庫早已有實現(grpc/csharp),但服務端沒有很好地與 ASP.NET Core 集成,使用起來還需要自己進行一些集成擴展。
而 ASP.NET Core 3.0 新增了 gRPC 服務的託管功能,能讓 gRPC 與 ASP.NET Core 框架本身的特性很好地結合,如日誌、依賴注入、身份認證和授權,並由 Kestrel 服務器提供 HTTP/2 鏈接,性能上得到充分保障。

推薦把項目中已有的 RPC 框架或者內部服務間 REST 調用都遷移到 gRPC 上,因為它已經是雲原生應用的標準 RPC 框架,在整個 CNCF 主導下的雲原生應用開發生態里 gRpc 有着舉足輕重的地位。

對於 gRPC 的使用方式,前段時間已經有其他大神寫的幾篇文章了,這裏就不再贅述了。
本文主要介紹的是區別於標準使用規範的,但對.NET 應用更加友好的使用方式,最後會提供源碼來展示。

作為對比,還是要列一下標準的使用步驟:

  1. 定義 proto 文件,包含服務、方法、消息對象的定義
  2. 引入 Grpc.Tools Nuget 包並添加指定 proto 路徑和生成模式
  3. 生成項目,得到服務端的抽象類或客戶端的調用客戶端組件
  4. 實現服務端抽象類,並在 ASP.NET Core 註冊這個服務的路由端點
  5. DI 註冊 gRPC 服務。
  6. 客戶端用 Grpc.Net.ClientFactory Nuget 包進行統一配置和依賴注入

.NET Core 對 gRPC 的大力支持使開發者開發效率大大提高,入門難度也減少了許多,完全可以成為跟 WebApi 等一樣的 .NET Core 技術棧的標配。

proto 在單一語言系統架構中的局限性

使用 proto 文件的好處是多語言支持,同一份 proto 可以生成各種語言的服務和客戶端,可以讓用不同語言開發的微服務直接互相遠程調用。但 proto 文件作為不同服務間的契約,不可以經常修改,否則就會對使用了它的服務造成不同程度的影響,因此對 proto 文件的版本控制需要得到重視。

另外,我們的應用程序還不應該與 gRPC 耦合,否則就會導致系統架構被這些實現細節所綁架。直接依賴 proto 文件和由它生成的代碼,就是對 gRPC 的強耦合。

例如,當應用程序在演進的過程中,複雜度還未達到完全部署隔離的必要時,為了避免因“完全邊界”引入的部署運維複雜性,又能預留隔離的可能性,需要有一層接口層作為“不完全邊界”。

又比如,目前在 windows 系統的 iis 上還不支持 grpc-dotnet,當有 windows 上的應用程序需要使用 RPC,就需要換成 REST 的實現了。

因此,為了不讓應用程序對 gRPC 過於依賴,還應該使用一層抽象(接口)層與其解耦,用接口來隔離對 RPC 實現的依賴,這樣在需要使用不同的實現時,可以通過註冊不同的實現來方便地切換。

在這些場景下,本文要介紹的 Code-First gRPC 使用方法就發揮作用了。

Code-First gRPC

說了這麼久,我好像還沒正式介紹 Code-First gRPC,到底他有多適合在單一語言系統架構中實現 gRPC 呢?下面要介紹的就是基於大名鼎鼎的 protobuf-net 實現的 gRPC 框架,protobuf-net.Grpc

protobuf-net 是在過去十幾年前到現在一直在 .NET 中有名的 Protobuf 庫,想用 Protobuf 序列化時就會用到這個庫。他的特性就是可以把 C# 代碼編寫的類能以 Protobuf 的協議進行序列化和反序列化,而不是 proto 文件再生成這些類。而 protobuf-net.Grpc 則是一脈相承,可以把 C# 寫的接口,在服務端方便地把接口的實現類註冊成 ASP.NET Core 的 gRPC 服務,在客戶端把接口動態代理實現為調用客戶端,調用前面的這個服務端。

用法很簡單,只要聲明一個接口為您的服務契約:

[ServiceContract]
public interface IMyAmazingService {
    ValueTask<SearchResponse> SearchAsync(SearchRequest request);
    // ...
}

然後實現該接口的服務端:

public class MyServer : IMyAmazingService {
    // ...
}

或者向系統獲取客戶端:

var client = http.CreateGrpcService<IMyAmazingService>();
var results = await client.SearchAsync(request);

這相當於以下 .proto 中的服務:

service MyAmazingService {
    rpc Search (SearchRequest) returns (SearchResponse) {}
    // ...
}

protobuf-net.Grpc 同樣通過普通類型定義支持 gRPC 的四種模式,把 C# 8.0 中最新的 IAsyncEnumerable 類型識別成 proto 中的 stream,單向流、雙向流都可以實現!而且用 IAsyncEnumerable 實現可比 proto 生成的類方便很多。

例如 proto 雙向流定義:

rpc chat(stream ChatRequest) returns ( stream ChatResponse);

生成出來的方法是:

Task BathTheCat(IAsyncStreamReader<ChatRequest> requestStream, IServerStreamWriter<ChatResponse> responseStream)

protobuf-net.Grpc 只要定義一個方法:

IAsyncEnumerable<ChatResponse> SubscribeAsync(IAsyncEnumerable<ChatRequest> requestStream);

由此可見,protobuf-net.Grpc 無需在契約層引入第三方庫,充分運用了 C# 類型系統,把方法、類型映射到兼容了 gRPC 的服務定義上。

上文所說的 proto 局限也迎刃而解了,函數調用、gRPC、REST 都能方便切換。(REST 實現可以參考我的開源框架 shriek-fx 中的 Shriek.ServiceProxy.Http )組件。

下一篇,我將主要介紹利用 protobuf-net.Grpc 的 gRPC 雙向流模式與 Blazor 實現一個簡單的在線即時聊天室。

相關鏈接:

  • protobuf-net.Grpc:
  • shriek-fx:
  • GrpcChat:

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

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

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

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

Java多線程系列——多線程方法詳解

Java多線程系列文章是Java多線程的詳解介紹,對多線程還不熟悉的同學可以先去看一下我的這篇博客,這篇博客從宏觀層面介紹了多線程的整體概況,接下來的幾篇文章是對多線程的深入剖析。

 

多線程的常用方法

1、currentThread()方法:

介紹:currentThread()方法可返回該代碼正在被哪個線程調用的信息。

示例

例1:

public class Test01 {

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName());
	}
	
}

結果:
main

  

結果說明,main方法被名為main的線程調用

 

例2:

class Mythread extends Thread{
	
	public Mythread() {
		System.out.println("構造方法的打印:"+Thread.currentThread().getName());
	}
	
	@Override
	public void run() {
		System.out.println("run方法的打印:"+Thread.currentThread().getName());
	}
}

public class Test01 {

	public static void main(String[] args) {
		Mythread t=new Mythread();
		t.start();//①
	}
	
}

結果:
構造方法的打印:main
run方法的打印:Thread-0

  

從結果可知:Mythread的構造方法是被main線程調用的,而run方法是被名稱為Thread-0的線程調用的,run方法是線程自動調用的

現在我們將①處的代碼改為t.run(),現在的輸出結果如下:

構造方法的打印:main
run方法的打印:main

  

從結果中我們可以看到兩次的結果显示都是main線程調用了方法,因為當你使用t.start()方法的時候是線程自動調用的run()方法,所以輸出的是Thread-0,當你直接調用run()方法時,和調用普通方法沒有什麼區別,所以是main線程調用run()

 

2、isAlive()方法:

介紹:isAlive()方法的功能是判斷當前的線程是否處於活動狀態

示例:

例1:

class Mythread extends Thread{
	
	@Override
	public void run() {
		System.out.println("run =="+this.isAlive());
	}
	
}

public class Test01 {

	public static void main(String[] args) {
		Mythread thread=new Mythread();
		System.out.println("begin =="+thread.isAlive());//①
		thread.start();//②
		System.out.println("end =="+thread.isAlive());//③
	}
	
}

結果:
begin ==false
end ==true
run ==true

  

方法isAlive()的作用是測試線程是否處於活動狀態。那麼什麼情況下是活動狀態呢?活動狀態就是線程已經啟動且尚未停止。線程處於正在運行或準備開始運行的狀態,就認為線程是存活的

①處代碼的結果為false,因為此時線程還未啟動;

②處代碼調用了run()方法輸出結果為run ==true,此時線程處於活動狀態;

③處代碼的結果為true,有的同學看到這個輸出可能會不理解,不是說線程處於活動狀態isAlive()方法的結果才是true,現在程序都已經運行結束了為什麼還是true?這裏的輸出結果是不確定的,我們再來看下面一段代碼

我們將例1中的代碼稍做修改,代碼如下:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		System.out.println("begin =="+thread.isAlive());//①
		thread.start();//②
		Thread.sleep(1000);//這裏加了一行代碼,讓當前線程沉睡1秒
		System.out.println("end =="+thread.isAlive());//③
	}
	
}

結果:
begin ==false
run ==true
end ==false

  

現在我們看到③處的代碼結果為end ==false,因為thread對象已經在1秒內執行完畢,而上面代碼輸出結果為true是因為thread線程未執行完畢。

 

3、sleep()方法:

介紹:

方法sleep()的作用是在指定的毫秒數內讓當前“正在執行的線程”休眠(暫停執行),這個“正在執行的線程”是指this.currentThread()返回的線程。

示例:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		try {
			System.out.println("run threadName="+this.currentThread().getName()+" begin");
			Thread.sleep(2000);
			System.out.println("run threadName="+this.currentThread().getName()+" end");
			
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		System.out.println("begin ="+System.currentTimeMillis());
		thread.run();//①
		System.out.println("end ="+System.currentTimeMillis());
	}
	
}

結果:
begin =1574660731663
run threadName=main begin
run threadName=main end
end =1574660733665

  

從結果中可以看出main線程暫停了2秒(因為這裏調用的是thread.run())

下面我們將①處的代碼改成thread.start(),再來看下運行結果:

begin =1574661491412
end =1574661491412
run threadName=Thread-0 begin
run threadName=Thread-0 end

  

由於main線程與thread線程是異步執行的,所以首先打印的信息為begin和end,而thread線程是隨後運行的,在最後兩行打印run begin和run end的信息。

 

4、getId()方法:

介紹:getId()方法的作用是取得線程的唯一標識

示例

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Thread thread=Thread.currentThread();
		System.out.println(thread.getName()+" "+thread.getId());
	}
	
}

結果:main 1

  

從運行結果可以看出,當前執行代碼的線程名稱是main,線程id值為1

 

5、停止線程:

介紹:停止線程是在多線程開發時很重要的技術點,掌握此技術可以對線程的停止進行有效的處理。停止線程在Java語言中並不像break語句那樣乾脆,需要一些技巧性的處理。

在java中有三種方法可以停止線程

  1. 使用退出標誌,讓線程正常退出,也就是當run方法執行完之後終止
  2. 使用stop方法強制終止線程,但是不推薦使用,因為stop和suspend及resume一樣,是java廢棄的方法
  3. 使用interrupt方法中斷線程(推薦使用)

示例:

例1:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		for(int i=0;i<5000;i++) {
			System.out.println("i="+(i+1));
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(2000);
		thread.interrupt();
	}
	
}

  

運行結果:

 

從運行結果我們可以看出最後i=500000,調用interrupt方法沒有停止線程,那麼該如何停止線程呢?

在介紹如何停止線程時,我們先來介紹一下如何判斷線程是否處於停止狀態

Thread類中提供了兩種方法用來判斷線程是否停止:

1、this.interrupted():測試當前線程是否已經中斷,執行后具有將狀態標誌清除為false的功能

public static boolean interrupted() {
        return currentThread().isInterrupted(true);
}

  

2、this.isInterrupted():測試線程Thread對象是否已經中斷,但是不清除狀態標誌

public boolean isInterrupted() {
        return isInterrupted(false);
}

  

讀者可以仔細觀看一下這兩個方法的聲明有什麼不同?

 

例2:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		for(int i=0;i<5000;i++) {
			System.out.println("i="+(i+1));
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(1000);
		thread.interrupt();
		System.out.println("是否停止1?="+thread.interrupted());
		System.out.println("是否停止2?="+thread.interrupted());
		System.out.println("end!");
	}
	
}

  

結果:

 

 

 輸出結果显示調用了thread.interrupt()方法后線程並未停止,這也就證明了interrupted()方法的解釋:測試當前線程是否已經中斷。這個當前線程是main,它從未斷過,所以打印的結果是兩個false。

如果想讓main線程結束該怎麼做?

將main方法改成如下:

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		Thread.currentThread().interrupt();
		System.out.println("是否停止1?="+Thread.interrupted());
		System.out.println("是否停止2?="+Thread.interrupted());
		System.out.println("end!");
	}
	
}

結果:
是否停止1?=true
是否停止2?=false
end!

  

從輸出結果我們可以看出,方法interrupted()的確判斷出當前線程是否是停止狀態。但為什麼第2個值是false?

查看一下官方文檔的介紹:

測試當前線程是否已經中斷。線程的中斷狀態由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用將返回false(在第一次調用已清除了其中斷狀態之後,且第二次調用檢驗完中斷狀態前,當前線程再次中斷的情況除外)。

 文檔中說明的非常清楚,interrupted()方法具有清除狀態的功能,所以第二次調用interrupted方法返回的值時false。

下面我們來看一下isInterrupted()方法,將main方法改成如下代碼:

public static void main(String[] args) throws InterruptedException {
		Mythread thread=new Mythread();
		thread.start();
		thread.interrupt();
		Thread.sleep(1000);
		System.out.println("是否停止1?="+thread.isInterrupted());
		System.out.println("是否停止2?="+thread.isInterrupted());
		System.out.println("end");
}
結果:

是否停止1?=true
是否停止2?=true
end

  

從結果可以看出,方法isInterrrupted()並未清除狀態,所以結果為兩個true。

 

例3:在沉睡中停止

當線程調用sleep()方法后再調用interrupt()方法後會有什麼結果:

class Mythread extends Thread{
	
	@Override
	public void run() {
		
		try {
			System.out.println("run begin");
			Thread.sleep(200000);
			System.out.println("run end");
		} catch (InterruptedException e) {
			System.out.println("在沉睡中被停止,進入catch!"+this.isInterrupted());
			e.printStackTrace();
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		try {
			Mythread thread=new Mythread();
			thread.start();
			Thread.sleep(200);
			thread.interrupt();
		}catch(Exception e) {
			System.out.println("main catch");
			e.printStackTrace();
		}
		System.out.println("end!");
	}
	
}

  

 

 

6、暫停線程:

暫停線程意味着此線程還可以恢復運行。在java多線程中,可以使用suspend()方法暫停線程,使用resume()方法恢複線程的執行

例1:

class Mythread extends Thread{
	
	private long i=0;
	public long getI() {
		return i;
	}
	
    public void setI(long i) {
		this.i = i;
	}
	
	@Override
	public void run() {
		while(true) {
			i++;
		}
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		Mythread thread=new Mythread();
		thread.start();
		Thread.sleep(5000);
		//A段
		thread.suspend();
		System.out.println("A= "+System.currentTimeMillis()+" i="+thread.getI());
		Thread.sleep(5000);
		System.out.println("A= "+System.currentTimeMillis()+" i="+thread.getI());
		
		//B段
		thread.resume();
		Thread.sleep(5000);
		
		//C段
		thread.suspend();
		System.out.println("B= "+System.currentTimeMillis()+" i="+thread.getI());
		Thread.sleep(5000);
		System.out.println("B= "+System.currentTimeMillis()+" i="+thread.getI());
		
	}
	
}

  

結果:

 


從控制台打印的時間上來看,線程的確被暫停了,而且還可以恢復成運行狀態。

 

7、yield方法:

介紹:yield()方法的作用是放棄當前的CPU資源,將它讓給其他的任務去佔用CPU執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片

示例:

class Mythread extends Thread{
	
	@Override
	public void run() {
		long beginTime=System.currentTimeMillis();
		int count=0;
		for(int i=0;i<500000;i++) {
               //Thread.yield();① count=count+(i+1); } long endTime=System.currentTimeMillis(); System.out.println("用時:"+(endTime-beginTime)+"毫秒!"); } } public class Test01 { public static void main(String[] args) throws InterruptedException { Mythread thread=new Mythread(); thread.start(); } }

結果:用時:2毫秒!

  

現在將①處的代碼取消註釋,我們再來看一下運行結果:

用時:213毫秒!

  

將CPU讓給其他資源導致速度變慢

 

8、線程優先級:

介紹:

在操作系統中,線程可以劃分優先級,優先級較高的線程得到的CPU資源較多,也就是CPU優先執行優先級較高的線程對象中的任務。

設置線程優先級有助於幫“線程規劃器”確定在下一次選擇哪一個線程來優先執行。

設置線程的優先級使用setPriority()方法,此方法在JDK的源代碼如下:

public final void setPriority(int newPriority) {
        ThreadGroup g;
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
}

  

在Java中,線程的優先級為1-10這10個等級,如果小於1或大於10,則JDK拋出異常throw new IllegalArgumentException()。

通常高優先級的線程總是先執行完,但是並不是一定的,高優先級和低優先級的線程會交替進行,高優先級執行的次數多一些

 

線程優先級的繼承特性:

在Java中,線程的優先級具有繼承性,比如A線程啟動B線程,則B線程的優先級與A是一樣的。

class Mythread2 extends Thread{
	@Override
	public void run() {
		System.out.println("Mythread2 run priority="+this.getPriority());
	}
}


class Mythread1 extends Thread{
	
	@Override
	public void run() {
		System.out.println("Mythread run priority="+this.getPriority());
		Mythread2 thread2=new Mythread2();
		thread2.start();
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		System.out.println("main thread begin priority="+Thread.currentThread().getPriority());
    	        //Thread.currentThread().setPriority(6);①
		System.out.println("main thread end priority="+Thread.currentThread().getPriority());
		Mythread1 thread1=new Mythread1();
		thread1.start();
	}
	
}

結果:
main thread begin priority=5
main thread end priority=5
Mythread run priority=5
Mythread2 run priority=5

  

可以看到上面幾個線程的優先級都為5

現在將①處的代碼註釋掉后的結果是:

main thread begin priority=5
main thread end priority=6
Mythread run priority=6
Mythread2 run priority=6

  

優先級被更改后再繼續繼承

 

9、守護線程:

在java中有兩種線程,一種是用戶線程,另一種是守護線程。

守護線程是一種特殊的線程,它的特性有“陪伴”的含義,當進程中不存在非守護線程了,則守護線程自動銷毀。典型的守護線程就是垃圾回收線程,當進程中沒有非守護線程了,則垃圾回收線程也就沒有存在的必要了,自動銷毀。用個比較通俗的比喻來解釋一下:“守護線程”:任何一個守護線程都是整個JVM中所有非守護線程的“保姆”,只要當前JVM實例中存在任何一個非守護線程沒有結束,守護線程就在工作,只有當最後一個非守護線程結束時,守護線程才隨着JVM一同結束工作。Daemon的作用是為其他線程的運行提供便利服務,守護線程最典型的應用就是GC(垃圾回收器),它就是一個很稱職的守護者。

 

class Mythread extends Thread{
	
	private int i=0;
	
	
	@Override
	public void run() {
		
		try {
			while(true) {
				i++;
				System.out.println("i="+(i));
				Thread.sleep(1000);
			}
		}catch(Exception e) {
			e.printStackTrace();
		}
		
	}
	
}

public class Test01 {

	public static void main(String[] args) throws InterruptedException {
		
		try {
			Mythread thread=new Mythread();
			thread.setDaemon(true);
			thread.start();
			Thread.sleep(5000);
			System.out.println("我離開Thread對象就不再打印了,也就是停止了");
		}catch(Exception e) {
			e.printStackTrace();
		}
	}
	
}

  

 

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

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

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

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