如何使用偽類選擇器

偽類選擇器介紹

  • 偽類選擇器就是用來給超級鏈接設置不同的狀態樣式。
  • 超級鏈接分為4種狀態如:正常狀態、訪問過後狀態、鼠標放上狀態、激活狀態。

偽類選擇器說明表

選擇器 描述
:link 向未被訪問的超級鏈接添加樣式,正常狀態。
:visited 向已經被訪問的超級鏈接添加樣式,訪問過後狀態。
:hover 當鼠標懸浮在超級鏈接上方時,向超級鏈接添加樣式,鼠標放上狀態。
:active 鼠標放在超級鏈接上並且點擊的一瞬間,向超級鏈接添加樣式,激活狀態。

偽類選擇器實踐

  • 讓我們進入偽類選擇器實踐,實踐內容將超級鏈接4種狀態進行演示,演示效果如:將向未被訪問的超級鏈接文本顏色設置為紅色、已經被訪問的超級鏈接文本顏色設置為綠色、當鼠標懸浮在超級鏈接上文本顏色設置為紫色、用鼠標點擊超級鏈接的一瞬間文本顏色設置為藍色

  • 代碼塊

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>偽類選擇器</title>
    <style>
        a:link{
            color:red;
        }
        a:visited{
            color: lime;
        }
        a:hover{
            color: purple;
        }
        a:active{
            color: blue;
        }
    </style>
</head>
  
<body>
    <a href="https://www.cnblogs.com/lq000122/">微笑是最初的信仰</a>
</body>
</html>
  • 正常狀態結果圖

  • 鼠標放上狀態結果圖

  • 激活狀態結果圖

  • 訪問過後狀態

總結

  • 超級鏈接的不同狀態他其實是由順序,也就是說偽類選擇器設置其實是順序的,如果按照偽類選擇器的順序,那麼設置的樣式就不會被渲染。
  • 順序:linkvisitedhoveractive

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

Clean Code 筆記 之 第四章 如何應用註釋

繼上一篇筆記之後,今天我們討論一下 代碼中是存在註釋是否是一件好的事情。

 

在我們開發的過程中講究“名副其實,見名識意”,這也往往是很多公司的要求,但是有了這些要求是不是我們的代碼中如果存在註釋是不是意味着我們的 函數,變量,以及類 的命名就不符合了“名副其實,見名識意”。

我們先區分一下註釋的類別,註釋一般分為以下幾種:

  • 1, 單行註釋
  • 2, 多行註釋
  • 3, 文檔註釋
  • 4, #region 摺疊註釋,可以將 代碼摺疊

 註釋的類別

1, 單行註釋:

在以 “//” 開頭,用以說明一行代碼的作用放置位置 看習慣或者公司要求合理就行。常用於函數內部,在很多的開源代碼中文件的頭部我同樣見到很多人使用單行註釋進行說明,靈活就好。
體現形式如下:

 public List<string> getVipUserNameByUserType()
          {
            // Vip user name list
            var vipUserNames = new List<string>();

            foreach (var user in Users)
            {

                if (user.Type = "VIP")

                    vipUserNames.Add(user.Name);
            }
            return vipUserNames;

          }

View Code

2, 多行註釋:

以“/*”開頭 “*/” 結尾 常用於描述說明一段代碼以及類註釋或者說代碼塊常用於文件的頂部。說明作者信息,時間如果是開源的這包含開源的更多說明。
通常使用如下:

/*
    * 作者:〈版權〉
    * 描述:〈描述〉
    * 創建時間:YYYY-MM-DD
    * 作用:
*/

View Code

3, 文檔註釋:

也就是常用的XML 註釋:它的說明性更加的強烈,多用於類以及函數上,當然屬性上同樣可以使用:
如下所示:

        /// <summary>
        /// MyClass
        /// </summary>
        public class MyClass
        {
            /// <summary>
            /// MyProperty
            /// </summary>
            public int MyProperty { get; set; }
            /// <summary>
            /// MyMethod
            /// </summary>
            public void MyMethod(){  }
        }

View Code

以下是官方建議的文檔標記 點擊標籤會制動跳轉

 

4, #region : 摺疊註釋,常用於描述多個函數的基本作用

書中最喜歡的話

好的註釋不能美化糟糕的代碼,真正好的註釋是你想盡辦法不去謝的註釋。懷註釋都是糟糕代碼的支撐或借口,或者是對錯誤決策的修正。

下面看一個例子

       //Check to see if the employee is eligible for full benefits1)If((employee.flags & HOURLY_FLAG)&& (employee.age>65))

(2)If(employee.isEligibleForFullBenefits()))

  這兩個你更喜歡哪個

View Code

好的註釋的特徵:

1:表示法律信息(這樣的註釋一般出現在文檔頂部說明作用以及協議)

// Copyright (c) .NET Foundation. All rights reserved
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

View Code

2:提供信息的註釋(指無法通過命名提供信息如要 註釋輔助的)

public void ConfigureServices(IServiceCollection services)
{

// These two middleware are registered via an IStartupFilter in UseIISIntegration but you can configure them here.

services.Configure<IISOptions>(options =>
{});

}

View Code

3:對意圖的解釋

4: 警示:告知其他人會出現某種後果的註釋

5: TODO註釋: 主要描述應該做的目前還沒有做的事情。

6: 放大:提示不合理之物的重要性。

應避免的註釋

應該避免以下幾點:

1: 誤導性註釋

2: 日誌式註釋

3: 廢話註釋

4: 標記位置的註釋

5: 括號后的註釋

6: 歸屬與簽名

7: 註釋掉的代碼

8: Html 註釋

以上沒有一一舉例的原因是我的PPT是一份演示的PPT,裏面很多公司的代碼不便貼出,抱歉。

不足之處還請指出

 

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

※公開收購3c價格,不怕被賤賣!

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

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

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

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

PHP 的 self 關鍵字用法

之前有人詢問 self 關鍵字的用法,答案是比較明顯的:靜態成員函數內不能用 this 調用非成員函數,但可以用 self 調用靜態成員函數/變量/常量;其他成員函數可以用 self 調用靜態成員函數以及非靜態成員函數。隨着討論的深入,發現 self 並沒有那麼簡單。鑒於此,本文先對幾個關鍵字做對比和區分,再總結 self 的用法。

parentstatic 以及 this 的區別

要想將徹底搞懂 self ,要與 parentstatic 以及 this 區分開。以下分別做對比。

parent

selfparent 的區分比較容易: parent 引用父類/基類被隱蓋的方法(或變量), self則引用自身方法(或變量)。例如構造函數中調用父類構造函數:

class Base {
    public function __construct() {
        echo "Base contructor!", PHP_EOL;
    }
}

class Child {
    public function __construct() {
        parent::__construct();
        echo "Child contructor!", PHP_EOL;
    }
}

new Child;
// 輸出:
// Base contructor!
// Child contructor!

 

static

static 常規用途是修飾函數或變量使其成為類函數和類變量,也可以修飾函數內變量延長其生命周期至整個應用程序的生命周期。但是其與 self 關聯上是PHP 5.3以來引入的新用途:靜態延遲綁定。

有了 static 的靜態延遲綁定功能,可以在運行時動態確定歸屬的類。例如:

class Base {
    public function __construct() {
        echo "Base constructor!", PHP_EOL;
    }

    public static function getSelf() {
        return new self();
    }

    public static function getInstance() {
        return new static();
    }

    public function selfFoo() {
        return self::foo();
    }

    public function staticFoo() {
        return static::foo();
    }

    public function thisFoo() {
        return $this->foo();
    }

    public function foo() {
        echo  "Base Foo!", PHP_EOL;
    }
}

class Child extends Base {
    public function __construct() {
        echo "Child constructor!", PHP_EOL;
    }

    public function foo() {
        echo "Child Foo!", PHP_EOL;
    }
}

$base = Child::getSelf();
$child = Child::getInstance();

$child->selfFoo();
$child->staticFoo();
$child->thisFoo();

 

程序輸出結果如下:

Base constructor!
Child constructor!
Base Foo!
Child Foo!
Child Foo!

 

在函數引用上, selfstatic 的區別是:對於靜態成員函數, self 指向代碼當前類, static 指向調用類;對於非靜態成員函數, self 抑制多態,指向當前類的成員函數, static 等同於 this ,動態指向調用類的函數。

parentselfstatic 三個關鍵字聯合在一起看挺有意思,分別指向父類、當前類、子類,有點“過去、現在、未來”的味道。

this

selfthis 是被討論最多,也是最容易引起誤用的組合。兩者的主要區別如下:

  1. this 不能用在靜態成員函數中, self 可以;
  2. 對靜態成員函數/變量的訪問, 建議 用 self ,不要用 $this::$this-> 的形式;
  3. 對非靜態成員變量的訪問,不能用 self ,只能用 this ;
  4. this 要在對象已經實例化的情況下使用, self 沒有此限制;
  5. 在非靜態成員函數內使用, self 抑制多態行為,引用當前類的函數;而 this 引用調用類的重寫(override)函數(如果有的話)。

self 的用途

看完與上述三個關鍵字的區別, self 的用途是不是呼之即出?一句話總結,那就是: self總是指向“當前類(及類實例)”。詳細說則是:

  1. 替代類名,引用當前類的靜態成員變量和靜態函數;
  2. 抑制多態行為,引用當前類的函數而非子類中覆蓋的實現;

槽點

  1. 這幾個關鍵字中,只有 this 要加 $ 符號且必須加,強迫症表示很難受;
  2. 靜態成員函數中不能通過 $this-> 調用非靜態成員函數,但是可以通過 self:: 調用,且在調用函數中未使用 $this-> 的情況下還能順暢運行。此行為貌似在不同PHP版本中表現不同,在當前的7.3中ok;
  3. 在靜態函數和非靜態函數中輸出 self ,猜猜結果是什麼?都是 string(4) "self" ,迷之輸出;
  4. return $this instanceof static::class; 會有語法錯誤,但是以下兩種寫法就正常:
    $class = static::class;
    return $this instanceof $class;
    // 或者這樣:
    return $this instanceof static;

 

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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

高併發編程學習(1)——併發基礎

為更良好的閱讀體驗,請訪問原文:

一、前言

當我們使用計算機時,可以同時做許多事情,例如一邊打遊戲一邊聽音樂。這是因為操作系統支持併發任務,從而使得這些工作得以同時進行。

  • 那麼提出一個問題:如果我們要實現一個程序能一邊聽音樂一邊玩遊戲怎麼實現呢?
public class Tester {

    public static void main(String[] args) {
        System.out.println("開始....");
        playGame();
        playMusic();
        System.out.println("結束....");
    }

    private static void playGame() {
        for (int i = 0; i < 50; i++) {
            System.out.println("玩遊戲" + i);
        }
    }

    private static void playMusic() {
        for (int i = 0; i < 50; i++) {
            System.out.println("播放音樂" + i);
        }
    }
}

我們使用了循環來模擬過程,因為播放音樂和打遊戲都是連續的,但是結果卻不盡人意,因為函數體總是要執行完之後才能返回。那麼到底怎麼解決這個問題?

并行與併發

并行性和併發性是既相似又有區別的兩個概念。

并行性是指兩個或多個事件在同一時刻發生。而併發性是指兩個或多個事件在同一時間間隔內發生。

