• Druid源码解析(三):初始化方法中的一些小技巧 lcl


    一、双重检测锁

      在线程池初始化时,为了保证并发安全,同时为了保证初始化性能,使用了双重检测锁进行初始化,代码如下所示,

      第一步的判断是为了保证效率:毕竟初始化只是在连接池没有初始化或没有初始化完成时才会去调用初始化逻辑,在大多数场景,连接池已经初始化完成,所以如果不加第一个是否已完成初始化的判断,那么只要获取连接,就要加锁,会影响性能。

      第二步加锁是为了保证并发安全:如果当前没有初始化,多个线程都通过了第一个判断,那么就会都去初始化,从而造成线程安全,因此加锁可以避免多个线程同时初始化连接池。

      第三步判断也是为了保证并发安全:如果多个线程都通过了第一次判断

      使用 volatile 保证了变量 inited 的可见性,如果做了修改,保证其他线程可见,不会因为一个线程更新后,没有将本地变量更新到共享内存导致的线程安全问题。

    protected volatile boolean inited = false;
    
    if (inited) {
                return;
            }
    
            // 获取驱动
            // bug fixed for dead lock, for issue #2980
            DruidDriver.getInstance();
    
            // 加锁,防止重复初始化
            final ReentrantLock lock = this.lock;
            try {
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                throw new SQLException("interrupt", e);
            }
    
            boolean init = false;
            try {
                if (inited) {
                    return;
                }
        ......

    二、并发控制

      首先双重检测锁的部分上面已经分析,就不再做分析了。

      异步创建连接:

      使用了 ScheduledExecutorService 创建线程池来创建连接。然后在提交的 Task 任务 CreateConnectionTask 中,使用到了 ReentrantLock 加锁处理。

        private void submitCreateTask(boolean initTask) {
            createTaskCount++;
            CreateConnectionTask task = new CreateConnectionTask(initTask);
            if (createTasks == null) {
                createTasks = new long[8];
            }
    
            boolean putted = false;
            for (int i = 0; i < createTasks.length; ++i) {
                if (createTasks[i] == 0) {
                    createTasks[i] = task.taskId;
                    putted = true;
                    break;
                }
            }
            if (!putted) {
                long[] array = new long[createTasks.length * 3 / 2];
                System.arraycopy(createTasks, 0, array, 0, createTasks.length);
                array[createTasks.length] = task.taskId;
                createTasks = array;
            }
    
            this.createSchedulerFuture = createScheduler.submit(task);
        }

      使用了 CountDownLatch 修饰的变量 initedLatch,去报了并发创建的线程到这一步都执行完毕。

    三、SPI机制

      SPI 机制是很多开源组件常用的扩展机制,例如在 JDK、dubbo、SpringBoot、Spring Cloud 中都有广泛的使用,在 Druid 中也使用了 SPI 机制加载,加载所有配置的 Filter,可以达到可插拔的 Filter 配置。

        private void initFromSPIServiceLoader() {
            // 跳过使用 spi 加载 filter
            if (loadSpifilterSkip) {
                return;
            }
    
            if (autoFilters == null) {
                List<Filter> filters = new ArrayList<Filter>();
                ServiceLoader<Filter> autoFilterLoader = ServiceLoader.load(Filter.class);
    
                for (Filter filter : autoFilterLoader) {
                    AutoLoad autoLoad = filter.getClass().getAnnotation(AutoLoad.class);
                    if (autoLoad != null && autoLoad.value()) {
                        filters.add(filter);
                    }
                }
                autoFilters = filters;
            }
    
            for (Filter filter : autoFilters) {
                if (LOG.isInfoEnabled()) {
                    LOG.info("load filter from spi :" + filter.getClass().getName());
                }
                addFilter(filter);
            }
        }

      从上面的代码可以看到其通过 SPI 机制加载了 Filter 接口的实现类,

        public static <S> ServiceLoader<S> load(Class<S> service) {
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            return ServiceLoader.load(service, cl);
        }
    
        public static <S> ServiceLoader<S> load(Class<S> service,
                                                ClassLoader loader)
        {
            return new ServiceLoader<>(service, loader);
        }
    
        private ServiceLoader(Class<S> svc, ClassLoader cl) {
            service = Objects.requireNonNull(svc, "Service interface cannot be null");
            loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
            acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
            reload();
        }
    
        public void reload() {
            providers.clear();
            lookupIterator = new LazyIterator(service, loader);
        }

      可以看到,最终创建了一个懒加载的迭代器 LazyIterator,其实在使用的时候才会真正的加载 SPI 配置类。

      在迭代器 LazyIterator 中可以看到有一个 hasNextService 方法,该方法中就是真正处理 SPI 的加载。

    private static final String PREFIX = "META-INF/services/";
    
        private class LazyIterator
            implements Iterator<S>
        {
    
            Class<S> service;
            ClassLoader loader;
            Enumeration<URL> configs = null;
            Iterator<String> pending = null;
            String nextName = null;
    
            private LazyIterator(Class<S> service, ClassLoader loader) {
                this.service = service;
                this.loader = loader;
            }
    
            private boolean hasNextService() {
                if (nextName != null) {
                    return true;
                }
                if (configs == null) {
                    try {
                        String fullName = PREFIX + service.getName();
                        if (loader == null)
                            configs = ClassLoader.getSystemResources(fullName);
                        else
                            configs = loader.getResources(fullName);
                    } catch (IOException x) {
                        fail(service, "Error locating configuration files", x);
                    }
                }
                while ((pending == null) || !pending.hasNext()) {
                    if (!configs.hasMoreElements()) {
                        return false;
                    }
                    pending = parse(service, configs.nextElement());
                }
                nextName = pending.next();
                return true;
            }

      在 hasNextService 方法中,先获取了全路径名称,即 META-INF/services/ 目录下的 Filter 实现,然后通过类加载器加载,如果没有传入类加载器,则调用 ClassLoader.getSystemResources 根据全限定名生成 Enumeration<URL> 对象,如果传入的有指定的类加载器,则用指定的类加载器按照全限定名加载实例。

    四、类加载器

      在上面的代码中,看到了类加载的相关信息,如果没有传入类加载器,则使用系统的资源类加载器进行加载,在 getSystemResources() 方法中,首先调用 getSystemClassLoader() 方法获取一个类加载器,如果获取成功,则调用该类加载器的 getResources() 方法进行类加载,如果没有获取到,则使用 getBootstrapResources(name) 使用启动类加载器进行加载。

        public static Enumeration<URL> getSystemResources(String name)
            throws IOException
        {
            ClassLoader system = getSystemClassLoader();
            if (system == null) {
                return getBootstrapResources(name);
            }
            return system.getResources(name);
        }

      1、获取类加载器

      首先看获取系统类加载器的方法 getSystemClassLoader(),由于 ClassLoader 是JVM内部实现的,Java代码中是拿不到的,但是可以使用sun.misc.Launcher 来获取,可以看到下面代码使用了 sun.misc.Launcher.getLauncher() 的 getClassLoader() 方法获取了类加载器。

      如果存在安全管理器,并且调用者的类加载器不为空,并且调用者的类加载器与系统类加载器的祖先不同,则此方法使用 RuntimePermission("getClassLoader" 调用安全管理器的 checkPermission 方法) 验证对系统类加载器的访问权限。

        public static ClassLoader getSystemClassLoader() {
            initSystemClassLoader();
            if (scl == null) {
                return null;
            }
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                checkClassLoaderPermission(scl, Reflection.getCallerClass());
            }
            return scl;
        }
    
        private static synchronized void initSystemClassLoader() {
            if (!sclSet) {
                if (scl != null)
                    throw new IllegalStateException("recursive invocation");
                sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
                if (l != null) {
                    Throwable oops = null;
                    scl = l.getClassLoader();
                    try {
                        scl = AccessController.doPrivileged(
                            new SystemClassLoaderAction(scl));
                    } catch (PrivilegedActionException pae) {
                        oops = pae.getCause();
                        if (oops instanceof InvocationTargetException) {
                            oops = oops.getCause();
                        }
                    }
                    if (oops != null) {
                        if (oops instanceof Error) {
                            throw (Error) oops;
                        } else {
                            // wrap the exception
                            throw new Error(oops);
                        }
                    }
                }
                sclSet = true;
            }
        }

      2、查找资源

      上一步获取的类加载器不为空,则调用 getResources() 方法查找具有给定名称的所有资源,可以看到其定义了一个长度为2的 Enumeration<URL> 数组,这是资源的 URL 对象的枚举数组,如果当前类加载器存在父类加载器,则递归调用父类加载器的该方法,如果没有父类加载器,则说明当前类加载器是启动类加载器,则调用 getBootstrapResources(name)。

      另外调用了 findResources(name) 从当前类加载器中查找资源。

      实际上如果传入的有指定的类加载器,也是调用的 getResources 方法做的类加载,如果在第一步中获取的类加载器为空,调用的也是该步骤中的 getBootstrapResources(name) 方法让启动类加载器加载资源。

        public Enumeration<URL> getResources(String name) throws IOException {
            @SuppressWarnings("unchecked")
            Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
            if (parent != null) {
                tmp[0] = parent.getResources(name);
            } else {
                tmp[0] = getBootstrapResources(name);
            }
            tmp[1] = findResources(name);
    
            return new CompoundEnumeration<>(tmp);
        }

      3、从 VM 的内置类加载器中查找资源

      在上一步中调用了getBootstrapResources(name) 方法从 VM 内置的类加载器中查找资源,代码如下。实际上如果在第一步中获取的类加载器为空,调用的也是该步骤中的 getBootstrapResources(name) 方法从 VM 内置的类加载器中查找资源。

        private static Enumeration<URL> getBootstrapResources(String name)
            throws IOException
        {
            final Enumeration<Resource> e =
                getBootstrapClassPath().getResources(name);
            return new Enumeration<URL> () {
                public URL nextElement() {
                    return e.nextElement().getURL();
                }
                public boolean hasMoreElements() {
                    return e.hasMoreElements();
                }
            };
        }

    五、反射类加载

      在处理驱动相关的配置时,根据不同的驱动创建驱动类的实例。

      首先,其判断是否设置了驱动类属性 driverClass,如果没有,则根据 jdbcurl 使用简单工厂模式设置 driverClass 属性,然后根据 driverClass 属性判断是 MockDriver、BalancedClickhouseDriver还是其他,分别做类加载。

      如果是 MockDriver,其使用了静态变量来做类的初始化,保证了 MockDriver 是一个单例。

    public final static MockDriver         instance              = new MockDriver();

      如果是 BalancedClickhouseDriver ,直接调用的构造函数做的加载,是多例的

        public BalancedClickhouseDriver(final String url, Properties properties) {
            this.url = url;
            this.dataSource = new BalancedClickhouseDataSource(url, properties);
        }
    
        public BalancedClickhouseDataSource(String url, Properties properties) {
            this(splitUrl(url), new ClickHouseProperties(properties));
        }

      如果不是上述两种驱动,则使用 classLoader.loadClass 或者 Class.forName 加载类,然后使用 newInstance() 生成实例。

        public static Driver createDriver(ClassLoader classLoader, String driverClassName) throws SQLException {
            Class<?> clazz = null;
            if (classLoader != null) {
                try {
                    clazz = classLoader.loadClass(driverClassName);
                } catch (ClassNotFoundException e) {
                    // skip
                }
            }
    
            if (clazz == null) {
                try {
                    ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
                    if (contextLoader != null) {
                        clazz = contextLoader.loadClass(driverClassName);
                    }
                } catch (ClassNotFoundException e) {
                    // skip
                }
            }
    
            if (clazz == null) {
                try {
                    clazz = Class.forName(driverClassName);
                } catch (ClassNotFoundException e) {
                    throw new SQLException(e.getMessage(), e);
                }
            }
    
            try {
                return (Driver) clazz.newInstance();
            } catch (IllegalAccessException e) {
                throw new SQLException(e.getMessage(), e);
            } catch (InstantiationException e) {
                throw new SQLException(e.getMessage(), e);
            }
        }

      

  • 相关阅读:
    Vue学习(十七)修饰符
    组件学习(一)开发组件前必读
    vue学习(十六)“就地更新”策略
    nrm学习(一)
    帧动画的多种实现方式与性能对比
    JavaScript中的二进制对象
    生成式模型与判别式模型
    堡垒机
    vue生产环境nginx配置代理转发跨域
    Ceph nautilus 集群部署
  • 原文地址:https://www.cnblogs.com/liconglong/p/16259407.html
Copyright © 2020-2023  润新知