在Lazy Load Getter上同步

时间:2012-08-10 10:01:43

标签: java lazy-loading synchronized

由于我没有发现涉及此主题的任何问题,我认为我将分享我的解决方案以下方案。答案可能很明显,但我走了很长的路才找出答案。 :)我非常感谢对问题和答案以及其他解决方案的反馈。

情境:

假设您有一个多线程程序,并希望程序中的某些功能具有数据库连接(或其他一些共享对象),而程序的其他部分根本不需要它。但是,应该只有一个与数据库的连接。

同时,您希望检测数据库连接丢失并尝试动态重新连接。

为了解决这个问题,你实现了一个延迟加载模式“getter”,它还会在返回连接对象之前检查连接的有效性。

您的代码可能如下所示:

public class Main {
  private DB _db;

  public static void main(String[] args) {
    new Main().start();
  }

  private void start() {
    // Program code goes here
    // You create several threads, some of which may call getDB() whenever they need DB access
  }

  public DB getDB() {
    if (_db == null) {
      _db = getDBConnection();
    } else if (!_db.isConnectionValid()) {
      /*
       * DB connection is not valid anymore. Let's close it and
       * try to get a new connection.
       */
      _db.close();
      _db = getDBConnection();
    }

    return _db;
  }

  private DB getDBConnection() {
    DB db;

    // Obtain a new connection...
    ...

    return db;
  }
}

问题

几个线程可能会在几乎同时尝试获取数据库连接。当某些类保持对它们的引用时,甚至可能存在多个连接共存。

3 个答案:

答案 0 :(得分:2)

同步可用于避免同时创建多个连接。如果两个(或多个)线程几乎同时调用它,则其中一个阻塞(等待)直到另一个完成。这可以确保第二个线程获得刚刚由第一个线程创建的连接,而不是建立另一个连接。

我首先尝试在这个对象上进行同步:

public DB getDB() {
  synchronized (_db) {
    if (_db == null) {
      _db = getDBConnection();
    } else if (!_db.isConnectionValid()) {
      /*
       * DB connection is not valid anymore. Let's close it and
       * try to get a new connection.
       */
      _db.close();
      _db = getDBConnection();
    }
  }

  return _db;
}

这里的问题是,这不适用于延迟加载。您无法在null上同步(您获得NullPointerException),但在第一次调用getDB()时尚无对象。

解决方案是在整个方法上同步:

public synchronized DB getDB() {
  if (_db == null) {
    _db = getDBConnection();
  } else if (!_db.isConnectionValid()) {
    /*
     * DB connection is not valid anymore. Let's close it and
     * try to get a new connection.
     */
    _db.close();
    _db = getDBConnection();
  }


  return _db;
}

此外,您需要确保没有其他方法可以访问私有字段_db或直接致电getDBConnection()。这将不再同步。

您的类不应该保留对连接的引用,因为这样可以防止对死连接对象进行垃圾回收。但是,建议不要经常调用getter,因为每个get都可能会发出查询来检查连接的有效性(取决于驱动程序)。如果每个方法在执行期间保持引用(除非它执行多年),这可能没问题。

答案 1 :(得分:2)

  

几个线程可能会在几乎同时尝试获取数据库连接。当某些类保持对它们的引用时,甚至可能存在多个连接共存。

在这种情况下,您需要一个池,因为您可以获得多个不同的实例。有许多可用的DatabaseConnection池,有些JDBC驱动程序有自己的。我建议你使用JDBC驱动程序附带的那个或使用C3P0等作为数据库连接池。

更具体地说,您需要以另一个线程无法获得相同连接的方式进行连接(而不仅仅是获取连接)。一个简单的例子是使用队列。

private final Queue<DB> freeDBs = new ConcurrentLinkedQueue<>();

public DB acquireDB() {
    DB db = freeDBs.poll();
    if (db != null && db.isConnectionValid()) 
        return db;
    if (db != null)
        db.close();
    return getDBConnection();
}

public void release(DB db) {
    if (freeDBs.size() >= MAX_FREE_SIZE)
        db.close();
    else
        freeDBs.add(db);
}

答案 2 :(得分:2)

这是我的2c:

首先,关于用于进行同步的Object实例:如果你使用的_db对象在你无法得到你想要的东西的情况下是坏的。这里的想法是确保如果多个线程尝试“同时”创建一个_db实例(就JDK进程而言),一旦其中一个线程创建了一个实例,其他线程应该立即意识到该实例存在而不是尝试创建另一个实例。现在,如果你在那个实例上同步代码块我们试图在线程之间进行同步,即使所述实例永远不会为null,你仍然处于竞争状态,其中两个线程各自设法创建一个_db的实例,并且由于代码块在该实例上是同步的,因此锁定不会阻塞任何线程,因为确实存在2个单独的锁。 显然,同步整个方法更好。这相当于写作

public DB getDB() {
        synchronized (this) {
            if (_db == null) {
                _db = getDBConnection();
            } else if (!_db.isConnectionValid()) {
                /*
                 * DB connection is not valid anymore. Let's close it and
                 * try to get a new connection.
                 */
                _db.close();
                _db = getDBConnection();
            }
            return _db;
        }
    }

调用创建_db实例的方法的所有线程将在同一个锁(Main类的实例)上“战斗”,因此您可以确定一旦线程获得该锁定,其他线程将阻塞直到该线程完成,然后,当轮到他们执行该方法时,if检查将阻止他们创建_db对象的第二个实例。

现在,另一个问题是天气你真的希望在多个线程中拥有相同的_db实例。这个问题真的减少了天气_db是线程安全的,换句话说,它是无国籍的吗?如果它是有状态的并且由多个线程共享,并且如果该状态没有防止多线程调用,那么您将得到奇怪的行为甚至错误。例如:JDBC Connection对象不是线程安全的,因为它包含有关诸如事务之类的事物的状态,如果多个线程同时访问同一个JDBC Connection,则可以无法更改。因此,建议在多线程环境中使用JDBC连接时使用某种程度的(对象实例)隔离。您可以为每个线程创建一个新的JDBC Connection实例,或者只创建一个,但是在每个线程中它将保留为ThreadLocal字段,以便每个线程真正获得自己的实例,只有他自己才能更改/访问。

另一个例子是HasmMap和ConcurrentHashMap。在这里,如果您使用具有多个线程的相同HashMap,您肯定会收到错误(例如,如果一个线程迭代Map条目,而另一个线程尝试修改它,您将获得并发修改异常)或者如果没有错误,则至少会出现错误性能瓶颈,因为Map会从多个线程发送多个写入,因此会执行大量的重新哈希。另一方面,ConcurrentHashMap非常适合在多个线程之间共享一个实例。你不会得到并发修改异常,当多个线程同时写入时,Map的性能要好得多。