Using Multiple JDBC Drivers in an Application

For running queries, stored procedures, etc. in a relational/SQL database from a Java application, JDBC drivers are used. These JDBC drivers have been made available by various providers. For a SQL database, all of these providers need to provide an implementation of java.sql.Driver interface. Two such implementations are available as JTDS driver and Microsoft’s JDBC driver.

The application may need access to multiple databases, which could be hosting different versions of MS SQL Server. While one driver type can be a better choice for SQL Server 2012, other drivers could provide access to more features in SQL Server 2016. Each implementation has its own benefits and limitations. It may become apt to use different database drivers for different databases

I encountered one such application; it had JTDS and Microsoft’s JDBC drivers both in the same application. The application was configured to call multiple stored procedures from multiple databases using these two drivers. All was working fine until we made a @Component bean instead of an @Bean, which blocked the application startup. I tried various hypotheses and was able to find that the problem was with loading multiple JDBC drivers concurrently. The application had a multi-threaded set up to load stored procedures’ meta-data.

Although I had found the trigger for the problem, I still wasn’t sure about the root cause of it. After a few hours of code debugging, I found the sequence of events that was blocking application start-up — it was a deadlock keeping it from starting up. Let me explain.

When the following statement is executed, it triggers the creation of a database connection. Note that the jdbcCall compile statement is being submitted to an executor service with a threadpool.

executorService.execute(() -> simpleJdbcCall.compile());

This requires loading the jdbc driver class. If two such statements are executed for different database drivers, they trigger the creation of two database connections using two different database drivers. These two database driver classes are ‘com.microsoft.sqlserver.jdbc.SQLServerDriver’ and ‘net.sourceforge.jtds.jdbc.Driver’. Both implement java.sql.Driver interface.

Class-loading requires initializing static class members and running static blocks. Both of these driver classes have a static block, which must be executed in order to create instances of these drivers.

Static block in com.microsoft.sqlserver.jdbc.SQLServerDriver

static {
        ...
        try {
            DriverManager.registerDriver(new SQLServerDriver());
        } catch (SQLException var1) {
            var1.printStackTrace();
        }
    }

Static block in net.sourceforge.jtds.jdbc.Driver

static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            ;
        }
    }

As shown in the code snippet above, both of these classes register themselves to java.sql.DriverManager by calling DriverManager.registerDriver(..). Since the call for both database sproc compile statement is being executed by different threads, let me call JTDS static block calling thread — Thread-1 and MSSQL driver static block executing thread — Thread-2.

Calling DriverManager class’s static method registerDriver triggers DriverManager class loading operation. Now, DriverManager class also has a static block, which needs to be executed as part of class loading routine. Following is a snippet from the static block of this class:

static {
  ...
  AccessController.doPrivileged(new PrivilegedAction() {
            public Void run() {
                ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
  ...
}

DriverManager class uses Service Locator pattern to find the implementations of java.sql.Driver interface and then attempts to load those classes in its static block. Suppose Thread-1 attempts to calls DriverManager.registerDriver method first (calls are concurrent and not parallel, remember! 🙂 ). Then, Thread-1 will take care of DriverManager class loading and hence execute its static block before it reaches there it will acquire “initiaization lock” of this class (Thread-2 would wait until the DriverManager class is loaded — class loading op started by Thread-1). In DriverManager’s static block, it will find class names ‘com.microsoft.sqlserver.jdbc.SQLServerDriver’ and ‘net.sourceforge.jtds.jdbc.Driver’ in the following statement:

Once it has class names, it tries to load these classes and create an instance in the following statement.

The class loading operation for both of these is already in progress by Thread-1 and Thread-2 for JTDS and MSSQL driver respectively. So, when Thread-1 events sequence is like JTDS driver class loading -> running its static block -> DriverManager class loading -> executing DriverManager’s static block -> waiting for seeing MSSQL SQLServerDriver class loaded by Thread-2. And Thread-2 events sequence is like MSSQL SQLServerDriver class loading -> running its static block -> waiting for DriverManager class loading to finish by Thread-1.

This sequence of events would look like this:

Events sequence for both threads

From java.sql.DriverManager java documentation

Applications no longer need to explicitly load JDBC drivers using Class.forName(). Existing programs, which currently load JDBC drivers using Class.forName() will continue to work without modification.

When the method getConnection is called, the DriverManager will attempt to locate a suitable driver from amongst those loaded at initialization and those loaded explicitly using the same classloader as the current applet or application.

The problem is that DataSource class loads drivers using Class.forName() method and hence creates the deadlock.

org.apache.tomcat.jdbc.pool.DataSource

Code for demonstrating this deadlock behavior can be seen here.