繁体   English   中英

字符串线程安全吗?

[英]is String thread safe?

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

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

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

考虑下面的代码:

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是如何线程安全的吗?

更新:

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

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

了解内存在编程语言中的工作方式非常重要。 变量str不是像您想象的那样是String对象。 但是,它是对带有某些地址的String对象的引用

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

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

  • 字符串1-“ Hello”,地址: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的方式。

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

我认为可以总结如下:

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

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


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

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

它不会打印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引用,或者更好的是它们具有相同的字符串引用,但是当它们进行操作时(新字符串存储在新引用中)。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM