zoukankan      html  css  js  c++  java
  • MyBatis温故而知新-底层运行原理

    准备工作

    public class MainClass {
      public static void main(String[] args) throws Exception {
        String resources = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resources);
    
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
    
        Student student = sqlSession.selectOne("org.apache.ibatis.dao.StudentMapper.getStudent",1);
        System.out.println(student.toString());
        sqlSession.close();
      }
    }
    

    MyBatis是如何获取数据源的

    这是我们mybatis-config.xml中配置数据库的4个关键属性,也就是看看MyBatis是怎么来解析这个配置文件块的。

    <environments default="development">
       <environment id="development">
          <transactionManager type="JDBC"/>
          <dataSource type="POOLED">
            <property name="driver" value="${jdbc.driver}"/>
            <property name="url" value="${jdbc.url}"/>
            <property name="username" value="${jdbc.username}"/>
            <property name="password" value="${jdbc.password}"/>
          </dataSource>
        </environment>
     </environments>
    

    数据源获取,我们从上面的代码片段中开始分析,在SqlSessionFactoryBuilder().build(inputStream) 方法中入手,这里看到实例了XMLConfigBuilder类。

    SqlSessionFactoryBuilder

    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
          //parser.parse()方法返回Configuration对象,然后调用build(Configuration config)
          return build(parser.parse());
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
          ErrorContext.instance().reset();
          try {
            inputStream.close();
          } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
          }
        }
    }
    
    public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
    }
    

    XMLConfigBuilder#parse()

    public Configuration parse() {
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }
    
      private void parseConfiguration(XNode root) {
        try {
          propertiesElement(root.evalNode("properties"));
          Properties settings = settingsAsProperties(root.evalNode("settings"));
          loadCustomVfs(settings);
          loadCustomLogImpl(settings);
          typeAliasesElement(root.evalNode("typeAliases"));
          pluginElement(root.evalNode("plugins"));
          objectFactoryElement(root.evalNode("objectFactory"));
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          reflectorFactoryElement(root.evalNode("reflectorFactory"));
          settingsElement(settings);
          environmentsElement(root.evalNode("environments"));
          databaseIdProviderElement(root.evalNode("databaseIdProvider"));
          typeHandlerElement(root.evalNode("typeHandlers"));
          mapperElement(root.evalNode("mappers"));
        } catch (Exception e) {
          throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
      }
    

    看到parseConfiguration参数为XNode就知道这个方法里肯定是要解析xml节点了,可以debug查看下root参数的值,尝试debug后发现,其内容就是我们的配置文件mybatis-config.xml的文件内容。 接着我们在22行这里则看到了熟悉的environments节点,那就直接看下environmentsElement方法。

    <configuration>
        <properties resource="db.properties"/>    
        <settings>
            <setting name="logImpl" value="STDOUT_LOGGING"/>        
            <setting name="cacheEnabled" value="true"/>        
            <setting name="lazyLoadingEnabled" value="true"/>        
            <setting name="aggressiveLazyLoading" value="false"/>        
            <setting name="localCacheScope" value="SESSION"/>        
        </settings>
        <typeAliases>
            <typeAlias alias="Student" type="org.apache.ibatis.domain.Student"/>        
        </typeAliases>
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>            
                <dataSource type="POOLED">
                    <property name="driver" value="${jdbc.driver}"/>                
                    <property name="url" value="${jdbc.url}"/>                
                    <property name="username" value="${jdbc.username}"/>               
                    <property name="password" value="${jdbc.password}"/>               
                </dataSource>
            </environment>
        </environments>
        <mappers>
            <mapper resource="mapper/StudentMapper.xml"/>        
        </mappers>
    </configuration>
    
    

    XMLConfigBuilder#environmentsElement

    这个方法参数XNode内容则是配置文件里的部分。这里就看到了解析dataSource节点的地方,这里使用DataSourceFactory返回一个DataSource, 那么这里是怎么拿到DataSourceFactory的,又是怎么拿到DataSource的。

    private void environmentsElement(XNode context) throws Exception {
        if (context != null) {
          if (environment == null) {
            environment = context.getStringAttribute("default");
          }
          for (XNode child : context.getChildren()) {
            String id = child.getStringAttribute("id");
            if (isSpecifiedEnvironment(id)) {
              TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
              DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
              DataSource dataSource = dsFactory.getDataSource();
              Environment.Builder environmentBuilder = new Environment.Builder(id)
                  .transactionFactory(txFactory)
                  .dataSource(dataSource);
              configuration.setEnvironment(environmentBuilder.build());
              break;
            }
          }
        }
      }
    

    解析DataSourceFactory

    这里看到了getDeclaredConstructor().newInstance()反射实例化一个PoolDataSourceFactory。

    private DataSourceFactory dataSourceElement(XNode context) throws Exception {
        if (context != null) {
          String type = context.getStringAttribute("type");
          Properties props = context.getChildrenAsProperties();
          DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
          factory.setProperties(props);
          return factory;
        }
        throw new BuilderException("Environment declaration requires a DataSourceFactory.");
      }
    
    public class PooledDataSourceFactory extends UnpooledDataSourceFactory {
    
      public PooledDataSourceFactory() {
        this.dataSource = new PooledDataSource();
      }
    
    }
    

    解析DataSource

    拿到PooledDataSourceFactory后则调用dsFactory.getDataSource()拿到数据源, 这里重要的代码是最后一句configuration.setEnvironment,这里最后会把解析后的Environment对象给Configuration对象赋值, Configuration对象也是MyBatis框架中相当重量级的一个对象。

    MyBatis是如何获取SQL语句的

    解析SQL语句Code Chain

    MyBatis解析SQL语句.jpg
    文章开头我们是用sqlSession.selectOne()方法来获取SQL语句的,那么这个SQL语句是怎么解析并获取呢?上面我们分析获取数据源时提到XMLConfigBuilder.parseConfiguration方法时看到会调用mapperElement方法。

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            if ("package".equals(child.getName())) {
              String mapperPackage = child.getStringAttribute("name");
              configuration.addMappers(mapperPackage);
            } else {
              String resource = child.getStringAttribute("resource");
              String url = child.getStringAttribute("url");
              String mapperClass = child.getStringAttribute("class");
              if (resource != null && url == null && mapperClass == null) {
                ErrorContext.instance().resource(resource);
                try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
                  XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                  mapperParser.parse();
                }
              } else if (resource == null && url != null && mapperClass == null) {
                ErrorContext.instance().resource(url);
                try(InputStream inputStream = Resources.getUrlAsStream(url)){
                  XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                  mapperParser.parse();
                }
              } else if (resource == null && url == null && mapperClass != null) {
                Class<?> mapperInterface = Resources.classForName(mapperClass);
                configuration.addMapper(mapperInterface);
              } else {
                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
              }
            }
          }
        }
      }
    

    该方法会根据配置文件依次解析package, resource, url, class4种配置mapper的方式。

    <mappers>
        <mapper resource="mapper/StudentMapper.xml"/>
    </mappers>
    

    这里我们是用resource方式配置的,所以这个方法程序会在Line 12 的逻辑开始执行。

    XMLStatementBuilder

    解析SQL语句的关键逻辑都在这个类的parseStatementNode方法里,方法最后会调用MapperBuilderAssistant#.addMappedStatement(), 这个方法会很到MyBatis里很重要的一个类MappedStatement对象,这个方法最关键的一行代码是先生成MappedStatement对象,最后会把生成的MappedStatement对象放入Configuration对象的Map字典中。
    看到这个方法里的代码应该很直观,就是在解析XML中select标签的一些属性,比如useCache, flushCache等,这些标签可以查看MyBatis文档,真的很详细。

    public void parseStatementNode() {
        String id = context.getStringAttribute("id");
        String databaseId = context.getStringAttribute("databaseId");
    
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
          return;
        }
    
        String nodeName = context.getNode().getNodeName();
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
    
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        includeParser.applyIncludes(context.getNode());
    
        String parameterType = context.getStringAttribute("parameterType");
        Class<?> parameterTypeClass = resolveClass(parameterType);
    
        String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
    
        // Parse selectKey after includes and remove them.
        processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        if (configuration.hasKeyGenerator(keyStatementId)) {
          keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
          keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
              configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
              ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }
    
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        Integer fetchSize = context.getIntAttribute("fetchSize");
        Integer timeout = context.getIntAttribute("timeout");
        String parameterMap = context.getStringAttribute("parameterMap");
        String resultType = context.getStringAttribute("resultType");
        Class<?> resultTypeClass = resolveClass(resultType);
        String resultMap = context.getStringAttribute("resultMap");
        String resultSetType = context.getStringAttribute("resultSetType");
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
        if (resultSetTypeEnum == null) {
          resultSetTypeEnum = configuration.getDefaultResultSetType();
        }
        String keyProperty = context.getStringAttribute("keyProperty");
        String keyColumn = context.getStringAttribute("keyColumn");
        String resultSets = context.getStringAttribute("resultSets");
    
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered,
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }
    

    MapperBuilderAssistant

    public MappedStatement addMappedStatement(
          String id,
          SqlSource sqlSource,
          StatementType statementType,
          SqlCommandType sqlCommandType,
          Integer fetchSize,
          Integer timeout,
          String parameterMap,
          Class<?> parameterType,
          String resultMap,
          Class<?> resultType,
          ResultSetType resultSetType,
          boolean flushCache,
          boolean useCache,
          boolean resultOrdered,
          KeyGenerator keyGenerator,
          String keyProperty,
          String keyColumn,
          String databaseId,
          LanguageDriver lang,
          String resultSets) {
    
        if (unresolvedCacheRef) {
          throw new IncompleteElementException("Cache-ref not yet resolved");
        }
    
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
            .resource(resource)
            .fetchSize(fetchSize)
            .timeout(timeout)
            .statementType(statementType)
            .keyGenerator(keyGenerator)
            .keyProperty(keyProperty)
            .keyColumn(keyColumn)
            .databaseId(databaseId)
            .lang(lang)
            .resultOrdered(resultOrdered)
            .resultSets(resultSets)
            .resultMaps(getStatementResultMaps(resultMap, resultType, id))
            .resultSetType(resultSetType)
            .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
            .useCache(valueOrDefault(useCache, isSelect))
            .cache(currentCache);
    
        ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
        if (statementParameterMap != null) {
          statementBuilder.parameterMap(statementParameterMap);
        }
    
        MappedStatement statement = statementBuilder.build();
        configuration.addMappedStatement(statement);
        return statement;
    }
    

    MyBatis是如何操作数据库的

    执行器(Executor)

    执行器在MyBatis中是用来封装Statement执行JDBC操作,MyBatis共包含3种类型的执行器:SimpleExecutor, ReuseExecutor, BatchExecutor,默认值使用SimpleExecutor。

     @Override
     public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
          Configuration configuration = ms.getConfiguration();
          StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
          stmt = prepareStatement(handler, ms.getStatementLog());
          return handler.query(stmt, resultHandler);
        } finally {
          closeStatement(stmt);
        }
     }
    

    MySQL查询的元数据和实体建立映射关系

    那么我们的select语句查询出来的一条mysql元数据和java实体到底是怎样建立映射关系的? 我们先从PreparedStatementHandler#query方法着手分析。

     @Override
     public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        //这两行代码是不是很亲切了,在JDBC中我们就是这么干的。
        //1.执行SQL
        PreparedStatement ps = (PreparedStatement) statement;
        //2.处理结果集
        ps.execute();
        return resultSetHandler.handleResultSets(ps);
     }
    

    结果集映射

    DefaultResultSetHandler#handleResultSets

    @Override
     public List<Object> handleResultSets(Statement stmt) throws SQLException {
        ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
        //1.<select>标签的resultMap属性,用来装映射后的实体对象数据
        final List<Object> multipleResults = new ArrayList<>();
    
        int resultSetCount = 0;
        //2.获取第一个结果集数据
        ResultSetWrapper rsw = getFirstResultSet(stmt);
        //3.这里就是取出需要映射的ResultMap
        List<ResultMap> resultMaps = mappedStatement.getResultMaps();
        //4.这里就是需要映射的ResultMap的数量
        int resultMapCount = resultMaps.size();
        validateResultMapsCount(rsw, resultMapCount);
        //5.循环处理每个ResultMap
        while (rsw != null && resultMapCount > resultSetCount) {
          //6.循环取出需要映射的ResultMap(id,type)实体名称
          ResultMap resultMap = resultMaps.get(resultSetCount);
          //7.从rsw结果集参数中获取查询结果,再根据resultMap映射信息,将查询结果装到到multipleResults中
          handleResultSet(rsw, resultMap, multipleResults, null);
          rsw = getNextResultSet(stmt);
          cleanUpAfterHandlingResultSet();
          resultSetCount++;
        }
        
        //这里会获取select标签的resultSet属性
        String[] resultSets = mappedStatement.getResultSets();
        if (resultSets != null) {
          while (rsw != null && resultSetCount < resultSets.length) {
            ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
            if (parentMapping != null) {
              String nestedResultMapId = parentMapping.getNestedResultMapId();
              ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
              handleResultSet(rsw, resultMap, null, parentMapping);
            }
            rsw = getNextResultSet(stmt);
            cleanUpAfterHandlingResultSet();
            resultSetCount++;
          }
        }
    
        return collapseSingleResultList(multipleResults);
    }
    

    DefaultResultSetHandler#handleResultSet

    private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
        try {
          if (parentMapping != null) {
            handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
          } else {
            if (resultHandler == null) {
              //1.创建DefaultResultHandler 来处理结果集
              DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
              //2.这里就是处理映射结果集的最终方法
              handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
              multipleResults.add(defaultResultHandler.getResultList());
            } else {
              handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
            }
          }
        } finally {
          closeResultSet(rsw.getResultSet());
        }
      }
    

    DefaultResultSetHandler#handleRowValues

    public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
        if (resultMap.hasNestedResultMaps()) {
          ensureNoRowBounds();
          checkResultHandler();
          //嵌套结果映射
          handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
        } else {
          //简单结果映射
          handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
        }
      }
    

    ResultSetWrapper

    这里还有一个关键的类在上述方法调用链里没有提到就是ResultSetWrapper, 它的构造器里有3个集合分别是columnNames,jdbcTypes, classNames, 当你看到这3个数组时脑海里是不是浮现出MyBatis框架的意图呢,代替你管理JDBC,并从结果集建立到java对象的映射。

    public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
        super();
        this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
        this.resultSet = rs;
        final ResultSetMetaData metaData = rs.getMetaData();
        final int columnCount = metaData.getColumnCount();
        for (int i = 1; i <= columnCount; i++) {
          columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
          jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
          classNames.add(metaData.getColumnClassName(i));
        }
      }
    

    DefaultResultSetHandler#handleRowValuesForSimpleResultMap

    private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
          throws SQLException {
        DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
        //1.因为在创建ResultSetWrapper的时候 我们将结果集封装进去了 现在将结果集取出来
        ResultSet resultSet = rsw.getResultSet();
        //2.分页信息
        skipRows(resultSet, rowBounds);
        while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
          ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
          ///3.将查询结果封装到POJO中(这一行代码重点关注)
          Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
          storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
        }
      }
    

    DefaultResultSetHandler#getRowValue

    private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
        final ResultLoaderMap lazyLoader = new ResultLoaderMap();
        Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
        if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
          final MetaObject metaObject = configuration.newMetaObject(rowValue);
          boolean foundValues = this.useConstructorMappings;
          if (shouldApplyAutomaticMappings(resultMap, false)) {
            //封装结果集 将sql的结果与实体类封装对应起来
            foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
          }
          foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
          foundValues = lazyLoader.size() > 0 || foundValues;
          rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
        }
        return rowValue;
      }
    

    DefaultResultSetHandler#applyAutomaticMappings

    这个方法里会循环处理行列字段并映射实体属性。

    private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
        //1.封装你的返回结果集对象
        List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
        boolean foundValues = false;
        if (!autoMapping.isEmpty()) {
          //2.遍历 给实体类对象赋值
          for (UnMappedColumnAutoMapping mapping : autoMapping) {
            //3.根据实体属性 去sql中取值,拿到SQL的结果值和实体类的属性值,并封装在metaObject对象中。
            final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
            if (value != null) {
              foundValues = true;
            }
            if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
              metaObject.setValue(mapping.property, value);
            }
          }
        }
        //返回值为boolean值
        return foundValues;
      }
    

    这里autoMapping集合里能看到就是存放三个字段的元数据。OK,执行到这一步,外层的getRowValue的返回值就拿到了返回的实体对象。
    截屏2021-07-05 21.35.45.png

    博客地址:http://www.cnblogs.com/sword-successful/
    博客版权:本文以学习、研究和分享为主,欢迎转载,但必须在文章页面明显位置给出原文连接。
    如果文中有不妥或者错误的地方还望高手的你指出,以免误人子弟。如果觉得本文对你有所帮助不如【推荐】一下!如果你有更好的建议,不如留言一起讨论,共同进步!
    再次感谢您耐心的读完本篇文章。
  • 相关阅读:
    Java多线程_同步工具CountDownLatch
    Java多线程_Semaphore信号量
    Java多线程_ReentrantLock
    Java多线程_CAS算法和ABA问题
    Java多线程_Atomic
    Java多线程_生产者消费者模式2
    Java多线程_生产者消费者模式1
    Java多线程_阻塞队列
    Java多线程_wait/notify/notifyAll方法
    Java多线程同步_synchronized
  • 原文地址:https://www.cnblogs.com/sword-successful/p/14979230.html
Copyright © 2011-2022 走看看