經過String的不變性案例分析Java變量的可變性

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

一、String變量是什麼不變?final修飾變量時的不變性指的又是什麼不變,是引用?仍是內存地址?仍是值?node

二、java對象進行重賦值或者改變屬性時在內存中是如何實現的?spring

三、如下是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     }

若是你對以上幾個問題通通能很清晰的答出來,那麼就不用閱讀本文了,不然還請慢慢讀來。this

正文spa

一、從工做中的問題出發3d

寫這篇文章的原由,是工做中遇到了一個場景,大致是這樣的。code

公司項目用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採用了以下的改進版:blog

 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與問題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。

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

相關文章
相關標籤/搜索