2

由于我没有发现涉及该主题的问题,我想我会分享我对以下场景的解决方案。答案可能很明显,但我走了很长一段路才找到答案。:) 我很感激对问题和答案以及其他解决方案的反馈。

设想:

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

同时,您想检测数据库连接丢失并尝试即时重新连接。

为了解决这个问题,您实现了一个延迟加载模式“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;
  }
}

问题

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

4

3 回答 3

2

好吧,这是我的 2c:

首先,关于您用来进行同步的 Object 实例:如果您使用 _db 对象,从某种意义上说,您将无法获得您想要的东西。这里的想法是确保如果多个线程尝试“同时”创建一个 _db 实例(就 JDK 进程而言),一旦其中一个线程创建一个实例,其他线程应该立即意识到该实例存在并且不尝试创建另一个。现在,如果您在我们试图在线程之间同步的那个实例上同步代码块,即使该实例永远不会为空,您仍然会处于竞争状态,其中两个线程各自设法创建一个_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 实例,要么只创建一个,

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

于 2012-08-10T10:28:43.863 回答
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);
}
于 2012-08-10T10:09:37.837 回答
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;
}

此外,您需要确保没有其他方法访问私有字段_dbgetDBConnection()直接调用。那将不再同步。

您的类不应保留对连接的引用,因为这会阻止对死连接对象进行垃圾收集。但也不建议过于频繁地调用 getter,因为每个 get 都可能发出查询以检查连接有效性(取决于驱动程序)。如果每个方法在执行期间都保留一个引用,这可能是可以的(除非它执行很长时间)。

于 2012-08-10T10:01:42.673 回答