1. 程式人生 > >JAVA中synchronized和String引出的一系列內容

JAVA中synchronized和String引出的一系列內容

背景

最近,在開發一個功能,由於會存在併發問題(發生機率不大),因此想上個鎖避免一下,但是又因為處於效能考慮,不想鎖整個方法或者都去鎖住同一個物件,這樣會使得所有請求進入這個方法後,都會變成序列進行排隊,但是很多時候,不同的請求之間是沒有資源競爭的,應該是可以並行的,對於有競爭的請求,才應該採取序列的方式。該方法只有一個String型別的入參,於是我一開始就打算用這個入參作為被鎖的物件,因為一開始簡單的想著,string型別的都是在常量池中的,返回的都是同一個物件,理論上可以做到入參為一樣的字串時,序列執行,不一樣的字串時,並行執行。但是謹慎起見,在自己寫了幾個相關測試後,發現背後內容挺多的。

測試一:

測試結果:符合預期。一樣的字串,執行變為序列。不一樣的字串,執行仍然是並行。

測試程式碼:

public class Test {

	public static void main(String... args) throws Exception {
		new Thread() {
			public void run() {
				try {
					remoteCall("102017071807514199868487451");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

		new Thread() {
			public void run() {
				try {
					remoteCall("102017071807514199868487451");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

		new Thread() {
			public void run() {
				try {
					remoteCall("1111111111111111111111111111");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

	}

	private static void remoteCall(String aaa) throws InterruptedException {
		System.out.println("進入方法:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());

		synchronized (aaa) {
			System.out.println("進入鎖內:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
			Thread.sleep(5000);
		}
		System.out.println("退出鎖:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
	}

}
輸出結果:
進入方法:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501125434326
進入鎖內:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501125434326
進入方法:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501125434327
進入方法:Thread-2資料為:1111111111111111111111111111 hashcode:-872251968 執行時間:1501125434328
進入鎖內:Thread-2資料為:1111111111111111111111111111 hashcode:-872251968 執行時間:1501125434328
退出鎖:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501125439327
進入鎖內:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501125439327
退出鎖:Thread-2資料為:1111111111111111111111111111 hashcode:-872251968 執行時間:1501125439329
退出鎖:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501125444327

從輸出結果來看,我們的目的好像是達到了。但是如果我們對String型別有一定了解的話,其實就會發現上面的方法有一個很大的漏洞,那就是我們的字串並不是動態傳入的。所謂的不是動態傳入的意思就是,我們要傳入方法的字串,其實在程式編譯期間就已經知道了。問題就出在了這裡:

在程式編譯期,編譯程式先去字串常量池檢查,是否存在“102017071807514199868487451”,如果不存在,則在常量池中開闢一個記憶體空間存放“102017071807514199868487451”;如果存在的話,則不用重新開闢空間。然後在棧中開闢一塊空間,命名為變數名,存放的值為常量池中“102017071807514199868487451”的記憶體地址。

因此,上面的兩個字串“102017071807514199868487451”是同一個物件,因此鎖就生效了。但是可惜的是,我的程式關於這個入參的字串內容,很多時候都是動態生成的,也就是編譯期間是不知道的,於是就有了下面的測試二。

測試二:

測試結果:不符合預期。一樣內容的字串都是並行了。

測試程式碼:

public class Test {

	public static void main(String... args) throws Exception {
		new Thread() {
			public void run() {
				try {
					remoteCall("102017071807514199868487451");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

		new Thread() {
			public void run() {
				try {
					Scanner scanner = new Scanner(System.in);
					remoteCall(scanner.nextLine());
					scanner.close();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();
	}

	private static void remoteCall(String aaa) throws InterruptedException {
		System.out.println("進入方法:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
		synchronized (aaa) {
			System.out.println("進入鎖內:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
			Thread.sleep(5000);
		}
		System.out.println("退出鎖:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
	}

}
輸出結果:
進入方法:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126502734
進入鎖內:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126502734
102017071807514199868487451
進入方法:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126503698
進入鎖內:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126503698
退出鎖:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126507735
退出鎖:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126508699
從輸出結果可以明顯看出,我在控制檯中再一次輸入102017071807514199868487451後,發現它立刻就獲得鎖了。雖然hashcode和字串內容都一樣,但是從它立馬獲得鎖我們就可以明顯看出,兩個102017071807514199868487451字串並不是同一個物件。

要知道hashcode雖然相同,但是因為這個是一個可以複寫的方法,所以因此對於String的hashcode而言,它是不具有唯一性的,這一點我們從可以從String的原始碼中看出:

public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }
又因為,當我們使用Scanner通過System.in從控制檯讀取使用者的輸入時,其實是產生了一個新的物件,這一點也可以輕鬆從Scanner的原始碼中看出:
 public String nextLine() {
        if (hasNextPattern == linePattern())
            return getCachedResult();
        clearCaches();

        String result = findWithinHorizon(linePattern, 0);
        if (result == null)
            throw new NoSuchElementException("No line found");
        MatchResult mr = this.match();
        String lineSep = mr.group(1);
        if (lineSep != null)
            result = result.substring(0, result.length() - lineSep.length());
        if (result == null)
            throw new NoSuchElementException();
        else
            return result;
    }
其實,我們可以修改一下remoteCall方法,再測試一次,就非常明白了:
private static void remoteCall(String aaa) throws InterruptedException {
		System.out.println("真正的記憶體地址:" + System.identityHashCode(aaa));
		System.out.println("進入方法:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
		synchronized (aaa) {
			System.out.println("進入鎖內:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
			Thread.sleep(5000);
		}
		System.out.println("退出鎖:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
	}
此時的輸出為:
真正的記憶體地址:1616247035
進入方法:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126946265
進入鎖內:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126946266
102017071807514199868487451
真正的記憶體地址:455326124
進入方法:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126947367
進入鎖內:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126947367
退出鎖:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126951266
退出鎖:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501126952367
可以明顯的看出,其實兩者的記憶體地址是不一樣的,是明顯的兩個不同的物件。

那對於這種情況,有沒有方法解決呢?能不能做到字串內容的時候,就序列執行,內容不一樣的時候就並行執行呢?答案是有的,就是利用String的intern()方法。

測試三:

測試結果:符合預期。一樣的字串,執行變為序列。不一樣的字串,執行仍然是並行。

測試程式碼:

public class Test {

	public static void main(String... args) throws Exception {
		new Thread() {
			public void run() {
				try {
					remoteCall("102017071807514199868487451");
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();

		new Thread() {
			public void run() {
				try {
					Scanner scanner = new Scanner(System.in);
					remoteCall(scanner.nextLine());
					scanner.close();
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			};
		}.start();
	}

	private static void remoteCall(String aaa) throws InterruptedException {
		System.out.println("真正的記憶體地址:" + System.identityHashCode(aaa));
		aaa = aaa.intern();
		System.out.println("真正的記憶體地址:" + System.identityHashCode(aaa));
		System.out.println("進入方法:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
		synchronized (aaa) {
			System.out.println("進入鎖內:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
			Thread.sleep(5000);
		}
		System.out.println("退出鎖:" + Thread.currentThread().getName() + "資料為:" + aaa + " hashcode:" + aaa.hashCode() + " 執行時間:" + System.currentTimeMillis());
	}

}

輸出結果:
真正的記憶體地址:1616247035
真正的記憶體地址:1616247035
進入方法:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501127094470
進入鎖內:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501127094470
102017071807514199868487451
真正的記憶體地址:1917485542
真正的記憶體地址:1616247035
進入方法:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501127095710
進入鎖內:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501127099471
退出鎖:Thread-0資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501127099471
退出鎖:Thread-1資料為:102017071807514199868487451 hashcode:618974246 執行時間:1501127104471

看著輸出應該就一目瞭然了,這裡就不多加解釋了。

需要注意的是,intern可以使得內容一樣的字串,指向同一個記憶體地址,這會使得大量字串難以清理,導致記憶體上升。因此我們,使用這種方法的時候,還要對入參的字串的取值範圍進行一定估量。如果經常呼叫這個方法的時候,傳入的引數都是不一樣,並且變化很大的字串,可能就需要小心使用了。在我實際的工作環境中,呼叫這個方法時,傳入的字串都是在一定的範圍內的,並且總體個數我可以接受,因此我不必考慮這個記憶體消耗的問題。因此,我可以做到犧牲一部分記憶體和一部分方法的執行效率,以此來提高整個方法執行的並行度。

相關推薦

JAVAsynchronizedString引出一系列內容

背景 最近,在開發一個功能,由於會存在併發問題(發生機率不大),因此想上個鎖避免一下,但是又因為處於效能考慮,不想鎖整個方法或者都去鎖住同一個物件,這樣會使得所有請求進入這個方法後,都會變成序列進行排隊,但是很多時候,不同的請求之間是沒有資源競爭的,應該是可以並行的,對於有

Javasynchronized同步方法

在多執行緒中,有一個經典問題:存票售票問題 如果只用兩個Thread子類則容易陷入死迴圈。 有一個很好的解決辦法就是synchronized。 方法一:在thread子類的run中直接通過synchronized來申請物件的鎖旗標,即用synchronized把存售票程式碼框起來。 方法二:在票類中直

JavacharString的轉換

Java中char是一個基本型別,而String是一個引用型別。有時候我們需要在它們之間互相轉換。 String轉換為char 在Java中將String轉換為char是非常簡單的。 1. 使

Javasynchronized ReentrantLock 有什麼不同?

        Java在過去很長一段時間只能通過synchronized關鍵字來實現互斥,它有一些缺點。比如你不能擴充套件鎖之外的方法或者塊邊界,嘗試獲取鎖時不能中途取消等。Java 5 通過Lock

JavaListString【】陣列互轉

將List轉成陣列 1.List轉陣列 /** * List to Array * @param list * @return

javachar String的區別 (轉載 https://blog.csdn.net/li_xiao_ming/article/details/78109462 )

student 進行 sdn 面向對象 有一個 相對 結果 編譯期 分別是 一 char和string的區別: 1 char是表示的是字符,定義的時候用單引號,只能存儲一個字符。例如; char=‘d‘. 而String表示的是字符串,定義的時候

java型別轉string的一些方法區別

在java專案開發中,常常用到一些將型別轉換為string的功能,特地做個總結。 1.(String)要轉換的物件 這是標準的型別轉換,將object轉成String型別的值。使用這種方法時,需要注意的是型別必須能轉成String型別。因此最好用instanceof做個型別檢查,以判斷是否可

javasynchronized 用在例項方法物件方法上面的區別

https://bijian1013.iteye.com/blog/1836575    在Java中,synchronized 是用來表示同步的,我們可以synchronized 來修飾一個方法。也可以synchronized 來修飾方法裡面的一個語句塊。    

JavaVolatileSynchronized的區別

個人部落格:小景哥哥 Java中Volatile和Synchronized的區別 JMM Java Memory Model. 併發過程中如何處理可見性、原子性、有序性的問題 Runnable、Thread 併發程式設計中的兩個關鍵問題 a.執行緒之間如何通訊 wai

javavolatilesynchronized

JMM java Memory Model 併發過程中如何處理可見性,原子性,有序性的問題 併發過程中兩個關鍵性的問題 a 執行緒之間如何通訊:wait() notify() notifall() a)共享記憶體 - 隱式通訊 b) 訊息傳遞 - 顯示通訊 b 執行緒之間如

三分鐘理解Java字串(String)的儲存賦值原理

可能很多java的初學者對String的儲存和賦值有迷惑,以下是一個很簡單的測試用例,你只需要花幾分鐘時間便可理解。 1.在看例子之前,確保你理解以下幾個術語: 棧:由JVM分配區域,用於儲存執行緒執行的動作和資料引用。棧是一個執行的單位,Java中一個執行緒就會相應有一個

Java多執行緒 synchronizedLock的區別

我們已經瞭解了Java多執行緒程式設計中常用的關鍵字synchronized,以及與之相關的物件鎖機制。這一節中,讓我們一起來認識JDK 5中新引入的併發框架中的鎖機制。 我想很多購買了《Java程式設計師面試寶典》之類圖書的朋友一定對下面這個面試題感到非常熟悉: 問:請對比synchronized與java

Java顏色的StringColor物件之間的互相轉換

package org.signsmile;import java.awt.Color;public class ColorConverter { public static Color String2Color(String str) {  int i =   Intege

java同步鎖synchronizedLock介面類的區別

Lock提供了和synchronized類似的同步功能,只是在使用時需要顯示地獲取和釋放鎖。雖然Lock缺少了synchronized隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與是釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized所不具備的同步特性

JavaIntegerint比較大小出現的錯誤

最好 裏的 pan 轉換 als 範圍 urn 返回 錯誤 Java在某一處維護著一個常量池,(我記得)在小於128的範圍內,直接用 1 Integer i = 100; 2 int j = 100; 3 return i == j;//true 這裏返回的是true.

javaArrayListLinkedList區別

插入 list 新的 查找 arr tro 基於 列表 時間復雜度 ArrayList和LinkedList最主要的區別是基於不同數據結構 ArrayList是基於動態數組的數據結構,LinkedList基於鏈表的數據結構,針對這點,從時間復雜度和空間復雜度來看主要區別:

java棧的區別

mem 線程 所有 生成 werror 空間 調用 訪問 指向 01,各司其職;         棧內存用來存儲局部變量和方法的調用,         而堆內存用來存儲java中的對象,無論是成員變量,局部變量,還是類變量         他們指向的對象都存儲在堆內存中。

JavaPreparedStatementStatement的用法區別

aik txt 實例 什麽 一點 所有 一個 drop passwd Java中PreparedStatement和Statement的用法區別 (2012-08-01 11:06:44) 轉載▼ 標簽: 雜談 1、 PreparedStatem

javaComparator Comparable的區別

true public arr ins ride err instance ural code 1、Comparable的代碼如下: public interface Comparable<T> { public int compareTo(T o);

JAVA日期時間的格式化選項

println 對象 bsp lec pub cti class 日子 月份 一、使用printf方法 1 import java.util.Date; 2 import java.util.Scanner; 3 4 5 public class Test