在多道程序環境下,併發性是指在一段時間內宏觀上有多個程序在同時運行,但在單處理機環境下(一個處理器),每一時刻卻僅能有一道程序執行,故微觀上這些程序只能是分時地交替執行。例如,在 1 秒鐘時間內,0 – 15 ms 程序 A 運行;15 – 30 ms 程序 B 運行;30 – 45 ms 程序 C 運行;45 – 60 ms 程序 D 運行,因此可以說,在 1 秒鐘時間間隔內,宏觀上有四道程序在同時運行,但微觀上,程序 A、B、C、D 是分時地交替執行的。

如果在計算機系統中有多個處理機,這些可以併發執行的程序就可以被分配到多個處理機上,實現併發執行,即利用每個處理機愛處理一個可併發執行的程序。這樣,多個程序便可以同時執行。以此就能提高系統中的資源利用率,增加系統的吞吐量。

進程和線程

進程是指一個內存中運行的應用程序。一個應用程序可以同時啟動多個進程,那麼上面的問題就有了解決的思路:我們啟動兩個進程,一個用來打遊戲,一個用來播放音樂。這當然是一種解決方案,但是想象一下,如果一個應用程序需要執行的任務非常多,例如 LOL 遊戲吧,光是需要播放的音樂就有非常多,人物本身的語音,技能的音效,遊戲的背景音樂,塔攻擊的聲音等等等,還不用說遊戲本身,就光播放音樂就需要創建許多許多的進程,而進程本身是一種非常消耗資源的東西,這樣的設計顯然是不合理的。更何況大多數的操作系統都不需要一個進程訪問其他進程的內存空間,也就是說,進程之間的通信很不方便,此時我們就得引入“線程”這門技術,來解決這個問題。

線程是指進程中的一個執行任務(控制單元),一個進程可以同時併發運行多個線程。我們可以打開任務管理器,觀察到幾乎所有的進程都擁有着許多的「線程」(在 WINDOWS 中線程是默認隱藏的,需要在「查看」裏面點擊「選擇列」,有一個線程數的勾選項,找到並勾選就可以了)。

進程和線程的區別

進程:有獨立的內存空間,進程中的數據存放空間(堆空間和棧空間)是獨立的,至少有一個線程。

線程:堆空間是共享的,棧空間是獨立的,線程消耗的資源也比進程小,相互之間可以影響的,又稱為輕型進程或進程元。

因為一個進程中的多個線程是併發運行的,那麼從微觀角度上考慮也是有先後順序的,那麼哪個線程執行完全取決於 CPU 調度器(JVM 來調度),程序員是控制不了的。我們可以把多線程併發性看作是多個線程在瞬間搶 CPU 資源,誰搶到資源誰就運行,這也造就了多線程的隨機性。下面我們將看到更生動的例子。

Java 程序的進程(Java 的一個程序運行在系統中)里至少包含主線程和垃圾回收線程(後台線程),你可以簡單的這樣認為,但實際上有四個線程(了解就好):

  • [1] main——main 線程,用戶程序入口
  • [2] Reference Handler——清除 Reference 的線程
  • [3] Finalizer——調用對象 finalize 方法的線程
  • [4] Signal Dispatcher——分發處理髮送給 JVM 信號的線程

多線程和單線程的區別和聯繫?

  1. 單核 CPU 中,將 CPU 分為很小的時間片,在每一時刻只能有一個線程在執行,是一種微觀上輪流佔用 CPU 的機制。

  2. 多線程會存在線程上下文切換,會導致程序執行速度變慢,即採用一個擁有兩個線程的進程執行所需要的時間比一個線程的進程執行兩次所需要的時間要多一些。

結論:即採用多線程不會提高程序的執行速度,反而會降低速度,但是對於用戶來說,可以減少用戶的響應時間。

多線程的優勢

儘管面臨很多挑戰,多線程有一些優點仍然使得它一直被使用,而這些優點我們應該了解。

優勢一:資源利用率更好

想象一下,一個應用程序需要從本地文件系統中讀取和處理文件的情景。比方說,從磁盤讀取一個文件需要 5 秒,處理一個文件需要 2 秒。處理兩個文件則需要:

1| 5秒讀取文件A
2| 2秒處理文件A
3| 5秒讀取文件B
4| 2秒處理文件B
5| ---------------------
6| 總共需要14秒

從磁盤中讀取文件的時候,大部分的 CPU 時間用於等待磁盤去讀取數據。在這段時間里,CPU 非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用 CPU 資源。看下面的順序:

1| 5秒讀取文件A
2| 5秒讀取文件B + 2秒處理文件A
3| 2秒處理文件B
4| ---------------------
5| 總共需要12秒

CPU 等待第一個文件被讀取完。然後開始讀取第二個文件。當第二文件在被讀取的時候,CPU 會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU 大部分時間是空閑的。

總的說來,CPU 能夠在等待 IO 的時候做一些其他的事情。這個不一定就是磁盤 IO。它也可以是網絡的 IO,或者用戶輸入。通常情況下,網絡和磁盤的 IO 比 CPU 和內存的 IO 慢的多。

優勢二:程序設計在某些情況下更簡單

在單線程應用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個文件讀取和處理的狀態。相反,你可以啟動兩個線程,每個線程處理一個文件的讀取和操作。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時候,其他的線程能夠使用 CPU 去處理已經讀取完的文件。其結果就是,磁盤總是在繁忙地讀取不同的文件到內存中。這會帶來磁盤和 CPU 利用率的提升。而且每個線程只需要記錄一個文件,因此這種方式也很容易編程實現。

優勢三:程序響應更快

有時我們會編寫一些較為複雜的代碼(這裏的複雜不是說複雜的算法,而是複雜的業務邏輯),例如,一筆訂單的創建,它包括插入訂單數據、生成訂單趕快找、發送郵件通知賣家和記錄貨品銷售數量等。用戶從單擊“訂購”按鈕開始,就要等待這些操作全部完成才能看到訂購成功的結果。但是這麼多業務操作,如何能夠讓其更快地完成呢?

在上面的場景中,可以使用多線程技術,即將數據一致性不強的操作派發給其他線程處理(也可以使用消息隊列),如生成訂單快照、發送郵件等。這樣做的好處是響應用戶請求的線程能夠盡可能快地處理完成,縮短了響應時間,提升了用戶體驗。

其他優勢

多線程還有一些優勢也顯而易見:

  • 進程之前不能共享內存,而線程之間共享內存(堆內存)則很簡單。
  • 系統創建進程時需要為該進程重新分配系統資源,創建線程則代價小很多,因此實現多任務併發時,多線程效率更高.
  • Java 語言本身內置多線程功能的支持,而不是單純地作為底層系統的調度方式,從而簡化了多線程編程.

上下文切換

即使是單核處理器也支持多線程執行代碼,CPU 通過給每個線程分配 CPU 時間片來實現這個機制。時間片是 CPU 分配給各個線程的時間,因為時間片非常短,所以 CPU 通過不停地切換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒(ms)。

CPU 通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務的時候,可以再加載這個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。

這就像我們同時讀兩本書,當我們在讀一本英文的技術書時,發現某個單詞不認識,於是打開中英文字典,但是在放下英文技術書之前,大腦必須先記住這本書獨到了多少頁的多少行,等查完單詞之後,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。

二、創建線程的兩種方式

繼承 Thread 類

public class Tester {

    // 播放音樂的線程類
    static class PlayMusicThread extends Thread {

        // 播放時間,用循環來模擬播放的過程
        private int playTime = 50;

        public void run() {
            for (int i = 0; i < playTime; i++) {
                System.out.println("播放音樂" + i);
            }
        }
    }

    // 方式1:繼承 Thread 類
    public static void main(String[] args) {
        // 主線程:運行遊戲
        for (int i = 0; i < 50; i++) {
            System.out.println("打遊戲" + i);
            if (i == 10) {
                // 創建播放音樂線程
                PlayMusicThread musicThread = new PlayMusicThread();
                musicThread.start();
            }
        }
    }
}

運行結果發現打遊戲和播放音樂交替出現,說明已經成功了。

實現 Runnable 接口

public class Tester {

    // 播放音樂的線程類
    static class PlayMusicThread implements Runnable {

        // 播放時間,用循環來模擬播放的過程
        private int playTime = 50;

        public void run() {
            for (int i = 0; i < playTime; i++) {
                System.out.println("播放音樂" + i);
            }
        }
    }

    // 方式2:實現 Runnable 方法
    public static void main(String[] args) {
        // 主線程:運行遊戲
        for (int i = 0; i < 50; i++) {
            System.out.println("打遊戲" + i);
            if (i == 10) {
                // 創建播放音樂線程
                Thread musicThread = new Thread(new PlayMusicThread());
                musicThread.start();
            }
        }
    }
}

也能完成效果。

以上就是傳統的兩種創建線程的方式,事實上還有第三種,我們後邊再講。

多線程一定快嗎?

先來一段代碼,通過并行和串行來分別執行累加操作,分析:下面的代碼併發執行一定比串行執行快嗎?

import org.springframework.util.StopWatch;

// 比較并行和串行執行累加操作的速度
public class Tester {

    // 執行次數
    private static final long COUNT = 100000000;
    private static final StopWatch TIMER = new StopWatch();

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
        // 打印比較測試結果
        System.out.println(TIMER.prettyPrint());
    }

    private static void serial() {
        TIMER.start("串行執行" + COUNT + "條數據");

        int a = 0;
        for (long i = 0; i < COUNT; i++) {
            a += 5;
        }
        // 串行執行
        int b = 0;
        for (long i = 0; i < COUNT; i++) {
            b--;
        }

        TIMER.stop();
    }

    private static void concurrency() throws InterruptedException {
        TIMER.start("并行執行" + COUNT + "條數據");

        // 通過匿名內部類來創建線程
        Thread thread = new Thread(() -> {
            int a = 0;
            for (long i = 0; i < COUNT; i++) {
                a += 5;
            }
        });
        thread.start();

        // 并行執行
        int b = 0;
        for (long i = 0; i < COUNT; i++) {
            b--;
        }
        // 等待線程結束
        thread.join();
        TIMER.stop();
    }
}

大家可以自己測試一下,每一台機器 CPU 不同測試結果可能也會不同,之前在 WINDOWS 本兒上測試的時候,多線程的優勢從 1 千萬數據的時候才開始體現出來,但是現在換了 MAC,1 億條數據時間也差不多,到 10 億的時候明顯串行就比并行快了… 總之,為什麼併發執行的速度會比串行慢呢?就是因為線程有創建和上下文切換的開銷。

繼承 Thread 類還是實現 Runnable 接口?

想象一個這樣的例子:給出一共 50 個蘋果,讓三個同學一起來吃,並且給蘋果編上號碼,讓他們吃的時候順便要說出蘋果的編號:

運行結果可以看到,使用繼承方式實現,每一個線程都吃了 50 個蘋果。這樣的結果顯而易見:是因為顯式地創建了三個不同的 Person 對象,而每個對象在堆空間中有獨立的區域來保存定義好的 50 個蘋果。

而使用實現方式則滿足要求,這是因為三個線程共享了同一個 Apple 對象,而對象中的 num 數量是一定的。

