由于我没有发现涉及此主题的任何问题,我认为我将分享我的解决方案以下方案。答案可能很明显,但我走了很长的路才找出答案。 :)我非常感谢对问题和答案以及其他解决方案的反馈。
情境:
假设您有一个多线程程序,并希望程序中的某些功能具有数据库连接(或其他一些共享对象),而程序的其他部分根本不需要它。但是,应该只有一个与数据库的连接。
同时,您希望检测数据库连接丢失并尝试动态重新连接。
为了解决这个问题,你实现了一个延迟加载模式“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;
}
}
问题:
几个线程可能会在几乎同时尝试获取数据库连接。当某些类保持对它们的引用时,甚至可能存在多个连接共存。
答案 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的性能要好得多。