java 的多线程问题 ?

如图所示, 位置 2 和位置3 为什么可以访问 位置1 (也就是主线程)的 point 局部变量 ?
毕竟 位置 2 和位置3 是另外两个线程啊 !!

当我加上 第10行代码后,thread1 和 thead2 中都不能访问主线程中的point 了。我知道这是内部类的“事实最终变量” 的限制。

如下图所示,就是我不理解的地方。(在 “栈内存” 层面)

我的猜测:之所以 thread1 和 tread2 这两个新的线程并没有初始化 point 这个变量 但是还能使用的原因是因为 Runnable 的两个实现类 内部都各自 生成了一个 point 实例变量 ? (虽然书上的解释是针对局部内部类,针对外面的方法执行完毕局部变量就不复存在的原因, 但是我不知道 多线程 能不能也可以同样这样解释)。

阅读 3.4k
10 个回答

堆栈封闭:局部变量,无并发问题,每个线程会各自拷贝一份到各自新建的线程中。

只可使用,不可修改该局部变量的值(因为是复制到新新线程中的,所以只能改副本数据,不能改main函数中局部变量的值)

栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

    public static void main(String[] args) {
        t1();
    }


    public static void t1() {
        AtomicReference<User> user = new AtomicReference<>(new User());
        user.set(new User("defaultName"));
        Runnable runnable = () -> {
            user.set(new User("name1"));
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(()-> user.set(new User("name2")));
        thread1.start();
        thread2.start();
        System.out.println(user);
    }

打印结果:
User(name=defaultName)

因为对象在堆里
堆是所有线程共享的
所以可以访问

能不能访问跟在哪个线程创建没有什么关系。

你引用的讨论跟是否多线程貌似也没有关系。

能否访问变量只与变量的作用域有关,跟在哪一个线程无关。

你这里同处于一个块级作用域,当然能访问到了。

至于跨线程访问同一个变量是否会有“线程安全”问题,这是另一个问题,跟“能否访问”是两码事儿,没有一点儿关系。

我觉得这不算栈封闭,栈封闭限于对象不能逸出。比如这样。适用于基础数据类型

class User(val name: String){
    val c:Int = 0

    fun demo(){
        Thread{
            val b = c
            println(b)
        }
    }
}

即c在怎么改变,也无法对b造成影响。但如果是一个对象。显然结论不成立。第一个回答者的测试在我这里没通过,如果加上thread.join() 就会发现,其实是能够修改,看到之后我真以为不能修改,所以去做了测试。

所以结论就应该是普通的堆调用。堆内存是共享的,所以不需要什么初始化,哪个线程都能使用。至于为什么是final,我觉得可能是为了防止在线程里面随意赋值,导致变量在其他线程里的浅拷贝对象出现错乱。

最后,如果真的对多线程有兴趣,还是推荐《java并发编程实践》 在并发这方面。没有哪本书比这本书更加权威。里面也有关于栈封闭的解释。

新手上路,请多包涵

对象是保存在堆里,对象的引用才是在栈里,堆内存是共享的

回答1的代码:

public static void t1() {
    AtomicReference<User> user = new AtomicReference<>(new User());
    user.set(new User("defaultName"));
    System.out.println("defaultName:"+JSON.toJSONString(user));
    Runnable runnable = () -> {
        user.set(new User("name1"));
        System.out.println("name1"+JSON.toJSONString(user));
    };
    Thread thread1 = new Thread(runnable);
    Thread thread2 = new Thread(()-> {
        user.set(new User("name2"));
        System.out.println("name2"+JSON.toJSONString(user));});
    thread1.start();
    thread2.start();
}

感觉改成这样就能说明各个线程拷贝副本问题了

强答一波:你的这个问题很有意思,你会发现,如果你将对象写在线程内部,外部或者其他线程无法访问,写在外部,你新建的线程则可以访问,这是一个很有意思的问题。在Java中,闭包(lambdas 或匿名内部类)可以捕获并使用其外部作用域中的变量。这种机制允许你在一个匿名内部类(或lambda表达式)内部访问并修改外部变量。所以这就是你为什么位置 2 和位置3 为什么可以访问 位置1 (也就是主线程)的 point 局部变量 ? 的答案,在Java 8及以后版本中,如果一个局部变量在初始化之后没有被修改,即使没有明确声明为final,它也被视为事实上的final.
。所以线程是可以访问外部的final或事实上的final变量,如果你在线程内部修改这个变量,那么会爆红提示.
希望能解答你的疑问.

“不能修改变量的值”,其实翻译还是有点不够亲民,改成不能修改变量堆栈关系不能修改对象引用与对象之间的关系;它不是不能使用对象的方法,这是一个对象内存访问权限的问题。

新手上路,请多包涵

楼主太较真了,这是Java内部规定的,在Java中,匿名类会隐式地持有对外部变量的引用,而Java要求在匿名类内部访问外部变量时,这些外部变量必须是final的,以确保在匿名类中对这些变量的使用不会出现问题。这是你那个IDE工具可以检查出来的,这是Java的硬性要求,因为你写的是内部匿名类,你换种写法他就检查不出来编译错误了。

public void runTest(){
        CarInfoVO carInfoVO = new CarInfoVO();
        carInfoVO = (CarInfoVO)new Object();
        // 提取为独立的类
        Runnable runnable2 = new MyRunnable(carInfoVO);
        Thread thread2 = new Thread(runnable2);
        thread2.start();
    }
    // 提取的独立类
    class MyRunnable implements Runnable {
        private CarInfoVO carInfoVO;

        public MyRunnable(CarInfoVO carInfoVO) {
            this.carInfoVO = carInfoVO;
        }

        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                carInfoVO.setBarCode("");
            }
        }
    }

这样就会在运行时报错了,这是Java要求的写法问题。我写的比较简单,正常来说只要转换成一样类型有同样方法的一样不会报错
如果非要较真,可以这样理解, 如果将 carInfoVO 重新赋值为其他对象,那么原先的类型信息会丢失,导致无法再使用原先的 carInfoVO 变量。这是因为匿名类是根据变量的声明类型来确定其可访问的方法和属性的,如果变量的类型变更,匿名类中可能定义的特定方法和属性就无法直接访问了。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题