【问题标题】:Java Thread Safety - Lock an entire static class but only for one methodJava 线程安全 - 锁定整个静态类,但只锁定一个方法
【发布时间】:2012-07-17 13:01:32
【问题描述】:

我实现了一个静态帮助器类,它帮助缓存和检索数据库中的一些只读、非可变、非易失性数据。

(剥离)示例:

public class CacheHelper
{
    private static HashMap foos, bars;

    public static Foo getFoo(int fooId) { /* etc etc */ }
    public static Bar getBar(int barId) { /* etc etc */ }

    public static void reloadAllCaches()
    {
        //This is where I need it to lock access to all the other static methods
    }
}

按照我对静态类的阅读方式,如果我将 synchronized 关键字添加到 reloadAllCaches() 方法,这将在该方法执行时对整个类应用锁定。它是否正确? (编辑:是的,不正确。感谢您的回复。)

注意:我希望对 getter 方法的线程安全性和它们返回的对象保持不可知论,因为这些数据永远不会发生变异,并且希望它尽快返回。

【问题讨论】:

  • 不能使用 ConcurrentMap 吗?您是否需要一次更新整个地图,或者是否可以更新一些值而一些不更新?
  • 它正在清除地图,然后(或多或少)在格式化后将表格的全部内容转储到其中。虽然如果我包含一些逻辑以仅更新实体等,您的建议是可行的,但我宁愿它每次都是一个干净的表转储。
  • 您不需要为此锁定。构建全新的地图并将它们分配给volatile 字段。地图本身可以是普通的HashMaps,为了安全起见,可能会包裹在immutableMaps 中。我假设只有一个线程会进行重建。
  • 没错,单线程会构建。在重建它们时,我清除了现有地图并重新填充....但是你的建议也很有趣,如果我将它构建为新地图,然后将其分配到旧的易失地图之上,它可能会实现相同的目标。

标签: java multithreading static static-methods


【解决方案1】:

如果将 synchronized 关键字添加到 reloadAllCaches() 函数,则类中获得 synchronized 关键字的所有其他静态函数都无法在 reloadAllCaches() 函数运行时执行。

非静态函数如何执行,不管它们是否有synchronized 关键字。没有synchronized关键字的所有其他函数也可以执行。

毕竟带有synchronized 的函数可以这样看:

public class Bar
{
    public static void foo()
    {
        synchronized (Bar.class)
        {
            // your code
        }
    }
}

带有synchronized 关键字的非静态函数可以这样查看:

public class Bar
{
    public void foo()
    {
        synchronized (this)
        {
            // your code
        }
    }
}

所以静态和非静态函数有不同的同步上下文,并且不会用 synchronized 关键字阻塞彼此的执行。

对于您的情况,我建议使用ReentrantReadWriteLock。此类将允许任意数量的函数同时获得读锁,但只有一个函数获得写锁。只有在没有读锁的情况下才会获取写锁,只要有写锁就不会获取读锁。

你可以让你的重载函数获取一个写锁,而你所有的读取函数获取一个写锁。您必须使用 ReentrantReadWriteLock 的静态实例。

我的建议是这样实现:

public class CacheHelper
{
    private static HashMap foos, bars;
    private static java.util.concurrent.locks.ReadWriteLock lock = new java.util.concurrent.locks.ReentrantReadWriteLock();

    public static Foo getFoo(int fooId)
    {
        lock.readLock().lock();
        try {
            /* etc etc */
        } finally {
            lock.readLock().unlock();
        }
    }
    public static Bar getBar(int barId)
    {
        lock.readLock().lock();
        try {
            /* etc etc */
        } finally {
            lock.readLock().unlock();
        }
    }

    public static void reloadAllCaches()
    {
        lock.writeLock().lock();
        try {
            //This is where I need it to lock access to all the other static methods
        } finally {
            lock.writeLock().unlock();
        }
    }
}

【讨论】:

  • 这正是我想要模仿的行为。为确保我正确理解您,lock.readLock.lock() 只会在有写锁时阻塞?一次可以有多个 readLock()(没有写入时)?
  • 没错。在没有写锁的情况下,可以有尽可能多的读锁。一旦请求写锁,就不允许新的读锁,一旦解锁所有读锁,就会获得写锁。
  • 你摇滚。感谢您的建议。我不知道这样的构造已经存在。我希望我不必自己动手:)
  • 感谢您将如此复杂的事物如此简洁和雄辩地归结为最基本的原则:)
