转载

HikariPool源码(六)使用到的一些有用JAVA特性

HikariPool源码(六)使用到的一些有用JAVA特性
Java极客  |  作者  /  铿然一叶
这是 Java极客 的第 57 篇原创文章

1. AutoCloseable接口

1.1 接口定义

public interface AutoCloseable {
    void close() throws Exception;
}
复制代码

1.2 用法

实现了AutoCloseable接口的类,按照如下语法编写代码,在try块结束会自动调用close方法,而不需要显示调用,这样就不需要在finally块中显示调用close方法。

try (Connection connection = new Connection()) {
            // do something
        }
复制代码

也就是可以少写一个finally块代码。

1.3 代码示例

public class AutoCloseableDemo {

    public static void main(String[] args) {
        // 按照这个语法初始化连接,在使用完连接后(try块结束后)就会自动关闭
        try (Connection connection = new Connection(); Statement statement = connection.createStatement()) {
            // 模拟使用连接
            System.out.println("use connection.");
            // 模拟抛出异常,看看抛出异常后是否还会关闭数据库连接
            throw new Exception("use connection exception.");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 看看关闭连接和finally块的执行顺序
            System.out.println("enter finally block.");
        }
    }

    // 模拟数据库连接并实现AutoCloseable接口
    private static class Connection implements AutoCloseable {
        @Override
        // 注意,重载接口方法时,接口方法定义的异常也可以不抛出,这样在调用点就不需要捕捉异常
        public void close() throws Exception {
            System.out.println("close Connection be called.");
        }

        public Statement createStatement() {
            return new Statement();
        }
    }

    private static class Statement implements AutoCloseable {
        @Override
        // 注意,重载接口方法时,接口方法定义的异常也可以不抛出,这样在调用点就不需要捕捉异常
        public void close() throws Exception {
            System.out.println("close Statement be called.");
        }
    }
}
复制代码

输出:

use connection.
close Statement be called.
close Connection be called.
java.lang.Exception: use connection exception.
	at com.javageektour.hikaricp.demo.AutoCloseableTest.main(AutoCloseableTest.java:19)
enter finally block.
复制代码

可以看到执行顺序是:

  1. 关闭连接在异常处理前
  2. 异常处理在finally块前
  3. 关闭连接在finally块前

即关闭连接总是最先执行。

2. 通过反射来做类拷贝

当某个类的属性很多,需要拷贝时,人工一个个写拷贝方法很繁琐,此时可以通过反射写一个通用方法来做拷贝。

//HikariConfig.java
   public void copyStateTo(HikariConfig other)
   {
      // 遍历所有属性
      for (Field field : HikariConfig.class.getDeclaredFields()) {
         if (!Modifier.isFinal(field.getModifiers())) {
            field.setAccessible(true);
            try {
               // 拷贝属性值
               field.set(other, field.get(this));
            }
            catch (Exception e) {
               throw new RuntimeException("Failed to copy HikariConfig state: " + e.getMessage(), e);
            }
         }
      }

      other.sealed = false;
   }
复制代码

3. 高并发场景使用ThreadLocalRandom来获取随机数

高并发场景下ThreadLocalRandom获取随机数的性能比Math.Random()高。

3.1 例子

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;

public class ThreadLocalRandomDemo {
    private static final int MAX_TIMES = 10000000;       // 获取次数
    private static final int THREAD_SIZE = 100;          // 并发线程数
    private static final int GET_MODE_RANDOM = 0;
    private static final int GET_MODE_THREAD_RANDOM = 1;
    private static volatile int getTimes = 0;            // 已获取次数
    private static long startTime = 0;
    private static CountDownLatch countDown = new CountDownLatch(THREAD_SIZE);

    public static void main(String[] args) throws Exception {
        startTime = System.currentTimeMillis();
        // 可修改getMode参数来测试 GET_MODE_RANDOM or GET_MODE_THREAD_RANDOM
        int getMode = GET_MODE_THREAD_RANDOM;
        for (int i = 0; i < THREAD_SIZE; i++) {
            new Thread(new GetRandomWorker(getMode)).start();
        }
        countDown.await();

        long costTime = System.currentTimeMillis() - startTime;
        System.out.println((getMode == GET_MODE_RANDOM ? "Random" : "ThreadRandom") + " costTime: " + costTime);
    }

