局部变量的线程安全

时间:2020-12-27 16:35:55

标签: java multithreading thread-safety local-variables

<块引用>

局部变量总是线程安全的。但请记住, 局部变量指向的对象,可能不是这样。如果对象是 在方法内部实例化,永不转义,不会有 问题。

我是 Java 多线程的新手,我不明白“对象转义方法”是什么意思。任何人都可以向我展示此语句的代码示例,其中对象通过转义方法变得非线程安全。另外请解释为什么它变得非线程安全

这是否意味着如果我们简单地将这个对象传递给另一个方法,它将成为非线程安全的?

注意:对你们中的许多人来说,这听起来可能是一个简单的问题。但对我来说,我阅读了多篇文章以查看代码示例,但找不到。

4 个答案:

答案 0 :(得分:3)

<块引用>

这是否意味着如果我们简单地将这个对象传递给另一个方法,它将成为非线程安全的?

是的。

好吧,它可能变得非线程安全:这取决于其他方法如何处理它。

一个非线程安全的例子是这样的:

class YourClass {
  List<String> field;

  void foo() {
    List<String> local = new ArrayList<>(List.of("foo"));
    unsafe(local);        
  }

  void unsafe(List<String> param) {
    field = param;
  }
}

这是线程不安全的,因为其他线程现在可以通过字段访问局部变量所引用的对象。

当然,如果值是可变的,它只能成为线程不安全的。不可变值本质上是线程安全的,因此它可以被传递、存储在字段中等等。

一个仍然线程安全的例子可能是这样的:

void stillSafe(List<String> param) {
  System.out.println(param);
}

这仍然是线程安全的(如果使用局部变量作为参数调用),因为 param 以线程受限的方式使用(当前执行线程之外的任何东西都不能看到它的值)。

>

你甚至可以在方法中改变它:

void stillSafe2(List<String> param) {
  param.add("Hello!");
}

尽管发生了变化,但这仍然是线程安全的,因为仍然只有当前线程可以看到该值。

答案 1 :(得分:1)

Java 是一种基于引用的语言。这只是指针的另一种说法。

当你写作时:

String foo = "hello";

这只是语法糖:

String foo; // [1]
foo = "hello"; // [2]

第 1 行声明了一个名为 'foo' 的变量(一个指针)。 第 2 行做了两件事:它创建了一个全新的对象。然后更新 foo 以引用它。就像"hello"是宝藏,而foo是藏宝图。 foo = "Hello" 创造新的宝藏,将它埋在沙子里,然后在你的纸上画一张宝藏的地图;您标记为 foo 的那张纸。 “foo”不是宝物,说这个字符串是“foo”字符串是不正确的。不是——它只是宝物,宝物没有名字。不可能有通向它的地图(这意味着垃圾收集器最终会挖掘它并清除它),可能有一千个地图通向它,以及介于两者之间的任何地图。在上面的片段中,有一张地图通向它。你的地图。你叫foo的那个。

但是您可以与他人共享该地图。然后他们也可以找到你的宝藏。

'本地人是不可变的'。是的。他们是。但是,您的本地变量是 foo 变量,而 foo不是宝藏。是地图。这是你的地图。没有人可以惹它。地图看起来会有所不同的唯一方法是,如果您在自己的代码中的某处编写 foo = 。将 foo 传递给其他方法的次数不会改变该映射。

您阅读的文本所指的是,唯一不可变的是您的地图。地图指向的宝藏?谁知道。如果你把你的地图交给其他人,他们不能改变你的地图,但他们可以复制它。他们可以遵循它。他们可以挖掘那个宝藏,然后把它砸成碎片。它们不会影响您的地图,但是如果您按照您的地图进行操作,您会在那里找到什么?如果您将宝藏的位置与其他任何东西共享,那么现在可能会完全不同。

现在,对于弦而言,这是一个有争议的问题:弦宝是无敌的。它们不能以任何方式被粉碎或修改。它们是不可变对象,没有改变它的方法,也没有公共(非最终)字段。但并不是所有的物体都是这样。一些宝物可以改变或粉碎。例如,一个简单的列表:

List<String> x = new ArrayList<String>();
x.add("Hello");
foo(x);
System.out.println(x);

在上面的代码中,你不知道它打印了什么,因为你不知道 foo 做了什么。您确实知道您的 x 不可能打印 null。 null 指的是你有一张空白的藏宝图,没有人可以弄乱你的藏宝图。但是foo 可以把宝藏弄得一团糟:

public void foo(List<String> x) {
    x = List.of();
}

这没有任何作用。此方法获取您的藏宝图的副本。然后它创建新的宝藏,将它埋在沙子里,用橡皮擦到它的 x 藏宝图(它曾经保存你的副本),然后在上面绘制一个全新的藏宝图,其中 X 标记新创建的宝藏的位置。这对您的地图或您的地图通向的宝藏完全没有任何影响。您的代码将继续打印 [Hello]

然而:

public void foo(List<String> x) {
    x.add("World");
}

这是完全不同的。这会获取您给它的藏宝图副本,跟随它并向下挖掘(. 是 java-ese 表示:跟随地图并挖掘)。然后打开宝箱,将“世界”放入其中(从技术上讲,它在其中放置了一张指向“世界”宝藏的地图,字符串也是对象,因此是基于引用的)。

如果您稍后按照地图进行挖掘,您就会看到这一点。您的代码将打印 [Hello, World]

这就是正文所讨论的内容。通过制作副本或使用不可变对象来避免这种情况,或者意识到如果您共享了地图,则您的地图指向的任何宝藏可能已被其他代码更改的概念。

答案 2 :(得分:1)

逐条看应该不难理解。

如果程序中的两个或多个线程访问相同的数据(也称为“共享数据”),那么您需要确保您的代码以“线程安全”的方式访问数据。

与 C++ 等其他一些语言不同,Java 编程语言使得线程不可能共享局部变量。唯一可以共享的变量是 static 变量,以及共享对象的成员变量(也称为“字段”)。

在函数调用中将对象引用作为参数传递本身并不会允许对象引用“转义”到另一个线程。但它可以允许它。这一切都取决于被调用的函数对您传递的对象做了什么。如果您编写的代码在多线程程序中调用函数,则有责任知道该函数将如何处理您提供给它的对象(例如,该函数是否将与另一个线程与否。)


对象可以共享的一些方式:

  • 您将对该对象的引用存储到由线程共享的 static 变量中,

  • 对象是您赋予新的 RunnableThread 对象,或者它是 Runnable 对象的成员变量之一的所指,

  • 对象是您提交给 thread poolRunnableCallable,或者它是您提交给一个对象的成员变量的指涉对象线程池,

  • 该对象是先前共享的某个其他对象的成员变量的指代对象,尤其是

    • 您将对象添加到共享的容器(例如,ListSetMap
    • 您将对象放入一个 Queue 中,该 showDialog 专为在线程之间共享对象而设置。

答案 3 :(得分:0)

对方法进行转义意味着:它被使用或存储在方法之外的某个地方。

示例:您有一个全局变量 counter 并且您在本地方法中计算某些内容。现在只要你使用本地计数器一切都很好,但是一旦你设置了 globalcounter = localcounter 你就不能假设 globalcounter == localcounter,因为它总是可以被外部因素改变。