在Java中同步String对象

问题描述 投票:40回答:17

我有一个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操作,看起来使用缓存键是一个很好的候选对象同步(这样,对电子邮件[email protected]的此方法的调用不会被方法调用阻止到[email protected])。

我将方法更新为如下所示:

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 [[email protected]]上的对象已过期 http-80-Processor253 cache.StaticCache - get:key [[email protected]]返回值[null] http-80-Processor263 jsp.view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor263 jsp.view-page - getSomeDataForEmail:内部同步块 http-80-Processor263 cache.StaticCache - get:对象[[email protected]]已过期 http-80-Processor263 cache.StaticCache - get:key [[email protected]]返回值[null] http-80-Processor131 jsp.view-page - getSomeDataForEmail:即将进入同步块 http-80-Processor131 jsp.view-page - getSomeDataForEmail:内部同步块 http-80-Processor131 cache.StaticCache - 获取:key [[email protected]]上的对象已过期 http-80-Processor131 cache.StaticCache - get:key [[email protected]]返回值[null] http-80-Processor104 jsp.view-page - getSomeDataForEmail:内部同步块 http-80-Processor104 cache.StaticCache - 获取:key [[email protected]]上的对象已过期 http-80-Processor104 cache.StaticCache - get:key [[email protected]]返回值[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服务的数据),我不想添加几乎重复的逻辑来获取和释放每个方法的锁。但这对于未来的使用来说绝对是非常非常有价值的信息。我认为这些最终是关于如何最好地进行这种线程安全的操作的正确答案,如果可以的话,我会给这些答案更多的投票!

java multithreading synchronization thread-safety synchronized
17个回答
38
投票

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

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虚拟以及执行速度慢的操作,则可以将此代码用于多个缓存。将整个事物滚动到缓存上的方法可能不是一个坏主意。


2
投票

这个问题在我看来有点过于宽泛,因此它提出了同样广泛的答案。所以我会尝试回答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);
    }

1
投票

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

在这个例子中:

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。


0
投票

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


0
投票

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

final String key = "Data-" + email;

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

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

final String key = email;

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


0
投票

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

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
        }
    }
}

0
投票

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

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

        private int decrement() {
            return remaining.decrementAndGet();
        }
    }
}

在OP的情况下,它将像这样使用:

private KeySynchronizer<String> keySynchronizer = new KeySynchronizer<>();

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

如果不应从同步代码返回任何内容,则可以像这样编写synchronize方法:

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

0
投票

我添加了一个小锁类,可以锁定/同步任何键,包括字符串。

请参阅Java 8,Java 6的实现和一个小测试。

Java 8:

public class DynamicKeyLock<T> implements Lock
{
    private final static ConcurrentHashMap<Object, LockAndCounter> locksMap = new ConcurrentHashMap<>();

    private final T key;

    public DynamicKeyLock(T lockKey)
    {
        this.key = lockKey;
    }

    private static class LockAndCounter
    {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        return locksMap.compute(key, (key, lockAndCounterInner) ->
        {
            if (lockAndCounterInner == null) {
                lockAndCounterInner = new LockAndCounter();
            }
            lockAndCounterInner.counter.incrementAndGet();
            return lockAndCounterInner;
        });
    }

    private void cleanupLock(LockAndCounter lockAndCounterOuter)
    {
        if (lockAndCounterOuter.counter.decrementAndGet() == 0)
        {
            locksMap.compute(key, (key, lockAndCounterInner) ->
            {
                if (lockAndCounterInner == null || lockAndCounterInner.counter.get() == 0) {
                    return null;
                }
                return lockAndCounterInner;
            });
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

Java 6:

公共类DynamicKeyLock实现Lock {private final static ConcurrentHashMap locksMap = new ConcurrentHashMap();私人最终T键;

    public DynamicKeyLock(T lockKey) {
        this.key = lockKey;
    }

    private static class LockAndCounter {
        private final Lock lock = new ReentrantLock();
        private final AtomicInteger counter = new AtomicInteger(0);
    }

    private LockAndCounter getLock()
    {
        while (true) // Try to init lock
        {
            LockAndCounter lockAndCounter = locksMap.get(key);

            if (lockAndCounter == null)
            {
                LockAndCounter newLock = new LockAndCounter();
                lockAndCounter = locksMap.putIfAbsent(key, newLock);

                if (lockAndCounter == null)
                {
                    lockAndCounter = newLock;
                }
            }

            lockAndCounter.counter.incrementAndGet();

            synchronized (lockAndCounter)
            {
                LockAndCounter lastLockAndCounter = locksMap.get(key);
                if (lockAndCounter == lastLockAndCounter)
                {
                    return lockAndCounter;
                }
                // else some other thread beat us to it, thus try again.
            }
        }
    }

    private void cleanupLock(LockAndCounter lockAndCounter)
    {
        if (lockAndCounter.counter.decrementAndGet() == 0)
        {
            synchronized (lockAndCounter)
            {
                if (lockAndCounter.counter.get() == 0)
                {
                    locksMap.remove(key);
                }
            }
        }
    }

    @Override
    public void lock()
    {
        LockAndCounter lockAndCounter = getLock();

        lockAndCounter.lock.lock();
    }

    @Override
    public void unlock()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);
        lockAndCounter.lock.unlock();

        cleanupLock(lockAndCounter);
    }


    @Override
    public void lockInterruptibly() throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        try
        {
            lockAndCounter.lock.lockInterruptibly();
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }
    }

