关于拷贝对象引用到local变量的一些思考

在JDK的Java类源码里面,很多工具包的代码都有在使用某个成员变量之前,先拷贝该变量的对象引用到方法的局部变量之中,如下:

比如LinkedList:

public E peek() {
    final Node<E> f = first;
    return (f == null) ? null : f.item;
}

HashMap里面:

public Set<K> keySet() {
        Object var1 = this.keySet;
        if (var1 == null) {
            var1 = new HashMap.KeySet();
            this.keySet = (Set)var1;
        }

        return (Set)var1;
    }

同样的还有Stringlim的trim方法:

public String trim() {
        int len = value.length;
        int st = 0;
        char[] val = value;    /* avoid getfield opcode */

        while ((st < len) && (val[st] <= ' ')) {
            st++;
        }
        while ((st < len) && (val[len - 1] <= ' ')) {
            len--;
        }
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
    }

一些朋友,可能认为这是多此一举,明明直接使用成员变量就可以了,为什么还要拷贝一次呢,然后再使用,这样做的好处是什么?

JDK大神写的代码,肯定有其考虑的地方,我们来分析下,这么做的好处:

(1)首先方法里面的变量,都在栈上存储,而成员变量的地址是在堆上存储,而栈的存储,通常情况下都会在cpu的缓存里面,堆的存储一般是在主存里面,cpu的缓存要比主内存的块,所以这种写法,在一定程度上算是一种优化,在执行的时候会通过JIT来完成,尤其是在方法里面的循环中使用,比如trim的方法里例子。

(2)还有一种情况,在方法里面,先拷贝引用地址到本地变量,在栈里面是线程安全的,所以对于方法里面的引用本身来说是不会再存在被别的线程修改的风险,期间,如果另外一个线程修改了这个成员变量的引用地址,那么对于已经拷贝的引用,其实是没有感知的。所以从某种程度上说,拷贝引用地址相当于是一个视图。但是需要注意虽然引用地址不变,但是如果引用的内容变了,那么还能被看到的,所以在使用的时候应该注意二者的区别。

下面我们通过一个例子来看一下,先定义一个共享类:

public class ThreadShare {

    public String text="str-0";

    public int count=0;

    public Cat cat1;

    public Cat cat2=new Cat();

    static class Cat{

        public String name="none";

    }
    }

上面是共享类里面定义的一些成员变量,现在我们制造一个这样的场景,把同一个类,传递给两个线程,一个是print线程,一个是update线程,首先在print线程的run方法里面,会把这些成员变量都拷贝到local变量里面,然后接着打印一变,接着我们让print线程sleep几秒,同时启动update线程修改这些成员变量的值,最后,我们在分别打印本地local变量的值与直接访问成员变量的值,看看有什么变量。

public static void main(String[] args) throws InterruptedException {


        ThreadShare share=new ThreadShare();


        Thread printThread=new Thread(new PrintThread(share));
        printThread.setName("打印线程");
        printThread.start();

        Thread.sleep(1000);

        Thread updateThread=new Thread(new UpdateThread(share));
        updateThread.setName("更新线程");

        updateThread.start();




    }

    public static class UpdateThread implements Runnable{
        private ThreadShare threadShare;

        public UpdateThread(ThreadShare threadShare) {
            this.threadShare = threadShare;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+" 修改成员变量的值......");
            threadShare.text="update";
            threadShare.count=10;
//            threadShare.cat.name="i am tom";
            threadShare.cat1=new Cat();
            threadShare.cat2.name="cat2";

        }
    }


    public static class PrintThread implements Runnable{

        private ThreadShare threadShare;

        public PrintThread(ThreadShare threadShare) {
            this.threadShare = threadShare;
        }

        @Override
        public void run() {

            String threadName=Thread.currentThread().getName();
            String text=threadShare.text;
            int count=threadShare.count;
            Cat cat1=threadShare.cat1;
            Cat cat2=threadShare.cat2;

            System.out.println(threadName+"  初始值 "+text+" "+count+"  "+cat1+" "+cat2.name);
            System.out.println();
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println();
            System.out.println(threadName+"局部变量show: "+text+" "+count+"  "+cat1+"  "+cat2.name);
            System.out.println(threadName+"成员变量show: "+threadShare.text+"   "+threadShare.count+"   "+threadShare.cat1+" "+cat2.name);

        }
    }

