【问题标题】:is String thread safe?字符串线程安全吗?
【发布时间】:2019-02-27 16:30:21
【问题描述】:

字符串是不可变的,这意味着一旦你修改了值,它就会创建一个新的引用,并保持之前的引用值不变。

但是,当有人在争论时,我不明白:

字符串是线程安全的,因为它们是不可变的

考虑下面的代码:

private String str = "1";
ExecutorService executorService = Executors.newFixedThreadPool(10);
IntStream.range(0, 1000).forEach((i)-> executorService.submit(()-> {
    str = str +"1";
}));

executorService.awaitTermination(10, TimeUnit.SECONDS);

System.out.println(str.length());

如果它是线程安全的,那么它应该打印1001,而它总是打印小于预期值。

我了解上述代码将创建 1001 不可变引用,每个引用本身都是线程安全的,但作为开发人员,仍然不能使用不可变的东西并期望 end-result 是线程安全的。

恕我直言,不变性不能保证线程安全。

有人可以向我解释一下字符串是如何线程安全的吗?

更新:

感谢您的回答,我知道每个字符串都可以是线程安全的,但我的意思是,当您在其他方法中使用它们时,线程安全和不变性之间没有直接关系。

例如,不可变对象可以在有状态对象中使用并以非线程安全的结果结束,而可变对象也可以在同步方法中使用并以线程安全的结果结束。

【问题讨论】:

  • A 字符串是不可变的,因此是线程安全的。但是您没有使用 a 字符串,而是创建了一大堆 new 字符串。
  • 仅仅因为你使用了String,并不意味着你“继承”了它的所有优点。 您的 代码不是线程安全的,但这与 String 本身无关。所以在这个例子中你需要确保str = str +"1";不会被多个执行者同时执行。
  • 特别是 str 引用不是不可变的,每次重新分配它的值时都会改变它。由于您在没有同步或互斥锁的线程之间共享可变状态,因此结果不安全。
  • 只使用线程安全的对象并不能使你的代码线程安全。

标签: java multithreading thread-safety


【解决方案1】:

了解内存在编程语言中的工作原理很重要。变量 str 不像您想的那样是字符串对象。但它是一个字符串对象的reference,带有一些地址。

修改 str 指向的内容,不会修改它指向的字符串。事实上发生的事情是这样的:

我们有一个内存池,在我们的池中是三个字符串。每个字符串都有一个地址,可以让我们找到它。

  • 字符串 1 - “你好”,地址:0x449345
  • 字符串 2 - “那里”,地址:0x058345
  • 字符串 3 - “世界”,地址:0x004934

我们有一个指向每个变量的变量,我们称它们为 a、b 和 c。

如果我们说:System.out.println(a); Java 将打印Hello。但 a 不是 “Hello”。相反,a 是包含 0x449345 的东西。电脑接着说:“好的,我去把 0x449345 的内容打印出来。”当它查看该地址时,它会找到字符串“Hello”。

但是,如果您说:a = "NEW STRING"; a 将不会指向我们之前的任何地址。相反,会创建一个新地址并将 "NEW STRING" 放置在该内存位置内。

这也是垃圾收集在 Java 中的工作方式。一旦你设置一个等于“NEW STRING”的值,它就不再指向 0x449345,这告诉垃圾收集器该对象是可以安全移除的。这就是您的程序自行清理的方式,并且不会占用大量 RAM。

因此,指向字符串的引用不是线程安全的,但实际对象是!任何不可变对象都是安全的,因为您完全不能修改该对象,您只能修改变量指向的内容。您必须完全指向一个不同的对象才能“修改”您的不可变对象。

【讨论】:

  • 感谢您的回答,我同意,我认为我们以不同的方式说同样的话。 “above code will create 1001 immutable references which each one is thread-safe on its own”。我认为您同意我的观点 "immutability is not guaranteed thread safety" ,因为您可能会在有状态对象中使用不可变对象
【解决方案2】:

我认为可以总结如下:

  1. String 对象上的操作是线程安全的。 (它们是线程安全的因为 String 对象是不可变的,但为什么 与您的示例没有直接关系。)
  2. finalshared2变量上的非同步读写操作1不是线程安全的,与变量的类型无关。

你的例子是str = str + 1;。这将String 对象上的操作与非同步共享变量(str)上的操作相结合。由于后者,它不是线程安全的。


1 - 更准确地说,写入和读取之间没有发生在关系的操作可以保证所需的内存可见性属性,并且没有锁定来保证所需的原子性属性。 (“必需”是指算法正确性所必需的......)

2 - 共享意味着对多个线程可见和使用。如果一个变量只对一个线程可见或被一个线程使用,则称它是线程受限的,实际上是不共享的。

【讨论】:

    【解决方案3】:

    它不会打印1001,因为它取决于每个线程何时获取str 的当前内存引用(因为引用是可变,因此不是线程安全的)。

    看这个例子,我们有 3 个线程 {T1,T2,T3}。

    T1 获取 str 引用并更改它,因此我们有 str = "11"; T2 和 T3 获得 str 参考(同时)并改变它,现在你有 T2 -> str= "111" 和 T3 -> str = "111";

    str 更新时,它可以使用来自 T2 或 T3 的 str 值进行更新(取决于执行),但本质上你不能认为每个线程都按顺序执行操作。 所以Strings 是不可变的,因此是线程安全的,因为每个线程只是修改自己的引用,但如果需要,您必须同步更新逻辑。 如果您想从代码中打印1001,您需要同步对 str 的访问(监视器、锁、信号量、同步关键字等)。

    顺便说一句,String 是线程安全的,因为如果您尝试更改它(以任何方式),您将创建另一个内存引用,因此两个(或更多)线程无法操作相同的 String 引用或更好的引用相同的字符串引用,但当他们操作它时,新字符串存储在新引用中)。

    【讨论】:

      【解决方案4】:

      您的str 引用不是不可变的,每次重新分配它的值时都会改变它。由于您在没有同步或互斥锁的线程之间共享可变状态,因此结果不安全。

      以下内容在我尝试了 5 次后对我有用。注意我在连接你的字符串时添加了互斥锁。

      public class QuickyTest {
      
         private static String str = "1";
      
         public static void main( String[] args ) throws Exception {
            ExecutorService executorService = Executors.newFixedThreadPool( 10 );
      
            IntStream.range( 0, 1000 ).forEach( ( i ) -> executorService.submit( () -> {
               append( "1" );
            }
            ) );
            executorService.awaitTermination( 10, TimeUnit.SECONDS );
            System.out.println( str.length() );
            executorService.shutdown();
         }
      
         private static synchronized void append( String s ) {
            str = str + s;
         }
      }
      

      总是打印“1001”。

      【讨论】:

      • 是的,正确,您所做的正确,但是,如果字符串也不是不可变的,结果将是相同的,因为您强制同步进程。换句话说,一次只有一个线程运行它。我的问题是immutability is not guaranteed thread safety,或者换句话说,可以以非线程安全的方式使用不可变对象。
      • 这只是意味着您的特定问题无法利用 String 提供的线程安全性。这是错误的问题和错误的课程。并不意味着线程安全的定义是错误的或者String不是线程安全的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-30
      • 2011-08-16
      • 2015-09-05
      • 1970-01-01
      • 2020-04-15
      • 2011-08-13
      相关资源
      最近更新 更多