問(wèn)題描述

業(yè)務(wù)有一個(gè)需求,我把問(wèn)題描述一下:

通過(guò)代理IP訪問(wèn)國(guó)外某網(wǎng)站N,每個(gè)IP對(duì)應(yīng)一個(gè)固定的網(wǎng)站N的COOKIE,COOKIE有失效時(shí)間。

并發(fā)下,取IP是有一定策略的,取到IP之后拿IP對(duì)應(yīng)的COOKIE,發(fā)現(xiàn)COOKIE超過(guò)失效時(shí)間,則調(diào)用腳本訪問(wèn)網(wǎng)站N獲取一次數(shù)據(jù)。

為了防止多線程取到同一個(gè)IP,同時(shí)發(fā)現(xiàn)該IP對(duì)應(yīng)的COOKIE失效,同時(shí)去調(diào)用腳本更新COOKIE,針對(duì)IP加了鎖。為了保證鎖的全局唯一性,在鎖前面加了標(biāo)識(shí)業(yè)務(wù)的前綴,使用synchronized(lock){...}的方式,鎖住"鎖前綴+IP",這樣保證多線程取到同一個(gè)IP,也只有一個(gè)IP會(huì)更新COOKIE。

不知道這個(gè)問(wèn)題有沒(méi)有說(shuō)清楚,沒(méi)說(shuō)清楚沒(méi)關(guān)系,寫一段測(cè)試代碼:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

public class StringThread implements Runnable {    private static final String LOCK_PREFIX = "XXX---";    
    private String ip;    
    public StringThread(String ip) {        this.ip = ip;
    }

    @Override    public void run() {
        String lock = buildLock();        synchronized (lock) {
            System.out.println("[" + JdkUtil.getThreadName() + "]開始運(yùn)行了");            // 休眠5秒模擬腳本調(diào)用
            JdkUtil.sleep(5000);
            System.out.println("[" + JdkUtil.getThreadName() + "]結(jié)束運(yùn)行了");
        }
    }    
    private String buildLock() {
        StringBuilder sb = new StringBuilder();
        sb.append(LOCK_PREFIX);
        sb.append(ip);
        
        String lock = sb.toString();
        System.out.println("[" + JdkUtil.getThreadName() + "]構(gòu)建了鎖[" + lock + "]");        
        return lock;
    }
    
}

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

簡(jiǎn)單說(shuō)就是,傳入一個(gè)IP,盡量構(gòu)建一個(gè)全局唯一的字符串(這么做的原因是,如果字符串的唯一性不強(qiáng),比方說(shuō)鎖的"192.168.1.1",如果另外一段業(yè)務(wù)代碼也是鎖的這個(gè)字符串"192.168.1.1",這就意味著兩段沒(méi)什么關(guān)聯(lián)的代碼塊卻要串行執(zhí)行,代碼塊執(zhí)行時(shí)間短還好,代碼塊執(zhí)行時(shí)間長(zhǎng)影響極其大),針對(duì)字符串加鎖。

預(yù)期的結(jié)果是并發(fā)下,比如5條線程傳入同一個(gè)IP,它們構(gòu)建的鎖都是字符串"XXX---192.168.1.1",那么這5條線程針對(duì)synchronized塊,應(yīng)當(dāng)串行執(zhí)行,即一條運(yùn)行完畢再運(yùn)行另外一條,但是實(shí)際上并不是這樣。

寫一段測(cè)試代碼,開5條線程看一下效果:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

public class StringThreadTest {    private static final int THREAD_COUNT = 5;
    
    @Test    public void testStringThread() {
        Thread[] threads = new Thread[THREAD_COUNT];        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i] = new Thread(new StringThread("192.168.1.1"));
        }        
        for (int i = 0; i < THREAD_COUNT; i++) {
            threads[i].start();
        }        
        for (;;);
    }
    
}

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

執(zhí)行結(jié)果為:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

[Thread-1]構(gòu)建了鎖[XXX---192.168.1.1]
[Thread-1]開始運(yùn)行了
[Thread-3]構(gòu)建了鎖[XXX---192.168.1.1]
[Thread-3]開始運(yùn)行了
[Thread-4]構(gòu)建了鎖[XXX---192.168.1.1]
[Thread-4]開始運(yùn)行了
[Thread-0]構(gòu)建了鎖[XXX---192.168.1.1]
[Thread-0]開始運(yùn)行了
[Thread-2]構(gòu)建了鎖[XXX---192.168.1.1]
[Thread-2]開始運(yùn)行了
[Thread-1]結(jié)束運(yùn)行了
[Thread-3]結(jié)束運(yùn)行了
[Thread-4]結(jié)束運(yùn)行了
[Thread-0]結(jié)束運(yùn)行了
[Thread-2]結(jié)束運(yùn)行了

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

