分類: 3C資訊

  • 沙烏地首間狗狗咖啡廳開張 愛犬人士好去處

    摘錄自2020年9月29日中央社報導

    沙烏地阿拉伯沿海城市霍巴(Khobar)在6月新開一間寵物咖啡廳The Barking Lot,這是非常保守的沙國境內首家狗狗友善咖啡廳。在伊斯蘭世界,狗被視為不潔的動物,沙烏地阿拉伯的公共場所通常禁止犬類出沒。

    沙國曾禁止民眾在街上遛寵物,但這項禁令普遍被人民忽視,民眾在街上遛寵物的景象越來越常見,好幾個城市的動物收容所如雨後春筍般出現。沙烏地王儲穆罕默德.沙爾曼(Crown Prince Mohammed bin Salman)推動現代化改革,領養流浪動物也變得越來越普遍。

    The Barking Lot老闆阿邁德(Dalal Ahmed)告訴法新社:「我之前帶著狗狗來到沙烏地阿拉伯,但被禁止跟牠一起在海灘上散步。」、「我非常難過,因此決定開一家咖啡廳幫助有養狗的人,甚至是那些沒有養狗的人。」

    生物多樣性
    國際新聞
    沙烏地阿拉伯
    同伴動物
    流浪動物

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 巴西廢除保護紅樹林法規 環團怒轟犯罪

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

    巴西波索納洛(Jair Bolsonaro)政府28日廢除了保護紅樹林和其他脆弱沿海生態系統的法規,將使這類土地得以開發,環保團體警告此舉將造成災難性影響,直言這是危害社會的「罪行」。

    綜合外媒報導,28日巴西的國家環境委員會會議決議撤銷一系列環保法規,其中,2002年創設、保護巴西許多熱帶紅樹林和大西洋沿岸沙丘灌木叢的「永久保護區」被廢除。

    環保人士警告,放寬法規將使這類土地得以開發,可能對其生態系統造成災難性影響。巴西非政府組織「搶救大西洋叢林」(SOS Mata Atlantica)負責人曼托瓦尼(Mario Mantovani)說,「這些地區已經受到房地產開發帶來的巨大壓力」。

    外媒指出,4000平方公尺紅樹林可吸收的二氧化碳量,與同等面積亞馬遜雨林吸收的二氧化碳量幾乎相同。

    土地利用
    國際新聞
    巴西
    紅樹林
    大西洋

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • Python 圖像處理 OpenCV (10):圖像處理形態學之頂帽運算與黑帽運算

    Python 圖像處理 OpenCV (10):圖像處理形態學之頂帽運算與黑帽運算

    前文傳送門:

    「Python 圖像處理 OpenCV (1):入門」

    「Python 圖像處理 OpenCV (2):像素處理與 Numpy 操作以及 Matplotlib 显示圖像」

    「Python 圖像處理 OpenCV (3):圖像屬性、圖像感興趣 ROI 區域及通道處理」

    「Python 圖像處理 OpenCV (4):圖像算數運算以及修改顏色空間」

    「Python 圖像處理 OpenCV (5):圖像的幾何變換」

    「Python 圖像處理 OpenCV (6):圖像的閾值處理」

    「Python 圖像處理 OpenCV (7):圖像平滑(濾波)處理」

    「Python 圖像處理 OpenCV (8):圖像腐蝕與圖像膨脹」

    「Python 圖像處理 OpenCV (9):圖像處理形態學開運算、閉運算以及梯度運算」

    引言

    今天是圖形處理形態學的最後一篇,我們介紹頂帽運算和黑帽運算。

    建議先閱讀前面兩篇圖像處理的內容:

    「Python 圖像處理 OpenCV (8):圖像腐蝕與圖像膨脹」

    「Python 圖像處理 OpenCV (9):圖像處理形態學開運算、閉運算以及梯度運算」

    形態學之頂帽運算

    圖像處理頂帽運算是一個獲取圖像噪聲的運算,它是由原始圖像減去圖像開運算而得到的結果:

    頂帽運算 = 原始圖像 - 開運算
    

    圖像頂帽運算同樣是使用形態學擴展函數 morphologyEx() ,它的參數是 MORPH_TOPHAT ,示例如下:

    import cv2 as cv
    import numpy as np
    import matplotlib.pyplot as plt
    
    # 讀取圖片
    source = cv.imread("demo_noise_white.jpg", cv.IMREAD_GRAYSCALE)
    
    # 設置卷積核
    kernel = np.ones((5, 5), np.uint8)
    
    # 開運算
    open = cv.morphologyEx(source, cv.MORPH_OPEN, kernel)
    
    # 頂帽運算
    dst = cv.morphologyEx(source, cv.MORPH_TOPHAT, kernel)
    
    # 显示結果
    titles = ['Source Img','Open Img', 'Tophat Img']
    images = [source, open, dst]
    
    # matplotlib 繪圖
    for i in range(3):
       plt.subplot(1, 3, i+1), plt.imshow(images[i],'gray')
       plt.title(titles[i])
       plt.xticks([]),plt.yticks([])
    
    plt.show()
    

    形態學之黑帽運算

    圖像處理頂帽運算是一個獲取圖像內部的小孔,或者前景色中的小黑點的運算。

    它是由圖像閉運算減去原始圖像的操作:

    黑帽運算 = 閉運算圖像 - 原始圖像
    

    圖像頂帽運算同樣是使用形態學擴展函數 morphologyEx() ,它的參數是 MORPH_BLACKHAT ,示例如下:

    import cv2 as cv
    import numpy as np
    import matplotlib.pyplot as plt
    
    # 讀取圖片
    source = cv.imread("demo_noise_black.jpg", cv.IMREAD_GRAYSCALE)
    
    # 設置卷積核
    kernel = np.ones((5, 5), np.uint8)
    
    # 黑帽運算
    dst = cv.morphologyEx(source, cv.MORPH_BLACKHAT, kernel)
    
    # 構造显示結果數組
    titles = ['Source Img', 'Black Img']
    images = [source, dst]
    
    # matplotlib 繪圖
    for i in range(2):
       plt.subplot(1, 2, i+1), plt.imshow(images[i],'gray')
       plt.title(titles[i])
       plt.xticks([]),plt.yticks([])
    
    plt.show()
    

    今天的內容比較短,至此,圖像形態學的幾個基礎的運算已經全部介紹完畢,希望各位同學能理解這幾個運算的原理,而不是僅僅知道了幾個參數或者說幾個方法的調用。

    示例代碼

    如果有需要獲取源碼的同學可以在公眾號回復「OpenCV」進行獲取。

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

    【其他文章推薦】

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

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

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

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

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

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

  • 多應用下 Swagger 的使用,這可能是最好的方式!

    多應用下 Swagger 的使用,這可能是最好的方式!

    問題

    微服務化的時代,我們整個項目工程下面都會有很多的子系統,對於每個應用都有暴露 Api 接口文檔需要,這個時候我們就會想到 Swagger 這個優秀 jar 包。但是我們會遇到這樣的問題,假如說我們有5個應用,難道說我們每個模塊下面都要去引入這個 jar 包嗎?我作為一個比較懶的程序感覺這樣好麻煩,於是乎我思考了一種我認為比較好的方式,如果大家覺得有什麼不太好的地方希望指正,謝謝!

    基礎

    開始之前大家首先要了解一些基礎,主要有以下幾個方面:

    1. 單應用下 Swagger 的集成與使用
    2. 條件裝配 @Conditional 介紹
    3. 配置文件參數獲取 @ConfigurationProperties
    單體應用下 Swagger 集成與使用

    關於這部分從3方面講起分別是:什麼是、為什麼、如何用

    什麼是 Swagger ?

    Swagger 是一個規範且完整的框架,用於生成、描述、調用和可視化 RESTful 風格的 Web 服務。

    為什麼使用 Swagger ?

    主要的優點:

    1. 支持 API 自動生成同步的在線文檔:使用 Swagger 后可以直接通過代碼生成文檔,不再需要自己手動編寫接口文檔了,對程序員來說非常方便,可以節約寫文檔的時間去學習新技術。
    2. 提供 Web 頁面在線測試 API:光有文檔還不夠,Swagger 生成的文檔還支持在線測試。參數和格式都定好了,直接在界面上輸入參數對應的值即可在線測試接口。

    缺點的話就是但凡引入一個 jar 需要去了解下原理和使用,對於這個缺點我感覺相比於優點就是大巫見小巫,我簡單看了一下源碼,其實不算太難。

    如何使用 Swagger

    關於 Swagger 的使用其實也就是3板斧,大家一定很熟悉的;

    第一板斧就是引入 jar 包,這裏我使用的是2.9.2版本

            <!-- swagger 相關 -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>${swagger2.version}</version>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>${swagger2.version}</version>
            </dependency>

    第二板斧就是SpringBoot自動掃描配置類

    /**
     * SwaggerConfig
     *
     * @author wangtongzhou
     * @since 2020-06-09 09:41
     */

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {

        @Bean
        public Docket createRestApi() {
            return new Docket(DocumentationType.SWAGGER_2)
                    //生產環境的時候關閉 Swagger 比較安全
                    .apiInfo(apiInfo())
                    .select()
                    //Api掃描目錄
                    .apis(RequestHandlerSelectors.basePackage("com.springboot2.learning"))
                    .paths(PathSelectors.any())
                    .build();
        }

        private ApiInfo apiInfo() {
            return new ApiInfoBuilder()
                    .title("learn")
                    .description("learn")
                    .version("1.0")
                    .build();
        }
    }

    第三板斧使用 Swagger 註解

    /**
     * 用戶相關接口
     *
     * @author wangtongzhou
     * @since 2020-06-12 07:35
     */

    @RestController
    @RequestMapping("/user")
    @Api(value = "用戶相關接口")
    public class UserController {

        @PostMapping("/")
        @ApiOperation("添加用戶的接口")
        @ApiImplicitParams({
                @ApiImplicitParam(name = "userName", value = "用戶名", defaultValue =
                        "wtz")
    ,
                @ApiImplicitParam(name = "age", value = "年齡", defaultValue = "20")
        })
        public User addUser(String userName, Integer age) {
            User user = new User();
            user.setAge(age);
            user.setUserName(userName);
            return user;
        }

        @GetMapping("/{userId}")
        @ApiOperation("根據用戶id查詢用戶信息")
        @ApiImplicitParam(name = "userId", value = "用戶id", defaultValue = "20")
        public User queryUserByUserId(@PathVariable Long userId) {
            User user = new User();
            user.setUserId(userId);
            return user;
        }
    }
    /**
     * 用戶實體
     *
     * @author wangtongzhou
     * @since 2020-06-12 07:45
     */

    @ApiModel
    public class User {

        @ApiModelProperty(value = "用戶名稱")
        private String userName;

        @ApiModelProperty(value = "年齡")
        private Integer age;

        @ApiModelProperty(value = "用戶id")
        private Long userId;

        public String getUserName() {
            return userName;
        }

        public void setUserName(String userName) {
            this.userName = userName;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }

        public Long getUserId() {
            return userId;
        }

        public void setUserId(Long userId) {
            this.userId = userId;
        }
    }

    效果如下:

    實體註解

    接口描述

    接口參數

    執行接口

    返回地址

    條件裝配 @Conditional 介紹

    @Conditional 是Spring4.0提供的註解,位於 org.springframework.context.annotation 包內,它可以根據代碼中設置的條件裝載不同的bean。比如說當一個接口有兩個實現類時,我們要把這個接口交給Spring管理時通常會只選擇實現其中一個實現類,這個時候我們總不能使用if-else吧,所以這個@Conditional的註解就出現了。

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPEElementType.METHOD})
    public @interface Conditional {

        Class<? extends Condition>[] value();

    }
    使用方法

    這裏介紹一個MySQL和Oracle選擇方式,開始之前首先在properties文件中增加sql.name=mysql的配置,接下來步驟如下

    1. 實現Conditional接口, 實現matches方法
    /**
     * mysql條件裝配
     *
     * @author wangtongzhou
     * @since 2020-06-13 08:01
     */

    public class MysqlConditional implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String sqlName = context.getEnvironment().getProperty("sql.name");
            if ("mysql".equals(sqlName)){
                return true;
            }
            return false;
        }
    }
    /**
     * oracle條件裝配
     *
     * @author wangtongzhou
     * @since 2020-06-13 08:02
     */

    public class OracleConditional implements Condition {
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            String sqlName=context.getEnvironment().getProperty("sql.name");
            if ("oracle".equals(sqlName)){
                return true;
            }
            return false;
        }
    }
    1. 在需要判斷條件的bean上,加上@Conditional(***.class)即可在滿足條件的時候加載對應的類
    /**
     * conditional
     *
     * @author wangtongzhou
     * @since 2020-06-13 08:01
     */

    @Configuration
    public class ConditionalConfig {

        @Bean
        @Conditional(MysqlConditional.class)
        public Mysql mysql() {
            return new Mysql();
        }

        @Bean
        @Conditional(OracleConditional.class)
        public Oracle oracle() {
            return new Oracle();
        }
    }
    1. 調用測試
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class ConditionalTests {

        @Autowired
        private ApplicationContext applicationContext;

        @Test
        public void test_conditional() {
            Mysql mysql = (Mysql) applicationContext.getBean("mysql");
            Assert.assertNotNull(mysql);
            Assert.assertTrue("mysql".equals(mysql.getSqlName()));
        }
    }
    其他擴展註解
    1. @@ConditionalOnClass({Docket.class, ApiInfoBuilder.class})
      當存在Docket和ApiInfoBuilder類的時候才加載Bean;
    2. @ConditionalOnMissingClass不存在某個類的時候才會實例化Bean;
    3. @ConditionalOnProperty(prefix = “swagger”, value = “enable”, matchIfMissing = true)當存在swagger為前綴的屬性,才會實例化Bean;
    4. @ConditionalOnMissingBean當不存在某個Bean的時候才會實例化;

    這裏就介紹這幾個常用,org.springframework.boot.autoconfigure.condition這個下面包含全部的關於@Conditional相關的所有註解

    @Conditional擴展

    註解介紹

    配置文件參數獲取 @ConfigurationProperties

    @ConfigurationProperties是SpringBoot加入的註解,主要用於配置文件中的指定鍵值對映射到一個Java實體類上。關於這個的使用就在下面的方式引出。

    比較好的方式

    關於開篇中引入的問題,解題流程主要是以下3步:

    1. 抽象一個公共 Swagger jar;
    2. 如何定製化 Swagger jar;
    3. 使用定製化完成以後的 Swagger jar;
    抽象一個公共 Swagger jar

    主要是就是將 Swagger jar 和一些其他需要的 jar 進行引入,使其成為一個公共的模塊,軟件工程中把這個叫做單一原則;

    Maven包的引入

     

        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
            </dependency>
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
            </dependency>
        </dependencies>
    如何定製化 Swagger jar;

    定製化就是將 Swagger 相關的屬性進行配置化的處理,這裏也可以分為兩步;

    1. 將公共的屬性抽象成配置化的類,這裏就是關於@ConfigurationProperties的使用,將配置文件中的 swagger 開頭的屬性映射到配置類的屬性當中;
    /**
     * Swagger基本屬性
     *
     * @author wangtongzhou
     * @since 2020-05-24 16:58
     */

    @ConfigurationProperties("swagger")
    public class SwaggerProperties {

        /**
         * 子系統
         */

        private String title;

        /**
         * 描述
         */

        private String description;

        /**
         * 版本號
         */

        private String version;

        /**
         * api包路徑
         */

        private String basePackage;

        public String getTitle() {
            return title;
        }

        public SwaggerProperties setTitle(String title) {
            this.title = title;
            return this;
        }

        public String getDescription() {
            return description;
        }

        public SwaggerProperties setDescription(String description) {
            this.description = description;
            return this;
        }

        public String getVersion() {
            return version;
        }

        public SwaggerProperties setVersion(String version) {
            this.version = version;
            return this;
        }

        public String getBasePackage() {
            return basePackage;
        }

        public SwaggerProperties setBasePackage(String basePackage) {
            this.basePackage = basePackage;
            return this;
        }
    }
    1. 公共屬性賦值配置到 Swagger 的配置類中,該配置類中進行一些類條件的判斷和插件Bean是否已經注入過,然後就是將配置類中的屬性,賦值到 Swagger 的初始化工程中;
    /**
     * Swagger自動配置類
     *
     * @author wangtongzhou
     * @since 2020-05-24 16:35
     */

    @Configuration
    @EnableSwagger2
    @ConditionalOnClass({Docket.class, ApiInfoBuilder.class})
    @ConditionalOnProperty(prefix = "swagger", value = "enable", matchIfMissing = true)
    @EnableConfigurationProperties(SwaggerProperties.class)
    public class SwaggerConfig {

        @Bean
        @ConditionalOnMissingBean
        public SwaggerProperties swaggerProperties() {
            return new SwaggerProperties();
        }

        @Bean
        public Docket createRestApi() {
            SwaggerProperties properties = swaggerProperties();
            return new Docket(DocumentationType.SWAGGER_2)
                    //生產環境的時候關閉 Swagger 比較安全
                    .apiInfo(apiInfo(properties))
                    .select()
                    //Api掃描目錄
                    .apis(RequestHandlerSelectors.basePackage(properties.getBasePackage()))
                    .paths(PathSelectors.any())
                    .build();
        }

        private ApiInfo apiInfo(SwaggerProperties properties) {
            return new ApiInfoBuilder()
                    .title(properties.getTitle())
                    .description(properties.getDescription())
                    .version(properties.getVersion())
                    .build();
        }

    }

    完成以上兩步,就完成了 Swagger 模塊的定製化開發,接下來還要做一件事情,作為一個公共的模塊,我們要讓他自己進行自動化裝配,解放我們的雙手,我們在 resources 目錄下增加一個 spring.factories 配置文件,內容如下:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      com.springcloud.study.swagger.config.SwaggerConfig

    到此我們完成所有的開發;

    使用定製化完成以後的 Swagger jar;

    關於使用也分為兩步,

    1. 引入 jar;
            <dependency>
                <groupId>com.springcloud.study</groupId>
                <artifactId>common-swagger</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    1. 定製化的屬性配置;
    Swagger 配置項
    swagger:
      title: 用戶模塊
      description: 用戶子系統
      version: 1.0.0
      base-packagecom.springcloud.study.user.controller

    完成這兩步就可以開啟正常的使用了;

    後續的規劃

    1. 註解整理
      後續會將 Spring 註解進行一個統一的整理,包含一些使用說明或者原理等等,希望到時候能幫助到大家吧,目前計劃兩周一個吧;
    2. 開源項目
      Spring Cloud 的學習過於碎片化,希望通過自己搞一個開源項目,提升對各個組件的掌握能力,同時也能產出一套通用化權限管理系統,具備很高的靈活性、擴展性和高可用性,並且簡單易用,這塊是和未來做企業数字化轉型相關的事是重合的,慢慢的會做一些企業級通用化的的功能開發;前端部分的話希望是採用Vue,但是這塊有一個學習成本,還沒有進行研究,目前還沒排上日程。整體的里程碑是希望在6.22離職之前完成整套後端的開發,7月中旬完成第一次Commit。

    點點關注

    這邊文章限於篇幅,過多的關注於使用了,後續會把上面幾個註解的原理分析講講,歡迎大家點點關注,點點贊,感謝!

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心

  • 面試必問:分佈式鎖實現之zk(Zookeeper)

    面試必問:分佈式鎖實現之zk(Zookeeper)

    點贊再看,養成習慣,微信搜索【三太子敖丙】關注這個互聯網苟且偷生的工具人。

    本文 GitHub https://github.com/JavaFamily 已收錄,有一線大廠面試完整考點、資料以及我的系列文章。

    前言

    鎖我想不需要我過多的去說,大家都知道是怎麼一回事了吧?

    在多線程環境下,由於上下文的切換,數據可能出現不一致的情況或者數據被污染,我們需要保證數據安全,所以想到了加鎖。

    所謂的加鎖機制呢,就是當一個線程訪問該類的某個數據時,進行保護,其他線程不能進行訪問,直到該線程讀取完,其他線程才可使用。

    還記得我之前說過Redis在分佈式的情況下,需要對存在併發競爭的數據進行加鎖,老公們十分費解,Redis是單線程的嘛?為啥還要加鎖呢?

    看來老公們還是年輕啊,你說的不需要加鎖的情況是這樣的:

    單個服務去訪問Redis的時候,確實因為Redis本身單線程的原因是不用考慮線程安全的,但是,現在有哪個公司還是單機的呀?肯定都是分佈式集群了嘛。

    老公們你看下這樣的場景是不是就有問題了:

    你們經常不是說秒殺嘛,拿到庫存判斷,那老婆告訴你分佈式情況就是會出問題的。

    我們為了減少DB的壓力,把庫存預熱到了KV,現在KV的庫存是1。

    1. 服務A去Redis查詢到庫存發現是1,那說明我能搶到這個商品對不對,那我就準備減一了,但是還沒減。
    2. 同時服務B也去拿發現也是1,那我也搶到了呀,那我也減。
    3. C同理。
    4. 等所有的服務都判斷完了,你發現誒,怎麼變成-2了,超賣了呀,這下完了。

    老公們是不是發現問題了,這就需要分佈式鎖的介入了,我會分三個章節去分別介紹分佈式鎖的三種實現方式(Zookeeper,Redis,MySQL),說出他們的優缺點,以及一般大廠的實踐場景。

    正文

    一個騷里騷氣的面試官啥也沒拿的就走了進來,你一看,這不是你老婆嘛,你正準備叫他的時候,發現他一臉嚴肅,死鬼還裝嚴肅,肯定會給我放水的吧。

    B站搜:三太子敖丙

    咳咳,我們啥也不說了,開始今天的面試吧。

    正常線程進程同步的機制有哪些?

    • 互斥:互斥的機制,保證同一時間只有一個線程可以操作共享資源 synchronized,Lock等。
    • 臨界值:讓多線程串行話去訪問資源
    • 事件通知:通過事件的通知去保證大家都有序訪問共享資源
    • 信號量:多個任務同時訪問,同時限制數量,比如發令槍CDL,Semaphore等

    那分佈式鎖你了解過有哪些么?

    分佈式鎖實現主要以Zookeeper(以下簡稱zk)、Redis、MySQL這三種為主。

    那先跟我聊一下zk吧,你能說一下他常見的使用場景么?

    他主要的應用場景有以下幾個:

    • 服務註冊與訂閱(共用節點)
    • 分佈式通知(監聽znode)
    • 服務命名(znode特性)
    • 數據訂閱、發布(watcher)
    • 分佈式鎖(臨時節點)

    zk是啥?

    他是個數據庫,文件存儲系統,並且有監聽通知機制(觀察者模式)

    存文件系統,他存了什麼?

    節點

    zk的節點類型有4大類

    • 持久化節點(zk斷開節點還在)

    • 持久化順序編號目錄節點

    • 臨時目錄節點(客戶端斷開後節點就刪除了)

    • 臨時目錄編號目錄節點

    節點名稱都是唯一的。

    節點怎麼創建?

    我特么,這樣問的么?可是我面試只看了分佈式鎖,我得好好想想!!!

    還好我之前在自己的服務器搭建了一個zk的集群,我剛好跟大家回憶一波。

    create /test laogong // 創建永久節點 

    那臨時節點呢?

    create -e /test laogong // 創建臨時節點

    臨時節點就創建成功了,如果我斷開這次鏈接,這個節點自然就消失了,這是我的一個zk管理工具,目錄可能清晰點。

    如何創建順序節點呢?

    create -s /test // 創建順序節點

    臨時順序節點呢?

    我想聰明的老公都會搶答了

    create -e -s /test  // 創建臨時順序節點

    我退出后,重新連接,發現剛才創建的所有臨時節點都沒了。

    開篇演示這麼多呢,我就是想給大家看到的zk大概的一個操作流程和數據結構,中間涉及的搭建以及其他的技能我就不說了,我們重點聊一下他在分佈式鎖中的實現。

    zk就是基於節點去實現各種分佈式鎖的。

    就拿開頭的場景來說,zk應該怎麼去保證分佈式情況下的線程安全呢?併發競爭他是怎麼控制的呢?

    為了模擬併發競爭這樣一個情況,我寫了點偽代碼,大家可以先看看

    我定義了一個庫存inventory值為1,還用到了一個CountDownLatch發令槍,等10個線程都就緒了一起去扣減庫存。

    是不是就像10台機器一起去拿到庫存,然後扣減庫存了?

    所有機器一起去拿,發現都是1,那大家都認為是自己搶到了,都做了減一的操作,但是等所有人都執行完,再去set值的時候,發現其實已經超賣了,我打印出來給大家看看。

    是吧,這還不是超賣一個兩個的問題,超賣7個都有,代碼裏面明明判斷了庫存大於0才去減的,怎麼回事開頭我說明了。

    那怎麼解決這個問題?

    sync,lock也只能保證你當前機器線程安全,這樣分佈式訪問還是有問題。

    上面跟大家提到的zk的節點就可以解決這個問題。

    zk節點有個唯一的特性,就是我們創建過這個節點了,你再創建zk是會報錯的,那我們就利用一下他的唯一性去實現一下。

    怎麼實現呢?

    上面不是10個線程嘛?

    我們全部去創建,創建成功的第一個返回true他就可以繼續下面的扣減庫存操作,後續的節點訪問就會全部報錯,扣減失敗,我們把它們丟一個隊列去排隊。

    那怎麼釋放鎖呢?

    刪除節點咯,刪了再通知其他的人過來加鎖,依次類推。

    我們實現一下,zk加鎖的場景。

    是不是,只有第一個線程能扣減成功,其他的都失敗了。

    但是你發現問題沒有,你加了鎖了,你得釋放啊,你不釋放後面的報錯了就不重試了。

    那簡單,刪除鎖就釋放掉了,Lock在finally裏面unLock,現在我們在finally刪除節點。

    加鎖我們知道創建節點就夠了,但是你得實現一個阻塞的效果呀,那咋搞?

    死循環,遞歸不斷去嘗試,直到成功,一個偽裝的阻塞效果。

    怎麼知道前面的老哥刪除節點了嗯?

    監聽節點的刪除事件

    但是你發現你這樣做的問題沒?

    是的,會出現死鎖。

    第一個仔加鎖成功了,在執行代碼的時候,機器宕機了,那節點是不是就不能刪除了?

    你要故作沉思,自問自答,時而看看遠方,時而看看面試官,假裝自己什麼都不知道。

    哦我想起來了,創建臨時節點就好了,客戶端連接一斷開,別的就可以監聽到節點的變化了。

    嗯還不錯,那你發現還有別的問題沒?

    好像這種監聽機制也不好。

    怎麼個不好呢?

    你們可以看到,監聽,是所有服務都去監聽一個節點的,節點的釋放也會通知所有的服務器,如果是900個服務器呢?

    這對服務器是很大的一個挑戰,一個釋放的消息,就好像一個牧羊犬進入了羊群,大家都四散而開,隨時可能幹掉機器,會佔用服務資源,網絡帶寬等等。

    這就是羊群效應。

    那怎麼解決這個問題?

    繼續故作沉思,啊啊啊,好難,我的腦袋。。。。

    你TM別裝了好不好?

    好的,臨時順序節點,可以順利解決這個問題。

    怎麼實現老公你先別往下看,先自己想想。

    之前說了全部監聽一個節點問題很大,那我們就監聽我們的前一個節點,因為是順序的,很容易找到自己的前後。

    和之前監聽一個永久節點的區別就在於,這裏每個節點只監聽了自己的前一個節點,釋放當然也是一個個釋放下去,就不會出現羊群效應了。

    以上所有代碼我都會開源到我的https://github.com/AobingJava/Thanos其實上面的還有瑕疵,大家可以去拉下來改一下提交pr,我會看合適的會通過的。

    你說了這麼多,挺不錯的,你能說說ZK在分佈式鎖中實踐的一些缺點么?

    Zk性能上可能並沒有緩存服務那麼高。

    因為每次在創建鎖和釋放鎖的過程中,都要動態創建、銷毀瞬時節點來實現鎖功能。

    ZK中創建和刪除節點只能通過Leader服務器來執行,然後將數據同步到所有的Follower機器上。(這裏涉及zk集群的知識,我就不展開了,以後zk章節跟老公們細聊)

    還有么?

    使用Zookeeper也有可能帶來併發問題,只是並不常見而已。

    由於網絡抖動,客戶端可ZK集群的session連接斷了,那麼zk以為客戶端掛了,就會刪除臨時節點,這時候其他客戶端就可以獲取到分佈式鎖了。

    就可能產生併發問題了,這個問題不常見是因為zk有重試機制,一旦zk集群檢測不到客戶端的心跳,就會重試,Curator客戶端支持多種重試策略。

    多次重試之後還不行的話才會刪除臨時節點。

    Tip:所以,選擇一個合適的重試策略也比較重要,要在鎖的粒度和併發之間找一個平衡。

    有更好的實現么?

    基於Redis的分佈式鎖

    能跟我聊聊么?

    我看看了手上的表,老公,今天天色不早了,你全問完了,我怎麼多水幾篇文章呢?

    行確實很晚了,那你回家去把家務幹了吧?

    我????

    =

    總結

    zk通過臨時節點,解決掉了死鎖的問題,一旦客戶端獲取到鎖之後突然掛掉(Session連接斷開),那麼這個臨時節點就會自動刪除掉,其他客戶端自動獲取鎖。

    zk通過節點排隊監聽的機制,也實現了阻塞的原理,其實就是個遞歸在那無限等待最小節點釋放的過程。

    我上面沒實現鎖的可重入,但是也很好實現,可以帶上線程信息就可以了,或者機器信息這樣的唯一標識,獲取的時候判斷一下。

    zk的集群也是高可用的,只要半數以上的或者,就可以對外提供服務了。

    這周會寫完Redis和數據庫的分佈式鎖的,老公們等好。

    我是敖丙,一個在互聯網苟且偷生的工具人。

    最好的關係是互相成就老公們的「三連」就是丙丙創作的最大動力,我們下期見!

    注:如果本篇博客有任何錯誤和建議,歡迎老公們留言,老公你快說句話啊

    文章持續更新,可以微信搜索「 三太子敖丙 」第一時間閱讀,回復【資料】【面試】【簡歷】有我準備的一線大廠面試資料和簡歷模板,本文 GitHub https://github.com/JavaFamily 已經收錄,有大廠面試完整考點,歡迎Star。

    你知道的越多,你不知道的越多

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

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

  • 【設計模式】如何用組合替代繼承

    如果問面向對象的三大特性是什麼,多數人都能回答出來:封裝、繼承、多態。

    繼承 作為三大特性之一,近來卻越來越不推薦使用,更有極端的語言,直接語法中就不支持繼承,例如 Go。這又是為什麼呢?

    為什麼不推薦使用繼承?

    假設我們要設計一個關於鳥的類。

    我們將“鳥類”定義為一個抽象類 AbstractBird。所有更細分的鳥,比如麻雀、鴿子、烏鴉等,都繼承這個抽象類。

    大部分鳥都會飛,那我們可不可以在 AbstractBird 抽象類中,定義一個 Fly() 方法呢?

    答案是否定的。儘管大部分鳥都會飛,但也有特例,比如鴕鳥就不會飛。鴕鳥繼承具有 Fly() 方法的父類,那鴕鳥就具有“飛”這樣的行為,這顯然不符合我們對現實世界中事物的認識。

    解決方案一

    在鴕鳥這個子類中重寫 Fly() 方法,讓它拋出異常。

    public class AbstractBird
    {
        public virtual void Fly()
        {
            Console.WriteLine("I'm flying.");
        }
    }
    
    //鴕鳥
    public class Ostrich : AbstractBird
    {
        public override void Fly()
        {
            throw new NotImplementedException("I can't fly.");
        }
    }
    

    這種設計思路雖然可以解決問題,但不夠優美。因為除了鴕鳥之外,不會飛的鳥還有很多,比如企鵝。對於這些不會飛的鳥來說,我們都需要重寫 Fly() 方法,拋出異常。

    這違背了迪米特法則(也叫最少知識原則),暴露不該暴露的接口給外部,增加了類使用過程中被誤用的概率。

    解決方案二

    通過 AbstractBird 類派生出兩個更加細分的抽象類:會飛的鳥類 AbstractFlyableBird 和不會飛的鳥類 AbstractUnFlyableBird,讓麻雀、烏鴉這些會飛的鳥都繼承 AbstractFlyableBird,讓鴕鳥、企鵝這些不會飛的鳥,都繼承 AbstractUnFlyableBird 類。

    此時,繼承關係變成了三層,還行得通。

    如果要再添加一個游泳 Swim() 的方法,那情況就複雜了,要分為四中情況:

    • 會飛會游泳
    • 會飛不會游泳
    • 不會飛會游泳
    • 不會飛不會游泳

    如果再有其他行為加入,抽象類的數量就會幾何級數增長。

    我們要搞清楚某個類具有哪些方法、屬性,必須閱讀父類的代碼、父類的父類的代碼……一直追溯到最頂層父類的代碼。

    使用組合

    針對“會飛”這樣一個行為特性,我們可以定義一個 Flyable 接口,只讓會飛的鳥去實現這個接口。針對會游泳,定義一個 Swimable 接口,會叫定義一個 Tweetable 接口。

    public interface Flyable
    {
        void Fly();
    }
    
    public interface Swimable
    {
        void Swim();
    }
    
    public interface Tweetable
    {
        void Tweet();
    }
    
    //麻雀
    public class Sparrow : Flyable, Tweetable
    {
        public void Fly() => Console.WriteLine("I am flying.");
    
        public void Tweet() => Console.WriteLine("!@#$%^&*……");
    }
    
    //企鵝
    public class Penguin : Swimable, Tweetable
    {
        public void Swim() => Console.WriteLine("I am swimming.");
    
        public void Tweet() => Console.WriteLine("!@#$%^&*……");
    }
    

    麻雀和企鵝都會叫,Tweet 實現了兩遍,這是壞味道。我們可以用組合來消除這個壞味道。

    public interface Flyable
    {
        void Fly();
    }
    
    public interface Swimable
    {
        void Swim();
    }
    
    public interface Tweetable
    {
        void Tweet();
    }
    
    public class FlyAbility : Flyable
    {
        public void Fly() => Console.WriteLine("I am flying.");
    }
    
    public class SwimAbility : Swimable
    {
        public void Swim() => Console.WriteLine("I am swimming.");
    }
    
    public class TweetAbility : Tweetable
    {
        public void Tweet() => Console.WriteLine("!@#$%^&*……");
    }
    
    //麻雀
    public class Sparrow : Flyable, Tweetable
    {
        FlyAbility flyAbility = new FlyAbility();
        TweetAbility tweetAbility = new TweetAbility();
    
        public void Fly() => flyAbility.Fly();
    
        public void Tweet() => tweetAbility.Tweet();
    }
    
    //企鵝
    public class Penguin : Swimable, Tweetable
    {
        SwimAbility swimAbility = new SwimAbility();
        TweetAbility tweetAbility = new TweetAbility();
    
        public void Swim() => swimAbility.Swim();
    
        public void Tweet() => tweetAbility.Tweet();
    }
    

    雖然現在主流的思想都是多用組合少用繼承,但是從上面的例子可以看出,繼承改寫成組合意味着要做更細粒度的類的拆分,要定義更多的類和接口。類和接口的增多也就或多或少地增加代碼的複雜程度和維護成本。所以,在實際的項目開發中,我們還是要根據具體的情況,來具體選擇該用繼承還是組合。

    本文出自極客時間 王爭 老師的課程《設計模式之美》。原文示例為 java,因為我是做 C# 的,所以本文示例代碼我改成了 C# 。

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

    ※超省錢租車方案

  • Golang簡單入門教程——函數進階篇

    Golang簡單入門教程——函數進階篇

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

    今天是golang專題的第八篇,我們來聊聊golang當中的函數。

    我們在之前的時候已經介紹過了函數的基本用法,知道了怎麼樣設計或者是定義一個函數,以及怎麼樣調用一個函數,還了解了defer的用法。今天這篇文章我們來繼續深入這個話題,來看看golang當中關於函數的一些進階的用法。

    返回error

    前文當中我們曾經提到過,在golang當中並沒有try catch捕獲異常的機制。在其他語言當中異常只有一種,可以通過try catch語句進行捕獲,而golang當中做了區分,將異常分為兩種,一種是可以在函數當中返回的error,另外一種是嚴重的會引起程序崩潰的panic

    在golang中,error也是一個數據類型,由於golang支持函數的多值返回,所以我們可以設置一個返回值是error。我們通過對這個error的判斷來獲取運行函數的情況。

    舉個例子,比如說,假設我們實現一個Divide函數實現兩個int相除。那麼顯然我們需要除數不能為0,當除數為0的時候我們需要返回一個異常。這個時候我們可以把代碼寫成這樣:

    // Divide test
    func Divide(a, b int) (ret int, err error) {
     if b == 0 {
      err = errors.New("divisor is zero")
      return
     }
     return a / b, nil
    }
    

    當我們調用函數的時候,我們用兩個變量去接收這個函數返回的結果,第二個變量的類型是error。當這個函數成功執行的時候第二個變量的結果為nil,我們只需要判斷它是否等於nil,就可以知道函數執行是否成功。如果不成功,我們還可以記錄失敗的原因。

    func main() {
     ret, err := Divide(5, 2)
     if err == nil {
      fmt.Println(ret)
     } else {
      fmt.Println(err)
     }
    }
    

    這種用法在golang當中非常常見,我們之前在介紹字符串相關操作的時候也介紹過返回error的用法。我們在設計函數的時候如果需要判斷輸入的合法性可以使用error,這樣就可以保證handle住非法的情況,並且也能讓下游感知到。

    不定參數

    不定參數的用法在很多語言當中都有,比如在Python當中,不定參數是*args。通過*args我們可以接受任何數量的參數,由於Python是弱變量類型的語言,所以args這些參數的類型可以互不相同。但是golang不行,golang嚴格限制類型,不定參數必須要保證類型一樣。除此之外,其他的用法和Python一樣,不定參數會以數組的形式傳入函數內部,我們可以使用數組的api進行訪問。

    我們來看一個例子,我們通過…來定義不定參數。比如我們可以實現一個sum函數,可以將任意個int進行累加。

    func Sum(nums ... int) int{
        ret := 0
        for _, num := range nums {
            ret += num
        }
        return ret
    }
    

    我們來仔細研究一下上面這個例子,在這個例子當中,我們通過…傳入了一個不定參數,我們不定參數的類型只寫一次,寫在…的後面。從底層實現的機制上來說,不定參數本質上是將傳入的參數轉化成數組的切片。但是這就有了一個問題,既然傳入的是一個數組的切片,我們為什麼要專門設置一個關鍵字,而不是規定傳入一個切片呢?

    比如上面的代碼我們完全可以寫成這樣:

    func Sum(nums []int) int{
        ret := 0
        for _, num := range nums {
            ret += num
        }
        return ret
    }
    

    無論從代碼的閱讀還是編寫上來看相差並不大,好像這樣做完全沒有意義,其實不是這樣的。這個關鍵字簡化的並不是函數的設計方,而是函數的使用方。如果我們規定了函數的輸入是一個切片,那麼當我們在傳入數據的時候,必須要使用強制轉化,將我們的數據轉化成切片,比如這樣:

    Sum([]int(3, 4, 6, 8))
    

    而使用…關鍵字我們則可以省略掉強制轉化的過程,上面的代碼我們寫成這樣就可以了:

    Sum(3, 4, 6, 8)
    

    很明顯可以看出差異,使用不定參數的話調用方會輕鬆很多,不需要再進行額外的轉換。如果我們要傳入的也是一個數組,那麼在傳遞的時候也需要用…符號將它展開

    a := make([]int)
    a = append(a, 3)
    a = append(a, 4)
    Sum(a...)
    Sum(a[1:]...)
    

    既然聊到不定參數的傳遞,那麼又涉及到了一個問題,當我們想要像Python那樣傳遞多個類型不同的參數的時候,應該怎麼辦呢?按照道理golang是靜態類型的語言,限制死了參數的類型,是不能隨便轉換的才對。但是偏偏這樣操作是可以的,因為golang當中有一個特殊的類型,叫做interface

    interface的用法很多,一個很重要的用法是用在面向對象當中充當結構體的接口。這裏我們不做過多深入,我們只需要知道,interface的一個用法是可以用來代替所有類型的變量。我們來看一個例子:

    func testInterface(args ...interface{}) {
        for _, arg := range args {
            switch arg.(type) {
                case int:
                 fmt.Println("it's a int")
             case string:
                 fmt.Println("it's a string")    
                case float32:
                 fmt.Println("it's a float")
                default:
                 fmt.Println("it's an unknown type")
            }
        }
    }
    
    
    func main() {
        testInterface(3, 4.5, "abc")
    }
    

    我們可以用.(type)獲取一個interface變量實際的類型,這樣我們就實現了任意類型任意數量參數的傳入。

    匿名函數和閉包

    匿名函數我們在Python當中經常使用到,其實這個概念出現已久,最早可以追溯到1958年Lisp語言。所以這並不是一個新鮮的概念,只是傳統的C、C++等語言沒有支持匿名函數的功能,所以顯得好像是一個新出現的概念一樣。golang當中也支持匿名函數,但是golang當中匿名函數的使用方式和Python等語言稍稍有些不同。

    在Python當中我們是通過lambda關鍵字來定義匿名函數,它可以被傳入另一個函數當中,也可以賦值給一個變量。golang當中匿名函數的定義方式和普通函數基本是一樣的,只是沒有函數名而已,不過它也可以被傳入函數或者是賦值給另一個變量。

    比如:

    s := func(a, b int) int {
        return a + b
    }
    
    c := s(3, 4)
    

    除了匿名函數之外,golang還支持閉包。閉包的概念我們在之前Python閉包的介紹當中曾經提到過,我們之前也用過好幾次,閉包的本質不是一個包,而是一個函數,是一個持有外部環境變量的函數。比如在Python當中,我們經常可以看到這樣的寫法:

    def outside(x):
        def inside(y):
            print(x, y)
        return inside
    
    
    ins = outside(3)
    ins(5) #3, 5
    

    我們可以看到outside這個函數返回了inside這個函數,對於inside這個函數而言,它持有了x這個變量。x這個變量並不是屬於它的,而是定義在它的外部域的。並且我們在調用inside的時候是無法干涉這個變量的,這就是一個閉包的典型例子。根據輪子哥的說法,閉包的閉的意思並不是封閉內部,而是封閉外部。當外部scope失效的時候,函數仍然持有一份外部的環境的值。

    golang當中閉包的使用方法大同小異,我們來看一個類似的例子:

    func main() {
        a := func(x int) (func(int)) {
            return func(y int){
                fmt.Println(x, y)
            }
        }
        b := a(4)
        b(5)
    }
    

    這個閉包的例子和剛才上面Python那個例子是一樣的,唯一不同的是由於golang是強類型的語言,所以我們需要在定義閉包的時候將輸入和輸出的類型定義清楚。

    總結

    關於golang當中函數的高級用法就差不多介紹完了,這些都是實際編程當中經常使用的方法,如果想要學好golang這門語言的話,這些是基本功。如果你之前有其他語言的基礎,來寫go的話,整體上手的難度還是不大的,很多設計都可以在其他的語言當中找到影子,有了參照來學會簡單得多。

    我很難描述實際工作當中寫golang的體驗,和我寫任何一門其他的語言都不一樣,有一種一開始期望很低,慢慢慢慢總能發現驚喜的感覺。我強烈建議大家去實際感受一下。

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

    本文使用 mdnice 排版

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

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

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

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

  • 自己動手實現深度學習框架-8 RNN文本分類和文本生成模型

    自己動手實現深度學習框架-8 RNN文本分類和文本生成模型

    代碼倉庫: https://github.com/brandonlyg/cute-dl

    目標

            上階段cute-dl已經可以構建基礎的RNN模型。但對文本相模型的支持不夠友好, 這個階段的目標是, 讓框架能夠友好地支持文本分類和本文生成任務。具體包括:

    1. 添加嵌入層, 為文本尋找高效的向量表示。
    2. 添加類別抽樣函數, 根據模型輸出的類別分佈抽樣得到生成的文本。
    3. 使用imdb-review數據集驗證文本分類模型。
    4. 使用一個古詩數據集驗證文本生成模型。

            這階段涉及到的代碼比較簡單因此接下來會重點描述RNN語言相關模型中涉及到的數學原理和工程方法。

    數學原理

    文本分類模型

            可以把文本看成是一個詞的序列\(W=[w_1, w_2, …, w_T]\), 在訓練數據集中每個文本屬於一個類別\(a_i\), \(a_i∈A\), 集合 \(A = \{ a_1, a_2, …, a_k \}\) 是一個類別別集合. 分類模型要做的是給定一個文本W, 計算所有類別的后驗概率:

    \[P(a_i|W) = P(a_i|w_1,w_2,…,w_T), \quad i=1,2,…k \]

            那麼文本序列W的類別為:

    \[a = arg \max_{a_i} P(a_i|w_1,w_2,…,w_T) \]

            即在給定文本的條件下, 具有最大后驗概率的類別就是文本序列W所屬的類別.

    文本預測模型

            設任意一個文本序列為\(W=[w_1,w_2,…,W_T]\), 任意一個詞\(w_i ∈ V\), V是所有詞彙的集合,也叫詞彙表, 這裏需要強調的是\(w_i\)在V中是無序的, 但在W中是有序的, 文本預測的任務是, 計算任意一個詞\(w_i ∈ V\)在給定一個序列中的任意一個位置出現的概率:

    \[P(w_1,…,W_T) = ∏_{t=1}^T P(w_t|w_1,…,w_{t-1}) \]

            文本預測輸出一個\(w_i ∈ V\)的分佈列, 根據這個分佈列從V中抽取一個詞即為預測結果。不同於分類任務,這裏不是取概率最大的詞, 這裏的預測結果是某個詞出現的在一個序列特定位置的個概率,只要概率不是0都有可能出現,所以要用抽樣的方法確定某次預測的結果。

    詞的数字化表示

            任意一條數據在送入模型之前都要表示為一個数字化的向量, 文本數據也不例外。一個文本可以看成詞的序列,因此只要把詞数字化了,文本自然也就数字化了。對於詞來說,最簡單的方式是用詞在詞彙表中的唯一ID來表示, ID需要遵守兩個最基本的規則:

    1. 每個詞的ID在詞彙表中必須是唯一的.
    2. 每個詞的ID一旦確定不能變化.

            這種表示很難表達詞之間的關係, 例如: 在詞彙表中把”好”的ID指定為100, 如果希望ID能夠反映詞意的關係, 需要把”好”的近意詞: “善”, “美”, “良”, “可以”編碼為98, 99, 101, 102. 目前為止這看起還行. 如果還希望ID能夠反映詞之間的語法關係, “好”前後經常出現的詞: “友”, “人”, “的”, 這幾個詞的ID就很難選擇, 不論怎樣, 都會發現兩個詞它們在語義和語法上的關係都很遠,但ID卻很接近。這也說明了標量的表達能力很有限,無法表達多個維度的關係。為了能夠表達詞之間多個維度的的關係,多維向量是一個很好的選擇. 向量之間的夾大小衡量它們之間的關係:

    \[cos(θ) = \frac{<A, B>}{|A||B|} \]

            對於兩個向量A, B使用它們的點積, 模的乘積就能得到夾角θ餘弦值。當cos(θ)->1表示兩個向量的相似度高, cos(θ)->0 表示兩個向量是不相關的, cos(θ)->-1 表示兩個向量是相反的。

            把詞的ID轉換成向量,最簡單的辦法是使用one-hot編碼, 這樣得到的向量有兩個問題:

    1. 任意兩個向量A,B, <A,B>=0, 夾角的餘弦值cos(θ)=0, 不能表達詞之間的關係.
    2. 向量的維度等於詞彙表的大小, 而且是稀疏向量,這和導致模型有大量的參數,模型訓練過程的運算量也很大.

            詞嵌入技術就是為解決詞表示的問題而提出的。詞嵌入把詞ID映射到一個合適維度的向量空間中, 在這個向量空間中為每個ID分配一個唯一的向量, 把這些向量當成參數看待, 在特定任務的模型中學習這些參數。當模型訓練完成后, 這些向量就是詞在這個特定任務中的一個合適的表示。詞嵌入向量的訓練步驟有:

    1. 收集訓練數據集中的詞彙, 構建詞彙表。
    2. 為詞彙表中的每個詞分配一個唯一的ID。假設詞彙表中的詞彙量是N, 詞ID的取值為:0,1,2,…,N-1, 對人任意一個0<ID<N-1, 必然存在ID-1, ID+1.
    3. 隨機初始化N個D維嵌入向量, 向量的索引為0,1,2,…,N-1. 這樣詞ID就成了向量的索引.
    4. 定義一個模型, 把嵌入向量作為模型的輸入層參与訓練.
    5. 訓練模型.

    嵌入層實現

            代碼: cutedl/rnn_layers.py, Embedding類.

            初始化嵌入向量, 嵌入向量使用(-1, 1)區間均勻分佈的隨機變量初始化:

    '''
    dims 嵌入向量維數
    vocabulary_size 詞彙表大小
    need_train 是否需要訓練嵌入向量
    '''
    def __init__(self, dims, vocabulary_size, need_train=True):
        #初始化嵌入向量
        initializer = self.weight_initializers['uniform']
        self.__vecs = initializer((vocabulary_size, dims))
    
        super().__init__()
    
        self.__params = None
        if need_train:
            self.__params = []
            self.__cur_params = None
            self.__in_batch = None
    

            初始化層參數時把所有的嵌入向量變成參与訓練的參數:

    def init_params(self):
        if self.__params is None:
            return
    
        voc_size, _ = self.__vecs.shape
        for i in range(voc_size):
            pname = 'weight_%d'%i
            p = LayerParam(self.name, pname, self.__vecs[i])
            self.__params.append(p)
    

            向前傳播時, 把形狀為(m, t)的數據轉換成(m, t, n)形狀的數據, 其中t是序列長度, n是嵌入向量的維數.

    '''
    in_batch shape=(m, T)
    return shape (m, T, dims)
    '''
    def forward(self, in_batch, training):
        m,T = in_batch.shape
        outshape = (m, T, self.outshape[-1])
        out = np.zeros(outshape)
    
        #得到每個序列的嵌入向量表示
        for i in range(m):
            out[i] = self.__vecs[in_batch[i]]
    
        if training and self.__params is not None:
            self.__in_batch = in_batch
    
        return out
    

            反向傳播時只關注當前批次使用到的向量, 注意同一個向量可能被多次使用, 需要累加同一個嵌入向量的梯度.

    def backward(self, gradient):
        if self.__params is None:
            return
    
        #pdb.set_trace()
        in_batch = self.__in_batch
        params = {}
        m, T, _ = gradient.shape
        for i in range(m):
            for t in range(T):
                grad = gradient[i, t]
                idx = self.__in_batch[i, t]
    
                #更新當前訓練批次的梯度
                if idx not in params:
                    #當前批次第一次發現該嵌入向量
                    params[idx] = self.__params[idx]
                    params[idx].gradient = grad
                else:
                    #累加當前批次梯度
                    params[idx].gradient += grad
    
        self.__cur_params = list(params.values())
    

    驗證

    imdb-review數據集上的分類模型

            代碼: examples/rnn/text_classify.py.

            數據集下載地址: https://pan.baidu.com/s/13spS_Eac_j0uRvCVi7jaMw 密碼: ou26

    數據集處理

            數據集處理時有幾個需要注意的地方:

    1. imdb-review數據集由長度不同的文本構成, 送入模型的數據形狀為(m, t, n), 至少要求一個批次中的數據具有相同的序列長度, 因此在對數據進行分批時, 對數據按批次填充.
    2. 一般使用0為填充編碼. 在構建詞彙表時, 假設有v個詞彙, 詞彙的編碼為1,2,…,v.
    3. 由於對文本進行分詞, 編碼比較耗時。可以把編碼后的數據保存起來,作為數據集的預處理數據, 下次直接加載使用。

    模型

    def fit_gru():
        print("fit gru")
        model = Model([
                    rnn.Embedding(64, vocab_size+1),
                    wrapper.Bidirectional(rnn.GRU(64), rnn.GRU(64)),
                    nn.Filter(),
                    nn.Dense(64),
                    nn.Dropout(0.5),
                    nn.Dense(1, activation='linear')
                ])
        model.assemble()
        fit('gru', model)
    

            訓練報告:

    這個模型和tensorflow給出的模型略有差別, 少了一個RNN層wrapper.Bidirectional(rnn.GRU(32), rnn.GRU(32)), 這個模型經過16輪的訓練達到了tensorflow模型的水平.

    文本生成模型

            我自己收集了一個古由詩詞構成的小型數據集, 用來驗證文本生成模型. 代碼: examples/rnn/text_gen.py.

            數據集下載地址: https://pan.baidu.com/s/14oY_wol0d9hE_9QK45IkzQ 密碼: 5f3c

            模型定義:

    def fit_gru():
        vocab_size = vocab.size()
        print("vocab size: ", vocab_size)
        model = Model([
                    rnn.Embedding(256, vocab_size),
                    rnn.GRU(1024, stateful=True),
                    nn.Dense(1024),
                    nn.Dropout(0.5),
                    nn.Dense(vocab_size, activation='linear')
                ])
    
        model.assemble()
        fit("gru", model)
    

            訓練報告:

            生成七言詩:

    def gen_text():
        mpath = model_path+"gru"
    
        model = Model.load(mpath)
        print("loadding model finished")
        outshape = (4, 7)
    
        print("vocab size: ", vocab.size())
    
        def do_gen(txt):
            #編碼
            #pdb.set_trace()
            res = vocab.encode(sentence=txt)
    
            m, n = outshape
    
            for i in range(m*n - 1):
                in_batch = np.array(res).reshape((1, -1))
                preds = model.predict(in_batch)
                #取最後一維的預測結果
                preds = preds[:, -1]
                outs = dlmath.categories_sample(preds, 1)
                res.append(outs[0,0])
    
            #pdb.set_trace()
            txt = ""
            for i in range(m):
                txt = txt + ''.join(vocab.decode(res[i*n:(i+1)*n])) + "\n"
    
            return txt
    
    
        starts = ['雲', '故', '畫', '花']
        for txt in starts:
            model.reset()
            res = do_gen(txt)
            print(res)
    

            生成的文本:

    雲填纜首月悠覺
    纜濯醉二隱隱白
    湖杖雨遮雙雨鄉
    焉秣都滄楓寓功
    
    故民民時都人把
    陳雨積存手菜破
    好纜簾二龍藕卻
    趣晚城矣中村桐
    
    畫和春覺上蓋騎
    滿楚事勝便京兵
    肯霆唇恨朔上楊
    志月隨肯八焜著
    
    花夜維他客陳月
    客到夜狗和悲布
    關欲摻似瓦闊靈
    山商過牆灘幽惘
    

            是不是很像李商隱的風格?

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

    【其他文章推薦】

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

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

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

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

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

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

  • RocketMQ系列(六)批量發送與過濾

    RocketMQ系列(六)批量發送與過濾

    今天我們再來看看RocketMQ的另外兩個小功能,消息的批量發送和過濾。這兩個小功能提升了我們使用RocketMQ的效率。

    批量發送

    以前我們發送消息的時候,都是一個一個的發送,這樣效率比較低下。能不能一次發送多個消息呢?當然是可以的,RocketMQ為我們提供了這樣的功能。但是它也有一些使用的條件:

    • 同一批發送的消息的Topic必須相同;
    • 同一批消息的waitStoreMsgOK 必須相同;
    • 批量發送的消息不支持延遲,就是上一節說的延遲消息;
    • 同一批次的消息,大小不能超過1MiB;

    好了,只要我們滿足上面的這些限制,就可以使用批量發送了,我們來看看發送端的代碼吧,

    @Test
    public void producerBatch() throws Exception {
    
        List<Message> messages = new ArrayList<>();
        for (int i = 0;i<3;i++) {
            MessageExt message = new MessageExt();
            message.setTopic("cluster-topic");
            message.setKeys("key-"+i);
            message.setBody(("this is batchMQ,my NO is "+i+"---"+new Date()).getBytes());
            messages.add(message);
        }
        SendResult sendResult = defaultMQProducer.send(messages);
        System.out.println("sendResult:" + sendResult.getSendStatus().toString());
    }
    
    • 其實批量發送很簡單,我們只是把消息放到一個List當中,然後統一的調用send方法發送就可以了。

    再來看看消費端的代碼,

    @Bean(initMethod = "start",destroyMethod = "shutdown")
    public DefaultMQPushConsumer pushConsumer()  {
        try {
            DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("DefaultMQPushConsumer");
            consumer.setNamesrvAddr("192.168.73.130:9876;192.168.73.131:9876;192.168.73.132:9876;");
            consumer.subscribe("cluster-topic", "*");
            consumer.registerMessageListener(new MessageListenerConcurrently() {
                @Override
                public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                    System.out.println("msgs.size():"+msgs.size());
                    if (msgs != null && msgs.size() > 0) {
                        for (MessageExt msg : msgs) {
                            System.out.println(new String(msg.getBody()));
                        }
                    }
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
            });
            return consumer;
        }catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    
    • 消費端的代碼沒有任何的變化,正常的接收消息就可以了,我們只是打印出了msgs.size(),看看一次接收一個消息,還是一次可以批量的接收多個消息。

    我們啟動項目,批量發送一下,看看效果吧,

    發送端的日誌如下:

    sendResult:SEND_OK
    

    發送成功,看來我們批量發送的3個消息都進入到了隊列中,再看看消費端,是一次消費一個,還是一次消費3個,如下:

    msgs.size():1
    this is batchMQ,my NO is 0---Mon Jun 15 09:31:04 CST 2020
    msgs.size():1
    this is batchMQ,my NO is 1---Mon Jun 15 09:31:04 CST 2020
    msgs.size():1
    this is batchMQ,my NO is 2---Mon Jun 15 09:31:04 CST 2020
    

    看樣子是一次只消費了一個消息,那麼能不能一次消費3個消息呢?當然是可以的,不過要進行特殊的設置,

    consumer.setConsumeMessageBatchMaxSize(5);
    

    在消費端,我們設置批量消費消息的數量是5,這個值默認是1。我們再看看消費端的日誌,

    msgs.size():3
    this is batchMQ,my NO is 0---Mon Jun 15 09:35:47 CST 2020
    this is batchMQ,my NO is 1---Mon Jun 15 09:35:47 CST 2020
    this is batchMQ,my NO is 2---Mon Jun 15 09:35:47 CST 2020
    

    這次一次消費了3個消息,如果消息比較多的話,最大一次能消費5個。這就是RocketMQ的批量發送和批量消費。

    消息過濾

    其實我們在大多數情況下,使用tag標籤就能夠很好的實現消息過濾。雖然tag標籤咱們並沒有過多的介紹,其實也很好理解,就是一個子Topic的概念,咱們在構建消息message的時候,message.setTags("xxx")。然後在消費的時候,訂閱Topic的時候,也可以指定訂閱的tag,

    consumer.subscribe("cluster-topic", "*");
    

    看到那個”*”了嗎?它就是訂閱的tag,”*”代表全部的tag,如果您想訂閱其中的一個或幾個,可以使用這種方式”tagA || tagB || tagC”,這是訂閱了cluster-topic下的3個tag,其他的tag是不會被消費的。

    這裏我們所說的消息過濾比tag要高級很多,是可以支持sql的,怎麼樣?高級吧。比如:我們訂閱”a > 5 and b = ‘abc’”的消息,如下圖:

    但是,RocketMQ畢竟不是數據庫,它只能支持一些基礎的SQL語句,並不是所有的SQL都支持,

    • 数字型的支持,>, >=, <, <=, BETWEEN, =

    • 字符串支持,=, <>, IN

    • IS NULL或者IS NOT NULL

    • 邏輯判斷,ANDORNOT

    字段的類型也只是簡單的幾種,

    • 数字型,支持123,543.123,整型、浮點都可以;
    • 字符串,必須使用單引號”括起來;
    • 空值,NULL;
    • 布爾型,TRUE或者FALSE;

    並且對消費者的類型也有一定的限制,只能使用push consumer才可以進行消息過濾。好了,說了這麼多了,我們看看怎麼使用吧,消費端和生產端都要進行相應的改造,先看看生產端吧,

    @Test
    public void producerBatch() throws Exception {
    
        List<Message> messages = new ArrayList<>();
        for (int i = 0;i<3;i++) {
            MessageExt message = new MessageExt();
            message.setTopic("cluster-topic");
            message.setKeys("key-"+i);
            message.setBody(("this is batchMQ,my NO is "+i+"---"+new Date()).getBytes());
    
            int a = i+4;
            message.putUserProperty("a",String.valueOf(a));
    
            messages.add(message);
        }
        SendResult sendResult = defaultMQProducer.send(messages);
        System.out.println("sendResult:" + sendResult.getSendStatus().toString());
    }
    

    我們在之前批量發送的基礎上進行了修改,定義了a的值,等於i+4,這樣循環3次,a的值就是4,5,6。然後調用message.putUserProperty("a",String.valueOf(a))注意,在使用消息過濾的時候,這些附加的條件屬性都是通過putUserProperty方法進行設置。這裏,我們設置了a的值。再看看消費端,

    consumer.subscribe("cluster-topic", MessageSelector.bySql("a > 5"));
    

    消費端,整體上沒有變化,只是在訂閱的方法中,使用MessageSelector.bySql("a > 5"),進行了條件的過濾。有的小夥伴可能會有疑問,我既想用sql過濾又想用tag過濾怎麼辦?當然也是可以,我們可以使用MessageSelector.bySql("a > 5").byTag("xx),byTag和bySql不分前後,怎麼樣,很強大吧。我們運行一下程序,看看效果吧。

    我們啟動一下服務,報錯了,怎麼回事?錯誤信息如下:

    The broker does not support consumer to filter message by SQL92
    

    隊列不支持過濾消息,我們查詢了RocketMQ源碼中的BrokerConfig類,這個類就是對broker的一些設置,其中發現了這兩個屬性,

    // whether do filter when retry.
    private boolean filterSupportRetry = false;
    private boolean enablePropertyFilter = false;
    
    • filterSupportRetry是在重試的時候,是否支持filter;
    • enablePropertyFilter,這個就是是否支持過濾消息的屬性;

    我們把這兩個屬性在broker的配置文件改為true吧,如下:

    filterSupportRetry=true
    enablePropertyFilter=true
    

    然後,再重新部署一下我們兩主兩從的集群環境。環境部署完以後,我們再重啟應用,沒有報錯。在生產端發送一下消息看看吧,

    sendResult:SEND_OK
    

    生產端發送消息沒有問題,說明3個消息都發送成功了。再看看消費端的日誌,

    msgs.size():1
    this is batchMQ,my NO is 2---Mon Jun 15 10:59:37 CST 2020
    

    只消費了一個消息,並且這個消息中i的值是2,那麼a的值就是2+4=6,它是>5的,滿足SQL的條件,所以被消費掉了。這完全符合我們的預期。

    總結

    今天的兩個小功能還是比較有意思的,但裡邊也有需要注意的地方,

    • 消息的批量發送,只要我們滿足它的條件,然後使用List發送就可以了;批量消費,默認的消費個數是1,我們可以調整它的值,這樣就可以一次消費多個消息了;
    • 過濾消息中,最大的坑就是隊列的配置里,需要設置enablePropertyFilter=true,否則消費端在啟動的時候報不支持SQL的錯誤;

    我們在使用的時候,多加留意就可以了,有問題,評論區留言吧~

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

    【其他文章推薦】

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

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心

  • 分析ThreadLocal的弱引用與內存泄漏問題-Java8,利用線性探測法解決hash衝突

    分析ThreadLocal的弱引用與內存泄漏問題-Java8,利用線性探測法解決hash衝突

    目錄

    一.介紹

    二.問題提出

      2.1內存原理圖

      2.2幾個問題

    三.回答問題

      3.1為什麼會出現內存泄漏

      3.2若Entry使用弱引用

      3.3弱引用配合自動回收

    四.總結  

     

     

     

    一.介紹

      之前使用ThreadLocal的時候,就聽過ThreadLocal怎麼怎麼的可能會出現內存泄漏,具體原因也沒去深究,就是一種不清不楚的狀態。最近在看JDK的源碼,其中就包含ThreadLocal,在對ThreadLocal的使用場景介紹以及源碼的分析后,對於ThreadLocal中可能存在的內存泄漏問題也搞清楚了,所以這裏專門寫一篇博客分析一下。

      在分析內存泄漏之前,先了解2個概念,就是內存泄漏和內存溢出:

      內存溢出(memory overflow):是指不能申請到足夠的內存進行使用,就會發生內存溢出,比如出現的OOM(Out Of Memory)

      內存泄漏(memory lack):內存泄露是指在程序中已經動態分配的堆內存由於某種原因未釋放或者無法釋放(已經沒有用處了,但是沒有釋放),造成系統內存的浪費,這種現象叫“內存泄露”。

      當內存泄露到達一定規模后,造成系統能申請的內存較少,甚至無法申請內存,最終導致內存溢出,所以內存泄露是導致內存溢出的一個原因。

     

    二.問題提出

    2.1內存原理圖

      下圖是程序運行中的內存分布圖,簡要介紹一下這種圖:當前線程有一個threadLocals屬性(ThreadLocalMap屬性),該map的底層是數組,每個數組元素時Entry類型,Entry類型的key是ThreadLocal類型(也就是創建的ThreadLocal對象),而value是則是ThreadLocal.set()方法設置的value。

      

      需要注意的是ThreadLocalMap的Entry,繼承自弱引用,定義如下,關於Java的引用介紹,可以參考:Java-強引用、軟引用、弱引用、虛引用

    /**
     * ThreadLocalMap中存放的元素類型,繼承了弱引用類
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        // key對應的value,注意key是ThreadLocal類型
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    

     

    2.2問題提出

      在看了上面ThreadLocal和ThreadLocalMap相關的內存分佈以及關聯后,提出這樣幾個問題:

      1.ThreadLocal為什麼會出現內存溢出?

      2.Entry的key為什麼要用弱引用?

      3.使用弱引用是否就能解決內存溢出?

      為了回答上面這3個問題,我寫了一段代碼,後面根據這段代碼進行分析:

    public void step1() {
        // some action
        
        step2();
        step3();
        
        // other action
    }
    
    // 在stepX中都會創建threadLocal對象
    public void step2() {
        ThreadLocal<String> tl = new ThreadLocal<>();
        tl.set("this is value");
    }
    public void step3() {
        ThreadLocal<Integer> tl = new ThreadLocal<>();
        tl.set(99);
    }
    

      在step1中會調用step2和step3,step2和step3都會創建ThreadLocal對象,當step2和step3執行完畢后,其中的棧內存中ThreadLocal引用就會被清除。

     

    三.回答問題

     

      

      現在針對這個圖,一步一步的分析問題,中途會得出一些臨時的結論,但是最終的結論才是正確的

     

    3.1為什麼會出現內存泄露

      現在有2點假設,本小節的分析都是基於這兩個假設之上的:

      1.Entry的key使用強引用,key對ThreadLocal對象使用強引用,也就是上面圖中連線5是強引用(key強引用ThreadLocal對象);

      2.ThreadLocalMap中不會對過期的Entry進行清理。

      上面代碼中,如果ThreadLocalMap的key使用強引用,那麼即使棧內存的ThreadLocal引用被清除,但是堆中的ThreadLocal對象卻並不會被清除,這是因為ThreadLocalMap中Entry的key對ThreadLocal對象是強引用。

      如果當前線程不結束,那麼堆中的ThreadLocal對象將會一直存在,對應的內存就不會被回收,與之關聯的Entry也不會被回收(Entry對應的value也不會被回收),當這種情況出現數量比較多的時候,未釋放的內存就會上升,就可能出現內存泄漏的問題。

      上面的結論是暫時的,有前提假設!!!最終結論還需要看後面分析。

     

    3.2若Entry使用弱引用

      

      仍舊有1個假設,就是ThreadLocalMap中不會對過期的Entry進行清理,陳舊的Entry是指Entry的key為null。

      按照源碼,Entry繼承弱引用,其Key對ThreadLocal是弱引用,也就是上圖中連線5是弱引用,連線6仍為強引用。

      同樣以上面代碼為例,step2和step3創建了ThreadLocal對象,step2和step3執行完后,棧中的ThreadLocal引用被清除了;由於堆內存中ThreadLocalMap的Entry key弱引用ThreadLocal對象,根據垃圾收集器對弱引用對象的處理:

    當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

      此時堆中ThreadLocal對象會被gc回收(因為現在沒有對ThreadLocal的強引用,只有一個弱引用ThreadLocal對象),Entry的key為null,但是value不為null,且value也是強引用(連線6),所以Entry仍舊不能回收,只能釋放ThreadLocal的內存,仍舊可能導致內存泄漏

      在沒有自動清理陳舊Entry的前提下,即使Entry使用弱引用,仍可能出現內存泄漏。

     

    3.3弱引用配合自動回收

      通過3.2的分析,其實只要陳舊的Entry能自動被回收,就能解決內存泄漏的問題,其實JDK就是這麼做的。

      如果看過源碼,就知道,ThreadLocalMap底層使用數組來保存元素,使用“線性探測法”來解決hash衝突,關於線性探測法的介紹可以查看:利用線性探測法解決hash衝突

      在每次調用ThreadLocal類的get、set、remove這些方法的時候,內部其實都是對ThreadLocalMap進行操作,對應ThreadLocalMap的get、set、remove操作。

      重點來了!重點來了!重點來了!

      ThreadLocalMap的每次get、set、remove,都會清理過期的Entry,下面以get操作解釋,其他操作也是一個意思,大致如下:

      1.ThreadLocalMap底層用數組保存元素,當get一個Entry時,根據key的hash值(非hashCode)計算出該Entry應該出在什麼位置;

      2.計算出的位置可能會有衝突,比如預期位置是position=5,但是position=5的位置已經有其他Entry了;

      3.出現衝突后,會使用線性探測法,找position=6位置上的Entry是否匹配(匹配是指hash相同),如果匹配,則返回position=6的Entry。

      4.在這個過程中,如果position=5位置上的Entry已經是陳舊的Entry(Entry的key為null),此時position=5的key就應該被清理;

      5.光清理position=5的Entry還不夠,為了保證線性探測法的規則,需要判斷數組中的其他元素是否需要調整位置(如果需要,則調整位置),在這個過程中,也會進行清理陳舊Entry的操作。

      上面這5個步驟就保證了每次get都會清理數組中(map)的陳舊Entry,清理一個陳舊的Entry,就是下面這三行代碼:

    Entry.value = null; // 將Entry的value設為null
    table[index] = null;// 將數組中該Entry的位置設置null
    size--;	// map的size減一
    

      對於ThreadLocal的set、remove也類似這個原理。

      有了自動回收陳舊Entry的操作,需要注意的是,在這個時候,key使用弱引用就是至關重要的一點!!!

      因為key使用弱引用后,當弱引用的ThreadLocal對象被會回收后,該key的引用為null,則該Entry在下一次get、set、remove的時候就才會被清理,從未避免內存泄漏的問題。

      

    四.總結

      在上面的分析中,看到ThreadLocal基本不會出現內存泄漏的問題了,因為ThreadLocalMap中會在get、set、remove的時候清理陳舊的Entry,與Entry的key使用弱引用密不可分。

      當然我們也可以在代碼中手動調用ThreadLocal的remove方法進行清除map中key為該threadLocal對象的Entry,同時清理過期的Entry。

      

     

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

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

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

    ※超省錢租車方案