    private static class GetRandomWorker implements Runnable {
        private int getMode;
        public GetRandomWorker(int getMode) {
            this.getMode = getMode;
        }
        @Override
        public void run() {
            while(true) {
                if (getMode == GET_MODE_RANDOM) {
                    int i = (int) (Math.random() * 10);
                } else {
                    int i = ThreadLocalRandom.current().nextInt(10);
                }
                getTimes++;
                if (getTimes > MAX_TIMES) {
                    countDown.countDown();
                    break;
                }
            }
        }
    }
}
复制代码

测试结果:

Random costTime: 2303
ThreadRandom costTime: 989
复制代码

4. 调用栈应用

在HikariPool中有代码使用了调用栈信息,如下:

//TestElf.java
   public static HikariConfig newHikariConfig()
   {
      final StackTraceElement callerStackTrace = Thread.currentThread().getStackTrace()[2];

      String poolName = callerStackTrace.getMethodName();
      if ("setup".equals(poolName)) {
         poolName = callerStackTrace.getClassName();
      }
复制代码

调用栈信息使得我们可以知道A方法是被谁调用的,这样如果我们想监控是谁获取了数据库连接,而没有释放就变得可能。

4.1 例子

import java.util.concurrent.*;

public class StackTraceDemo {
    // 允许消费者持有数据库连接的最大时间,超过这个时间则认为连接泄漏。 (为方便测试,这里设置的值并不大)
    private static final int MAX_HOLD_TIME_SECONDS = 10;

    public static void main(String[] args) {
        // 模拟消费者正常使用连接
        Consumer consumerA = new Consumer(5);
        consumerA.exec();

        // 模拟消费者长时间不释放连接,监控程序将监控到是谁没有释放连接
        Consumer consumerB = new Consumer(MAX_HOLD_TIME_SECONDS + 100);
        consumerB.exec();
    }

    private static void quietSleep(long senonds) {
        try {
            Thread.sleep(senonds * 1000);
        } catch (InterruptedException ie) {
            Thread.currentThread().interrupt();
        }
    }

    // 模拟消费者
    private static class Consumer {
        private int execCostTimeSeconds;  // 模拟执行耗时
        public Consumer(int execCostTimeSeconds) {
            this.execCostTimeSeconds = execCostTimeSeconds;
        }

        public void exec() {
            Connection conn = null;
            try {
                System.out.println("Consumer start... " + this);
                conn = ConnectionFactory.getConnection();
                quietSleep(execCostTimeSeconds); // 模拟执行耗时操作,如果超过最大允许持有连接时间,则会被监控到
                System.out.println("Consumer end. " + this);
            } finally {
                conn.close();
            }
        }
    }

    // 连接工厂
    private static class ConnectionFactory {
        public static Connection getConnection() {
            // 获取调用栈信息,这样可以直到是谁使用了连接
            StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();

            // 通过一个延迟任务去执行连接泄漏监控任务
            ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, new DefatulyThreadFactory(), new ThreadPoolExecutor.DiscardPolicy());
            executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
            executor.setRemoveOnCancelPolicy(true);
            ScheduledFuture<?> scheduledFuture =  executor.schedule(new ConnectionLeakMonitor(stackTraceElements), MAX_HOLD_TIME_SECONDS, TimeUnit.SECONDS);
            // 入参的目的是当连接正常关闭时,可以终止监控任务
            return new Connection(scheduledFuture);
        }
    }

    // 线程工厂
    private static class DefatulyThreadFactory implements ThreadFactory {
        @Override
        public Thread newThread(Runnable r) {
            // 简单实现
            return new Thread(r);
        }
    }

    // 数据库连接泄漏监控者,如果超过最大持有时间未关闭连接,则认为是连接泄漏
    private static class ConnectionLeakMonitor implements Runnable {
        private StackTraceElement[] stackTraceElements;
        public ConnectionLeakMonitor(StackTraceElement[] stackTraceElements) {
            this.stackTraceElements = stackTraceElements;
        }

        @Override
        public void run() {
            if (stackTraceElements != null) {
                // 这里仅仅打印调用者堆栈信息,实际应用时可上报到监控平台中
                for (StackTraceElement stackTraceElement: stackTraceElements) {
                    System.out.println(stackTraceElement.toString());
                }
            }
        }
    }

    // 模拟数据库连接,实际应用中对应连接代理类
    private static class Connection {
        private ScheduledFuture<?> scheduledFuture;
        public Connection(ScheduledFuture<?> scheduledFuture) {
            this.scheduledFuture = scheduledFuture;
        }

        public void close() {
            System.out.println("connection be closed.");
            // 如果连接正常关闭,则取消监控任务
            scheduledFuture.cancel(false);
        }
    }
}
复制代码

