日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

记一次synchronized锁字符串引发的坑兼再谈Java字符串

發(fā)布時間:2024/4/17 java 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 记一次synchronized锁字符串引发的坑兼再谈Java字符串 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

問題描述

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

通過代理IP訪問國外某網(wǎng)站N,每個IP對應一個固定的網(wǎng)站N的COOKIE,COOKIE有失效時間。

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

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

不知道這個問題有沒有說清楚,沒說清楚沒關(guān)系,寫一段測試代碼:

public class StringThread implements Runnable {private static final String LOCK_PREFIX = "XXX---";private String ip;public StringThread(String ip) {this.ip = ip;}@Overridepublic void run() {String lock = buildLock();synchronized (lock) {System.out.println("[" + JdkUtil.getThreadName() + "]開始運行了");// 休眠5秒模擬腳本調(diào)用JdkUtil.sleep(5000);System.out.println("[" + JdkUtil.getThreadName() + "]結(jié)束運行了");}}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;}}

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

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

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

public class StringThreadTest {private static final int THREAD_COUNT = 5;@Testpublic 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 (;;);}}

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

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

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

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

?

問題原因

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

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

因為synchronized鎖對象的時候,保證同步代碼塊中的代碼執(zhí)行是串行執(zhí)行的前提條件是鎖住的對象是同一個,因此既然多線程在synchronized部分是并行執(zhí)行的,那么可以推測出多線程下傳入同一個IP,構(gòu)建出來的lock字符串并不是同一個。

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

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; }

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

public String toString() {// Create a copy, don't share the arrayreturn new String(value, 0, count); }

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

?

如何解決?

上面的問題原因找到了,就是每次StringBuilder構(gòu)建出來的對象都是new出來的對象,那么應當如何解決?這里我先給解決辦法就是sb.toString()后再加上intern(),下一部分再說原因,因為我想對String再做一次總結(jié),加深對String的理解。

OK,代碼這么改:

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 @Override 12 public void run() { 13 14 String lock = buildLock(); 15 synchronized (lock) { 16 System.out.println("[" + JdkUtil.getThreadName() + "]開始運行了"); 17 // 休眠5秒模擬腳本調(diào)用 18 JdkUtil.sleep(5000); 19 System.out.println("[" + JdkUtil.getThreadName() + "]結(jié)束運行了"); 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 }

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

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

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

?

再看String

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

  • JDK6的版本,常量池在持久代PermGen中分配
  • JDK7的版本,常量池在堆Heap中分配
  • 字符串是存儲在常量池中的,有兩種類型的字符串數(shù)據(jù)會存儲在常量池中:

  • 編譯期就可以確定的字符串,即使用""引起來的字符串,比如String a = "123"String b = "1" + B.getStringDataFromDB() + "2" + C.getStringDataFromDB()、這里的"123"、"1"、"2"都是編譯期間就可以確定的字符串,因此會放入常量池,而B.getStringDataFromDB()、C.getStringDataFromDB()這兩個數(shù)據(jù)由于編譯期間無法確定,因此它們是在堆上進行分配的
  • 使用String的intern()方法操作的字符串,比如String b = B.getStringDataFromDB().intern(),盡管B.getStringDataFromDB()方法拿到的字符串是在堆上分配的,但是由于后面加入了intern(),因此B.getStringDataFromDB()方法的結(jié)果,會寫入常量池中
  • 常量池中的String數(shù)據(jù)有一個特點:每次取數(shù)據(jù)的時候,如果常量池中有,直接拿常量池中的數(shù)據(jù);如果常量池中沒有,將數(shù)據(jù)寫入常量池中并返回常量池中的數(shù)據(jù)

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

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

    因此不管多少條線程,只要取"XXX-192.168.1.1",取出的一定是同一個對象,就是常量池中的"XXX-192.168.1.1"
    這一切,都是String的intern()方法的作用

    ?

    后記

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

    這個業(yè)務場景并不復雜,整個代碼實現(xiàn)也不是很復雜,但是運行的時候它就出了并發(fā)問題了。如果沒有扎實的基礎(chǔ):知道String里面除了常用的那些方法indexOf、subString、concat外還有很不常用的intern()方法 不了解一點JVM:JVM內(nèi)存分布,尤其是常量池 不去看一點JDK源碼:StringBuilder的toString()方法 不對并發(fā)有一些理解:synchronized鎖代碼塊的時候怎么樣才能保證多線程是串行執(zhí)行代碼塊里面的代碼的這個問題出了,是根本無法解決的,甚至可以說如何下手去分析都不知道。

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

    與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖

    總結(jié)

    以上是生活随笔為你收集整理的记一次synchronized锁字符串引发的坑兼再谈Java字符串的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。