需求: 在并發(fā)情況下,根據(jù)不同的id,查詢庫中數(shù)據(jù),放在內(nèi)存中,僅僅第一次查庫,下次查詢不再查庫。。
思路:查詢時,將id加鎖,第一個獲取鎖的,就先查詢,放入緩存中。后面獲取鎖的,先判斷緩存
最開始按造下面設(shè)計:
private static final ConcurrentMap<String, String> KEYMAP = new ConcurrentHashMap<String, String>();
//開始從緩存map中取,取不到往下查
? ? public static String getKey(String keyId) throws Exception {
? ? ? ? String content = KEYMAP.get(keyId);
? ? ? ? if (null == content) {
? ? ? ? ? ? content = loadKey(keyId);
? ? ? ? } else {
? ? ? ? ? ? System.out.println("時間+--" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + "Map已獲取,直接返回");
? ? ? ? }
? ? ? ? return content;
? ? }
//同步控制,對keyId加鎖,同一個keyId的只查一次數(shù)據(jù)庫,不同id互不影響
? ? public static String loadKey(String keyId) throws Exception {
? ? ? ? synchronized (keyId) {//錯誤在這
? ? ? ? ? ? String content = KEYMAP.get(keyId);
? ? ? ? ? ? if (null != content) {
? ? ? ? ? ? ? ? System.out.println("時間:" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + "【進(jìn)入同步塊直接返回】。。。。");
? ? ? ? ? ? ? ? return content;
? ? ? ? ? ? }
? ? ? ? ? ? System.out.println("時間:" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + "【開始查詢數(shù)據(jù)庫】。。。。");
? ? ? ? ? ? Thread.sleep(2222);
? ? ? ? ? ? System.out.println("時間:" + System.currentTimeMillis() + "," + Thread.currentThread().getName() + "id" + keyId + "獲取緩存為" + "4525262");
? ? ? ? ? ? KEYMAP.put(keyId, "4525262");
? ? ? ? ? ? return content;
? ? ? ? }
? ? }
? ? public static void main(String aa[]) {
? ? ? ? for (int i = 0; i < 10; i++) {
? ? ? ? ? ? new Thread("線程-" + i + "-") {
? ? ? ? ? ? ? ? @Override
? ? ? ? ? ? ? ? public void run() {
? ? ? ? ? ? ? ? ? ? try {
? ? ? ? ? ? ? ? ? ? //測試10個線程同時查詢id為'1'的數(shù)據(jù)
? ? ? ? ? ? ? ? ? ? ? ? KeyLoadUtil.getKey(new String("1"));
? ? ? ? ? ? ? ? ? ? } catch (Exception e) {
? ? ? ? ? ? ? ? ? ? ? ? e.printStackTrace();
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }.start();
? ? ? ? }
? ? }
預(yù)想結(jié)果:

將上面的keyId加鎖,10個線程,當(dāng)同時查詢同一個keyid時候,第一個獲取到keyid鎖的線程,會查詢一次數(shù)據(jù)庫,后面的不再查詢,直接從map中讀取。
而實際并不是如此:
可以看到上圖中,10個線程,同時進(jìn)入了同步塊,synchronized (keyId)這句話并沒有起到作用。
要知道當(dāng)synchronized在作用在同一個對象上才有用,想當(dāng)然的認(rèn)為,每次進(jìn)來的String都是”1”,而且每個”1”都是相互equals,hashcode也相等,屬于同一對象,可以鎖住。。但是,實際上測試并不是,因為String類重寫了equals和hashcode方法,所以正常情況使用equals方法沒有問題。。如果使用System.identityHashCode(keyId),可以發(fā)現(xiàn)這10個‘keyid’真正的hashcode卻是不同,所以在根本上來說并不是同一個對象。
通過System.out.println(“真正code”+System.identityHashCode(keyId)); 打印真正的hashcode可以發(fā)現(xiàn),10個對象真實地址都不相同,如下圖

解決辦法:
解決辦法也很簡單:只要將synchronized (keyId) 改成synchronized (keyId.intern()) 即可。
String#intern方法中看到,這個方法是一個 native 的方法,但注釋寫的非常明了。“如果常量池中存在當(dāng)前字符串, 就會直接返回當(dāng)前字符串. 如果常量池中沒有此字符串, 會將此字符串放入常量池中后, 再返回”。
所以每次返回同一個對象。

注:在海量不同keyid數(shù)據(jù)情況下,不建議用keyId.intern(),雖然keyId.intern()節(jié)省了大量內(nèi)存空間(具體可以查查,大概就是同值的地址指向同一個地址),但是海量數(shù)據(jù)情況下keyId.intern()確是相當(dāng)耗時。
如下是測試耗時,何時使用根據(jù)自身系統(tǒng)考量
int MAX = 1000;
long start = System.currentTimeMillis();
? ? ? ? String[] arr = new String[MAX];
? ? ? ? for (int i = 0; i < MAX; i++) {
? ? ? ? ? ? arr[i] = (i+"");
? ? ? ? }
? ? ? ? System.out.println("原始方法耗時"+String.valueOf(System.currentTimeMillis()-start));
? ? ? ? long startintern = System.currentTimeMillis();
? ? ? ? String[] arr2 = new String[MAX];
? ? ? ? for (int i = 0; i < MAX; i++) {
? ? ? ? ? ? arr2[i] = (i+"").intern();
? ? ? ? }
? ? ? ? System.out.println("intern方法耗時"+String.valueOf(System.currentTimeMillis()-startintern));
MAX為1000筆時,原始1ms,intern 1ms

MAX為10000筆時,原始4ms,intern 4ms

MAX為100000筆時,此時耗時已經(jīng)有明顯不同了,原始16ms,intern 115ms

MAX為1000000筆時,原始方式只要272ms,而intern卻是13446ms

所以一般在字符范圍變法不大情況使用
原文鏈接:https://blog.csdn.net/a718515028/article/details/80003355