所以可以簡單總結出繼承方式和實現方式的區別:

繼承方式:

  1. Java 中類是單繼承的,如果繼承了 Thread 了,該類就不能再有其他的直接父類了;
  2. 從操作上分析,繼承方式更簡單,獲取線程名字也簡單..(操作上,更簡單)
  3. 從多線程共享同一個資源上分析,繼承方式不能做到…

實現方式:

  1. Java 中類可以實現多個接口,此時該類還可以繼承其他類,並且還可以實現其他接口(設計上,更優雅)..
  2. 從操作上分析,實現方式稍微複雜點,獲取線程名字也比較複雜,需要使用 Thread.currentThread() 來獲取當前線程的引用..
  3. 從多線程共享同一個資源上分析,實現方式可以做到..

在這裏,三個同學完成搶蘋果的例子,使用實現方式才是更合理的方式。

對於這兩種方式哪種好並沒有一個確定的答案,它們都能滿足要求。就我個人意見,我更傾向於實現 Runnable 接口這種方法。因為線程池可以有效的管理實現了 Runnable 接口的線程,如果線程池滿了,新的線程就會排隊等候執行,直到線程池空閑出來為止。而如果線程是通過實現 Thread 子類實現的,這將會複雜一些。

有時我們要同時融合實現 Runnable 接口和 Thread 子類兩種方式。例如,實現了 Thread 子類的實例可以執行多個實現了 Runnable 接口的線程。一個典型的應用就是線程池。

常見錯誤:調用 run() 方法而非 start() 方法

創建並運行一個線程所犯的常見錯誤是調用線程的 run() 方法而非 start() 方法,如下所示:

1| Thread newThread = new Thread(MyRunnable());
2| newThread.run();  //should be start();

起初你並不會感覺到有什麼不妥,因為 run() 方法的確如你所願的被調用了。但是,事實上,run() 方法並非是由剛創建的新線程所執行的,而是被創建新線程的當前線程所執行了。也就是被執行上面兩行代碼的線程所執行的。想要讓創建的新線程執行 run() 方法,必須調用新線程的 start() 方法。

三、線程的安全問題

吃蘋果遊戲的不安全問題

我們來考慮一下上面吃蘋果的例子,會有什麼問題?

儘管,Java 並不保證線程的順序執行,具有隨機性,但吃蘋果比賽的案例運行多次也並沒有發現什麼太大的問題。這並不是因為程序沒有問題,而只是問題出現的不夠明顯,為了讓問題更加明顯,我們使用 Thread.sleep() 方法(經常用來模擬網絡延遲)來讓線程休息 10 ms,讓其他線程去搶資源。(注意:在程序中並不是使用 Thread.sleep(10)之後,程序才出現問題,而是使用之後,問題更明顯.)

為什麼會出現這樣的錯誤呢?

先來分析第一種錯誤:為什麼會吃重複的蘋果呢?就拿 B 和 C 都吃了編號為 47 的蘋果為例吧:

  • A 線程拿到了編號為 48 的蘋果,打印輸出然後讓 num 減 1,睡眠 10 ms,此時 num 為 47。
  • 這時 B 和 C 同時都拿到了編號為 47 的蘋果,打印輸出,在其中一個線程作出了減一操作的時候,A 線程從睡眠中醒過來,拿到了編號為 46 的蘋果,然後輸出。在這期間並沒有任何操作不允許 B 和 C 線程不能拿到同一個編號的蘋果,之前沒有明顯的錯誤僅僅可能只是因為運行速度太快了。

再來分析第二種錯誤:照理來說只應該存在 1-50 編號的蘋果,可是 0 和-1 是怎麼出現的呢?

  • 當 num = 1 的時候,A,B,C 三個線程同時進入了 try 語句進行睡眠。
  • C 線程先醒過來,輸出了編號為 1 的蘋果,然後讓 num 減一,當 C 線程醒過來的時候發現 num 為 0 了。
  • A 線程醒過來一看,0 都沒有了,只有 -1 了。

歸根結底是因為沒有任何操作來限制線程來獲取相同的資源並對他們進行操作,這就造成了線程安全性問題。

如果我們把打印和減一的操作分成兩個步驟,會更加明顯:

ABC 三個線程同時打印了 50 的蘋果,然後同時做出減一操作。

像這樣的原子操作,是不允許分步驟進行的,必須保證同步進行,不然可能會引發不可設想的後果。

要解決上述多線程併發訪問一個資源的安全性問題,就需要引入線程同步的概念。

線程同步

多個執行線程共享一個資源的情景,是最常見的併發編程情景之一。為了解決訪問共享資源錯誤或數據不一致的問題,人們引入了臨界區的概念:用以訪問共享資源的代碼塊,這個代碼塊在同一時間內只允許一個線程執行。

為了幫助編程人員實現這個臨界區,Java(以及大多數編程語言)提供了同步機制,當一個線程試圖訪問一個臨界區時,它將使用一種同步機制來查看是不是已經有其他線程進入臨界區。如果沒有其他線程進入臨界區,他就可以進入臨界區。如果已經有線程進入了臨界區,它就被同步機制掛起,直到進入的線程離開這個臨界區。如果在等待進入臨界區的線程不止一個,JVM 會選擇其中的一個,其餘的將繼續等待。

synchronized 關鍵字

如果一個對象已用 synchronized 關鍵字聲明,那麼只有一個執行線程被允許訪問它。使用 synchronized 的好處顯而易見:保證了多線程併發訪問時的同步操作,避免線程的安全性問題。但是壞處是:使用 synchronized 的方法/代碼塊的性能比不用要低一些。所以好的做法是:盡量減小 synchronized 的作用域。

我們還是先來解決吃蘋果的問題,考慮一下 synchronized 關鍵字應該加在哪裡呢?

發現如果還再把 synchronized 關鍵字加在 if 裏面的話,0 和 -1 又會出來了。這其實是因為當 ABC 同是進入到 if 語句中,等待臨界區釋放的時,拿到 1 編號的線程已經又把 num 減一操作了,而此時最後一個等待臨界區的進程拿到的就會是 -1 了。

同步鎖 Lock

Lock 機制提供了比 synchronized 代碼塊和 synchronized 方法更廣泛的鎖定操作,同步代碼塊/ 同步方法具有的功能 Lock 都有,除此之外更強大,更體現面向對象。在併發包的類族中,Lock 是 JUC 包的頂層接口,它的實現邏輯並未用到 synchronized,而是利用了 volatile 的可見性。

使用 Lock 最典型的代碼如下:

class X {

    private final ReentrantLock lock = new ReentrantLock();

    public void m() {
        lock.lock();
        try {
            // ..... method body
        } finally {
            lock.unlock();
        }
    }
}

線程安全問題

線程安全問題只在多線程環境下才會出現,單線程串行執行不存在此類問題。保證高併發場景下的線程安全,可以從以下四個維度考量:

維度一:數據單線程可見

單線程總是安全的。通過限制數據僅在單線程內可見,可以避免數據被其他線程篡改。最典型的就是線程局部變量,它存儲在獨立虛擬機棧幀的局部變量表中,與其他線程毫無瓜葛。TreadLocal 就是採用這種方式來實現線程安全的。

維度二:只讀對象

只讀對象總是安全的。它的特性是允許複製、拒絕寫入。最典型的只讀對象有 String、Integer 等。一個對象想要拒絕任何寫入,必須要滿足以下條件:

  • 使用 final 關鍵字修飾類,避免被繼承;
  • 使用 private final 關鍵字避免屬性被中途修改;
  • 沒有任何更新方法;
  • 返回值不能為可變對象。

維度三:線程安全類

某些線程安全類的內部有非常明確的線程安全機制。比如 StringBuffer 就是一個線程安全類,它採用 synchronized 關鍵字來修飾相關方法。

維度四:同步與鎖機制

如果想要對某個對象進行併發更新操作,但又不屬於上述三類,需要開發工程師在代碼中實現安全的同步機制。雖然這個機制支持的併發場景很有價值,但非常複雜且容易出現問題。

處理線程安全的核心理念

要麼只讀,要麼加鎖。

合理利用好 JDK 提供的併發包,往往能化腐朽為神奇。Java 併發包(java.util.concurrent,JUC)中大多數類註釋都寫有:@author Doug Lea。如果說 Java 是一本史書,那麼 Doug Lea 絕對是開疆拓土的偉大人物。Doug Lea 在當大學老師時,專攻併發編程和併發數據結構設計,主導設計了 JUC 併發包,提高了 Java 併發編程的易用性,大大推進了 Java 的商用進程。

參考資料

  • 《Java 零基礎入門教程》 –
  • 《Java 併發編程的藝術》
  • 《Java 7 併發編程實戰手冊》
  • 《碼出高效 Java 開發手冊》 – 楊冠寶(孤盡) 高海慧(鳴莎)著

按照慣例黏一個尾巴:

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

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

※高價收購3C產品,價格不怕你比較

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

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

drf組件之jwt認證

drf組件之jwt認證模塊

一、認證規則

全稱:json web token
解釋:加密字符串的原始數據是json,後台產生,通過web傳輸給前台存儲
格式:三段式 – 頭.載荷.簽名 – 頭和載荷用的是base64可逆加密,簽名用md5不可逆加密
內容:
頭(基礎信息,也可以為空):加密方式、公司信息、項目組信息、…
載荷(核心信息):用戶信息、過期時間、…
簽名(安全保障):頭加密結果+載荷加密結果+服務器秘鑰 的md5加密結果

認證規則:
後台一定要保障 服務器秘鑰 的安全性(它是jwt的唯一安全保障)
後台簽發token(login接口 ) -> 前台存儲 -> 發送需要認證的請求帶着token -> 後台校驗得到合法的用戶 -> 權限管理

為什麼要有jwt認證:
1)服務器壓力小, 後台不需要存儲token,只需要存儲簽發與校驗token的算法,效率遠遠大於後台存儲和取出token完成校驗
2) jwt算法認證,更適合服務器集群部署

二、認證模塊

安裝:pip install djangorestframework-jwt
模塊包:rest_framework_jwt

採用drf-jwt框架,後期任務只需要書寫登錄
為什麼要重寫登錄:drf-jwt只完成了賬號密碼登錄,我們還需要手機登錄,郵箱登錄
為什麼不需要重寫認證類:因為認證規則已經完成且固定不變,變得只有認證字符串的前綴,前綴可以在配置文件中配置

三、JWT使用

jwt配置;

在settings.py文件中配置,如果不配置,默認走jwt默認的

jwt插件的三個接口:

在urls.py中配置

在postman中測試一下籤發token

注意:上面三個接口都是發送POST請求

四、利用JWT實現多方式登錄

注:APIResponse 為自定義Response對象

# views.py
from rest_framework.views import APIView
from . import models,serializers
from utils.response import APIResponse

class LoginAPIView(APIView):
    # 登錄接口應該禁用所有的認證和、權限,因為不管是誰都應該能進來
    authentication_classes = []
    permission_classes = []
    def post(self, request, *args, **kwargs):
        # 將數據傳到序列化組件進行校驗
        user_ser = serializers.LoginSerializer(data=request.data)
        user_ser.is_valid(raise_exception=True)

        return APIResponse(msg='login success', data={
            'username': user_ser.user.username,
            'token': user_ser.token
        })

