字符串线程安全吗?

时间:2019-02-27 16:30:22

标签: java multithreading thread-safety

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

但是,当有人吵架时,我不明白:

  

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

考虑以下代码:

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将是线程安全。

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

有人可以向我解释一个String如何安全地线程化吗?

更新

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

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

4 个答案:

答案 0 :(得分:3)

了解内存如何以编程语言工作非常重要。变量 str 不是一个字符串对象,就像您想的那样。但这是对带有某些地址的字符串对象的引用

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

我们有一个内存池,在我们的池中是三个字符串。每个字符串都有一个地址供我们查找。

  • 字符串1-“ Hello”,地址:0x449345
  • 字符串2-“有”,地址:0x058345
  • 字符串3-“世界”,地址:0x004934

我们有一个指向每个变量,我们将它们分别称为a,b和c。

如果我们说:System.out.println(a);,Java将打印Hello。但是 a 不是“你好” 。而是 0x449345 。然后,计算机运行:“好吧,我将取0x449345的值并打印出来。”转到该地址时,它会找到字符串“ Hello”。

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

这也是Java中垃圾收集的工作方式。一旦将其设置为等于“ NEW STRING”,它将不再指向0x449345,这将告诉垃圾回收器该对象可以安全删除。这样您的程序便会自行清理,并且不会消耗大量的RAM。

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

答案 1 :(得分:2)

我认为可以总结如下:

  1. String对象上的操作是线程安全的。 (它们是线程安全的,因为 String对象是不可变的,但是为什么与您的示例并不直接相关。)
  2. 在非final共享的 2 变量上,
  3. 不同步的读写操作 1 不是线程安全的,无论变量的类型如何。

您的示例中有str = str + 1;。该操作将String对象上的操作与未同步共享变量(str)上的操作结合在一起。由于后者,它不是线程安全的。


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

2-共享表示多个线程可见并使用。如果一个变量仅对一个线程可见或被一个线程使用,则称该变量是线程限制的,实际上是不共享的。

答案 2 :(得分:0)

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

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

  

T1获取str引用并对其进行更改,因此我们有str =“ 11”;   T2和T3(同时)获得str参考,并进行更改,现在您有了T2-> str= "111"和T3-> str = "111";

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

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

答案 3 :(得分:0)

您的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”。