【问题标题】:Limiting concurrent access to a method限制对方法的并发访问
【发布时间】:2011-07-25 12:16:55
【问题描述】:

我在限制对方法的并发访问时遇到问题。我有一个方法MyService,可以在很多地方多次调用。这个方法必须返回一个String,它应该根据一些规则进行更新。为此,我有一个updatedString 课程。在获取String 之前,它确保String 已更新,如果没有,则更新它。许多线程可以同时读取String,但如果String 已过期,则只有一个线程可以同时更新它。

public final class updatedString {

private static final String UPstring;
private static final Object lock = new Object();

public static String getUpdatedString(){
    synchronized(lock){
        if(stringNeedRenewal()){
           renewString();
        }
    }
    return getString();
}

...

这很好用。如果我有 7 个线程获取字符串,它保证在必要时只有一个线程正在更新字符串。

我的问题是,拥有所有这些static 是个好主意吗?如果不是,为什么?快吗?有没有更好的方法来做到这一点?

我读过这样的帖子: What Cases Require Synchronized Method Access in Java? 这表明静态可变变量不是一个好主意,静态类也是。但我看不到代码中的任何死锁或更好的有效解决方案。只是某些线程必须等待String 更新(如果需要)或等待其他线程离开同步块(这会导致小延迟)。

如果方法不是static,那么我有一个问题,因为同步方法只对线程正在使用的当前实例起作用,所以这不起作用。同步的方法也不起作用,似乎锁是特定于实例的而不是特定于类的。 另一种解决方案可能是拥有一个避免创建多个实例的 Singleton,然后使用单个同步的非静态类,但我不太喜欢这种解决方案。

其他信息:

stringNeedRenewal() 虽然必须从数据库中读取,但并不太贵。 renewString() 反而很贵,需要从数据库上的几个表中读取才能最终得出答案。 String 需要任意更新,但这并不经常发生(从每小时一次到每周一次)。

@forsvarir 让我思考……我认为他/她是对的。 return getString(); 必须在同步方法中。乍一看,它似乎可以脱离它,因此线程将能够同时读取它,但是如果一个线程停止运行同时调用getString(),而其他线程部分执行renewString(),会发生什么?我们可能会遇到这种情况(假设一个处理器):

  1. 线程 1 开始 getString()。操作系统 开始将字节复制到内存中 被退回。
  2. 线程 1 在完成复制之前被操作系统停止。

  3. 线程 2 进入同步 阻止并启动renewString(), 把原来的String改成 记忆。

  4. 线程 1 重新获得控制权 并使用 a 完成 getString 损坏的String!!所以它复制了一个 旧字符串的一部分和另一个 从新的。

在同步块中进行读取会使一切变得非常缓慢,因为线程只能一个一个地访问它。

正如@Jeremy Heiler 所指出的,这是一个缓存的抽象问题。如果缓存是旧的,请更新它。如果没有,请使用它。最好更清楚地描绘这样的问题而不是单个String(或者想象有2个字符串而不是一个)。那么如果有人在读的同时有人在修改缓存会怎样呢?

【问题讨论】:

    标签: java concurrency static synchronized


    【解决方案1】:

    首先,您可以删除锁和同步块,然后简单地使用:

    public static synchronized String getUpdatedString(){
        if(stringNeedRenewal()){
           renewString();
        }
        return getString();
    }
    

    这在 UpdatedString.class 对象上同步。

    您可以做的另一件事是使用双重检查锁定来防止不必要的等待。将字符串声明为volatile 并且:

    public static String getUpdatedString(){
        if(stringNeedRenewal()){
            synchronized(lock) {
                if(stringNeedRenewal()){
                    renewString();
                }
            }
        }
        return getString();
    }
    

    然后,是否使用静态 - 似乎它应该是静态的,因为您想在没有任何特定实例的情况下调用它。

    【讨论】:

    • @Bozho,我自己是一个学习者.. 只是一个小小的确认.. 如果我们在类中有更多的静态方法,在另一个'锁上使用同步不是一个更好的主意' 对象而不是类本身?
    • 请记住,锁定类也会锁定其他类同步方法调用,即使它们作用于单独的数据
    • @peakit 是的,@PaoloVictor 回答了你的问题 :)
    • @peakit - 检查字符串是否需要更新。只有当它这样做时,你才锁定。但是因为可能有多个线程进入了那里,并且值可能在当前线程尝试这样做之前已经被更改 - 还有另一个检查,这次由锁保护。
    • 当然,如果stringNeedRenewal() 比锁定尝试更有效,则双重检查锁定在这里只是一种优化。 (这取决于它的使用频率与它真正需要的更新频率。)
    【解决方案2】:

    我建议查看ReentrantReadWriteLock。 (是否高效由您决定。)这样您可以同时进行许多读取操作。

    这是文档中的示例:

     class CachedData {
       Object data;
       volatile boolean cacheValid;
       ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
       void processCachedData() {
         rwl.readLock().lock();
         if (!cacheValid) {
            // Must release read lock before acquiring write lock
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            // Recheck state because another thread might have acquired
            //   write lock and changed state before we did.
            if (!cacheValid) {
              data = ...
              cacheValid = true;
            }
            // Downgrade by acquiring read lock before releasing write lock
            rwl.readLock().lock();
            rwl.writeLock().unlock(); // Unlock write, still hold read
         }
    
         use(data);
         rwl.readLock().unlock();
       }
     }
    

    【讨论】:

      【解决方案3】:

      这不是您所追求的,而且我不是 Java 专家,所以请谨慎对待:)

      也许您提供的代码示例是人为的,但如果不是,我不清楚该类的目的是什么。您只希望一个线程将字符串更新为新值。为什么?是为了节省精力(因为您宁愿在其他事情上使用处理器周期)?是为了保持一致性(一旦达到某个点,就必须更新字符串)?

      所需更新之间的周期是多久?

      查看您的代码...

      public final class updatedString {
      
      private static final String UPstring;
      private static final Object lock = new Object();
      
      public static String getUpdatedString(){
          synchronized(lock){
              // One thread is in this block at a time
              if(stringNeedRenewal()){
                 renewString();  // This updates the shared string?
              }
          }
          // At this point, you're calling out to a method.  I don't know what the
          // method does, I'm assuming it just returns UPstring, but at this point, 
          // you're no longer synchronized.  The string actually returned may or may  
          // not be the same one that was present when the thread went through the 
          // synchronized section hence the question, what is the purpose of the
          // synchronization...
          return getString();  // This returns the shared string?
      }
      

      正确的锁定/优化取决于您放置它们的原因、需要写入的可能性以及如 Paulo 所说的所涉及操作的成本。

      对于某些写入很少的情况,显然取决于 renewString 的作用,可能需要使用乐观的写入方法。每个线程检查是否需要刷新,继续在本地执行更新,然后仅在最后,将值分配给正在读取的字段(如果您遵循这种方法,您需要跟踪更新的年龄) .这对于阅读来说会更快,因为可以在同步部分之外执行“是否需要更新字符串”的检查。根据具体情况,可以使用各种其他方法...

      【讨论】:

      • 有人删除了你最后的cmets,你知道我怎样才能再次看到它们吗?
      • @Sarah Oneill:我也注意到了这一点。我以为是你删除了你的答案......我不知道如何让他们回来,不。本质上,我说杰里米是对的,在一般情况下,读/写锁可能是最可靠的方法。您不必担心在更新缓存时从缓存中读取任何内容,因为一旦取出写锁,唯一可以取出读锁的线程就是持有写锁的线程。
      • @Sarah Oneill:您似乎担心的另一件事是,如果您将“getString”放入同步部分,这会由于线程阻塞而导致延迟/并发问题。实际上,除非字符串很大,否则与同步块中的每个线程已经执行的更昂贵的数据库读取操作相比,我希望将返回的字符串副本添加到关键部分的影响可以忽略不计。
      【解决方案4】:

      只要您锁定是静态的,其他一切都不必是,一切都会像现在一样工作

      【讨论】:

      • 我建议您使用私有静态最终对象 lock = new Object();保持静止,因此您将锁定同一件事。其他一切都不必是静态的。
      • 如果锁是静态的,但其余的不是,那么您可以跨多个实例锁定对代码块的访问,但您为什么要这样做?假设我理解你,updatedString 的每个实例都有它自己的 UPString 副本。这不会否定对锁的需求(以及使 UPString 的内容在多实例场景中有些不可预测)吗?
      猜你喜欢
      • 1970-01-01
      • 2012-02-15
      • 2017-10-03
      • 2018-09-21
      • 2011-05-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多