注意:

通過user對象生成payload載荷
payload = jwt_payload_handler(user)

通過payload簽發token
token = jwt_encode_handler(payload)

# serializer.py
from rest_framework.serializers import ModelSerializer, CharField, ValidationError, SerializerMethodField
from . import models
from django.contrib.auth import authenticate
import re
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler

class LoginSerializer(ModelSerializer):
    username = CharField(write_only=True)
    password = CharField(write_only=True)
    class Meta:
        model = models.User
        fields = ('username', 'password')

    def validate(self, attrs):
        # user_obj = authenticate(**attrs)
        # if not user_obj:
        #     raise ValidationError('用戶名或密碼錯誤')

        # 賬號密碼登錄 ==》 多方式登錄
        user = self._many_method_login(**attrs)

        # 通過user對象生成payload載荷
        payload = jwt_payload_handler(user)
        # 通過payload簽發token
        token = jwt_encode_handler(payload)

        # 將user和token存放在序列化對象中,方便返回到前端去
        self.user = user
        self.token = token

        return attrs

    # 多方式登錄 (用戶名、郵箱、手機號三種方式登錄)
    def _many_method_login(self, **attrs):
        username = attrs.get('username')
        password = attrs.get('password')
        # 利用正則匹配判斷用戶輸入的信息
        # 1.判斷郵箱登錄
        if re.match(r'.*@.*', username):
            user = models.User.objects.filter(email=username).first()  # type: models.User
        # 2.判斷手機號登錄
        elif re.match(r'^1[3-9][0-9]{9}$',username):
            user = models.User.objects.filter(mobile=username).first()
        # 3.用戶名登錄
        else:
            user = models.User.objects.filter(username=username).first()

        if not user:
            raise ValidationError({'username': '賬號有誤'})

        if not user.check_password(password):
            raise ValidationError({'password': '密碼錯誤'})

        return user

使用postman測試代碼:

五、前後台分離模式下信息交互規則

"""
1)任何人都能直接訪問的接口
    請求不是是get、還是post等,不需要做任何校驗

2)必須登錄后才能訪問的接口
    任何請求方式都可能做該方式的限制,請求必須在請求頭中攜帶認證信息 - authorization
    
3)前台的認證信息獲取只能通過登錄接口
    前台提供賬號密碼等信息,去後台換認證信息token
    
4)前台如何完成登錄註銷
    前台登錄成功一般在cookie中保存認證信息token,分離註銷就是前台主動清除保存的token信息
"""

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

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

平板收購,iphone手機收購,二手筆電回收,二手iphone收購-全台皆可收購

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

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

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

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

【前端知識體系-JS相關】10分鐘搞定JavaScript正則表達式高頻考點

1.正則表達式基礎

1.1 創建正則表達式

1.1.1 使用一個正則表達式字面量

const regex = /^[a-zA-Z]+[0-9]*\W?_$/gi;

1.1.2 調用RegExp對象的構造函數

const regex = new RegExp(pattern, [, flags])

1.1.3 特殊字符

 - ^ 匹配輸入的開始
 - $ 匹配輸入的結束
 - \* 0次或多次  {0,}
 - \+ 1次或多次  {1,}
 - ?
   - 0次或者1次 {0,1}。
   - 用於先行斷言
   - 如果緊跟在任何量詞 *、 +、? 或 {} 的後面,將會使量詞變為非貪婪
     - 對 "123abc" 用 /\d+/ 將會返回 "123",
     - 用 /\d+?/,那麼就只會匹配到 "1"。
 - . 匹配除換行符之外的任何單個字符
 - (x)  匹配 'x' 並且記住匹配項
 - (?:x)  匹配 'x' 但是不記住匹配項
 - x(?=y)  配'x'僅僅當'x'後面跟着'y'.這種叫做正向肯定查找。
 - x(?!y)  匹配'x'僅僅當'x'後面不跟着'y',這個叫做正向否定查找。
 - x|y  匹配‘x’或者‘y’。
 - {n}  重複n次
 - {n, m}  匹配至少n次,最多m次
 - [xyz]   代表 x 或 y 或 z
 - [^xyz]  不是 x 或 y 或 z
 - \d  数字
 - \D  非数字
 - \s  空白字符,包括空格、製表符、換頁符和換行符。
 - \S  非空白字符
 - \w  單詞字符(字母、数字或者下劃線)  [A-Za-z0-9_]
 - \W  非單字字符。[^A-Za-z0-9_]
 - \3  表示第三個分組
 - \b   詞的邊界
   - /\bm/匹配“moon”中得‘m’;
 - \B   非單詞邊界

1.2 使用正則表達式的方法

  • exec 一個在字符串中執行查找匹配的RegExp方法,它返回一個數組(未匹配到則返回null)。
  • test 一個在字符串中測試是否匹配的RegExp方法,它返回true或false。
  • match 一個在字符串中執行查找匹配的String方法,它返回一個數組或者在未匹配到時返回null。
  • search 一個在字符串中測試匹配的String方法,它返回匹配到的位置索引,或者在失敗時返回-1。
  • replace 一個在字符串中執行查找匹配的String方法,並且使用替換字符串替換掉匹配到的子字符串。
  • split 一個使用正則表達式或者一個固定字符串分隔一個字符串,並將分隔后的子字符串存儲到數組中的String方法。

1.2.1 正則對象的三個方法

        //①test()判斷字符串中是否出現某個字符串,返回布爾值
        var re = /abc/;
        var str = '00abc66';
        console.log(re.test(str));  // true
        //②exec()查找並返回字符串中指定的某個字符串,只匹配一次
        var re = /abc/;
        var str = 'a0bc88abc00abc';
        console.log(re.exec(str));  // ["abc", index: 6, input: "a0bc88abc00abc", groups: undefined]
        //③compile()方法用於改變正則匹配的內容
        var re = /ab/;
        var str = "aabcdef";
        console.log(re.test(str));  //true
        re.compile(/bd/);
        console.log(re.test(str));  //false
        re.compile('66');
        console.log(re.test(str));  //false

1.2.2 字符串中與正則相關的方法

        //①search()方法,返回符合條件的字符串首次出現的位置(下標)
        var re = /abc/;
        var str = '00abc66';
        console.log(str.search(re));        // 2
        //②match()方法,返回查找的結果,如果查詢不到返回NULL
        console.log(str.match(re));         // ["abc", index: 2, input: "00abc66", groups: undefined]
        //③replace()方法,將匹配到的內容替換成指定內容
        console.log(str.replace(re, "*"));
        //④split()方法,將字符串分割成字符串數組
        console.log(str.split(re));

1.3 正則表達式子表達式相關

1.3.1 子表達式

在正則表達式中,通過一對圓括號括起來的內容,我們就稱之為“子表達式”。如:var re = /\d(\d)\d/;

1.3.2 捕獲

在正則表達式中,子表達式匹配到相應的內容時,系統會自動捕獲這個行為,然後將子表達式匹配到的內容放入系統的緩存區中。我們把這個過程就稱之為“捕獲”。

1.3.3 反向引用

在正則表達式中,我們可以使用\n(n>0,正整數,代表系統中的緩衝區編號)來獲取緩衝區中的內容,我們把這個過程就稱之為“反向引用”。

    var str = "d1122jj7667h6868s9999";
    //查找AABB型的数字
    console.log(str.match(/(\d)\1(\d)\2/)); //1122
    //查找ABBA型的数字
    console.log(str.match(/(\d)(\d)\2\1/)); //7667
    //查找ABAB型的数字
    console.log(str.match(/(\d)(\d)\1\2/)); //6868
    //查找四個連續相同的数字
    console.log(str.match(/(\d)\1\1\1/));   //9999

1.4 限定符

[!NOTE]
限定符可以指定正則表達式的一個給定字符必須要出現多少次才能滿足匹配。

    *:匹配前面的子表達式零次或多次,0到多
    +:匹配前面的子表達式一次或多次,1到多
    ?:匹配前面的子表達式零次或一次,0或1
    {n}:匹配確定的 n 次 
    {n,}:至少匹配 n 次 
    {n,m}:最少匹配 n 次且最多匹配 m 次

[!WARNING]
注意:針對於{n,m},正則在匹配到一個符合多種次數的字符串時,優先匹配次數多的,即能匹配到m次就不會匹配n次,這就是貪婪模式(默認)。

如果在其後加?即{n,m}?則會更改為非貪婪模式(惰性模式),則此時正則優先匹配n次。

    var str = "aa1a22a333a6a8a";
    console.log(str.match(/a\d*/));      //a
    console.log(str.match(/a\d+/));      //a1
    console.log(str.match(/a\d?/));      //a
    console.log(str.match(/a\d{3}/));    //a333
    console.log(str.match(/a\d{2,}/));   //a22
    console.log(str.match(/a\d{1,3}/));  //a1
    console.log(str.match(/a\d{1,3}?/)); //a1

    //貪婪模式加深理解,案例如下:
    //貪婪模式下最開始的'a2就符合條件',但是它會返回'a22'
    //注意:它是在遇到一個同時符合多個次數條件的字符串時,取符合次數多字符串
    var str = "a22aa1a333a6a8a";
    console.log(str.match(/a\d{1,3}/));   //a22
    console.log(str.match(/a\d{1,3}/g));  //a22 a1 a333 a6 a8
    console.log(str.match(/a\d{1,3}?/));  //a2
    console.log(str.match(/a\d{1,3}?/g)); //a2 a1 a3 a6 a8

1.5 定位符

[!NOTE]
定位符可以將一個正則表達式固定在一行的開始或結束。也可以創建只在單詞內或只在單詞的開始或結尾處出現的正則表達式。

^ (脫字符):匹配輸入字符串的開始位置
$:匹配輸入字符串的結束位置
\b:匹配一個單詞邊界
\B:匹配非單詞邊界

1.6 正則表達式的匹配模式(修飾符)

