Java中的字符串和并发处理

4

这可能是一个相关的问题:Java赋值问题-这是原子性的吗?

我有与OP相同的类,该类对可变字符串引用进行操作。但set很少发生。 (基本上,这个字符串是服务器配置的一部分,只有在强制重新加载时才会重新加载)。

public class Test {
 private String s;

 public void setS(String str){
  s = str;
 }

 public String getS(){
  return s;
 }
}

多个线程将同时访问该变量以读取其值。在不声明为volatile的情况下,如何使其“安全”,同时又不必承担性能降级的问题?
我目前的想法是使用ReadWriteLock,但据我所知,ReadWrite锁并不能保证免受线程缓存的影响?除非进行同步处理?这意味着我已经走了一整圈,还不如直接使用volatile关键字?
我的理解正确吗?没有任何方法可以手动“通知”其他线程有关主内存中变量更新的信息,以便它们可以在一个完整的月球上更新其本地缓存一次吗?
考虑到服务器应用程序旨在长时间运行而无需重新启动,因此在此设置volatile似乎过于繁琐。 我认为最好将String设置为static final,并在没有完整应用程序和JVM重新启动的情况下禁止其改变。

2
我会先为您的应用程序测量易失性解决方案的开销。如果可以忽略不计,那么这个问题可能就没有意义了。 - Ingo Kegel
我相信你指的是“不必要时不要进行优化”。但这并没有完全回答我的问题。无论如何,感谢您的评论。 - user986139
我和Ingo的想法一样:在追求速度之前,先保证正确性。使用volatile读取并不会显著降低应用程序的性能。 - JB Nizet
也许我应该加一条注释,说明这只是出于科学(如果您喜欢)兴趣而非应用领域目的? - user986139
4个回答

2

对引用的读写是原子操作。可能出现的问题是尝试进行读取和写入(更新),或者保证在写入后所有线程都能在下一次读取时看到此更改。不过,只有您能确定您的要求。

当您使用volatile时,需要读取或写入缓存一致的副本。这不需要将副本从主内存复制到/复制自主内存,因为缓存之间相互通信,甚至在套接字之间也是如此。这会对性能产生影响,但并不意味着不使用缓存。

即使访问确实到达了主内存,您仍然可以每秒执行数百万次访问。


需求主要是在主存储器设置新引用后通过使用volatile而无需线程从主存储器中检索来使本地缓存失效。但除此之外,我不确定我的任何理解或假设是否正确。 - user986139
@RonaldChan,我已经添加了一些关于volatile使用的注释。 - Peter Lawrey

0
为什么要使用可变字符串?为什么不使用一个简单的静态字符串配置类呢?当配置更新时,您只需更改此静态引用即可,这是一个原子操作,对于读取线程不会有问题。然后您就不需要同步和锁定惩罚了。

我可以问一下,对于 Config 类的静态引用是否需要处理线程缓存?如果是这样,我仍然需要在 Config 的静态引用上声明 volatile 关键字吗?以确保所有内容都获取到最新的 Config 类对象的引用? - user986139
抱歉,我不知道“线程缓存”是什么。我知道在简单类型(JIT寄存器、JNI等)上有时会出现非常短的缓存,但在我看来,多线程的基本原则是许多进程共享同一块内存(因此没有缓存)。在您的情况下,我推测对于这个配置字符串上的任何缓存都会非常短暂,不应该是问题。 - solendil
不幸的是,我从许多关于并发性的文章以及在StackOverflow上得到了相互矛盾的论点。即运行线程的本地缓存不能保证始终从主内存中读取最新的引用,除非进行特定的同步步骤。 - user986139
1
@solendil:你的假设是错误的。缓存时间可能比你想象的要长,引用应该被声明为volatile或以同步方式访问。 - JB Nizet

0
为了通知客户端到这个服务器,您可以使用观察者模式,任何对获取服务器更新信息感兴趣的人都可以注册事件,服务器会发送通知。正如您所提到的,重新加载并不经常发生,因此这不应该成为瓶颈。
现在,为了使其线程安全,您可以有一个单独的线程处理服务器状态的更新,如果您要获取状态,则检查状态是否为“正在更新”,如果是,则等待它完成,然后进入睡眠状态。一旦您的更新线程完成,它应该将状态从“正在更新”更改为“已更新”,一旦您退出睡眠状态,请检查状态是否为“正在更新”,如果是,则继续睡眠,否则开始服务请求。
这种方法将在您的代码中添加额外的if语句,但它将使您能够重新加载缓存而无需强制应用程序重新启动。
此外,由于服务器更新不频繁,这也不应该成为瓶颈。
希望这有些意义。

0
为了避免使用volatile关键字,您可以在Test类中添加一个"内存屏障"方法,该方法仅在非常罕见的情况下调用,例如:
public synchronized void sync() {
}

这将强制线程从主内存重新读取字段的值。

此外,您还需要更改setter为

 public synchronized void setS(String str){
    s = str;
 }

synchronized 关键字将强制设置线程直接写入主内存。

有关同步和内存屏障的详细说明,请参见此处


我是否正确地假设每个线程都必须定期轮询同步方法?我希望有一种将更改“推送”到本地缓存的方法。 - user986139
据我所知,您无法将任何内容推送到另一个线程的本地缓存中。该线程必须自行遇到内存屏障。 - Ingo Kegel
+1 无论如何都是一个可行的选择,如果我不需要立即传播更改。我可能遇到了Java障碍。 - user986139

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接