通過String的不變性案例分析Java變數的可變性

淡墨痕發表於2020-04-19

閱讀本文之前,請先看以下幾個問題:

1、String變數是什麼不變?final修飾變數時的不變性指的又是什麼不變,是引用?還是記憶體地址?還是值?

2、java物件進行重賦值或者改變屬性時在記憶體中是如何實現的?

3、以下是AQS中的一個方法程式碼,請問第一次進入這個方法時,執行到return的時候,t==node? head==tail?node.prev==head?head.next==node?這四個比較分別是true還是false?

 1 private Node enq(final Node node) {
 2         for (;;) {
 3             Node t = tail;
 4             if (t == null) { // Must initialize
 5                 if (compareAndSetHead(new Node()))
 6                     tail = head;
 7             } else {
 8                 node.prev = t;
 9                 if (compareAndSetTail(t, node)) {
10                     t.next = node;
11                     return t;
12                 }
13             }
14         }
15     }

如果你對以上幾個問題統統能很清晰的答出來,那麼就不用閱讀本文了,否則還請慢慢讀來。

正文

1、從工作中的問題出發

寫這篇文章的起因,是工作中遇到了一個場景,大體是這樣的。

公司專案用Apollo作為配置中心,現在有5個簡訊驗證碼的傳送場景,每個場景都有最大傳送次數上限,因為場景不同所以這個上限也彼此不同。每次傳送簡訊前都會校驗一下已傳送次數是否已經超過這個上限,並且上限可能隨時動態調整所以需要將每個場景的傳送次數上限作為apollo配置項配置起來。而作為一個有追求的開發攻城獅,不能容忍通過場景碼用if else這種粗糙的手段來獲取配置項,所以BZ想到了Map。初步實現是這樣的:

 1 @Component
 2 @Getter
 3 public class ApolloDemo {
 4 
 5     @Value("scene1.times")
 6     private String scene1Times;
 7     @Value("scene2.times")
 8     private String scene2Times;
 9     @Value("scene3.times")
10     private String scene3Times;
11     @Value("scene4.times")
12     private String scene4Times;
13     @Value("scene5.times")
14     private String scene5Times;
15 
16     public static final Map<String, String> sceneMap = new HashMap<>();
17 
18     @PostConstruct
19     public void initMap () {
20         sceneMap.put("scene_code1", scene1Times);
21         sceneMap.put("scene_code2", scene2Times);
22         sceneMap.put("scene_code3", scene3Times);
23         sceneMap.put("scene_code4", scene4Times);
24         sceneMap.put("scene_code5", scene5Times);
25     }
26 }

但BZ是一個頗具智慧的攻城獅,這樣的程式碼很明視訊記憶體在問題:因為String是不變的,所以在initMap中初始化了Map之後,如果後續成員變數scene1Times改變了值,Map中的值是不會同步改變的。所以BZ採用瞭如下的改進版:

 1 package com.mydemo;
 2 
 3 import lombok.Getter;
 4 import org.springframework.beans.factory.annotation.Value;
 5 import org.springframework.stereotype.Component;
 6 import org.springframework.stereotype.Service;
 7 
 8 import javax.annotation.PostConstruct;
 9 import java.lang.reflect.Method;
10 import java.util.HashMap;
11 import java.util.Map;
12 
13 @Component
14 @Getter
15 public class ApolloDemo {
16 
17     @Value("scene1.times")
18     private String scene1Times;
19     @Value("scene2.times")
20     private String scene2Times;
21     @Value("scene3.times")
22     private String scene3Times;
23     @Value("scene4.times")
24     private String scene4Times;
25     @Value("scene5.times")
26     private String scene5Times;
27 
28     private static final Map<String, String> sceneMap = new HashMap<>();
29 
30     @PostConstruct
31     public void initMap () {
32         sceneMap.put("scene_code1", "getScene1Times");
33         sceneMap.put("scene_code2", "getScene2Times");
34         sceneMap.put("scene_code3", "getScene3Times");
35         sceneMap.put("scene_code4", "getScene4Times");
36         sceneMap.put("scene_code5", "getScene5Times");
37     }
38 
39     public String getTimesByScene(String sceneCode){
40         String methodName = sceneMap.get(sceneCode);
41         try {
42             Method method = ApolloDemo.class.getMethod(methodName);
43             Object result = method.invoke(this, null);
44             return (String)result;
45         } catch (Exception e) {
46             e.printStackTrace();
47         }
48         return "";
49     }
50 }

通過反射呼叫get方法來獲取實時的apollo配置值,功能算是交付出去了。但問題卻剛剛開始。

我們都知道String是不可變的,那它為什麼不可變呢?因為它的類由final修飾不可繼承,而它用於存放字串的成員變數char[]也是由final修飾的。繼續追問,final修飾的變數不可變是指什麼不可變?不可變有兩種,一種是引用不可變,一種是值不可變。此處答案是引用不可變。其實Java中,不管是給物件賦值,還是給物件中的屬性賦值,賦的值其實都是引用。針對String的不可變是引用不可變的結論,通過一個例子就可以證明:

 1 public static void main(String[] args) {
 2         String text = "text";
 3         System.out.println(text);
 4         try {
 5             Field value = text.getClass().getDeclaredField("value");
 6             value.setAccessible(true);
 7             char[] valueArr = (char[])value.get(text);
 8             valueArr[1]='a';
 9         } catch (Exception e) {
10             e.printStackTrace();
11         }
12         System.out.println(text);
13     }

執行結果:

text
taxt

BZ通過反射改變了String的值,說明它的值是可變的,如果用反射執行 value.set(text, "aaa"),則會報錯不讓改,即引用不可變。

由此問題1得到了解答,記憶體地址只是用於迷惑人的,一個物件建立完成之後,其記憶體地址是不可改變的,直到被回收後重新分配。

 

2、問題2與問題3一起分析

針對問題3的方法,BZ用記憶體示意圖來分析:

1)、剛進入enq方法時,tail、head、node的記憶體佈局是這樣:

 

2)、走完第一遍迴圈並之後,完成了對head和tail的賦值,此時記憶體分佈是這樣:

 

 3)、進入第二遍迴圈中,走完第三行程式碼 Node t = tail 和node.prev=t之後的記憶體分佈如下,因為賦值都是引用賦值,所以區域性變數t和node.prev均指向了new Node()的引用地址。

 

 4)、走完CAS tail之後是這樣,即CAS是將tail的引用從new Node()改為了 node:

 

 5)、走完最後一行t.next=node,記憶體分佈如下所示,t指向的一直都是new Node(),而將node賦值給t.next之後,node和new Node()就組成了一個雙向連結串列,new Node()是頭,正好head指向它;node是尾,正好tail指向它,至此完成了AQS中雙向連結串列的構建。

 

 通過上面5張截圖的變化,相信能對於問題2已經有答案了,至於問題3的答案,看最後一張圖也就水落石出了,t==node? head==tail?node.prev==head?head.next==node?答案分別是:false;false;true;true。

本文到此為止,其中有描述不清楚的或者理解不到位的地方,還請各位看官批評指正,謝謝!

相關文章