当前位置: 首页 > 面试题库 >

在Java中同步String对象

越星晖
2023-03-14
问题内容

我有一个Web应用程序正在进行负载/性能测试,特别是在一项功能上,我们希望数百名用户正在访问同一页面,并且每10秒刷新一次。我们发现可以使用此功能进行改进的一个方面是,由于数据未更改,因此将Web服务的响应缓存了一段时间。

在实现了基本的缓存之后,在进一步的测试中,我发现我没有考虑并发线程如何同时访问缓存。我发现在大约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操作,并且似乎使用cache键将是对象的一个​​很好的选择。同步(通过这种方式,对电子邮件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;
}

我还为诸如“同步块之前”,“内部同步块”,“即将离开同步块”和“同步块之后”之类的内容添加了日志记录行,因此可以确定我是否在有效地同步获取/设置操作。

但是,这似乎没有奏效。我的测试日志输出如下:

(日志输出为“线程名”“记录器名称”“消息”)
http-80-Processor253 jsp.view-page-getSomeDataForEmail:即将输入同步块 http-80-Processor253 jsp.view-page-getSomeDataForEmail:内部同步块 http -80-Processor253 cache.StaticCache-获取:键[SomeData-test@test.com]上的对象已过期 http-80-Processor253 cache.StaticCache-获取:键[SomeData-test@test.com]返回值[null] http-80-Processor263 jsp.view页-getSomeDataForEmail:即将进入同步块 http-80-Processor263 jsp.view页-getSomeDataForEmail:同步块 http-80-Processor263 cache.StaticCache-get中的对象:键[SomeData -test@test.com]已过期 http-80-Processor263 cache.StaticCache-获取:键[SomeData-test@test.com]返回值[空] http-80-Processor131 jsp.view-page-getSomeDataForEmail:即将进入同步块 http-80-Processor131 jsp .view-page-getSomeDataForEmail:在同步块 http-80-Processor131缓存内。StaticCache-get:密钥[SomeData-test@test.com]上的对象已过期 http-80-Processor131缓存.StaticCache-get:密钥[SomeData- test@test.com]返回值[null] http-80-Processor104 jsp.view-page-getSomeDataForEmail:内部同步块 http-80-Processor104 cache.StaticCache-get:键处的对象[SomeData-test@test.com]已过期 http-80-Processor104 cache.StaticCache-获取:键[SomeData-test@test.com]返回值[空] 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:在同步块内部

我只想一次只看到一个线程进入/退出获取/设置操作周围的同步块。

在String对象上同步是否存在问题?我认为cache-key将是一个不错的选择,因为它是操作唯一的,即使final String key在方法中声明了cache-key ,我也认为每个线程都将获取对同一对象的引用,因此将对此进行同步单个对象。

我在这里做错了什么?

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

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

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

更新2:谢谢大家的帮助!我接受了有关intern()ing字符串的第一个答案,它解决了我的第一个问题-多个线程正在进入同步块,而我认为不应这样做,因为的key值相同。

正如其他人指出的那样,将其intern()用于这样的目的并在这些Strings上进行同步确实是个坏主意-在针对Webapp运行JMeter测试以模拟预期的负载时,我看到已用的堆大小增长到将近1GB。不到20分钟。

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


问题答案:

快速浏览一下你所说的内容,而无需全神贯注,就好像你需要intern()你的Strings:

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

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

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

我假设你知道StaticCache仍然需要是线程安全的。但是,如果你锁定缓存而不是调用getSomeDataForEmail时的密钥,那么与你的争用应该是很小的。

对问题更新的回复:

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

编辑

其他人指出,在内部字符串上进行同步实际上是一个糟糕的主意 -部分是因为允许创建内部字符串使它们永久存在,部分原因是,如果程序中任何位置的代码多于一个内部字符串,你在这些代码位之间有依赖性,并且防止死锁或其他错误可能是不可能的。

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

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

  • 如果CPU繁忙,则此方法可能不够用,你需要另一种方法。
  • 如果CPU不忙,并且对服务器的访问不是瓶颈,那么这种方法就显得过大了,你可能会忘记此锁定和按键锁定,并在整个操作过程中放置​​一个较大的sync(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伪指令以及执行缓慢的操作,则该代码可能会与多个高速缓存一起使用。将整个过程都放入缓存中的方法可能不是一个坏主意。



 类似资料:
  • 我只在一个字符串对象上找到了同步的答案,而不是两个。 这不是一项真正的任务,而是一项任务。我有一个图书馆可以把钱从一个账户转到另一个账户。我无法访问帐户对象以锁定它。我只能用图书馆里的东西。传输(字符串从、字符串到),这不是线程安全的。我有一个帐户ID为字符串的方法。我需要在没有死锁的情况下锁定这两个字符串。 到目前为止,我所做的是: > 使用创建了新字符串。intern方法(字符串fr=from

  • 我正在寻找有关同步块的澄清。考虑一下这个类 - A是单例。getValue在整个应用程序中被多个线程大量访问。我添加了一个新方法remove,它从映射中删除一个键。如果如上所述执行删除, 当线程位于remove方法的同步块中时,我假设它将获取map对象上的锁。这是否意味着其他试图通过getValue方法访问映射的线程将被阻止?(我希望他们这样做。) 当remove方法的同步块中没有线程时,访问ge

  • 问题内容: 代码段-1 代码段-2 我在第一个代码段中遇到了竞争。我知道这是因为我正在获得对不可变对象(类型为Integer)的锁定。 我已经写了第二个代码片段,这再次使“布尔”不变。但这有效(输出运行中不显示竞争条件)。如果我正确理解了上一个问题的解决方案,则以下是出现问题的一种可能方法 线程1锁定由指向的对象(例如A) 线程2现在试图获取由指向的对象的锁,并进入A的等待队列。 线程1进入同步块

  • 在示例代码中 在这个页面上, lock1和lock2分别控制c1和c2上的更新。 然而, 正在获取对象lock1的锁并在同步块时释放它 被执行。 当这个代码块被执行时,这个对象的成员c1上可能还有一个更新——我看不出这个更新是如何被代码中的lock1上的同步所阻止的。 只有对象lock1可以独占访问——除此之外别无它物(?) 那么,实施情况如何 在上面的代码中不同于 甚至 当c1是一个对象而不是一

  • 假设我有两条线。Thread1正在访问一个同步方法,同时,Thread2正在访问同一对象的另一个同步方法。据我所知,Thread2应该等到Thread1完成它的任务。我的问题是,Thread2是否在对象的等待线程列表中?对我来说似乎是这样,但Thread2不调用wait()方法,那么作为逻辑结果,它不应该在对象的等待线程列表中。如果它不在对象的等待线程列表中,那么Thread2的状态是什么?

  • 本文向大家介绍如何在Java中创建String对象?,包括了如何在Java中创建String对象?的使用技巧和注意事项,需要的朋友参考一下 您可以通过以下方式创建字符串: 将包装在“”中的字符串值分配给String类型变量。 通过将字符串值作为其构造函数的参数传递,使用new关键字创建String类的对象。 将字符数组传递给String构造函数。