字符串是不可变的,这意味着一旦您修改了它的值,就创建一个新的引用,并保持原来的引用值不变。
但是,我不明白有人在争论:
Strings are thread safe as they are immutable
考虑下面的代码:
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是如何线程安全的吗?
更新:
感谢您的回答,我知道每个字符串都可以是线程安全的,但是我的意思是,当在其他方法中使用它们时,线程安全和不变性之间没有直接关系。
例如,可以在有状态对象中使用不可变对象,并以非线程安全结果结束,而在同步方法中使用可变对象,并以线程安全结果结束。
字符串是不可变的,因此是线程安全的。 但是您不能使用一个字符串,而是创建了一堆新字符串。
仅仅因为您使用String,并不意味着您"继承"了其所有优点。 您的代码不是线程安全的,但这与String本身无关。 因此,在此示例中,您需要确保str = str +"1";不会被多个执行者同时执行。
特别是str引用不是不可变的,您每次重新分配其值时都会对其进行突变。 由于您在没有同步或互斥的线程之间共享可变状态,因此结果是不安全的。
仅使用线程安全的对象不会使您的代码成为线程安全的。
了解内存在编程语言中的工作方式非常重要。变量str不是像您想象的那样是String对象。但是,它是对带有某些地址的String对象的引用。
修改str指向的内容,不会修改它指向的字符串。实际上发生的事情是这样的:
我们有一个内存池,在我们的池中是三个字符串。每个字符串都有一个地址供我们查找。
字符串1-" Hello",地址:0x449345
字符串2-"有",地址:0x058345
字符串3-"世界",地址:0x004934
我们有一个指向每个变量的变量,我们将它们分别称为a,b和c。
如果我们说:System.out.println(a); Java将打印Hello。但是一个不是"你好"。而是包含0x449345的东西。然后,计算机运行:"好吧,我将取0x449345的值并打印出来。"转到该地址时,它会找到字符串" Hello"。
但是,如果您说:a ="NEW STRING"; a则不会指向我们以前的任何地址。而是创建一个新地址,并在该存储位置内放置" NEW STRING"。
这也是Java中垃圾收集的工作方式。一旦将其设置为等于" NEW STRING",它将不再指向0x449345,这将告诉垃圾回收器该对象可以安全删除。这就是您的程序自己清理后不消耗大量RAM的方式。
因此,指向字符串的引用不是线程安全的,而是实际的对象IS!任何不可变的对象都是安全的,因为您根本无法修改该对象,因此只能修改变量指向的对象。您必须完全指向其他对象才能"修改"您的不可变对象。
感谢您的回答,我同意,我想我们以不同的方式说同样的话。" above code will create 1001 immutable references which each one is thread-safe on its own"。 而且我认为您同意" immutability is not guaranteed thread safety",因为您可能在有状态对象中使用不可变对象
我认为可以总结如下:
String对象上的操作是线程安全的。 (它们是线程安全的,因为String对象是不可变的,但是原因与您的示例并不直接相关。)
无论变量的类型如何,对非final shared2变量的未同步读写操作1都不是线程安全的。
您的示例执行str = str + 1;。该操作将对String对象的操作与对未同步的共享变量(str)的操作结合在一起。由于后者,它不是线程安全的。
1-更精确地讲,在写和读之间的关系没有发生之前的操作可以确保所需的内存可见性,而没有锁定可以确保所需的原子性。 ("必需"表示算法正确性是必需的...)
2-共享是指多个线程可以看到和使用。如果一个变量仅对一个线程可见或被一个线程使用,则称该变量是线程限制的,并且实际上不被共享。
您的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不是线程安全的。
它之所以不打印1001,是因为它取决于每个线程何时获取str的当前内存引用(因为该引用是可变的,因此不是线程安全的)。
看这个例子,我们有3个线程{T1,T2,T3}。
T1 gets str reference and change it so we have str ="11";
T2 and T3 get str reference (simultaneously) and change it so, now you have, T2 -> str="111" and T3 -> str ="111";
当更新str时,可以使用T2或T3中的str值对其进行更新(取决于执行情况),但从本质上讲,您不能认为每个线程都按顺序执行该操作。
因此String是不可变的,因此具有线程安全性,因为每个线程仅修改自己的引用,但是如果需要,您必须同步更新逻辑。
如果要从代码中打印1001,则需要同步对str的访问(监视器,锁,信号灯,同步关键字等)。
顺便说一句,String是线程安全的,因为如果尝试(以任何方式)更改它,则将创建另一个内存引用,因此两个(或更多)线程无法操纵相同的String引用,或者更好地,它们具有相同的字符串引用但是当他们操作它时,新字符串将存储在新引用中)。