看到Thread-0、Thread-1、Thread-2、Thread-3、Thread-4這5條線程盡管構(gòu)建的鎖都是同一個(gè)"XXX-192.168.1.1",但是代碼卻是并行執(zhí)行的,這并不符合我們的預(yù)期。

關(guān)于這個(gè)問(wèn)題,一方面確實(shí)是我大意了以為是代碼其他什么地方同步控制出現(xiàn)了問(wèn)題,一方面也反映出我對(duì)String的理解還不夠深入,因此專門寫一篇文章來(lái)記錄一下這個(gè)問(wèn)題并寫清楚產(chǎn)生這個(gè)問(wèn)題的原因和應(yīng)當(dāng)如何解決。

 

問(wèn)題原因

這個(gè)問(wèn)題既然出現(xiàn)了,那么應(yīng)當(dāng)從結(jié)果開始推導(dǎo)起,找到問(wèn)題的原因。先看一下synchronized部分的代碼:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

@Overridepublic void run() {
    String lock = buildLock();    synchronized (lock) {
        System.out.println("[" + JdkUtil.getThreadName() + "]開始運(yùn)行了");        // 休眠5秒模擬腳本調(diào)用
        JdkUtil.sleep(5000);
        System.out.println("[" + JdkUtil.getThreadName() + "]結(jié)束運(yùn)行了");
    }
}

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

因?yàn)?span style="margin: 0px; padding: 0px; color: rgb(255, 0, 0);">synchronized鎖對(duì)象的時(shí)候,保證同步代碼塊中的代碼執(zhí)行是串行執(zhí)行的前提條件是鎖住的對(duì)象是同一個(gè),因此既然多線程在synchronized部分是并行執(zhí)行的,那么可以推測(cè)出多線程下傳入同一個(gè)IP,構(gòu)建出來(lái)的lock字符串并不是同一個(gè)。

接下來(lái),再看一下構(gòu)建字符串的代碼:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

= ="[" + JdkUtil.getThreadName() + "]構(gòu)建了鎖[" + lock + "]"

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

lock是由StringBuilder生成的,看一下StringBuilder的toString方法:

      String(value, 0

那么原因就在這里:盡管buildLock()方法構(gòu)建出來(lái)的字符串都是"XXX-192.168.1.1",但是由于StringBuilder的toString()方法每次都是new一個(gè)String出來(lái),因此buildLock出來(lái)的對(duì)象都是不同的對(duì)象。

 

如何解決?

上面的問(wèn)題原因找到了,就是每次StringBuilder構(gòu)建出來(lái)的對(duì)象都是new出來(lái)的對(duì)象,那么應(yīng)當(dāng)如何解決?這里我先給解決辦法就是sb.toString()后再加上intern(),下一部分再說(shuō)原因,因?yàn)槲蚁雽?duì)String再做一次總結(jié),加深對(duì)String的理解。

OK,代碼這么改:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

 1 public class StringThread implements Runnable { 2  3     private static final String LOCK_PREFIX = "XXX---"; 4      5     private String ip; 6      7     public StringThread(String ip) { 8         this.ip = ip; 9     }10 11     @Override12     public void run() {13         14         String lock = buildLock();15         synchronized (lock) {16             System.out.println("[" + JdkUtil.getThreadName() + "]開始運(yùn)行了");17             // 休眠5秒模擬腳本調(diào)用18             JdkUtil.sleep(5000);19             System.out.println("[" + JdkUtil.getThreadName() + "]結(jié)束運(yùn)行了");20         }21     }22     23     private String buildLock() {24         StringBuilder sb = new StringBuilder();25         sb.append(LOCK_PREFIX);26         sb.append(ip);27         28         String lock = sb.toString().intern();29         System.out.println("[" + JdkUtil.getThreadName() + "]構(gòu)建了鎖[" + lock + "]");30         31         return lock;32     }33     34 }

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

看一下代碼執(zhí)行結(jié)果:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

[Thread-0]構(gòu)建了鎖[XXX---192.168.1.1-0-3]構(gòu)建了鎖[XXX---192.168.1.1-4]構(gòu)建了鎖[XXX---192.168.1.1-1]構(gòu)建了鎖[XXX---192.168.1.1-2]構(gòu)建了鎖[XXX---192.168.1.1-0-2-2-1-1-4-4-3-3

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

可以對(duì)比一下上面沒(méi)有加intern()方法的執(zhí)行結(jié)果,這里很明顯5條線程獲取的鎖是同一個(gè),一條線程執(zhí)行完畢synchronized代碼塊里面的代碼之后下一條線程才能執(zhí)行,整個(gè)執(zhí)行是串行的。

 

再看String

JVM內(nèi)存區(qū)域里面有一塊常量池,關(guān)于常量池的分配

  1. JDK6的版本,常量池在持久代PermGen中分配

  2. JDK7的版本,常量池在堆Heap中分配

字符串是存儲(chǔ)在常量池中的,有兩種類型的字符串?dāng)?shù)據(jù)會(huì)存儲(chǔ)在常量池中:

  1. 編譯期就可以確定的字符串,即使用""引起來(lái)的字符串,比如String a = "123"、String b = "1" + B.getStringDataFromDB() + "2" + C.getStringDataFromDB()、這里的"123"、"1"、"2"都是編譯期間就可以確定的字符串,因此會(huì)放入常量池,而B.getStringDataFromDB()、C.getStringDataFromDB()這兩個(gè)數(shù)據(jù)由于編譯期間無(wú)法確定,因此它們是在堆上進(jìn)行分配的

  2. 使用String的intern()方法操作的字符串,比如String b = B.getStringDataFromDB().intern(),盡管B.getStringDataFromDB()方法拿到的字符串是在堆上分配的,但是由于后面加入了intern(),因此B.getStringDataFromDB()方法的結(jié)果,會(huì)寫入常量池中

常量池中的String數(shù)據(jù)有一個(gè)特點(diǎn):每次取數(shù)據(jù)的時(shí)候,如果常量池中有,直接拿常量池中的數(shù)據(jù);如果常量池中沒(méi)有,將數(shù)據(jù)寫入常量池中并返回常量池中的數(shù)據(jù)

因此回到我們之前的場(chǎng)景,使用StringBuilder拼接字符串每次返回一個(gè)new的對(duì)象,但是使用intern()方法則不一樣:

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

"XXX-192.168.1.1"這個(gè)字符串盡管是使用StringBuilder的toString()方法創(chuàng)建的,但是由于使用了intern()方法,因此第一條線程發(fā)現(xiàn)常量池中沒(méi)有"XXX-192.168.1.1",就往常量池中放了一個(gè)
"XXX-192.168.1.1",后面的線程發(fā)現(xiàn)常量池中有"XXX-192.168.1.1",就直接取常量池中的"XXX-192.168.1.1"。

因此不管多少條線程,只要取"XXX-192.168.1.1",取出的一定是同一個(gè)對(duì)象,就是常量池中的"XXX-192.168.1.1"

這一切,都是String的intern()方法的作用

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

 

后記

就這個(gè)問(wèn)題解決完包括這篇文章寫完,我特別有一點(diǎn)點(diǎn)感慨,很多人會(huì)覺得一個(gè)Java程序員能把框架用好、能把代碼流程寫出來(lái)沒(méi)有bug就好了,研究底層原理、虛擬機(jī)什么的根本就沒(méi)什么用。不知道這個(gè)問(wèn)題能不能給大家一點(diǎn)啟發(fā):

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

這個(gè)業(yè)務(wù)場(chǎng)景并不復(fù)雜,整個(gè)代碼實(shí)現(xiàn)也不是很復(fù)雜,但是運(yùn)行的時(shí)候它就出了并發(fā)問(wèn)題了。

如果沒(méi)有扎實(shí)的基礎(chǔ):知道String里面除了常用的那些方法indexOf、subString、concat外還有很不常用的intern()方法
不了解一點(diǎn)JVM:JVM內(nèi)存分布,尤其是常量池
不去看一點(diǎn)JDK源碼:StringBuilder的toString()方法
不對(duì)并發(fā)有一些理解:synchronized鎖代碼塊的時(shí)候怎么樣才能保證多線程是串行執(zhí)行代碼塊里面的代碼的

這個(gè)問(wèn)題出了,是根本無(wú)法解決的,甚至可以說(shuō)如何下手去分析都不知道。

平面設(shè)計(jì)培訓(xùn),網(wǎng)頁(yè)設(shè)計(jì)培訓(xùn),美工培訓(xùn),游戲開發(fā),動(dòng)畫培訓(xùn)

因此,并不要覺得JVM、JDK源碼底層實(shí)現(xiàn)原理什么的沒(méi)用,恰恰相反,這些都是技術(shù)人員成長(zhǎng)路上最寶貴的東西。

================================================================================== 

我不能保證寫的每個(gè)地方都是對(duì)的,但是至少能保證不復(fù)制、不黏貼,保證每一句話、每一行代碼都經(jīng)過(guò)了認(rèn)真的推敲、仔細(xì)的斟酌。每一篇文章的背后,希望都能看到自己對(duì)于技術(shù)、對(duì)于生活的態(tài)度。

我相信喬布斯說(shuō)的,只有那些瘋狂到認(rèn)為自己可以改變世界的人才能真正地改變世界。面對(duì)壓力,我可以挑燈夜戰(zhàn)、不眠不休;面對(duì)困難,我愿意迎難而上、永不退縮。

其實(shí)我想說(shuō)的是,我只是一個(gè)程序員,這就是我現(xiàn)在純粹人生的全部。

http://www.cnblogs.com/xrq730/p/6662232.html