【解决方案2】:

如果您希望能够重新填充集合而不锁定它们,您可以将它们替换为不可变集合。

private static volatile Map foos, bars;

public static Foo getFoo(int fooId) { return foos.get(fooId); }
public static Bar getBar(int barId) { /* etc etc */ }

public static void reloadAllCaches()
{
    Map newFoo = ...
    // populate newFoo
    foos = newFoo;

    Map newBar = ...
    // populate newBar
    bars = newBar;
}

getFoo 将看到一个完全一致的副本,而不需要锁,因为 Map 总是被替换,从不修改。


synchronized 锁定对象而不是方法,在这种情况下,您锁定的是 CacheHelper.class 对象

为了使 getter 尽可能快,您可以使用 ConcurrentHashMap 而不是 synchronized


仅将同步用于更新的示例。

final ConcurrentMap<Key, ExpensiveObject> map =

public ExpensiveObject getOrNull(Key key) { 
     return map.get(key); 
}

public ExpensiveObject getOrCreate(Key key) {
     synchronized(map) {
         ExpensiveObject ret = map.get(key);
         if (ret == null)
              map.put(key, ret = new ExpensiveObject(key));
         return ret;
     }
}

【讨论】:

  • 除非他需要自动更新地图。如果他不这样做,那就是要走的路……
  • 如果需要,他可以在 ConcurrentMap 上使用同步更新。如果这一次只更改一个键,您可以在不同步的情况下获取()键。
  • 他需要在更新地图时屏蔽他的阅读方式。如果不同步所有有悖于 CHM 目的的东西,不确定如何做到这一点。在他的情况下,读写锁似乎是一个好方法。
  • 这是一种选择。添加我上面的意思的例子。
  • 在这种情况下他可以替换地图。这避免了同步的需要。
【解决方案3】:

简单来说,锁只是防止其他线程同时运行相同的方法,它不会为类中的任何其他内容提供任何锁定资源,无论是静态的还是其他的。

只有在拥有控制权的线程退出该方法之前,其他线程才会阻止对该方法的访问。所有线程仍然可以免费访问任何其他内容。

如果您需要对对象本身进行锁定控制,那么您需要考虑为缓存提供线程安全访问器或某种继承处理。

我的意思是,如果你在这个方法中构造了一个新的缓存,并且一旦构造了用这些新对象替换缓存助手中的引用对象,那么你只需同步 reloadAllCaches 方法就可以了。

但是,如果您打算重用/回收现有的缓存容器,那么您将不得不在容器级别使用锁定来防止在缓存被破坏和重建时读取。

如果您需要重新加载多个缓存地图(根据您的示例),那么您可能会发现有必要将缓存对象抽象到另一层,否则您可能会在重新应用缓存时失去对缓存的同步访问。

【讨论】:

    【解决方案4】:

    不,这是不正确的。仅在reloadAllCaches 方法中添加synchronized 意味着该方法的调用者必须获取类上的锁,但调用非同步方法的线程仍然可以同时访问该类。为了安全起见,您仍然需要在同一个锁上同步访问器,否则读取器线程可能看不到最新的更改并且会得到陈旧的数据。或者,您可以使用 ConcurrentHashMap。

    【讨论】:

    • 感谢您的澄清。 @Nitram 有一个有趣的解决方案,它不会阻止访问者彼此,仅来自 reload 方法。
    【解决方案5】:

    而不是在reloadAllCaches() 中的CacheHelper 类对象(CacheHelper.class) 上应用锁,您可以在此方法中的某些代码上应用此锁,因为我看到的所有方法都是static,如果您将它们全部设置为synchronized 那么如果任何线程正在访问任何方法,所有线程都会被阻塞。

    【讨论】:

    • 这正是我不想做的——我只想在重新加载发生时阻塞所有线程。 (reloadAllCaches())
    猜你喜欢
    • 2019-07-06
    • 2013-08-19
    • 1970-01-01
    • 2010-10-01
    • 1970-01-01
    • 2011-02-03
    • 1970-01-01
    • 2023-03-21
    相关资源
    最近更新 更多