在Java中同步String对象

我有一个web应用程序,我正在做一些负载/性能testing,特别是一个function,我们期望有几百个用户访问同一个页面,每10秒刷新一次。 我们发现使用这个函数可以改进的一个方面是caching来自Web服务一段时间的响应,因为数据没有变化。

在实现了这个基本的caching之后,在一些进一步的testing中,我发现我没有考虑到并发线程如何能够同时访问Cache。 我发现,在大约100ms的时间内,大约有50个线程试图从Cache中获取对象,发现它已经过期,敲击Web服务来获取数据,然后将对象放回到caching中。

原来的代码看起来像这样:

private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { final String key = "Data-" + email; SomeData[] data = (SomeData[]) StaticCache.get(key); if (data == null) { data = service.getSomeDataForEmail(email); StaticCache.set(key, data, CACHE_TIME); } else { logger.debug("getSomeDataForEmail: using cached object"); } return data; } 

所以,为了确保只有一个线程在key对象过期时调用Web服务,我想我需要同步Cache的get / set操作,而且好像使用caching密钥将是一个很好的候选对象(通过这种方式,对电子邮件b@b.com的调用不会被对a@a.com的方法调用阻止)。

我更新了这个方法:

 private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { SomeData[] data = null; final String key = "Data-" + email; synchronized(key) { data =(SomeData[]) StaticCache.get(key); if (data == null) { data = service.getSomeDataForEmail(email); StaticCache.set(key, data, CACHE_TIME); } else { logger.debug("getSomeDataForEmail: using cached object"); } } return data; } 

我还为“同步块之前”,“内同步块”,“即将离开同步块”和“同步块之后”添加了logging行,以便确定是否有效地同步获取/设置操作。

但是,这似乎并没有工作。 我的testing日志有如下输出:

(日志输出是'threadname''logging器名''消息')
http-80-Processor253 jsp.view-page – getSomeDataForEmail:即将进入同步块
http-80-Processor253 jsp.view-page – getSomeDataForEmail:在同步块内部
http-80-Processor253 cache.StaticCache – get:key [SomeData-test@test.com]的对象已过期
http-80-Processor253 cache.StaticCache – get:key [SomeData-test@test.com]返回值[null]
http-80-Processor263 jsp.view-page – getSomeDataForEmail:即将进入同步块
http-80-Processor263 jsp.view-page – getSomeDataForEmail:内部同步块
http-80-Processor263 cache.StaticCache – get:key [SomeData-test@test.com]处的对象已过期
http-80-Processor263 cache.StaticCache – get:key [SomeData-test@test.com]返回值[null]
http-80-Processor131 jsp.view-page – getSomeDataForEmail:即将进入同步块
http-80-Processor131 jsp.view-page – getSomeDataForEmail:内部同步块
http-80-Processor131 cache.StaticCache – get:key [SomeData-test@test.com]处的对象已过期
http-80-Processor131 cache.StaticCache – get:key [SomeData-test@test.com]返回值[null]
http-80-Processor104 jsp.view-page – getSomeDataForEmail:内部同步块
http-80-Processor104 cache.StaticCache – get:key [someData-test@test.com]处的对象已过期
http-80-Processor104 cache.StaticCache – get:key [SomeData-test@test.com]返回值[null]
http-80-Processor252 jsp.view-page – getSomeDataForEmail:即将进入同步块
http-80-Processor283 jsp.view-page – getSomeDataForEmail:即将进入同步块
http-80-Processor2 jsp.view-page – getSomeDataForEmail:即将进入同步块
http-80-Processor2 jsp.view-page – getSomeDataForEmail:内部同步块

我希望一次只能看到一个线程进入/退出get / set操作的同步块。

在同步string对象时是否存在问题? 我认为caching键将是一个不错的select,因为它是唯一的操作,即使在方法中声明了final String key ,我认为每个线程将获得对同一个对象的引用,因此会同步这个单一的对象。

