不可變
一、 日期轉換的問題
1、引入
下面的代碼在運行時,由於 SimpleDateFormat 不是線程安全的
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(sdf.parse("1951-04-21"));
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
運行之後會出現下面的結果:
Mon Apr 21 00:00:00 CST 4
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Thu Apr 21 00:00:00 CST 214
Fri Apr 21 00:00:00 CST 4000
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:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at concurrent.ch7_immutable.DataFormatTest.lambda$main$0(DataFormatTest.java:12)
at java.lang.Thread.run(Thread.java:745)
Sat Apr 21 00:00:00 CST 1951
Wed Apr 21 00:00:00 CST 1
Fri Apr 21 00:00:00 CST 4000
不僅結果不對,而且還會出現NumberFormatException異常。
2、解決方法:鎖
要解決SimpleDateFormat的線程安全問題,一種方法時使用synchronized鎖,代碼如下:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
//對sdf對象上鎖
synchronized (sdf) {
try {
System.out.println(sdf.parse("1951-04-21"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
結果:
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
Sat Apr 21 00:00:00 CST 1951
可以看到結果沒有問題,但是我們之前也介紹過使用synchronized加鎖會帶來性能上的問題,所以又引入另外一種解決方法:不可變類
2、解決方法:不可變
如果一個對象在不能夠修改其內部狀態(屬性),那麼它就是線程安全的,因爲不存在併發修改。這樣的對象在 Java 中有很多,例如在 Java 8 後,提供了一個新的日期格式化類:
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(dtf.parse("1951-04-21"));
}).start();
}
結果:
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
{},ISO resolved to 1951-04-21
雖然和上面的輸出格式不同,但是結果是沒問題的。DateTimeFormatter的註釋中也聲明它是一個不可變的線程安全類,不可變對象,實際是另一種避免競爭的方式。
/ *
* @implSpec
* This class is immutable and thread-safe.
*
* @since 1.8
*/
public final class DateTimeFormatter {
二、不可變設計
另一個大家更爲熟悉的 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
//...
}
1、final的使用
我們發現String類和類中所有屬性都是 final 的
- 屬性用 final 修飾保證了該屬性是隻讀的,不能修改
- 類用 final 修飾保證了該類中的方法不能被覆蓋,防止子類無意間破壞不可變性
2、保護性拷貝
當需要修改屬性的值,如char value[]
時,並沒有改變其中的值,而是直接新建(拷貝)一個對象賦值給該屬性。
例如:
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
這裏沒有直接使用this.value = value
,而是使用Arrays.copyOf
進行拷貝,就是爲了防止外部引用對參數value的改變會影響到this.value,而Arrays.copyOf
創建了一個值參數一樣但不是同一個對象,就避免了這種問題。
再以以 substring 爲例:
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//上面都是參數合法性檢查,最後調用new String創建了新的對象
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
可以看到substring並沒有直接修改成員變量char value[]
的值,而是創建了一個新的對象。而該構造函數中也使用了Arrays.copyOfRange
進行拷貝。構造新字符串對象時,會生成新的 char[] value,對內容進行復制 。這種通過創建副本對象來避 免共享的手段稱之爲保護性拷貝(defensive copy)。
public String(char value[], int offset, int count) {
//...(參數合法性檢查)
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
三、設計模式:享元模式
1、 簡介
定義 英文名稱:Flyweight pattern. 當需要重用數量有限的同一類對象時,可以使用享元模式
wikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects
2、體現
在JDK中 Boolean,Byte,Short,Integer,Long,Character 等包裝類提供了 valueOf 方法,例如 Long 的 valueOf 會緩存 -128~127 之間的 Long 對象,在這個範圍之間會重用對象,大於這個範圍,纔會新建 Long 對 象:
public static Long valueOf(long l) {
final int offset = 128;
if (l >= -128 && l <= 127) {
return LongCache.cache[(int) l + offset];
}
return new Long(l);
}
注意:
-
Byte, Short, Long 緩存的範圍都是 -128~127
-
Character 緩存的範圍是 0~127
-
Integer的默認範圍是 -128~127
- 最小值不能變
- 但最大值可以通過調整虛擬機參數
-Djava.lang.Integer.IntegerCache.high
來改變
-
Boolean 緩存了 TRUE 和 FALSE
除此之外還有String 串池 (JVM筆記)、 BigDecimal、BigInteger 等。
3、實踐DIY
例如:一個線上商城應用,QPS 達到數千,如果每次都重新創建和關閉數據庫連接,性能會受到極大影響。 這時 預先創建好一批連接,放入連接池。一次請求到達後,從連接池獲取連接,使用完畢後再還回連接池,這樣既節約 了連接的創建和關閉時間,也實現了連接的重用,不至於讓龐大的連接數壓垮數據庫
//連接池實現類
class Pool {
//定義連接池大小
private final int poolSize;
//連接對象數組
private Connection[] connections;
//定義連接狀態數組 0:表示空閒,1:表示繁忙
private AtomicIntegerArray states;
//構造方法初始化
public Pool(int poolSize) {
this.poolSize = poolSize;
this.connections = new Connection[poolSize];
this.states = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection("conn " + i);
}
}
//借連接
public Connection borrow() {
while (true) {
//遍歷連接狀態數組,找出空閒的連接並返回
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) {
//爲了先線程安全,需要使用CAS對連接狀態進行設置
if (states.compareAndSet(i, 0, 1)) {
System.out.println(Thread.currentThread().getName() + " borrow " + connections[i]);
return connections[i];
}
}
}
//當前沒有連接池時,進入等待狀態
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + " wait...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//歸還連接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
//由於此時只有一個線程持有connections[i],所以不會有線程安全問題
states.set(i, 0);
//歸還之後,通知等待的線程
synchronized (this) {
System.out.println(Thread.currentThread().getName() + " free " + conn);
this.notifyAll();
}
break;
}
}
}
}
//連接實現類,無具體內容,省略
class MockConnection implements Connection {
//...
}
測試代碼:
public static void main(String[] args) {
//創建2兩個連接
Pool pool = new Pool(2);
//創建5個線程使用鏈接
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//創建連接
Connection conn = pool.borrow();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//釋放連接
pool.free(conn);
}
}, "線程 " + i).start();
}
}
結果:
線程 1 borrow MockConnection{name='conn 0'}
線程 0 borrow MockConnection{name='conn 1'}
線程 2 wait...
線程 3 wait...
線程 4 wait...
線程 1 free MockConnection{name='conn 0'}
線程 4 borrow MockConnection{name='conn 0'}
線程 3 wait...
線程 2 wait...
線程 0 free MockConnection{name='conn 1'}
線程 2 borrow MockConnection{name='conn 1'}
線程 3 wait...
線程 4 free MockConnection{name='conn 0'}
線程 3 borrow MockConnection{name='conn 0'}
線程 2 free MockConnection{name='conn 1'}
線程 3 free MockConnection{name='conn 0'}
以上實現沒有考慮:
- 連接的動態增長與收縮
- 連接保活(可用性檢測)
- 等待超時處理
- 分佈式 hash
對於關係型數據庫,有比較成熟的連接池實現,例如c3p0, druid等
對於更通用的對象池,可以考慮使用apache commons pool,例如redis連接池可以參考jedis中關於連接池的實現
四、final 原理
1、設置 final 變量的原理
理解了 volatile 原理,再對比 final 的實現就比較簡單了
public class TestFinal {
final int a = 20;
}
字節碼:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 20
7: putfield #2 // Field a:I
<-- 寫屏障
10: return
發現 final 變量的賦值也會通過 putfield 指令來完成,同樣在這條指令之後也會加入寫屏障,保證在其它線程讀到 它的值時不會出現爲 0 的情況
2、獲取 final 變量的原理
讀取final變量時,如果值較小可以直接放入使用類的操作數棧中,如果值較大會放入使用類的常量池中。
五、無狀態
在 web 階段學習時,設計 Servlet 時爲了保證其線程安全,都會有這樣的建議,不要爲 Servlet 設置成員變量,這 種沒有任何成員變量的類是線程安全的
因爲成員變量保存的數據也可以稱爲狀態信息,因此沒有成員變量就稱之爲**【無狀態】**