在Java中同步String对象

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了在Java中同步String对象相关的知识,希望对你有一定的参考价值。

我有一个webapp,我正在进行一些负载/性能测试,特别是在我们希望有几百个用户访问同一页面并在此页面上每10秒点击一次刷新的功能。我们发现我们可以使用此功能进行改进的一个方面是在一段时间内缓存来自Web服务的响应,因为数据没有变化。

在实现这个基本缓存之后,在一些进一步的测试中,我发现我没有考虑并发线程如何同时访问Cache。我发现在大约100毫秒内,大约有50个线程试图从缓存中获取对象,发现它已经过期,命中Web服务以获取数据,然后将对象放回缓存中。

原始代码看起来像这样:

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操作,看起来使用缓存键是一个很好的候选对象同步(这样,对电子邮件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;
}

我还为“同步块之前”,“内同步块”,“即将离开同步块”和“同步块之后”之类的内容添加了日志行,因此我可以确定是否有效地同步了get / set操作。

然而,它似乎并没有起作用。我的测试日志输出如下:

(日志输出是'threadname''记录器名称''消息') http-80-Processor253 jsp.view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor253 jsp.view-page - getSomeDataForEmail:内部同步块 http-80-Processor253 cache.StaticCache - 获取: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:对象[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 - 获取: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 - 获取: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对象上同步是否存在问题?我认为缓存键是一个很好的选择,因为它对于操作是唯一的,即使在方法中声明了final String key,我也认为每个线程都会获得对同一对象的引用,因此会同步这个单一的对象。

我在这做错了什么?

更新:在进一步查看日志后,似乎具有相同同步逻辑的方法,其中密钥始终相同,例如

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

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

更新2:感谢大家的帮助!我接受了关于intern()ing Strings的第一个答案,它解决了我的初始问题 - 多线程进入同步块,我认为它们不应该,因为key具有相同的值。

正如其他人所指出的那样,使用intern()实现这一目的并同步这些字符串确实是一个坏主意 - 当针对webapp运行JMeter测试来模拟预期的负载时,我看到使用的堆大小增长到近1GB在不到20分钟。

目前我正在使用仅仅同步整个方法的简单解决方案 - 但我非常喜欢martinprobst和MBCook提供的代码示例,但由于我目前在这个类中有大约7个类似的getData()方法(因为它需要大约7个不同的部分来自Web服务的数据),我不想添加几乎重复的逻辑来获取和释放每个方法的锁。但这对于未来的使用来说绝对是非常非常有价值的信息。我认为这些最终是关于如何最好地进行这种线程安全的操作的正确答案,如果可以的话,我会给这些答案更多的投票!

答案

没有把我的大脑完全放入装备,从快速扫描你所说的内容看起来好像你需要实习()你的字符串:

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

具有相同值的两个字符串不一定是相同的对象。

请注意,这可能会引入新的争用点,因为在VM的深处,intern()可能必须获取锁。我不知道现代虚拟机在这个领域是什么样的,但人们希望它们能够进行极端优化。

我假设您知道StaticCache仍然需要是线程安全的。但是,如果你在调用getSomeDataForEmail时锁定缓存而不仅仅是密钥,那么与那里的争论相比应该是微不足道的。

对问题更新的回复:

我认为这是因为字符串文字总是产生相同的对象。戴夫·科斯塔在评论中指出它甚至比这更好:一个文字总是产生规范表示。因此,程序中任何位置具有相同值的所有String文字都将产生相同的对象。

编辑

其他人指出,实习生字符串的同步实际上是一个非常糟糕的主意 - 部分原因是允许创建实习字符串使其永久存在,部分原因是如果程序中任何地方的代码不止一位在实习字符串上同步,你有这些代码之间的依赖关系,防止死锁或其他错误可能是不可能的。

在我输入的其他答案中,正在开发通过为每个键字符串存储锁定对象来避免这种情况的策略。

这是一个替代方案 - 它仍然使用单一锁,但我们知道无论如何我们将需要其中一个用于缓存,而你谈论的是50个线程,而不是5000个,所以这可能不是致命的。我还假设这里的性能瓶颈是在DoSlowThing()中阻塞I / O很慢,因此不会被序列化。如果这不是瓶颈,那么:

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

显然,这种方法需要在使用前进行可靠性测试 - 我保证不会。

此代码不要求StaticCache同步或以其他方式线程安全。如果任何其他代码(例如计划的旧数据清理)触及缓存,则需要重新访问。

IN_PROGRESS是一个虚拟值 - 不完全干净,但代码很简单,它节省了两个哈希表。它不处理InterruptedException,因为在这种情况下我不知道你的应用程序想要做什么。此外,如果DoSlowThing()对于给定键始终失败,则此代码不是很完美,因为每个线程都将重试它。由于我不知道失败的标准是什么,以及它们是否可能是临时的或永久性的,我也不会处理这个问题,我只是确保线程不会永远阻塞。实际上,您可能希望在缓存中放置一个数据值,表示“不可用”,可能有原因,以及何时重试超时。

// 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();
        }
    }
}

每次将任何内容添加到缓存中时,所有线程都会唤醒并检查缓存(无论它们处于什么密钥之后),因此可以通过较少争用的算法获得更好的性能。但是,大部分工作将在I / O上大量空闲CPU时间阻塞期间进行,因此可能不是问题。

