阅读本文之前,请先看以下几个问题:

1、String变量是什么不变?final修饰变量时的不变性指的又是什么不变,是引用?还是内存地址?还是值?

2、java对象进行重赋值或者改变属性时在内存中是如何实现的?

3、以下是AQS中的一个方法代码,请问第一次进入这个方法时,执行到return的时候,t==node? head==tail?node.prev==head?head.next==node?这四个比较分别是true还是false?

 private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}

如果你对以上几个问题统统能很清晰的答出来,那么就不用阅读本文了,否则还请慢慢读来。

正文

1、从工作中的问题出发

写这篇文章的起因,是工作中遇到了一个场景,大体是这样的。

公司项目用Apollo作为配置中心,现在有5个短信验证码的发送场景,每个场景都有最大发送次数上限,因为场景不同所以这个上限也彼此不同。每次发送短信前都会校验一下已发送次数是否已经超过这个上限,并且上限可能随时动态调整所以需要将每个场景的发送次数上限作为apollo配置项配置起来。而作为一个有追求的开发攻城狮,不能容忍通过场景码用if else这种粗糙的手段来获取配置项,所以BZ想到了Map。初步实现是这样的:

 @Component
@Getter
public class ApolloDemo { @Value("scene1.times")
private String scene1Times;
@Value("scene2.times")
private String scene2Times;
@Value("scene3.times")
private String scene3Times;
@Value("scene4.times")
private String scene4Times;
@Value("scene5.times")
private String scene5Times; public static final Map<String, String> sceneMap = new HashMap<>(); @PostConstruct
public void initMap () {
sceneMap.put("scene_code1", scene1Times);
sceneMap.put("scene_code2", scene2Times);
sceneMap.put("scene_code3", scene3Times);
sceneMap.put("scene_code4", scene4Times);
sceneMap.put("scene_code5", scene5Times);
}
}

但BZ是一个颇具智慧的攻城狮,这样的代码很明显存在问题:因为String是不变的,所以在initMap中初始化了Map之后,如果后续成员变量scene1Times改变了值,Map中的值是不会同步改变的。所以BZ采用了如下的改进版:

 package com.mydemo;

 import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service; import javax.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map; @Component