输出:

Consumer start... com.javageektour.hikaricp.demo.StackTraceDemo$Consumer@28d93b30
Consumer end. com.javageektour.hikaricp.demo.StackTraceDemo$Consumer@28d93b30
connection be closed.
Consumer start... com.javageektour.hikaricp.demo.StackTraceDemo$Consumer@14ae5a5
java.lang.Thread.getStackTrace(Thread.java:1552)
com.javageektour.hikaricp.demo.StackTraceDemo$ConnectionFactory.getConnection(StackTraceDemo.java:53)
com.javageektour.hikaricp.demo.StackTraceDemo$Consumer.exec(StackTraceDemo.java:42)
com.javageektour.hikaricp.demo.StackTraceDemo.main(StackTraceDemo.java:22)
复制代码

根据输出内容可以看到,超出最大持有连接时间的消费者没有释放连接可以被监控到,具体的就是:

com.javageektour.hikaricp.demo.StackTraceDemo$Consumer.exec(StackTraceDemo.java:42)
复制代码

找到具体的调用点之后,就可以分析优化了。

5. JAVA动态代理用法-避免实现全部接口

有的时候我们要实现或者MOCK一个接口,但是又不需要实现他的所有方法(因为方法太多,都实现没有必要),那么就可以通过动态代理来处理,HikariPool中的代码如下:

//ProxyConnection.java
   private static final class ClosedConnection
   {
      static final Connection CLOSED_CONNECTION = getClosedConnection();

      private static Connection getClosedConnection()
      {
         // 这里是个特殊写法,因为InvocationHandler只有一个接口,跟写一个内部内一样
         InvocationHandler handler = (proxy, method, args) -> {
            final String methodName = method.getName();
            if ("isClosed".equals(methodName)) {
               return Boolean.TRUE;
            }
            else if ("isValid".equals(methodName)) {
               return Boolean.FALSE;
            }
            if ("abort".equals(methodName)) {
               return Void.TYPE;
            }
            if ("close".equals(methodName)) {
               return Void.TYPE;
            }
            else if ("toString".equals(methodName)) {
               return ClosedConnection.class.getCanonicalName();
            }

            throw new SQLException("Connection is closed");
         };

         return (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), new Class[] { Connection.class }, handler);
      }
   }
复制代码

5.1 验证例子

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class InvocationHandlerDemo {

    public static void main(String[] args) {
        Dog dog = getDog();
        dog.run();
        dog.jump();  // 并没有实现该方法,啥也不会做,也不会报错
    }

    private static Dog getDog()
    {
        // 这里是个特殊写法,因为InvocationHandler只有一个接口,跟写一个内部内一样
        InvocationHandler handler = (proxy, method, args) -> {
            final String methodName = method.getName();
            // 只想实现必要的方法
            if ("run".equals(methodName)) {
                System.out.println("run be called.");
            }
            return null;
        };

        return (Dog) Proxy.newProxyInstance(Dog.class.getClassLoader(), new Class[] { Dog.class }, handler);
    }

    private static interface Dog {
        void run();
        void jump();
    }
}
复制代码

输出:

run be called.
复制代码

6. 总结

任何开源Java代码中都可能有一些我们不曾使用过的Java特性,了解这些特性有助于我们提高编码能力和输出更优秀的实现方案。因此除了看书学习,多读读源码吧。

end.

<--感谢三连击,左边点赞和关注。

HikariPool源码(六)使用到的一些有用JAVA特性
原文  https://juejin.im/post/5e9134cf51882573a033910c
正文到此结束
Loading...