    @Override
    public boolean tryLock()
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired = lockAndCounter.lock.tryLock();

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
    {
        LockAndCounter lockAndCounter = getLock();

        boolean acquired;
        try
        {
            acquired = lockAndCounter.lock.tryLock(time, unit);
        }
        catch (InterruptedException e)
        {
            cleanupLock(lockAndCounter);
            throw e;
        }

        if (!acquired)
        {
            cleanupLock(lockAndCounter);
        }

        return acquired;
    }

    @Override
    public Condition newCondition()
    {
        LockAndCounter lockAndCounter = locksMap.get(key);

        return lockAndCounter.lock.newCondition();
    }
}

测试:

public class DynamicKeyLockTest
{
    @Test
    public void testDifferentKeysDontLock() throws InterruptedException
    {
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(new Object());
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(new Object());
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertTrue(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }

    @Test
    public void testSameKeysLock() throws InterruptedException
    {
        Object key = new Object();
        DynamicKeyLock<Object> lock = new DynamicKeyLock<>(key);
        lock.lock();
        AtomicBoolean anotherThreadWasExecuted = new AtomicBoolean(false);
        try
        {
            new Thread(() ->
            {
                DynamicKeyLock<Object> anotherLock = new DynamicKeyLock<>(key);
                anotherLock.lock();
                try
                {
                    anotherThreadWasExecuted.set(true);
                }
                finally
                {
                    anotherLock.unlock();
                }
            }).start();
            Thread.sleep(100);
        }
        finally
        {
            Assert.assertFalse(anotherThreadWasExecuted.get());
            lock.unlock();
        }
    }
}

-1
投票

如果可以合理地保证字符串值在整个系统中是唯一的,则可以安全地使用String.intern进行同步。 UUIDS是解决这个问题的好方法。您可以通过缓存,地图将UUID与实际的字符串键相关联,甚至可以将uuid存储为实体对象上的字段。

    @Service   
    public class MySyncService{

      public Map<String, String> lockMap=new HashMap<String, String>();

      public void syncMethod(String email) {

        String lock = lockMap.get(email);
        if(lock==null) {
            lock = UUID.randomUUID().toString();
            lockMap.put(email, lock);
        }   

        synchronized(lock.intern()) {
                //do your sync code here
        }
    }

25
投票

在intern'd String上进行同步可能根本不是一个好主意 - 通过实习,String变成一个全局对象,如果你在应用程序的不同部分同步interned字符串,你可能会变得非常奇怪,基本上是不可解决的同步问题,例如死锁。这似乎不太可能,但是当它发生时,你真的被搞砸了。作为一般规则,只能在本地对象上进行同步,您绝对确定模块外部的代码不会锁定它。

在您的情况下,您可以使用同步哈希表来存储密钥的锁定对象。

Ef。:

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
}

此代码具有竞争条件,其中两个线程可能会将对象彼此放入锁定表中。然而,这应该不是问题,因为那时你只有一个线程调用webservice并更新缓存,这应该不是问题。

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

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


9
投票

字符串不适合同步。如果必须同步String ID,可以使用字符串创建互斥锁(请参阅“synchronizing on an ID”)。该算法的成本是否值得,取决于调用您的服务是否涉及任何重要的I / O.

也:

  • 我希望StaticCache.get()和set()方法是线程安全的。
  • String.intern()需要付出代价(在虚拟机实施之间有所不同),应谨慎使用。

5
投票

其他人建议实习字符串,这将有效。

问题是Java必须保持内部字符串。有人告诉我,即使你没有持有引用也会这样做,因为下次有人使用该字符串时,该值必须相同。这意味着实习所有字符串可能会开始占用内存,而你所描述的负载可能是一个大问题。

我已经看到了两个解决方案:

您可以在另一个对象上进行同步

而不是电子邮件,使一个对象保存电子邮件(比如用户对象),将电子邮件的值保存为变量。如果您已经有另一个代表该人的对象(比如您已根据他们的电子邮件从数据库中提取了某些内容),则可以使用该对象。通过实现equals方法和hashcode方法,可以确保Java在执行静态cache.contains()时认为对象是相同的,以确定数据是否已经在缓存中(您必须在缓存上进行同步) )。

实际上,您可以为要锁定的对象保留第二个Map。像这样的东西:

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的LRUMaps就可以了。

这需要一些调整,但它可以解决您的问题。

使用其他密钥

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

摘要

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


5
投票

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

 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
}

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


3
投票

使用像ehcache这样的体面缓存框架。

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

关于String.intern()是内存泄漏源的注释,实际上并非如此。 Interned Strings是垃圾收集的,它可能需要更长的时间,因为在某些JVM'S(SUN)上它们存储在Perm空间中,只有完整的GC触及它。


3
投票

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

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); // vulnerable to race-conditions
        }
    }
    return data;
}

但随后流行的密钥将不断重新插入地图中,并重新分配锁定对象。

更新:当两个线程同时进入相同键但具有不同锁的同步部分时,这会留下竞争条件。

所以使用expiring Guava Cache可能更安全有效:

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是线程安全的,不会受到不同键的并发读写操作的影响。


2
投票

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


2
投票

电话:

   final String key = "Data-" + email;

每次调用方法时都会创建一个新对象。因为该对象是您用来锁定的对象,并且每次调用此方法都会创建一个新对象,所以您实际上并不是基于该键同步对地图的访问。

这进一步说明了您的编辑。当你有一个静态字符串,那么它将工作。

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

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

© www.soinside.com 2019 - 2024. All rights reserved.