[!NOTE]
表示正則匹配的附加規則,放在正則模式的最尾部。修飾符可以單個使用,也可以多個一起使用。

  • ①g全局匹配,找到所有匹配,而不是在第一個匹配后停止
  • ②i匹配全部大小寫
  • ③m多行,將開始和結束字符(^和$)視為在多行上工作(也就是,分別匹配每一行的開始和結束(由\n或\r分割),而不只是只匹配整個輸入字符串的最開始和最末尾處。
  • ④s與m相反,單行匹配
    var re = /^[a-z]/gim;   //可組合使用

1.7 轉義字符

[!NOTE]
因為在正則表達式中 . +  等屬於表達式的一部分,但有時也需要匹配這些特殊字符,所以,需要使用反斜杠對特殊字符進行轉義。

  需要轉義的字符:
  點號.
  小括號()
  中括號[]
  左斜杠/
  右斜杠\
  選擇匹配符|
  
  * 
  ?
  {}
  + 
  $
  ^

2. 正則練習題

2.1 匹配結尾的数字

/\d+$/g

2.2 統計空格個數

字符串內如有空格,但是空格的數量可能不一致,通過正則將空格的個數統一變為一個。

let reg = /\s+/g
str.replace(reg, " ");

2.3 判斷字符串是不是由数字組成

str.test(/^\d+$/);

2.4 電話號碼正則

  • 區號必填為3-4位的数字
  • 區號之後用“-”與電話號碼連接電話號碼為7-8位的数字
  • 分機號碼為3-4位的数字,非必填,但若填寫則以“-”與電話號碼相連接
/^\d{3,4}-\d{7,8}(-\d{3,4})?$/

2.5 手機號碼正則表達式

正則驗證手機號,忽略前面的0,支持130-139,150-159。忽略前面0之後判斷它是11位的。

/^0*1(3|5)\d{9}$/

2.6 使用正則表達式實現刪除字符串中的空格

funtion trim(str) {
  let reg = /^\s+|\s+$/g
  return str.replace(reg, '');
}

2.7 限制文本框只能輸入数字和兩位小數點等等

/^\d*\.\d{0,2}$/

2.8 只能輸入小寫的英文字母和小數點,和冒號,正反斜杠(:./)

/^[a-z\.:\/\\]*$/

2.9 替換小數點前內容為指定內容

例如:infomarket.php?id=197 替換為 test.php?id=197

var reg = /^[^\.]+/;
var target = '---------';
str = str.replace(reg, target)

2.10 只匹配中文的正則表達式

/[\u4E00-\u9FA5\uf900-\ufa2d]/ig

2.11 返回字符串的中文字符個數

先去掉非中文字符,再返回length屬性。

function cLength(str){
  var reg = /[^\u4E00-\u9FA5\uf900-\ufa2d]/g;
  //匹配非中文的正則表達式
  var temp = str.replace(reg,'');
  return temp.length;
}

2.12 正則表達式取得匹配IP地址前三段

只要匹配掉最後一段並且替換為空字符串就行了

function getPreThrstr(str) {
  let reg = /\.\d{1,3}$/;
  return str.replace(reg,'');
}

2.13 匹配ul標籤之間的內容

/<ul>[\s\S]+?</ul>/i

2.14 用正則表達式獲得文件名

c:\images\tupian\006.jpg

可能是直接在盤符根目錄下,也可能在好幾層目錄下,要求替換到只剩文件名。
首先匹配非左右斜線字符0或多個,然後是左右斜線一個或者多個。

function getFileName(str){
  var reg = /[^\\\/]*[\\\/]+/g;
  // xxx\ 或是 xxx/
  str = str.replace(reg,'');
  return str;
}

2.15 絕對路徑變相對路徑

“http://23.123.22.12/image/somepic.gif”轉換為:”/image/somepic.gif”

var reg = /http:\/\/[^\/]+/;
str = str.replace(reg,"");

2.16 用戶名正則

用於用戶名註冊,,用戶名只 能用 中文、英文、数字、下劃線、4-16個字符。

/^[\u4E00-\u9FA5\uf900-\ufa2d\w]{4,16}$/

2.17 匹配英文地址

規則如下:
包含 “點”, “字母”,”空格”,”逗號”,”数字”,但開頭和結尾不能是除字母外任何字符。

/^[a-zA-Z][\.a-zA-Z,0-9]*[a-zA-Z]$/

2.18 正則匹配價格

開頭数字若干位,可能有一個小數點,小數點後面可以有兩位数字。

/^\d+(\.\d{2})?$/

2.19 身份證號碼的匹配

身份證號碼可以是15位或者是18位,其中最後一位可以是X。其它全是数字

/^(\d{14}|\d{17})(X|x)$/

2.20 單詞首字母大寫

每單詞首字大寫,其他小寫。如blue idea轉換為Blue Idea,BLUE IDEA也轉換為Blue Idea

function firstCharUpper(str) {
  str = str.toLowerCase();
  let reg = /\b(\w)/g;
  return str.replace(reg, m => m.toUpperCase());
}

2.21 正則驗證日期格式

yyyy-mm-dd格式, 4位数字,橫線,1或者2位数字,再橫線,最後又是1或者2位数字。

/^\d{4}-\d{1,2}-\d{1,2}$/

2.22 去掉文件的後綴名

www.abc.com/dc/fda.asp 變為 www.abc.com/dc/fda

function removeExp(str) {
  return str.replace(/\.\w$/,'')
}

2.23 驗證郵箱的正則表達式

開始必須是一個或者多個單詞字符或者是-,加上@,然後又是一個或者多個單詞字符或者是-。然後是點“.”和單詞字符和-的組合,可以有一個或者多個組合。

/^[\w-]+@\w+\.\w+$/

2.24 正則判斷標籤是否閉合

標籤可能有兩種方式閉合,自閉和或者對稱閉合的方式。

/<([a-z]+)(\s*\w*?\s*=\s*".+?")*(\s*?>[\s\S]*?(<\/\1>)+|\s*\/>)/i

2.25 正則判斷是否為数字與字母的混合

不能小於12位,且必須為字母和数字的混合

/^(([a-z]+[0-9]+)|([0-9]+[a-z]+))[a-z0-9]*$/i

2.26 將阿拉伯数字替換為中文大寫形式

function replaceReg(reg,str){
  let arr=["零","壹","貳","叄","肆","伍","陸","柒","捌","玖"];
  let reg = /\d/g;
  return str.replace(reg,function(m){return arr[m];})
}

2.27 去掉標籤的所有屬性

<td style="width: 23px; height: 26px;" align="left">***</td>
變成沒有任何屬性的
<td>***</td>

思路:非捕獲匹配屬性,捕獲匹配標籤,使用捕獲結果替換掉字符串。正則如下:

/(<td)\s(?:\s*\w*?\s*=\s*".+?")*?\s*?(>)/

2.28 駝峰表示

String.prototype.camelCase = function () {
        // .*?是非貪婪的匹配,點可以匹配任意字符,星號是前邊的字符有0-n個均匹配,問號是則是0-1;
        // (^\w{1}): 用於匹配第一個首字母
        // (.*):用於匹配任意個的前面的字符,.表示的就是任意字符

        // - param 1: 匹配到的字符串
        // - param 2: 匹配的的子字符串
        // - param 3: 匹配的子字符串
        // - param的位置
        // - param 5: 原始字符串 4: 匹配到的字符串在字符串中

        return this.replace(/(^\w{1})(.*)/g, function (match, g1, g2) {
            return g1.toUpperCase() + g2.toLowerCase();
        });
    }

2.29 模板字符串

// str = 'name: @(name), age:@(age)'
       // data = {name : 'xiugang', age : 18}
       /**
        * 實現一個簡單的數據綁定
        * @param str
        * @param data
        * @return {*}
        */
       String.prototype.formateString = function (data) {
           return this.replace(/@\((\w+)\)/g, function (match, key) {
               // 注意這裏找到的值必須返回出去(如果是undefined,就是沒有數據)
               // 注意:判斷一個值的類型是不是undefined,可以通過typeof判斷
               console.log(typeof data[key] === 'undefined');
               return data[key] === 'undefined' ? '' : data[key];
           });

       }

2.30 去掉兩邊的空格

/**
        * 去掉兩邊的空格
        * @param str
        * @return {*}
        */
       String.prototype.trim = function () {
           return this.replace(/(^\s*)|(\s*$)/g, '');
       }

2.31 獲取url參數: 使用replace保存到一個數組裡面,然後從數組裡面取出數據

'http://www.189dg.com/ajax/sms_query.ashx?undefined&undefined&undefined-06-27&undefined-06-27'
 url.replace(/(\w+)=(\w+)/g, function(a, b, c){
   console.log(a, b, c)
 })
action=smsdetail action smsdetail
sid=22 sid 22
stime=2014 stime 2014
etime=2014 etime 2014


// 封裝為一個函數
var url = "http://127.0.0.1/e/action/ShowInfo.php?classid=9&id=2";
function parse_url(_url){
 var pattern = /(\w+)=(\w+)/ig;
 var parames = {};
 url.replace(pattern, function(a, b, c){
   parames[b] = c;
 });
 return parames;
}
var parames = parse_url(url);
alert(parames['classid'] + ", " + parames['id']);

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

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

※想要讓你的商品在網路上成為最夯、最多人討論的話題?

※高價收購3C產品,價格不怕你比較

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

ASP.NET Aries 高級開發教程:如何寫WebAPI接口

前提:

最近,有不少同學又問到,Aries里如何提供WebAPI接口?

針對這個問題,今天給順路寫個教程,其實呢,很簡單的。

方式一:直接用WebService提供接口。

用這種方式,直接添加接口就可以了,Aries只是默認處理了.html後綴的請求。對於WS的asmx後綴是沒有影響的,所以傳統怎麼添加就怎麼添加。

 

方式二:單獨用Taurus.MVC寫一個接口項目。

用這種方式,就是把接口獨立成一個項目,然後通過IIS部署成子應用程序就可以了。

只是部署成子應用程序的時候,需要注意一下子目錄和根目錄的web.config,出現重複的只能留根目錄的那個。

(一般都會建議用戶用這種方式,好處是可以在IIS里學會一下怎麼部署子應用程序。)

方式三:在Aries引入Taurus.MVC即可。

這個方式,其實也很簡單,下面介紹一下簡單的部署:

1、引用Taurus.MVC用於寫WebAPI:

在Web.UI項目添加引用Taurus.Core.dll(可以在Nuget上引用,也可以引用源碼項目再引用項目)

2、配置Taurus.MVC的兩個必備項:

A、在HttpModule中添加URL攔截。

//這是原來有的: 
<add name="Aries.Core" type="Aries.Core.UrlRewrite,Aries.Core" />
//這是新添加的:
 <add name="Taurus.Core" type="Taurus.Core.UrlRewrite,Taurus.Core"/>

B、在AppSetting中設置路徑模式:

<!--配置模式【值為0,1或2】[默認為1]
      值為0:匹配{Action}/{Para}
      值為1:匹配{Controller}/{Action}/{Para}
      值為2:匹配{Module}/{Controller}/{Action}/{Para}-->
    <add key="Taurus.RouteMode" value="1"/>

C、在AppSetting中設置接口代碼所在的項目:

<!--指定控制器所在的項目(Dll)名稱(可改,項目編繹的dll叫什麼名就寫什麼)
    <add key="Taurus.Controllers" value="Taurus.Controllers"/>-->

 

如果是用Nuget上引用的,默認都會有上面的兩個,其它默認生的,可以註釋掉。

3、開始寫應用接口代碼:

接口代碼寫在哪裡呢?放在哪個項目都可以,只要上面C點的配置指向就可以了,如果接口代碼分散在多個項目中,配置的value可以用“逗號”分隔。

按Taurus.MVC的方式寫接口,繼承自Taurus.Core.Controller即可:

如:

  /// <summary>
    /// API 接口
    /// </summary>
    public  class APIController : Taurus.Core.Controller
    {
        public void Hello()
        {
            Write("hello Controllers.API");
        }
    }

接口訪問:http://…/api/hello

總結說明:

Aries中默認處理的是.html後綴。

Taurus默認處理的是無後綴。

所以兩者並無衝突,直接引用,加配置就可以了,沒你想的複雜。

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

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

※高價3c回收,收購空拍機,收購鏡頭,收購 MACBOOK-更多收購平台討論專區

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

收購3c瘋!各款手機、筆電、相機、平板,歡迎來詢價!

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

串燒 JavaCAS相關知識

JMM與問題引入

為啥先說JMM,因為CAS的實現類中維護的變量都被volatile修飾, 這個volatile 是遵循JMM規範(不是百分百遵循,下文會說)實現的保證多線程併發訪問某個變量實現線程安全的手段

一連串的知識點慢慢縷

首先說什麼是JMM, JMM就是大家所說的java的內存模型, 它是人們在邏輯上做出的劃分, 或者可以將JMM當成是一種規範, 有哪些規範呢? 如下

  1. 可見性: 某一個線程對內存中的變量做出改動后,要求其他的線程在第一事件內馬上馬得到通知,在CAS的實現中, 可見性其實是通過不斷的while循環讀取而得到的通知, 而不是被動的得到通知
  2. 原子性: 線程在執行某個操作的時,要麼一起成功,要麼就一起失敗
  3. 有序性: 為了提高性能, 編譯器處理器會進行指令的重排序, 源碼-> 編譯器優化重排 -> 處理器優化重排 -> 內存系統重排 -> 最終執行的命令

JVM運行的實體是線程, 每一個線程在創建之後JVM都會為其創建一個工作空間, 這個工作空間是每一個線程之間的私有空間, 並且任何兩條線程之間的都不能直接訪問到對方的工作空間, 線程之間的通信,必須通過共享空間來中轉完成

JMM規定所有的變量全部存在主內存中,主內存是一塊共享空間,那麼如果某個線程相對主內存中共享變量做出修改怎麼辦呢? 像下面這樣:

  1. 將共享變量的副本拷貝到工作空間中
  2. 對變量進行賦值修改
  3. 將工作空間中的變量寫回到內存中

JMM還規定如下:

  • 任何線程在解鎖前必須將工作空間的共享變量立即刷新進內存中
  • 線程在加鎖前必須讀取主內存中的值更新到自己的工作空間中
  • 加鎖和解鎖是同一把鎖

問題引入

這時候如果多個線程併發按照上面的三步走去訪問主內存中的共享變量的話就會出現線程安全性的問題, 比如說 現在主內存中的共享變量是c=1, 有AB兩個線程去併發訪問這個c變量, 都想進行c++, 現在A將c拷貝到自己的工作空間進行c++, 於是c=2 , 於此同時線程B也進行c++, c在B的工作空間中=2, AB線程將結果寫回工作空間最終的結果就是2, 而不是我們預期的3

相信怎麼解決大家都知道, 就是使用JUC,中的原子類就能規避這個問題

而原子類的底層實現使用的就是CAS技術

什麼是CAS

CAS(compare and swap) 顧名思義: 比較和交換,在JUC中原子類的底層使用的都是CAS無鎖實現線程安全,是一門很炫的技術

如下面兩行代碼, 先比較再交換, 即: 如果從主內存中讀取到的值為4就將它更新為2019

        AtomicInteger atomicInteger = new AtomicInteger(4);
        atomicInteger.compareAndSet(4,2019);

跟進AtomicInteger的源碼如下, 底層維護着一個int 類型的 變量, (當然是因為我選擇的原來類是AtomicInteger類型), 並且這個int類型的值被 volatile 修飾

    private volatile int value;

    /**
     * Creates a new AtomicInteger with the given initial value.
     *
     * @param initialValue the initial value
     */
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

什麼是volatile

volatile是JVM提供的輕量的同步機制, 為什麼是輕量界別呢? , 剛才在上面說了JMM規範中提到了三條特性, 而JVM提供的volatile僅僅滿足上面的規範中的 2/3, 如下:

  1. 保證可見性
  2. 不保證原子性
  3. 禁止指令重排序

單獨的volatile是不能滿足原子性的,即如下代碼在多線程併發訪問的情況下依然會出現線程安全性問題

private volatile int value;
 
public void add(){
  value++;   
}

那麼JUC的原子類是如何實現的 可以滿足原子性呢? 於是就不得不說本片博文的主角, CAS

CAS源碼跟進

我們跟進AtomicInteger中的先遞增再獲取的方法 incrementAndGet()

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

通過代碼我們看到調用了Unsafe類來實現

什麼是Unsafe類?

進入Unsafe類,可以看到他裏面存在大量的 native方法,這些native方法全部是空方法,

這個unsafe類其實相當於一個後門,他是java去訪問調用系統上 C C++ 函數類庫的方法 如下圖

繼續跟進這個方法incrementAndGet() 於是我們就來到了我們的主角方法, 關於這個方法倒是不難理解,主要是搞清楚方法中的var12345到底代表什麼就行, 如下代碼+註釋

var1: 上一個方法傳遞進來的: this,即當前對象
var2: 上一個方法傳遞進來的valueOffset, 就是內存地址偏移量
      通過這個內存地址偏移量我能精確的找到要操作的變量在內存中的地址
      
var4: 上一個方法傳遞進來的1, 就是每次增長的值
var5: 通過this和內存地址偏移量讀取出來的當前內存中的目標值
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

注意它用的是while循環, 相對if(flag){} 這種寫法會多一次判斷, 整體的思路就是 在進行修改之前先進行一次比較,如果讀取到的當前值和預期值是相同的,就自增,否則的話就繼續輪詢修改

小總結

通過上面的過程, 其實就能總結出CAS的底層實現原理

  • volatile
  • 自旋鎖
  • unsafe類

補充: CAS通過Native方法的底層實現,本質上是操作系統層面上的CPU的併發原語,JVM會直接實現出彙編層面的指令,依賴於硬件去實現, 此外, 對於CPU的原語來說, 有兩條特性1,必定連續, 2.不被中斷

CAS的優缺點

優點:

它的底層我們看到了通過do-while 實現的自旋鎖來實現, 就省去了在多個線程之間進行切換所帶來的額外的上下文切換的開銷

缺點:

  1. 通過while循環不斷的嘗試獲取, 省去了上下文切換的開銷,但是佔用cpu的資源
  2. CAS只能保證一個共享變量的原子性, 如果存在多個共享變量的話不得不加鎖實現
  3. 存在ABA問題

ABA問題

什麼是ABA問題

我們這樣玩, 還是AB兩個線程, 給AtomicInteger賦初始值0

A線程中的代碼如下:

        Thread.sleep(3000);
        atomicInteger.compareAndSet(0,2019);

B線程中的代碼如下:

        atomicInteger.compareAndSet(0,1);
        atomicInteger.compareAndSet(1,0);

AB線程同時啟動, 雖然最終的結果A線程能成果的將值修改成2019,,但是它不能感知到在他睡眠過程中B線程對數據進行過改變, 換句話說就是A線程被B線程欺騙了

ABA問題的解決— AtomicStampedRefernce.java

帶時間戳的原子引用, 實現的機制就是通過 原子引用+版本號來完成, 每次對指定值的修改相應的版本號會加1, 實例如下

        // 0表示初始化, 1表示初始版本號
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(0, 1);
        reference.getStamp(); // 獲取版本號
        reference.attemptStamp(1,2); // 期待是1, 如果是1就更新為2

原子引用

JUC中我們可以找到像AtomicInteger這樣已經定義好了實現類, 但是JUC沒有給我們提供類似這樣 AtomicUser或者 AtomicProduct 這樣自定義類型的原子引用類型啊, 不過java仍然是提供了後門就是 原子引用類型

使用實例:

        User user  = getUserById(1);
        AtomicReference<User> userAtomicReference = new AtomicReference<User>();
        user.setUsername("張三");
        userAtomicReference.compareAndSet(user,user);

歡迎關注我, 會繼續更新筆記

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

3c收購,鏡頭 收購有可能以全新價回收嗎?

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

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

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

賣IPHONE,iPhone回收,舊換新!教你怎麼賣才划算?

[UWP]用Win2D和CompositionAPI實現文字的發光效果,並製作動畫

1. 成果

獻祭了周末的晚上,成功召喚出了上面的番茄鍾。正當我在感慨“不愧是Shadow大人,這難道就是傳說中的五彩斑斕的黑?”

“那才不是什麼陰影效果,那是發光效果。”被路過的老婆吐槽了。

系系系,老婆說的都系對的。我還以為我在做陰影動畫,現在只好改博客標題了?

要實現上面的動畫效果,首先使用CompositionDrawingSurface,在它上面用DrawTextLayout畫出文字,然後用GaussianBlurEffect模仿成陰影,然後用CanvasActiveLayer裁剪文字的輪廓,然後用這個CompositionDrawingSurface創建出CompositionSurfaceBrush,然後創建一個CompositionMaskBrush,將CompositionSurfaceBrush作為它的Mask,然後用CompositionLinearGradientBrush創建出漸變,再用BlendEffect將它變成四向漸變,再用ColorKeyFrameAnimation和ScalarKeyFrameAnimation在它上面做動畫並把它作為CompositionMaskBrush的Source,然後創建SpriteVisual將CompositionMaskBrush應用上去,然後使用兩個PointLight分別從左到右和從右到左照射這個SpriteVisual,再創建一個AmbientLight模仿呼吸燈。

仔細想想……好吧,老婆說得對,我還真的沒有用到任何Shadow的Api,這裏和Shadow大人半毛錢關係都沒有。

這個番茄鍾源碼可以在這裏查看:

也可以安裝我的番茄鍾應用試玩一下,安裝地址:

這篇文章將介紹其中幾個關鍵技術。

2. 使用GaussianBlurEffect模仿陰影

上一篇文章已經介紹過怎麼在CompositionDrawingSurface上寫字,這裏就不再重複。為了可以為文字添加陰影,需要用到CanvasRenderTargetGaussianBlurEffect

CanvasRenderTarget是一個可以用來畫圖的渲染目標。實現文字陰影的步驟如下:將文字畫到CanvasRenderTarget,然後用它作為GaussianBlurEffect.Source產生一張高斯模糊的圖片,這樣看上去就和文字的陰影一樣。然後再在這張模糊的圖片的前面畫上原本的文字。

代碼如下所示:

using (var session = CanvasComposition.CreateDrawingSession(drawingSurface))
{
    session.Clear(Colors.Transparent);
    using (var textLayout = new CanvasTextLayout(session, Text, textFormat, width, height))
    {
        var bitmap = new CanvasRenderTarget(session, width, height);
        using (var bitmapSession = bitmap.CreateDrawingSession())
        {
            bitmapSession.DrawTextLayout(textLayout, 0, 0, FontColor);
        }
        var blur = new GaussianBlurEffect
        {
            BlurAmount = (float)BlurAmount,
            Source = bitmap,
            BorderMode = EffectBorderMode.Hard
        };

        session.DrawImage(blur, 0, 0);
        session.DrawTextLayout(textLayout, 0, 0, FontColor);
    }
}

效果如下(因為我用了白色字體,這時候已經不怎麼像陰影了):

關於CavasRenderTaget,死魚的有詳細介紹。他的這個專欄的文章都很有趣。

3. 使用CanvasActiveLayer裁剪文字

關於裁剪文字,有幾件事需要做。

首先獲取需要裁剪的文字的輪廓,這使用上一篇文章介紹過的CanvasGeometry.CreateText就可以了,這個函數的返回值是一個。然後使用CanvasGeometry.CreateRectangle獲取整個畫布的CanvasGeometry,將他們用相減得出文字以外的部分,具體代碼如下:

var fullSizeGeometry = CanvasGeometry.CreateRectangle(session, 0, 0, width, height);
var textGeometry = CanvasGeometry.CreateText(textLayout);
var finalGeometry = fullSizeGeometry.CombineWith(textGeometry, Matrix3x2.Identity, CanvasGeometryCombine.Exclude);

這裏之所以不直接使用textGeometry,是因為我們並不是真的裁剪出文字的部分,而是像WPF的那樣用透明度控制显示的部分。就是用來實現這個功能。CanvasDrawingSession.CreateLayer函數使用透明度和CanvasGeometry創建一個CanvasActiveLayer,在創建Layer后CanvasDrawingSession的操作都會應用這個透明度,直到Layer關閉。

using (var layer = session.CreateLayer(1, finalGeometry))
{
    //DrawSth
}

最後效果如下:

關於CanvasActiveLayer的更多用法, 可以參考Lindexi的。

4. 製作有複雜顏色的陰影

如上圖所示,UWP中的DropShadow的Color只能有一種顏色,所以DropShadow不能使用複雜的顏色。這時候就要用到,CompositionMaskBrush有兩個主要屬性:Mask和Source。其中Mask是一個CompositionBrush類型的屬性,它指定不透明的蒙板源。簡單來說,CompositionMaskBrush的形狀就是它的Mask的形狀。而Source屬性則是它的顏色,這個屬性可以是 CompositionColorBrush、CompositionLinearGradientBrush、CompositionSurfaceBrush、CompositionEffectBrush 或 CompositionNineGridBrush 類型的任何 CompositionBrush。可以使用前面創建的CompositionDrawingSurface創建出CompositionSurfaceBrush,最後創建一個CompositionMaskBrush,將CompositionSurfaceBrush作為它的Mask。

var maskBrush = Compositor.CreateMaskBrush();
maskBrush.Mask = Compositor.CreateSurfaceBrush(DrawingSurface);
maskBrush.Source = Compositor.CreateLinearGradientBrush();

本來還想做到大紫大紅的,但被吐槽和本來低調內斂的目的不符合,所以復用了以前的配色,CompositionLinearGradientBrush加BlendEffect做成了有些複雜的配色(但實際上太暗了看不出來):

這時候效果如下:

5. 使用PointLight和AmbientLight製作動畫

我在這篇文章里介紹了PointLight的用法及基本動畫,這次豪華些,同時有從左到右的紅光以及從右到左的藍光,這兩個PointLight的動畫效果大致是這樣:

因為PointLight最多只能疊加兩個,所以再使用AmbientLight並對它的Intensity屬性做動畫,這樣動畫就會變得複雜些,最終實現了文章開頭的動畫。

var compositor = Window.Current.Compositor;
var ambientLight = compositor.CreateAmbientLight();
ambientLight.Intensity = 0;
ambientLight.Color = Colors.White;

var intensityAnimation = compositor.CreateScalarKeyFrameAnimation();
intensityAnimation.InsertKeyFrame(0.2f, 0, compositor.CreateLinearEasingFunction());
intensityAnimation.InsertKeyFrame(0.5f, 0.20f, compositor.CreateLinearEasingFunction());
intensityAnimation.InsertKeyFrame(0.8f, 0, compositor.CreateLinearEasingFunction());
intensityAnimation.Duration = TimeSpan.FromSeconds(10);
intensityAnimation.IterationBehavior = AnimationIterationBehavior.Forever;

ambientLight.StartAnimation(nameof(AmbientLight.Intensity), intensityAnimation);

6. 參考

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

※公開收購3c價格,不怕被賤賣!

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

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

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

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

帶你漲姿勢的認識一下 Kafka 消費者

之前我們介紹過了 Kafka 整體架構,Kafka 生產者,Kafka 生產的消息最終流向哪裡呢?當然是需要消費了,要不只產生一系列數據沒有任何作用啊,如果把 Kafka 比作餐廳的話,那麼生產者就是廚師的角色,消費者就是客人,只有廚師的話,那麼炒出來的菜沒有人吃也沒有意義,如果只有客人沒有廚師的話,誰會去這個店吃飯呢?!所以如果你看完前面的文章意猶未盡的話,可以繼續讓你爽一爽。如果你沒看過前面的文章,那就從現在開始讓你爽。

Kafka 消費者概念

應用程序使用 KafkaConsumer 從 Kafka 中訂閱主題並接收來自這些主題的消息,然後再把他們保存起來。應用程序首先需要創建一個 KafkaConsumer 對象,訂閱主題並開始接受消息,驗證消息並保存結果。一段時間后,生產者往主題寫入的速度超過了應用程序驗證數據的速度,這時候該如何處理?如果只使用單個消費者的話,應用程序會跟不上消息生成的速度,就像多個生產者像相同的主題寫入消息一樣,這時候就需要多個消費者共同參与消費主題中的消息,對消息進行分流處理。

Kafka 消費者從屬於消費者群組。一個群組中的消費者訂閱的都是相同的主題,每個消費者接收主題一部分分區的消息。下面是一個 Kafka 分區消費示意圖

上圖中的主題 T1 有四個分區,分別是分區0、分區1、分區2、分區3,我們創建一個消費者群組1,消費者群組中只有一個消費者,它訂閱主題T1,接收到 T1 中的全部消息。由於一個消費者處理四個生產者發送到分區的消息,壓力有些大,需要幫手來幫忙分擔任務,於是就演變為下圖

這樣一來,消費者的消費能力就大大提高了,但是在某些環境下比如用戶產生消息特別多的時候,生產者產生的消息仍舊讓消費者吃不消,那就繼續增加消費者。

如上圖所示,每個分區所產生的消息能夠被每個消費者群組中的消費者消費,如果向消費者群組中增加更多的消費者,那麼多餘的消費者將會閑置,如下圖所示

向群組中增加消費者是橫向伸縮消費能力的主要方式。總而言之,我們可以通過增加消費組的消費者來進行水平擴展提升消費能力。這也是為什麼建議創建主題時使用比較多的分區數,這樣可以在消費負載高的情況下增加消費者來提升性能。另外,消費者的數量不應該比分區數多,因為多出來的消費者是空閑的,沒有任何幫助。

Kafka 一個很重要的特性就是,只需寫入一次消息,可以支持任意多的應用讀取這個消息。換句話說,每個應用都可以讀到全量的消息。為了使得每個應用都能讀到全量消息,應用需要有不同的消費組。對於上面的例子,假如我們新增了一個新的消費組 G2,而這個消費組有兩個消費者,那麼就演變為下圖這樣

在這個場景中,消費組 G1 和消費組 G2 都能收到 T1 主題的全量消息,在邏輯意義上來說它們屬於不同的應用。

總結起來就是如果應用需要讀取全量消息,那麼請為該應用設置一個消費組;如果該應用消費能力不足,那麼可以考慮在這個消費組裡增加消費者

消費者組和分區重平衡

消費者組是什麼

消費者組(Consumer Group)是由一個或多個消費者實例(Consumer Instance)組成的群組,具有可擴展性和可容錯性的一種機制。消費者組內的消費者共享一個消費者組ID,這個ID 也叫做 Group ID,組內的消費者共同對一個主題進行訂閱和消費,同一個組中的消費者只能消費一個分區的消息,多餘的消費者會閑置,派不上用場。

我們在上面提到了兩種消費方式

  • 一個消費者群組消費一個主題中的消息,這種消費模式又稱為點對點的消費方式,點對點的消費方式又被稱為消息隊列
  • 一個主題中的消息被多個消費者群組共同消費,這種消費模式又稱為發布-訂閱模式

消費者重平衡

我們從上面的消費者演變圖中可以知道這麼一個過程:最初是一個消費者訂閱一個主題並消費其全部分區的消息,後來有一個消費者加入群組,隨後又有更多的消費者加入群組,而新加入的消費者實例分攤了最初消費者的部分消息,這種把分區的所有權通過一個消費者轉到其他消費者的行為稱為重平衡,英文名也叫做 Rebalance 。如下圖所示

重平衡非常重要,它為消費者群組帶來了高可用性伸縮性,我們可以放心的添加消費者或移除消費者,不過在正常情況下我們並不希望發生這樣的行為。在重平衡期間,消費者無法讀取消息,造成整個消費者組在重平衡的期間都不可用。另外,當分區被重新分配給另一個消費者時,消息當前的讀取狀態會丟失,它有可能還需要去刷新緩存,在它重新恢復狀態之前會拖慢應用程序。

消費者通過向組織協調者(Kafka Broker)發送心跳來維護自己是消費者組的一員並確認其擁有的分區。對於不同不的消費群體來說,其組織協調者可以是不同的。只要消費者定期發送心跳,就會認為消費者是存活的並處理其分區中的消息。當消費者檢索記錄或者提交它所消費的記錄時就會發送心跳。

如果過了一段時間 Kafka 停止發送心跳了,會話(Session)就會過期,組織協調者就會認為這個 Consumer 已經死亡,就會觸發一次重平衡。如果消費者宕機並且停止發送消息,組織協調者會等待幾秒鐘,確認它死亡了才會觸發重平衡。在這段時間里,死亡的消費者將不處理任何消息。在清理消費者時,消費者將通知協調者它要離開群組,組織協調者會觸發一次重平衡,盡量降低處理停頓。

重平衡是一把雙刃劍,它為消費者群組帶來高可用性和伸縮性的同時,還有有一些明顯的缺點(bug),而這些 bug 到現在社區還無法修改。

重平衡的過程對消費者組有極大的影響。因為每次重平衡過程中都會導致萬物靜止,參考 JVM 中的垃圾回收機制,也就是 Stop The World ,STW,(引用自《深入理解 Java 虛擬機》中 p76 關於 Serial 收集器的描述):

更重要的是它在進行垃圾收集時,必須暫停其他所有的工作線程。直到它收集結束。Stop The World 這個名字聽起來很帥,但這項工作實際上是由虛擬機在後台自動發起並完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應用來說都是難以接受的。

也就是說,在重平衡期間,消費者組中的消費者實例都會停止消費,等待重平衡的完成。而且重平衡這個過程很慢……

創建消費者

上面的理論說的有點多,下面就通過代碼來講解一下消費者是如何消費的

在讀取消息之前,需要先創建一個 KafkaConsumer 對象。創建 KafkaConsumer 對象與創建 KafkaProducer 對象十分相似 — 把需要傳遞給消費者的屬性放在 properties 對象中,後面我們會着重討論 Kafka 的一些配置,這裏我們先簡單的創建一下,使用3個屬性就足矣,分別是 bootstrap.serverkey.deserializervalue.deserializer

這三個屬性我們已經用過很多次了,如果你還不是很清楚的話,可以參考

還有一個屬性是 group.id 這個屬性不是必須的,它指定了 KafkaConsumer 是屬於哪個消費者群組。創建不屬於任何一個群組的消費者也是可以的

Properties properties = new Properties();
        properties.put("bootstrap.server","192.168.1.9:9092");     properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");   properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
KafkaConsumer<String,String> consumer = new KafkaConsumer<>(properties);

主題訂閱

創建好消費者之後,下一步就開始訂閱主題了。subscribe() 方法接受一個主題列表作為參數,使用起來比較簡單

consumer.subscribe(Collections.singletonList("customerTopic"));

為了簡單我們只訂閱了一個主題 customerTopic,參數傳入的是一個正則表達式,正則表達式可以匹配多個主題,如果有人創建了新的主題,並且主題的名字與正則表達式相匹配,那麼會立即觸發一次重平衡,消費者就可以讀取新的主題。

要訂閱所有與 test 相關的主題,可以這樣做

consumer.subscribe("test.*");

輪詢

我們知道,Kafka 是支持訂閱/發布模式的,生產者發送數據給 Kafka Broker,那麼消費者是如何知道生產者發送了數據呢?其實生產者產生的數據消費者是不知道的,KafkaConsumer 採用輪詢的方式定期去 Kafka Broker 中進行數據的檢索,如果有數據就用來消費,如果沒有就再繼續輪詢等待,下面是輪詢等待的具體實現

try {
  while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(100));
    for (ConsumerRecord<String, String> record : records) {
      int updateCount = 1;
      if (map.containsKey(record.value())) {
        updateCount = (int) map.get(record.value() + 1);
      }
      map.put(record.value(), updateCount);
    }
  }
}finally {
  consumer.close();
}
  • 這是一個無限循環。消費者實際上是一個長期運行的應用程序,它通過輪詢的方式向 Kafka 請求數據。
  • 第三行代碼非常重要,Kafka 必須定期循環請求數據,否則就會認為該 Consumer 已經掛了,會觸發重平衡,它的分區會移交給群組中的其它消費者。傳給 poll() 方法的是一個超市時間,用 java.time.Duration 類來表示,如果該參數被設置為 0 ,poll() 方法會立刻返回,否則就會在指定的毫秒數內一直等待 broker 返回數據。
  • poll() 方法會返回一個記錄列表。每條記錄都包含了記錄所屬主題的信息,記錄所在分區的信息、記錄在分區中的偏移量,以及記錄的鍵值對。我們一般會遍歷這個列表,逐條處理每條記錄。
  • 在退出應用程序之前使用 close() 方法關閉消費者。網絡連接和 socket 也會隨之關閉,並立即觸發一次重平衡,而不是等待群組協調器發現它不再發送心跳並認定它已經死亡。