我在这里做错了什么?

更新 :在进一步查看日志之后,看起来像具有相同同步逻辑的方法,其中密钥总是相同的,例如

 final String key = "blah"; ... synchronized(key) { ... 

不会出现相同的并发问题 – 一次只有一个线程正在进入该块。

更新2 :感谢大家的帮助! 我接受了关于intern()string的第一个答案,这个答案解决了我最初的问题 – 多个线程进入同步块,我认为他们不应该这样做,因为这个key的值相同。

正如其他人所指出的那样,使用intern()来达到这样的目的,并且对这些string进行同步确实是一个坏主意 – 当运行JMetertestingwebapp来模拟预期的负载时,我看到使用的堆大小增长到几乎在20分钟内差不多有1GB。

目前,我正在使用简单的解决scheme,只是同步整个方法 – 但我真的很喜欢martinprobst和MBCook提供的代码示例,但是因为我目前在这个类中有大约7个类似的getData()方法(因为它需要大约7个不同的从Web服务的数据片),我不想添加几乎重复的逻辑获取和释放锁到每个方法。 但这对于未来的使用肯定是非常非常有价值的信息。 我认为这些最终是如何最好地做出这样的线程安全操作的正确答案,如果可以的话,我会给出更多的选票给这些答案!

没有把我的大脑完全放入齿轮,从你所说的看起来好像你需要实习()你的string的快速扫描:

 final String firstkey = "Data-" + email; final String key = firstkey.intern(); 

两个具有相同值的string不一定是相同的对象。

请注意,这可能会引入一个新的争用点,因为深入VM,intern()可能需要获取一个锁。 我不知道现代虚拟机在这个领域是什么样的,但是我们希望它们被优化。

我假设你知道StaticCache仍然需要线程安全。 但是,如果你在lockingcaching的时候,争用应该是微小的,而不是在调用getSomeDataForEmail的时候。

回答问题更新

我认为这是因为一个string文字总是产生相同的对象。 戴夫·科斯塔(Dave Costa)在评论中指出,它甚至比这更好:一个文字总是产生规范的表示。 所以在程序中任何地方具有相同值的所有string都会产生相同的对象。

编辑

其他人指出, 在实习生string同步实际上是一个非常糟糕的想法 – 部分原因是创build实习生string是允许它们永久存在,部分原因是如果程序中的任何位置的多个代码在实习生string上同步,你有这些位代码之间的依赖关系,并防止死锁或其他错误可能是不可能的。

在我input的其他答案中正在开发通过为每个键string存储locking对象来避免这种情况的策略。

这里有一个select – 它仍然使用一个单独的锁,但是我们知道无论如何我们都需要其中的一个,而且你正在讨论50个线程,而不是5000个,所以这可能不是致命的。 我还假设这里的性能瓶颈是缓慢阻塞DoSlowThing()中的I / O,因此它将从未序列化中受益匪浅。 如果这不是瓶颈,那么:

  • 如果CPU很忙,那么这种方法可能还不够,你需要另一种方法。
  • 如果CPU不忙,并且访问服务器不是瓶颈,那么这种方法是矫枉过正的,你不妨忘记这个和每个键的locking,在整个操作周围放置一个大的同步(StaticCache),并且做这是简单的方法。

很明显,这种方法需要在使用之前进行浸泡testing以确保可扩展性 – 我保证一无所获。

这段代码并不要求StaticCache是​​同步的,否则就是线程安全的。 如果任何其他代码(例如对旧数据进行清理)接触到caching,则需要重新访问。

IN_PROGRESS是一个虚拟的值 – 不完全干净,但代码简单,并且节省了两个哈希表。 它不处理InterruptedException,因为我不知道你的应用程序在这种情况下想要做什么。 另外,如果DoSlowThing()对于一个给定的键一贯地失败,那么这个代码并不完美,因为每个线程都会重试它。 既然我不知道失败的标准是什么,也不知道它们是暂时的还是永久的,我也不去处理,只是确保线程不会永久阻止。 在实践中,您可能希望将数据值放在caching中,指示“不可用”,可能有一个原因,以及何时重试超时。

 // do not attempt double-check locking here. I mean it. synchronized(StaticObject) { data = StaticCache.get(key); while (data == IN_PROGRESS) { // another thread is getting the data StaticObject.wait(); data = StaticCache.get(key); } if (data == null) { // we must get the data StaticCache.put(key, IN_PROGRESS, TIME_MAX_VALUE); } } if (data == null) { // we must get the data try { data = server.DoSlowThing(key); } finally { synchronized(StaticObject) { // WARNING: failure here is fatal, and must be allowed to terminate // the app or else waiters will be left forever. Choose a suitable // collection type in which replacing the value for a key is guaranteed. StaticCache.put(key, data, CURRENT_TIME); StaticObject.notifyAll(); } } } 

每当有任何东西被添加到caching中时,所有线程都会唤醒并检查caching(不pipe他们之后是什么键),所以使用较less争议的algorithm可以获得更好的性能。 但是,这些工作大部分将在I / O上的CPU闲置时间过长时发生,所以这可能不成问题。

如果为高速caching及其关联的locking,它返回的数据,IN_PROGRESS虚拟和执行的缓慢操作定义适当的抽象,则此代码可以共同用于多个高速caching。 将整个事物整合到caching中的方法可能不是一个坏主意。

同步一个intern'dstring可能根本就不是一个好主意 – 通过实现它,String变成一个全局对象,如果你在你的应用程序的不同部分使用相同的string进行同步,你可能会变得很奇怪,基本上不可分割的同步问题,如死锁。 这似乎不太可能,但是当它发生时,你真的被搞砸了。 作为一般规则,只有在本地对象上进行同步时,才能确保模块外的代码不会locking它。

在你的情况下,你可以使用同步散列表来存储你的密钥的locking对象。

例如:

 Object data = StaticCache.get(key, ...); if (data == null) { Object lock = lockTable.get(key); if (lock == null) { // we're the only one looking for this lock = new Object(); synchronized(lock) { lockTable.put(key, lock); // get stuff lockTable.remove(key); } } else { synchronized(lock) { // just to wait for the updater } data = StaticCache.get(key); } } else { // use from cache } 

这段代码有一个竞争条件,两个线程可能会把一个对象放在锁表中。 这应该不是一个问题,因为那么你只有一个线程调用web服务和更新caching,这应该不成问题。

如果您在一段时间后使caching失效,则应在lock!= null情况下从caching中检索数据后再检查数据是否为空。

或者,更容易,您可以使整个caching查找方法(“getSomeDataByEmail”)同步。 这将意味着所有线程在访问caching时都必须同步,这可能是一个性能问题。 但是,一如既往,先试试这个简单的解决scheme,看看它是否真的有问题! 在很多情况下不应该如此,因为你可能花费更多的时间处理结果而不是同步。

string适合同步。 如果必须同步stringID,则可以使用该string来创build互斥锁(请参阅“ 在ID上同步 ”)。 该algorithm的成本是否值得,取决于调用服务是否涉及任何重要的I / O。

也:

  • 我希望StaticCache.get()set()方法是线程安全的。
  • String.intern()是有代价的(在VM实现之间有所不同),应小心使用。

您可以使用1.5个并发实用程序来提供一个旨在允许多个并发访问的caching,以及一个添加点(即只有一个线程执行昂贵的对象“创build”):

  private ConcurrentMap<String, Future<SomeData[]> cache; private SomeData[] getSomeDataByEmail(final WebServiceInterface service, final String email) throws Exception { final String key = "Data-" + email; Callable<SomeData[]> call = new Callable<SomeData[]>() { public SomeData[] call() { return service.getSomeDataForEmail(email); } } FutureTask<SomeData[]> ft; ; Future<SomeData[]> f = cache.putIfAbsent(key, ft= new FutureTask<SomeData[]>(call)); //atomic if (f == null) { //this means that the cache had no mapping for the key f = ft; ft.run(); } return f.get(); //wait on the result being available if it is being calculated in another thread } 

显然,这不会像你想要的那样处理exception,并且caching没有内置的驱逐。也许你可以使用它作为改变你的StaticCache类的基础。

其他人build议实习string,这将起作用。

问题是,Java必须保持internedstring。 我被告知这样做,即使你不持有一个引用,因为下一次有人使用该string的值需要相同。 这意味着实习所有的string可能会开始吃掉记忆,这与你所描述的负载是一个大问题。

我看到了两个解决scheme:

你可以同步另一个对象

制作一个保存电子邮件(比如说User对象)的对象来代替电子邮件,将电子邮件的值保存为一个variables。 如果你已经有另一个对象代表这个人(比如说你已经根据他们的电子邮件从数据库中提取了一些东西),你可以使用它。 通过实现equals方法和hashcode方法,可以确保Java在执行静态cache.contains()以确定数据是否已经存在于caching中时认为这些对象相同(必须在caching上进行同步)。

其实,你可以保留第二个地图来locking对象。 像这样的东西:

 Map<String, Object> emailLocks = new HashMap<String, Object>(); Object lock = null; synchronized (emailLocks) { lock = emailLocks.get(emailAddress); if (lock == null) { lock = new Object(); emailLocks.put(emailAddress, lock); } } synchronized (lock) { // See if this email is in the cache // If so, serve that // If not, generate the data // Since each of this person's threads synchronizes on this, they won't run // over eachother. Since this lock is only for this person, it won't effect // other people. The other synchronized block (on emailLocks) is small enough // it shouldn't cause a performance problem. } 

这将防止在同一个电子邮件地址15提取一个。 你需要一些东西来防止太多的条目在emailLocks映射中结束。 使用Apache Commons的LRUMap就可以了。

这将需要一些调整,但它可能会解决你的问题。

使用不同的密钥

如果你愿意忍受可能的错误(我不知道这有多重要),你可以使用String的哈希码作为关键字。 ints不需要实习。

概要

我希望这有帮助。 线程很有趣,不是吗? 您也可以使用会话来设置一个值,意思是“我已经在寻找这个”,并检查第二(第三,第N)线程是否需要尝试创build或等待结果显示在caching中。 我想我有三个build议。

您的主要问题不仅在于可能存在具有相同值的多个String实例。 主要的问题是你只需要一个监视器来同步访问StaticCache对象。 否则,多个线程最终可能会同时修改StaticCache(尽pipe在不同的键下),这很可能不支持并发修改。

电话:

  final String key = "Data-" + email; 

每次调用方法都会创build一个新的对象。 因为这个对象就是你用来locking的对象,而且每次调用这个方法都会创build一个新的对象,所以你并不是真的基于这个关键字来同步对地图的访问。

这进一步解释你的编辑。 当你有一个静态string,那么它将工作。

使用intern()解决了这个问题,因为它从String类保存的内部池中返回string,确保如果两个string相等,则会使用池中的一个。 看到

http://java.sun.com/j2se/1.4.2/docs/api/java/lang/String.html#intern();

使用一个体面的caching框架,如ehcache 。

实现一个好的caching并不像有些人认为的那么容易。

关于String.intern()是内存泄漏源的评论,实际上并不正确。 Internedstring垃圾收集的,它可能需要更长的时间,因为在某些JVM的(SUN)它们存储在Perm空间,只有全GC的接触。

这是相当晚,但这里提出了很多不正确的代码。

在这个例子中:

 private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { SomeData[] data = null; final String key = "Data-" + email; synchronized(key) { data =(SomeData[]) StaticCache.get(key); if (data == null) { data = service.getSomeDataForEmail(email); StaticCache.set(key, data, CACHE_TIME); } else { logger.debug("getSomeDataForEmail: using cached object"); } } return data; } 

同步的范围不正确。 对于支持get / put API的静态caching,至less应该有get和getIfAbsentPuttypes操作的同步,以便安全地访问caching。 同步的范围将是caching本身。

如果必须对数据元素本身进行更新,则会增加一个额外的同步层,这应该在各个数据元素上。

SynchronizedMap可以用来代替显式同步,但是必须小心谨慎。 如果使用了错误的API(取而代之,而不是putIfAbsent),那么尽pipe使用了同步映射,操作仍然没有必要的同步。 注意使用putIfAbsent引入的复杂性:即使在不需要的情况下(因为在检查高速caching内容之前无法知道put值是否需要),也必须计算put值,或者需要小心使用授权(比如使用未来,这是有效的,但有点不协调,见下文),如果需要,可以根据需要获得投入价值。

期货的使用是可能的,但似乎相当尴尬,也许有点过度工程。 Future API是asynchronous操作的核心,特别是对于那些不能立即完成的操作。 涉及未来很可能增加了一层线程创build – 额外可能不必要的复杂性。

Future使用这种types的操作的主要问题是Future在multithreading中固有的联系。 当一个新的线程不是必要的时候使用Future将意味着忽略Future的很多机器,使得它成为这个应用的过度的API。

为什么不只是呈现一个静态的HTML页面,获取用户和每隔x分钟重新生成?

如果你不需要的话,我还build议你完全去掉string连接。

 final String key = "Data-" + email; 

caching中是否还有其他东西/types的对象使用电子邮件地址,您需要在密钥的开头添加额外的“数据 – ”?

如果没有,我只是做到这一点

 final String key = email; 

并且避免了所有额外的string创build。

其他方式同步string对象:

 String cacheKey = ...; Object obj = cache.get(cacheKey) if(obj==null){ synchronized (Integer.valueOf(Math.abs(cacheKey.hashCode()) % 127)){ obj = cache.get(cacheKey) if(obj==null){ //some cal obtain obj value,and put into cache } } } 

这是一个安全的简短Java 8解决scheme,它使用专用锁对象的映射进行同步:

 private static final Map<String, Object> keyLocks = new ConcurrentHashMap<>(); private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { final String key = "Data-" + email; synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) { SomeData[] data = StaticCache.get(key); if (data == null) { data = service.getSomeDataForEmail(email); StaticCache.set(key, data); } } return data; } 

它有一个缺点,即键和锁对象将永远保留在地图中。

这可以像这样解决:

 private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { final String key = "Data-" + email; synchronized (keyLocks.computeIfAbsent(key, k -> new Object())) { try { SomeData[] data = StaticCache.get(key); if (data == null) { data = service.getSomeDataForEmail(email); StaticCache.set(key, data); } } finally { keyLocks.remove(key); } } return data; } 

但是随后stream行的键会不断重新插入映射中,锁对象被重新分配。

更新 :当两个线程同时为同一个键但是不同的锁同时input同步部分时,这会使竞争条件成为可能。

所以使用即将到期的番石榴高速缓冲存储器可能更安全,更高效:

 private static final LoadingCache<String, Object> keyLocks = CacheBuilder.newBuilder() .expireAfterAccess(10, TimeUnit.MINUTES) // max lock time ever expected .build(CacheLoader.from(Object::new)); private SomeData[] getSomeDataByEmail(WebServiceInterface service, String email) { final String key = "Data-" + email; synchronized (keyLocks.getUnchecked(key)) { SomeData[] data = StaticCache.get(key); if (data == null) { data = service.getSomeDataForEmail(email); StaticCache.set(key, data); } } return data; } 

请注意,这里假设StaticCache是线程安全的,不会因并发读取和写入不同的密钥而受到影响。