如果为缓存及其关联的锁定定义合适的抽象,返回的数据,IN_PROGRESS虚拟以及执行速度慢的操作,则可以将此代码用于多个缓存。将整个事物滚动到缓存上的方法可能不是一个坏主意。

另一答案

这个问题在我看来有点过于宽泛,因此它提出了同样广泛的答案。所以我会尝试回答the question我已被重定向,不幸的是,一个已被关闭为重复。

public class ValueLock<T> {

    private Lock lock = new ReentrantLock();
    private Map<T, Condition> conditions  = new HashMap<T, Condition>();

    public void lock(T t){
        lock.lock();
        try {
            while (conditions.containsKey(t)){
                conditions.get(t).awaitUninterruptibly();
            }
            conditions.put(t, lock.newCondition());
        } finally {
            lock.unlock();
        }
    }

    public void unlock(T t){
        lock.lock();
        try {
            Condition condition = conditions.get(t);
            if (condition == null)
                throw new IllegalStateException();// possibly an attempt to release what wasn't acquired
            conditions.remove(t);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }

在(外部)lock操作时,获取(内部)锁以获得对地图的独占访问一小段时间,并且如果对应的对象已经在地图中,则当前线程将等待,否则它将放置新的Condition到地图,释放(内部)锁并继续,并认为获得(外部)锁定。 (外部)unlock操作,首先获取(内部)锁定,将在Condition上发出信号,然后从地图中移除该对象。

该类不使用Map的并发版本,因为对它的每次访问都受到单个(内部)锁的保护。

请注意,这个类的lock()方法的语义与ReentrantLock.lock()的语义不同,重复的lock()调用没有配对的unlock()将无限期地挂起当前线程。

OP描述的可能适用于该情况的使用示例

    ValueLock<String> lock = new ValueLock<String>();
    // ... share the lock   
    String email = "...";
    try {
        lock.lock(email);
        //... 
    } finally {
        lock.unlock(email);
    }
另一答案

这已经很晚了,但是这里提供了很多不正确的代码。

在这个例子中:

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的静态缓存,至少应该围绕get和getIfAbsentPut类型操作进行同步,以便安全访问缓存。同步范围将是缓存本身。

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

可以使用SynchronizedMap代替显式同步,但仍必须注意。如果使用了错误的API(get和put而不是putIfAbsent),那么尽管使用了synchronized映射,但操作将没有必要的同步。注意使用putIfAbsent引入的复杂性:要么,即使在不需要的情况下也必须计算put值(因为put不知道在检查缓存内容之前是否需要put值),或者需要小心使用委托(例如,使用Future,它有效,但有些不匹配;见下文),如果需要,可根据需要获得put值。

期货的使用是可能的,但似乎相当尴尬,也许有点过度工程。 Future API是异步操作的核心,特别是对于可能无法立即完成的操作。涉及Future很可能会增加一层线程创建 - 额外可能是不必要的复杂问题。

使用Future进行此类操作的主要问题是Future在多线程中具有内在联系。当不需要新线程时使用Future意味着忽略Future的许多机制,使其成为这种用途的过重API。

另一答案

为什么不渲染一个静态的html页面,该页面被提供给用户并每隔x分钟重新生成一次?

另一答案

如果你不需要,我还建议完全摆脱字符串连接。

final String key = "Data-" + email;

缓存中是否有其他事物/类型的对象使用您需要在密钥开头添加额外“数据”的电子邮件地址?

如果没有,我就是这么做的

final String key = email;

并且你避免使用所有额外的字符串创建工具。

另一答案

其他方式同步字符串对象:

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
        }
    }
}
另一答案

如果其他人有类似的问题,以下代码可以工作,据我所知:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

public class KeySynchronizer<T> {

    private Map<T, CounterLock> locks = new ConcurrentHashMap<>();

    public <U> U synchronize(T key, Supplier<U> supplier) {
        CounterLock lock = locks.compute(key, (k, v) -> 
                v == null ? new CounterLock() : v.increment());
        synchronized (lock) {
            try {
                return supplier.get();
            } finally {
                if (lock.decrement() == 0) {
                    // Only removes if key still points to the same value,
                    // to avoid issue described below.
                    locks.remove(key, lock);
                }
            }
        }
    }

    private static final class CounterLock {

        private AtomicInteger remaining = new AtomicInteger(1);

        private CounterLock increment() {
            // Returning a new CounterLock object if remaining = 0 to ensure that
            // the lock is not removed in step 5 of the following execution sequence:
            // 1) Thread 1 obtains a new CounterLock object from locks.compute (after evaluating "v == null" to true)
            // 2) Thread 2 evaluates "v == null" to false in locks.compute
            // 3) Thread 1 calls lock.decrement() which sets remaining = 0
            // 4) Thread 2 calls v.increment() in locks.compute
            // 5) Thread 1 calls locks.remove(key, lock)
            return remaining.getAndIncrement() == 0 ? new CounterLock(

以上是关于在Java中同步String对象的主要内容,如果未能解决你的问题,请参考以下文章

多线程 Thread 线程同步 synchronized

Failed to convert property value of type ‘java.lang.String‘ to required type ‘int‘ for property(代码片段

ReleaseMutex:从非同步代码块调用对象同步方法

java并发线程锁技术的使用

面向对象---java代码块

java中ReentrantReadWriteLock读写锁的使用