字符串是不可变的,这意味着一旦修改了它的值,便会创建一个新的引用,并保持原来的引用值不变。
但是,当有人吵架时,我不明白:
字符串是线程安全的,因为它们是不可变的
考虑以下代码:
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如何安全地线程化吗?
更新:
感谢您的回答,我了解每个字符串都可以是线程安全的,但是我的意思是,当在其他方法中使用它们时,线程安全和不变性之间没有直接关系。
例如,可以在有状态对象中使用不可变对象,并以非线程安全的结果结束,而在同步方法中使用可变对象,并以线程安全的结果结束。
答案 0 :(得分:3)
了解内存如何以编程语言工作非常重要。变量 str 不是一个字符串对象,就像您想的那样。但这是对带有某些地址的字符串对象的引用。
修改 str 指向的内容,不会修改它指向的字符串。实际上发生的事情是这样的:
我们有一个内存池,在我们的池中是三个字符串。每个字符串都有一个地址供我们查找。
我们有一个指向每个变量,我们将它们分别称为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)
我认为可以总结如下:
String
对象上的操作是线程安全的。 (它们是线程安全的,因为 String
对象是不可变的,但是为什么与您的示例并不直接相关。)final
共享的 2 变量上,您的示例中有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”。