输出结果如下:

打印线程  初始值 str-0 0  null none

更新线程 修改成员变量的值......

打印线程局部变量show: str-0 0  null  cat2
打印线程成员变量show: update   10   basic.reference.ThreadShare$Cat@57eff98f cat2

从上面的结果里面,能够看到最后局部变量的值,对于拷贝的引用是没有变化的,但如果是引用的属性变化了(cat2),是可以看到的,这是因为对象数据是在堆上获取的,接着我们看成员变量的值,发现成员变量能看到所有最新的变换,这是因为成员变量的数据就是从堆上获取的。

在上篇文章里面,我们谈到过栈和堆的区别,其中栈里面能够存储基本类型的数据值,还有引用类型的地址值,如果在Java多个线程里面,都访问同一个共享对象,那么要记住,对于引用类型,多个线程里面都会拷贝一份引用地址,对于基本类型就是值本身,多个线程都对基本类型的修改,那么其他的线程是看不到的,谁最后完成就会覆盖之前的结果,对于引用类型,因为对象在堆里面是共享的,实际上引用地址操作的都是同一个对象,所以多线程修改会造成不可预料的结果,这也是为什么在操作共享变量的时候一定需要使用同步和锁的手段来保证正确性的原因。

总结:

本文主要介绍了在JDK的源码里面,针对一些拷贝成员变量到local变量的代码片段做了分析,理解这个问题的本质在于理解堆和栈的区别,以及Java引用(指针)概念,还有Java内存模型对操作系统映射抽象,此外,方法里面的local变量,还只能被final修饰,只要在使用这个变量前初始化即可,这种用法的目的,其实是为了更好的代码可读性,使用final标记的变量,用来明确的来告诉我们这个类型,在声明之后不能再改变了,这些良好的编程习惯,我们可以日常开发中都可以吸收借鉴,从而写出效率更好,可读性更强的代码。

原文发布于微信公众号 - 我是攻城师(woshigcs)

原文发表时间:2018-11-19

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏老司机的技术博客

golang学习笔记3:常量与变量

常量使用关键字 const 定义,用于存储不会改变的数据。 存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。 常量的定义格式: ...

11210
来自专栏JetpropelledSnake

Python入门之面向对象之类继承与派生

本章内容     一、继承     二、抽象类     三、继承的实现原理 ==========================================...

35780
来自专栏苦逼的码农

从jvm角度看懂类初始化、方法重载、重写。

类的声明周期可以分为7个阶段,但今天我们只讲初始化阶段。我们我觉得出来使用和卸载阶段外,初始化阶段是最贴近我们平时学的,也是笔试做题过程中最容易遇到的,假如你想...

13320
来自专栏Python数据科学

Python爬虫之快速入门正则表达式

当完成了网页html的download之后,下一步当然是从网页中解析我们想要的数据了。那如何解析这些网页呢?Python中有许多种操作简单且高效的工具可以协助我...

13130
来自专栏Linyb极客之路

JVM 方法内联

调用某个函数实际上将程序执行顺序转移到该函数所存放在内存中某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方。

23940
来自专栏用户2442861的专栏

python编码问题

我们已经讲过了,字符串也是一种数据类型,但是,字符串比较特殊的是还有一个编码问题。

18810
来自专栏机器学习算法与Python学习

python基础-字符串与编码

转载于:廖雪峰的官方网站-python教程 字符编码 我们已经讲过了,字符串也是一种数据类型,但是,字符串比较特殊的是还有一个编码问题。 因为计算机只能处理数字...

494110
来自专栏coder修行路

Java基础(三)面向对象(下)

成员常量:public static final 成员函数:public abstract

10600
来自专栏用户2442861的专栏

java泛型(一)、泛型的基本介绍和使用

http://blog.csdn.net/lonelyroamer/article/details/7864531

14710
来自专栏Coco的专栏

【优雅代码】深入浅出 妙用Javascript中apply、call、bind

9420

扫码关注云+社区

领取腾讯云代金券