为什么Java中的局部变量是线程安全的?

103

我正在阅读Java中的多线程,并发现了这个说法:

在Java中,局部变量是线程安全的。

从那以后,我一直在思考局部变量为什么是线程安全的,以及如何实现线程安全。

请问有人能告诉我吗?


29
因为它们被分配在栈中。而线程不共享栈,每个线程都有自己独特的栈。 - Rohit Jain
8个回答

111

当您创建线程时,它将拥有自己的 调用堆栈。两个线程将有两个堆栈,一个线程不与其他线程共享其堆栈。

程序中定义的所有局部变量都将在堆栈中分配内存(正如Jatin所评论的那样,此处的内存指对象的引用值和基本类型的值)(每个线程调用的方法在其自己的堆栈上创建一个堆栈帧)。一旦该线程执行方法完成,堆栈帧将被移除。

在youtube上,有一位斯坦福教授的精彩演讲(链接)或许能够帮助您理解这个概念。


14
抱歉,您错了。只有原始的局部变量存储在栈上,其余所有变量都存储在堆上。Java 7引入了逃逸分析,对于某些变量可能会将其分配到栈中。 - Jatin
8
栈只持有指向堆上对象的引用。由于栈会被清空,因此引用也会被清除,因此它可用于垃圾回收。 - Jatin
8
@Jatin:你说得对。当我提到“memory”时,我的意思是对象的引用值和基本类型的值(我认为初学者开发人员也知道对象存储在堆上)。 - kosa
2
@Nambari 但是如果引用值指向共享变量,那么我们如何说它是线程安全的呢? - H.Rabiee
3
一个变量何时成为共享变量?从这里开始。是实例变量还是类变量?不是局部变量,并且请在本帖中阅读Marko Toplink的答案,我认为那是你所困惑的关键点。 - kosa
显示剩余2条评论

20

本地变量存储在每个线程自己的堆栈中,这意味着本地变量永远不会在线程之间共享。这也意味着所有本地原始变量都是线程安全的。

public void someMethod(){

   long threadSafeInt = 0;

   threadSafeInt++;
}

本地对象的引用有所不同。 引用本身不是共享的。 然而,被引用的对象并没有存储在每个线程的本地堆栈中,而是存储在共享堆中。 如果在创建本地对象后,它从未逃逸到创建它的方法之外,则它是线程安全的。 实际上,只要这些方法或对象中没有一个使传递的对象可供其他线程使用,您也可以将其传递给其他方法和对象


参数有误,请查看@Nambari回复的注释。 - Jatin
如果你指的是localSafeInt将始终为0,然后为1,最后被删除,那么这很好。因此,它表明此变量不在线程之间共享,因此不受多线程影响。我认为你可以更加强调线程安全始终只是0或1。 - tObi

16

把方法看做功能的定义。当两个线程运行同一个方法时,它们并没有关联。它们将分别创建自己版本的每个局部变量,并且无法以任何方式相互交互。

如果变量不是局部变量(例如在类级别方法外定义的实例变量),那么它们将附加到该实例上(而不是单独运行方法)。在这种情况下,运行相同方法的两个线程都会看到同一个变量,这就不是线程安全的。

考虑以下两种情况:

public class NotThreadsafe {
    int x = 0;
    public int incrementX() {
        x++;
        return x;
    }
}

public class Threadsafe {
    public int getTwoTimesTwo() {
        int x = 1;
        x++;
        return x*x;
    }
}

第一种情况是,两个在同一个实例的NotThreadsafe上运行的线程将看到相同的x。这可能很危险,因为这些线程正在尝试更改x!第二种情况是,在同一个Threadsafe实例上运行的两个线程将看到完全不同的变量,并且彼此不能影响。


6
除了Nambari的其他答案,我想指出你可以在匿名类型方法中使用本地变量:
该方法可能会在其他线程中被调用,这可能会危及线程安全性,因此Java强制要求在匿名类型中使用的所有局部变量都必须声明为final。
考虑以下非法代码:
public void nonCompilableMethod() {
    int i=0;
    for(int t=0; t<100; t++)
    {
      new Thread(new Runnable() {
                    public void run() {
                      i++; //compile error, i must be final:
                      //Cannot refer to a non-final variable i inside an
                      //inner class defined in a different method
                    }
       }).start();
     }
  }

如果Java允许这样做(就像C#通过“闭包”一样),那么在所有情况下,局部变量将不再是线程安全的。在这种情况下,所有线程结束时i的值不能保证为100


嗨,韦斯顿, 从以上讨论和以下答案中,我了解到Java可以确保所有本地变量的线程安全性。那么synchronized关键字的实际用途是什么?你能否举个例子来解释一下,就像这个一样。 - Prabhu

6

线程将拥有自己的栈。两个线程将拥有两个栈,一个线程永远不会与其他线程共享其栈。局部变量存储在每个线程自己的栈中。这意味着局部变量永远不会在线程之间共享。


6

每个方法调用都有自己的本地变量,显然,方法调用发生在单个线程中。只由单个线程更新的变量天生就是线程安全的。

然而,要密切关注什么确切地被认为是“只有”:仅对变量本身的写操作是线程安全的;调用其引用对象的方法本质上不是线程安全的。直接更新对象变量也是如此。


1
你说“在对象上调用方法本身并不是线程安全的”。但是,一个方法局部引用所引用的对象 - 在该方法范围内实例化 - 如何被两个线程共享?你能举个例子吗? - Akshay Lokur
1
一个本地变量可能会或可能不会持有在方法范围内实例化的对象,这不是问题的一部分。即使如此,该方法仍然可以访问共享状态。 - Marko Topolnik

3

Java中有四种类型的存储方式来存储类信息和数据:

方法区,堆,JAVA栈,程序计数器

因此,方法区和堆是所有线程共享的,但每个线程都有自己的JAVA栈和程序计数器,并且不与其他线程共享。

Java中的每个方法都是一个栈帧。当一个线程调用一个方法时,该栈帧会加载到它的JAVA栈上。该栈帧中的所有本地变量和相关的操作数栈不会被其他线程共享。程序计数器将包含在方法字节码中执行下一条指令的信息。因此,所有的局部变量都是线程安全的。

@Weston 也给出了很好的答案。


2

Java本地变量的线程安全

只有本地变量存储在线程栈上。

基本类型本地变量(例如int、long等)存储在线程栈中,因此其他线程无法访问它。

引用类型(继承自Object)的本地变量包含两部分-地址(存储在线程栈中)和对象(存储在中)。

class MyRunnable implements Runnable() {
    public void run() {
        method1();
    }

    void method1() {
        int intPrimitive = 1;
    
        method2();
    }

    void method2() {
        MyObject1 myObject1 = new MyObject1();
    }
}

class MyObject1 {
    MyObject2 myObject2 = new MyObject2();
}

class MyObject2 {
    MyObject3 myObject3 = MyObject3.shared;
}

class MyObject3 {
    static MyObject3 shared = new MyObject3();

    boolean b = false;
}

在此输入图片描述

[JVM内存模型]


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接