線程安全性

在同一個群組中,我們無法讓一個線程運行多個消費者,也無法讓多個線程安全的共享一個消費者。按照規則,一個消費者使用一個線程,如果一個消費者群組中多個消費者都想要運行的話,那麼必須讓每個消費者在自己的線程中運行,可以使用 Java 中的 ExecutorService 啟動多個消費者進行進行處理。

消費者配置

到目前為止,我們學習了如何使用消費者 API,不過只介紹了幾個最基本的屬性,Kafka 文檔列出了所有與消費者相關的配置說明。大部分參數都有合理的默認值,一般不需要修改它們,下面我們就來介紹一下這些參數。

  • fetch.min.bytes

該屬性指定了消費者從服務器獲取記錄的最小字節數。broker 在收到消費者的數據請求時,如果可用的數據量小於 fetch.min.bytes 指定的大小,那麼它會等到有足夠的可用數據時才把它返回給消費者。這樣可以降低消費者和 broker 的工作負載,因為它們在主題使用頻率不是很高的時候就不用來回處理消息。如果沒有很多可用數據,但消費者的 CPU 使用率很高,那麼就需要把該屬性的值設得比默認值大。如果消費者的數量比較多,把該屬性的值調大可以降低 broker 的工作負載。

  • fetch.max.wait.ms

