Java 在延迟加载Getter上同步

Java 在延迟加载Getter上同步,java,lazy-loading,synchronized,Java,Lazy Loading,Synchronized,因为我没有发现任何关于这个主题的问题,所以我想我应该分享我对以下场景的解决方案。答案可能很明显,但我花了很长的时间才找到答案。:)我非常感谢对问题和答案以及其他解决方案的反馈 场景: 假设您有一个多线程程序,并且希望为程序中的某些功能建立数据库连接(或其他共享对象),而程序的其他部分根本不需要它。但是,应该只有一个到db的连接 同时,您希望检测数据库连接丢失并尝试动态重新连接 为了解决这个问题,您实现了一个延迟加载模式“getter”,它还可以在返回连接对象之前检查连接的有效性 您的代码可能如下

因为我没有发现任何关于这个主题的问题,所以我想我应该分享我对以下场景的解决方案。答案可能很明显,但我花了很长的时间才找到答案。:)我非常感谢对问题和答案以及其他解决方案的反馈

场景:

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

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

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


多个线程可能几乎同时尝试获取db连接。当某些类保留对多个连接的引用时,甚至可能多个连接共存。

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

我首先尝试在对象上进行如下同步:

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;
  }
}
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;
}
此外,您需要确保没有其他方法访问私有字段
\u db
或直接调用
getDBConnection()
。这将不再同步

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

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

在这种情况下,您需要一个池,因为您可以获得多个不同的实例。有许多数据库连接池可用,一些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);
}
private final Queue freeDBs=new ConcurrentLinkedQueue();
公共数据库acquireddb(){
DB=freeDBs.poll();
如果(db!=null&&db.isConnectionValid())
返回分贝;
如果(db!=null)
db.close();
返回getDBConnection();
}
公共无效释放(DB){
if(freeDBs.size()>=最大自由大小)
db.close();
其他的
freeDBs.add(db);
}
这是我的2c:

首先,关于你用来进行同步的对象实例:如果你使用_db对象,在某种意义上说,这是不好的,因为你得不到你想要的东西。这里的想法是确保如果多个线程试图“同时”创建一个_db实例(就JDK进程而言),一旦其中一个线程创建了一个实例,其他线程应该立即知道该实例存在,而不是尝试创建另一个实例。现在,如果你在我们试图在线程之间同步的实例上同步代码块,即使所述实例永远不会为null,你仍然会处于竞争条件下,其中两个线程各自创建一个_db的实例,并且由于代码块在该实例上是同步的,所有线程都不会被锁阻塞,因为实际上有两个单独的锁。 显然,最好同步整个方法。这相当于写作

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实例的方法的线程都将为同一个锁(主类的实例)而“斗争”,因此您可以确保,一旦一个线程获得该锁,其他线程将阻塞,直到该线程完成,然后,当轮到它们执行该方法时,if检查将阻止它们创建_db对象的第二个实例

现在,另一个问题是您是否真的希望跨多个线程拥有相同的_db实例。这个问题实际上归结到了天气,db是线程安全的,或者换句话说,它是无状态的吗?如果它是有状态的并且由多个线程共享,并且如果该状态不受多线程调用的保护,那么您将得到奇怪的行为甚至错误。例如:JDBC连接对象不是线程安全的,因为它包含事务之类的状态,如果多个线程同时访问同一个JDBC连接,事务状态可能会发生难以描述的变化。因此,在多线程环境中使用JDBC连接时,建议使用某种程度的(对象实例)隔离。您可以为每个线程创建一个新的JDBC连接实例,也可以只创建一个,但在每个线程中,它将作为ThreadLocal字段保留,这样每个线程就可以真正获得自己的实例,只有他自己才能更改/访问

另一个例子是HasmMap和ConcurrentHashMap。在这里,如果您对多个线程使用同一个HashMap,那么肯定会出现错误(例如,如果一个线程对Map条目进行了修改,而另一个线程尝试对其进行修改,那么您将得到一个并发修改异常),或者如果没有,则会出现错误