@Getter
public class ApolloDemo { @Value("scene1.times")
private String scene1Times;
@Value("scene2.times")
private String scene2Times;
@Value("scene3.times")
private String scene3Times;
@Value("scene4.times")
private String scene4Times;
@Value("scene5.times")
private String scene5Times; private static final Map<String, String> sceneMap = new HashMap<>(); @PostConstruct
public void initMap () {
sceneMap.put("scene_code1", "getScene1Times");
sceneMap.put("scene_code2", "getScene2Times");
sceneMap.put("scene_code3", "getScene3Times");
sceneMap.put("scene_code4", "getScene4Times");
sceneMap.put("scene_code5", "getScene5Times");
} public String getTimesByScene(String sceneCode){
String methodName = sceneMap.get(sceneCode);
try {
Method method = ApolloDemo.class.getMethod(methodName);
Object result = method.invoke(this, null);
return (String)result;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
}

通过反射调用get方法来获取实时的apollo配置值,功能算是交付出去了。但问题却刚刚开始。

我们都知道String是不可变的,那它为什么不可变呢?因为它的类由final修饰不可继承,而它用于存放字符串的成员变量char[]也是由final修饰的。继续追问,final修饰的变量不可变是指什么不可变?不可变有两种,一种是引用不可变,一种是值不可变。此处答案是引用不可变。其实Java中,不管是给对象赋值,还是给对象中的属性赋值,赋的值其实都是引用。针对String的不可变是引用不可变的结论,通过一个例子就可以证明:

 public static void main(String[] args) {
String text = "text";
System.out.println(text);
try {
Field value = text.getClass().getDeclaredField("value");
value.setAccessible(true);
char[] valueArr = (char[])value.get(text);
valueArr[1]='a';
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(text);
}

执行结果:

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。

本文到此为止,其中有描述不清楚的或者理解不到位的地方,还请各位看官批评指正,谢谢!

-------------------------------------------------------------2020-05-01补充分割线--------------------------------------------------------------------------

前两天偶然翻到Hollis发的一篇技术文章,探讨Java是值传递还是引用传递,Hollis不愧是年年纪最轻的阿里P8,在那篇文章中算是把Java值传递给讲清楚了,也是消除了BZ一直以来困扰在内心的疑云,下面BZ继续上面的话题,继续深入探讨下Java的值传递还是引用传递问题。如果想看原文,请关注公众号Hollis,搜文章题目【我要彻底给你讲清楚,Java就是值传递,不接受争辩的那种】。

在上文中,BZ有一句话是这样写的【不管是给对象赋值,还是给对象中的属性赋值,赋的值其实都是引用】。这句话其实没问题,但是会引起误解。

在严格求值策略中(对,你没看错,我们经常讨论的值传递还是引用传递,其对应的专有名词就叫求值策略,不理解没关系,先记住),有三种核心的求值策略,分别是:传值调用、传引用调用和传共享对象调用。

传值调用比较好理解,Java中的基本类型传递就是用的它。对于传引用调用,是指将对象的实际引用直接传给另一个变量(只看这一句解释可能看不出来关键,且看后面的例子)。对于第三种传共享对象调用,是指将对象的引用复制一份,给另一个对象赋值。这种求值策略传递的是引用的值,被划分到传值策略下面,是传值策略的一种特例。Java处理对象时用的就是这第三种求值策略。下面用一个伪代码来论证一下。

Object A = new Object();

Object B = A;

Object B = new Object();

此时如果是第二种引用传递,因为传递的就是A的引用本身,所以在完成B=new Object()的赋值之后,传递的A的引用就变成了另一个新的引用。但实测时会发现,A的引用还是A,只有B的引用变成了新的。由此可以得出结论,Java中传递的不是引用本身,而是引用的副本,副本改变了不会对原有引用造成影响,而且副本跟原引用都指向同一个对象,这个对象变化了副本引用和原引用都能感知到变化

至此,本话题结束。

通过String的不变性案例分析Java变量的可变性的更多相关文章

  1. Java虚拟机类加载机制——案例分析

    转载: Java虚拟机类加载机制--案例分析   在<Java虚拟机类加载机制>一文中详细阐述了类加载的过程,并举了几个例子进行了简要分析,在文章的最后留了一个悬念给各位,这里来揭开这个悬 ...

  2. 《深入理解Java虚拟机》-----第5章 jvm调优案例分析与实战

    案例分析 高性能硬件上的程序部署策略 例 如 ,一个15万PV/天左右的在线文档类型网站最近更换了硬件系统,新的硬件为4个CPU.16GB物理内存,操作系统为64位CentOS 5.4 , Resin ...

  3. java代码实现highchart与数据库数据结合完整案例分析(二)---折线图

    作者原创:未经博主允许不许转载 在上一篇的博客中,展示和分析了如何做一个饼状图,有疑问可以参考上一篇博客. 现在分析和展示折线图的绘制和案例分析, 先展示效果图: 与饼状图不同的是,折线图展现更多的数 ...

  4. 【转载】Java虚拟机类加载机制与案例分析

    出处:https://blog.csdn.net/u013256816/article/details/50829596 https://blog.csdn.net/u013256816/articl ...

  5. String的不变性到final在java中用法

    final在Java语言里面啥意思 final修饰一个类,那么这个类就是不可继承.string就是一个非常有名的被final修饰的类,不过他的更加有名的是“不可被修改”. 究竟什么是不可改变?stri ...

  6. Java设计模式—门面模式(带案例分析)

    1.门面模式的定义: 门面模式(Facade Pattern)也叫做外观模式,是一种比较常用的封装模式,其定义如下:       要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行.门面模式 ...

  7. Java多态案例分析

    一.多态的定义 同一事物,在不同时刻体现出不同状态. 例如:水在不同状态可能是:气态.液态.固态. 二.多态前提和体现 1.有继承关系 2.有方法重写 3.有父类引用指向子类对象 三.编译运行原理 1 ...

  8. java 并发基础,及案例分析

    对于我们开发的网站,如果网站的访问量非常大的话,那么我们就需要考虑相关的并发访问问题了,然而并发问题是令我们大多数程序员头疼的问题,但话又说回来了,既然逃避不掉,那我们就坦然面对吧~今天就让我们深入研 ...

  9. Java多线程——线程八锁案例分析

    Java多线程——线程八锁案例分析 摘要:本文主要学习了多线程并发中的一些案例. 部分内容来自以下博客: https://blog.csdn.net/dyt443733328/article/deta ...

随机推荐

  1. [AI开发]一个例子说明机器学习和深度学习的关系

    深度学习现在这么火热,大部分人都会有‘那么它与机器学习有什么关系?’这样的疑问,网上比较它们的文章也比较多,如果有机器学习相关经验,或者做过类似数据分析.挖掘之类的人看完那些文章可能很容易理解,无非就 ...

  2. 三层架构——ATM + 购物车

    三层架构:用户视图层.逻辑接口层.数据处理层. 一个功能,分成三层架构写,增加程序的可扩展性. 三层是互有联系的,从用户视图层开始写,涉及到那一层就到下一层去写,然后return 返回值,再写回来. ...

  3. coding++: java把一个整数拆分为单个值

    方式一: int num = 100; int[] ary = new int[(num+"").length()]; for(int i = ary.length-1;i> ...

  4. 电脑网络诊断显示Win10无法与设备或资源(DNS)通信解决办法

    最近是做多错多还是人有点儿衰神附体,软件,电脑系统,各种问题层出不穷,今天早上打开电脑发现不少软件都无法联网,神马百度商桥,腾讯浏览器,百度云...昨天百度商桥打不开还以为是软件出了问题,因为火狐浏览 ...

  5. iOS sign in with Apple 苹果ID登录

    http://www.cocoachina.com/articles/109104?filter=ios https://juejin.im/post/5deefc5e518825126416611d ...

  6. MiniUi遇到的一个Bug或者说坑,以div里面的内容自适应高度

    页面源码: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <tit ...

  7. C 怪兽游戏

    时间限制 : - MS   空间限制 : - KB  评测说明 : 1s,256m 问题描述 何老板在玩一款怪兽游戏.游戏虽然简单,何老板仍旧乐此不疲.游戏一开始有N只怪兽,编号1到N.其中第i只怪兽 ...

  8. linux硬件资源问题排查:cpu负载、内存使用情况、磁盘空间、磁盘IO

    在使用过程中之前正常的功能,突然无法使用,性能变慢,通常都是资源消耗问题,资源消耗可以从以下几个方面去排查.对于已经安装硬件资源监控软件(zabbix)的环境,直接使用硬件资源监控软件(zabbix) ...

  9. Mac电脑之间的文件共享 - 偏门

    文件共享是工作中经常要进行的. Mac用户之间可以通过AirDrop来共享文件.AirDrop要借助无线网络,而很多人都是将Mac做成个人热点供手机等Wifi连接,AirDrop时必须断开热点,不方便 ...

  10. golang--安装golang并安装grpc-grpcgateway环境

    安装goland环境 下载golang安装包,国内环境打开https://studygolang.com/dl,国外环境打开https://golang.google.cn/dl/下载对应系统的安装包 ...