我們通過上面的 fetch.min.bytes 告訴 Kafka,等到有足夠的數據時才會把它返回給消費者。而 fetch.max.wait.ms 則用於指定 broker 的等待時間,默認是 500 毫秒。如果沒有足夠的數據流入 kafka 的話,消費者獲取的最小數據量要求就得不到滿足,最終導致 500 毫秒的延遲。如果要降低潛在的延遲,就可以把參數值設置的小一些。如果 fetch.max.wait.ms 被設置為 100 毫秒的延遲,而 fetch.min.bytes 的值設置為 1MB,那麼 Kafka 在收到消費者請求后,要麼返回 1MB 的數據,要麼在 100 ms 后返回所有可用的數據。就看哪個條件首先被滿足。

  • max.partition.fetch.bytes

該屬性指定了服務器從每個分區里返回給消費者的最大字節數。它的默認值時 1MB,也就是說,KafkaConsumer.poll() 方法從每個分區里返回的記錄最多不超過 max.partition.fetch.bytes 指定的字節。如果一個主題有20個分區和5個消費者,那麼每個消費者需要至少4 MB的可用內存來接收記錄。在為消費者分配內存時,可以給它們多分配一些,因為如果群組裡有消費者發生崩潰,剩下的消費者需要處理更多的分區。max.partition.fetch.bytes 的值必須比 broker 能夠接收的最大消息的字節數(通過 max.message.size 屬性配置大),否則消費者可能無法讀取這些消息,導致消費者一直掛起重試。 在設置該屬性時,另外一個考量的因素是消費者處理數據的時間。消費者需要頻繁的調用 poll() 方法來避免會話過期和發生分區再平衡,如果單次調用poll() 返回的數據太多,消費者需要更多的時間進行處理,可能無法及時進行下一個輪詢來避免會話過期。如果出現這種情況,可以把 max.partition.fetch.bytes 值改小,或者延長會話過期時間。

  • session.timeout.ms

