一.不可變
理論
1.什么是不可變
如果一個對象他里面的所有成員變量都是不可變的,那么這個對象可以認為他是線程安全的,因為多個線程沒辦法修改共享變量所以肯定是安全的。這個就是不可變保障線程安全的一種思維。
2.目前已知的線程安全的幾種處理方案
- 最安全也是最粗暴的方式就是直接加鎖,直接在方法或者類上面加對象鎖,一個線程獲取鎖就可以執(zhí)行臨界區(qū)的代碼,其它線程都要等待獲取鎖,可以解決原子性,可見性,一致性等問題。但是也有缺點,就是增加了線程的上下文切換執(zhí)行效率并不高。
- 第二種方式是通過cas交換的方式來保障線程安全,他的實現(xiàn)是基于樂觀鎖,比較并且設置值,每次對成員變量賦值時都要判斷現(xiàn)在的成員變量是不是我線程本地保存的期望值,如果是期望值則把計算得到的值賦值給成員變量,如果不是期望值則不停的循環(huán)。常見的cas實現(xiàn)有AtomicInteger、AtomicIntegerArray,AtomicReference等,java.util.concurrent.atomic包下面的類。通過cas保障線程安全的優(yōu)勢就是在線程少cpu核多的情況下效率要比直接加鎖的效率高,但是如果線程過多的時候是不適合使用cas的,因為會頻繁的上下文切換,效率可能還沒直接加鎖的效率好。
- 第三種方式也是我今天重點要介紹的那就是不可變對象的設計,不可變對象的class一般是由final修飾的,并且對象內(nèi)部的成員變量也基本都是用final修飾或者private修飾,用final修飾class是為了保障這個對象是沒有子類的,防止子類對成員變量進行修改,final修飾成員變量是為了不讓線程去修改對象的成員變量,如果不存在修改那么線程安全問題也就不存在了。java中常見的不可變類有String,BigDecimal,所有的基礎類型的包裝類如Integer,Boolean等都是不可變對象,也都是線程安全的。這里有一點需要注意,這些不可變對象提供的方法一般都是線程安全的,但是這個線程安全只是說單獨調(diào)用一個方法是線程安全的,如果存在多個方法組合調(diào)用時還是不安全的。因為沒辦法阻止線程交互執(zhí)行時指令的執(zhí)行時間和執(zhí)行順序。
代碼分析
1.SimpleDateFormat和DateTimeFormatter線程安全分析
- 開啟10個線程執(zhí)行testTime1()方法可以查看到結(jié)果拋出了異常這是因為因為SimpleDateFormat不是線程安全對象也不是不可變對象所以多線程執(zhí)行parse方法時就會發(fā)生錯誤,sdf1雖然是局部變量但是多個線程可以共享sdf1并且調(diào)用他的parse()方法所以會報錯并且存在線程安全問題。
- 開啟10個線程執(zhí)行testTime2()方法,可以看到運行結(jié)果是正常的沒有報錯,因為testTime2()方法中使用了DateTimeFormatter他是不可變對象,所以時線程安全的。
public static void testTime1() {
SimpleDateFormat sdf1 = new SimpleDateFormat("YYYY-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
//多線程執(zhí)行sdf1.parse時會報錯,因為SimpleDateFormat不是線程安全對象也不是不可變對象所以
// 多線程執(zhí)行parse方法時就會發(fā)生錯誤,
// sdf1雖然是局部變量但是多個線程可以共享sdf1并且調(diào)用他的parse()方法所以會報錯并且存在線程
// 安全問題,線程安全問題通常都是由多線程同時運行,cpu交替執(zhí)行指令共同修改同一個共享變量引起的。
System.out.println(Thread.currentThread().getName()+" : "+sdf1.parse("2017-05-05"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
//testTime1()執(zhí)行結(jié)果
//======================================================================================================
Exception in thread "Thread-5" Exception in thread "Thread-2" Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.shiyiwei.test.deepStudy.Final.Test1.lambda$testTime1$1(Test1.java:63)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.shiyiwei.test.deepStudy.Final.Test1.lambda$testTime1$1(Test1.java:63)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.shiyiwei.test.deepStudy.Final.Test1.lambda$testTime1$1(Test1.java:63)
at java.lang.Thread.run(Thread.java:748)
Thread-8 : Sun Jan 01 00:00:00 GMT+08:00 2017
Thread-9 : Sun Jan 01 00:00:00 GMT+08:00 2017
Thread-6 : Sun Jan 01 00:00:00 GMT+08:00 2017
Thread-7 : Sun Jan 01 00:00:00 GMT+08:00 2017
Thread-4 : Sun Jan 01 00:00:00 GMT+08:00 2017
Thread-1 : Sun Jan 01 00:00:00 GMT+08:00 2017
Thread-3 : Sun Dec 27 00:00:00 GMT+08:00 2009
//======================================================================================================
public static void testTime2() {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//DateTimeFormatter 是不可變類,所以他是線程安全的。
LocalDate date = dtf.parse("2018-10-01", LocalDate::from);
// log.debug("{}", date);
System.out.println(date);
}).start();
}
}
//testTime2()執(zhí)行結(jié)果
//======================================================================================================
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
2018-10-01
//======================================================================================================
String不可變對象代碼分析
String類型內(nèi)部是由final char value[]不可變數(shù)組來存儲字符串數(shù)據(jù)的,也就是說String只能通過構(gòu)造函數(shù)賦值,他是不可變的。同時他的class也是用final修飾的所以String類是不能被繼承的,所以String他是一個不可變類。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
String類的substring方法是線程安全的,從方法1分析,每次調(diào)用String的substring方法都會創(chuàng)建一個新的String所以原來的String對象就不會發(fā)生改變,從方法2分析新建的String對象會把傳遞過來的數(shù)組拷貝一份然后賦值給value,這樣做的目的是為了防止value引用指向的數(shù)組同時也被其它引用指向,很有可能會改變數(shù)組。這樣就違背了不可變對象的設計原則,這種模式叫做保護性拷貝。
//方法1
public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
//方法2
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count <= 0) {
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
if (offset <= value.length) {
this.value = "".value;
return;
}
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
BigDecimal代碼分析
結(jié)果是打印10次26.75,為啥多線程執(zhí)行add方法不存在線程安全問題呢?是因為每次調(diào)用add方法都會創(chuàng)建一個新的BigDecimal對象,線程操作新的對象就不會對共享對象進行修改事實上BigDecimal被設計時里面的成員是用final修飾的通過構(gòu)造函數(shù)賦值后就不再可變,所以他的對象本身也是不可變的,這種不可變的對象是線程安全的。
public static void testBigDecimal() {
BigDecimal bigDecimal = new BigDecimal("25.25");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 結(jié)果是打印10次26.75,
// 為啥多線程執(zhí)行add方法不存在線程安全問題呢?
// 是因為每次調(diào)用add方法都會創(chuàng)建一個新的BigDecimal對象,線程操作新的對象就不會對共享對象進行修改事實上BigDecimal
// 被設計時里面的成員是用final修飾的通過構(gòu)造函數(shù)賦值后就不再可變,所以他的對象本身也是不可變的。這種不可變的對象是
// 線程安全的
BigDecimal addCount = bigDecimal.add(new BigDecimal("1.5"));
System.out.println(addCount);
}).start();
}
}
//testBigDecimal()執(zhí)行結(jié)果
//======================================================================================================
26.75
26.75
26.75
26.75
26.75
26.75
26.75
26.75
26.75
26.75
//======================================================================================================
二、享元模式
理論
享元模式是23種設計模式之一,并不是并發(fā)編程設計模式。享元模式就是提前實例化同一類對象的幾種常用并且會被復用的對象,把他們緩存起來等到需要用的時候直接從緩存中獲取這些常用對象,盡量避免不停的創(chuàng)建重復的對象可以節(jié)省內(nèi)存空間。常見的基礎類型的包裝類如Integer,Long等都使用了享元模式。
源碼理解
Integer源碼
Integer內(nèi)部類IntegerCache里面有一個static代碼塊,jvm加載內(nèi)部類IntegerCache時會運行static中的代碼創(chuàng)建int對象并且往cache里面塞值,初始化時一共會創(chuàng)建256個int對象,范圍是 -128~127 。
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
調(diào)用Integer的valueOf方法創(chuàng)建Integer對象時可以看到會先判斷傳入的參數(shù)是否在緩存中,如果在緩存中則直接從緩存中獲取,如果不在緩存中則創(chuàng)建一個新的Integer對象。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}