這個屬性指定了消費者在被認為死亡之前可以與服務器斷開連接的時間,默認是 3s。如果消費者沒有在 session.timeout.ms 指定的時間內發送心跳給群組協調器,就會被認定為死亡,協調器就會觸發重平衡。把它的分區分配給消費者群組中的其它消費者,此屬性與 heartbeat.interval.ms 緊密相關。heartbeat.interval.ms 指定了 poll() 方法向群組協調器發送心跳的頻率,session.timeout.ms 則指定了消費者可以多久不發送心跳。所以,這兩個屬性一般需要同時修改,heartbeat.interval.ms 必須比 session.timeout.ms 小,一般是 session.timeout.ms 的三分之一。如果 session.timeout.ms 是 3s,那麼 heartbeat.interval.ms 應該是 1s。把 session.timeout.ms 值設置的比默認值小,可以更快地檢測和恢復崩憤的節點,不過長時間的輪詢或垃圾收集可能導致非預期的重平衡。把該屬性的值設置得大一些,可以減少意外的重平衡,不過檢測節點崩潰需要更長的時間。

  • auto.offset.reset

該屬性指定了消費者在讀取一個沒有偏移量的分區或者偏移量無效的情況下的該如何處理。它的默認值是 latest,意思指的是,在偏移量無效的情況下,消費者將從最新的記錄開始讀取數據。另一個值是 earliest,意思指的是在偏移量無效的情況下,消費者將從起始位置處開始讀取分區的記錄。

  • enable.auto.commit

我們稍後將介紹幾種不同的提交偏移量的方式。該屬性指定了消費者是否自動提交偏移量,默認值是 true,為了盡量避免出現重複數據和數據丟失,可以把它設置為 false,由自己控制何時提交偏移量。如果把它設置為 true,還可以通過 auto.commit.interval.ms 屬性來控制提交的頻率

  • partition.assignment.strategy

我們知道,分區會分配給群組中的消費者。PartitionAssignor 會根據給定的消費者和主題,決定哪些分區應該被分配給哪個消費者,Kafka 有兩個默認的分配策略RangeRoundRobin

  • client.id

該屬性可以是任意字符串,broker 用他來標識從客戶端發送過來的消息,通常被用在日誌、度量指標和配額中

  • max.poll.records

該屬性用於控制單次調用 call() 方法能夠返回的記錄數量,可以幫你控制在輪詢中需要處理的數據量。

  • receive.buffer.bytes 和 send.buffer.bytes

socket 在讀寫數據時用到的 TCP 緩衝區也可以設置大小。如果它們被設置為 -1,就使用操作系統默認值。如果生產者或消費者與 broker 處於不同的數據中心內,可以適當增大這些值,因為跨數據中心的網絡一般都有比較高的延遲和比較低的帶寬。

提交和偏移量的概念

特殊偏移

我們上面提到,消費者在每次調用poll() 方法進行定時輪詢的時候,會返回由生產者寫入 Kafka 但是還沒有被消費者消費的記錄,因此我們可以追蹤到哪些記錄是被群組裡的哪個消費者讀取的。消費者可以使用 Kafka 來追蹤消息在分區中的位置(偏移量)

消費者會向一個叫做 _consumer_offset 的特殊主題中發送消息,這個主題會保存每次所發送消息中的分區偏移量,這個主題的主要作用就是消費者觸發重平衡後記錄偏移使用的,消費者每次向這個主題發送消息,正常情況下不觸發重平衡,這個主題是不起作用的,當觸發重平衡后,消費者停止工作,每個消費者可能會分到對應的分區,這個主題就是讓消費者能夠繼續處理消息所設置的。

如果提交的偏移量小於客戶端最後一次處理的偏移量,那麼位於兩個偏移量之間的消息就會被重複處理

如果提交的偏移量大於最後一次消費時的偏移量,那麼處於兩個偏移量中間的消息將會丟失

既然_consumer_offset 如此重要,那麼它的提交方式是怎樣的呢?下面我們就來說一下

提交方式

KafkaConsumer API 提供了多種方式來提交偏移量

自動提交

最簡單的方式就是讓消費者自動提交偏移量。如果 enable.auto.commit 被設置為true,那麼每過 5s,消費者會自動把從 poll() 方法輪詢到的最大偏移量提交上去。提交時間間隔由 auto.commit.interval.ms 控制,默認是 5s。與消費者里的其他東西一樣,自動提交也是在輪詢中進行的。消費者在每次輪詢中會檢查是否提交該偏移量了,如果是,那麼就會提交從上一次輪詢中返回的偏移量。

提交當前偏移量

auto.commit.offset 設置為 false,可以讓應用程序決定何時提交偏移量。使用 commitSync() 提交偏移量。這個 API 會提交由 poll() 方法返回的最新偏移量,提交成功后馬上返回,如果提交失敗就拋出異常。

commitSync() 將會提交由 poll() 返回的最新偏移量,如果處理完所有記錄后要確保調用了 commitSync(),否則還是會有丟失消息的風險,如果發生了在均衡,從最近一批消息到發生在均衡之間的所有消息都將被重複處理。

異步提交

異步提交 commitAsync() 與同步提交 commitSync() 最大的區別在於異步提交不會進行重試,同步提交會一致進行重試。

同步和異步組合提交

一般情況下,針對偶爾出現的提交失敗,不進行重試不會有太大的問題,因為如果提交失敗是因為臨時問題導致的,那麼後續的提交總會有成功的。但是如果在關閉消費者或再均衡前的最後一次提交,就要確保提交成功。

因此,在消費者關閉之前一般會組合使用commitAsync和commitSync提交偏移量

提交特定的偏移量

消費者API允許調用 commitSync() 和 commitAsync() 方法時傳入希望提交的 partition 和 offset 的 map,即提交特定的偏移量。

文章參考:

《極客時間-Kafka核心技術與實戰》

《Kafka 權威指南》

關注公眾號獲取更多優質电子書,關注一下你就知道資源是有多好了

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

收購3c,收購IPHONE,收購蘋果電腦-詳細收購流程一覽表

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

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

※公開收購3c價格,不怕被賤賣!

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