热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

[源码分析]从FlatMap用法到Flink的内部实现

本文将从FlatMap概念和如何使用开始入手,深入到Flink是如何实现FlatMap。希望能让大家对这个概念有更深入的理解。[源码分析]从FlatMap用法到Flink的内部实现

本文将从FlatMap概念和如何使用开始入手,深入到Flink是如何实现FlatMap。希望能让大家对这个概念有更深入的理解。

[源码分析] 从FlatMap用法到Flink的内部实现

0x00 摘要

本文将从FlatMap概念和如何使用开始入手,深入到Flink是如何实现FlatMap。希望能让大家对这个概念有更深入的理解。

0x01 Map vs FlatMap

首先我们先从概念入手。

自从响应式编程慢慢壮大以来,这两个单词现在越来越被大家熟悉了。前端能见到它们的身影,后台也能见到;安卓里面有,iOS也有。很多兄弟刚遇到它们时候是懵圈的,搞不清楚之间的区别。下面我就给大家简单讲解下。

map

它把数组流中的每一个值,使用所提供的函数执行一遍,一一对应。得到与元素个数相同的数组流。然后返回这个新数据流。

flatMap

flat是扁平的意思。所以这个操作是:先映射(map),再拍扁(join)。

flatMap输入可能是多个子数组流。所以flatMap先针对 每个子数组流的每个元素进行映射操作。然后进行扁平化处理,最后汇集所有进行扁平化处理的结果集形成一个新的列表(扁平化简而言之就是去除所有的修饰)。

flatMap与map另外一个不一样的地方就是传入的函数在处理完后返回值必须是List。

实例

比如拿到一个文本文件之后,我们是按行读取,按行处理。如果要对每一行的单词数进行计数,那么应该选择Map方法,如果是统计词频,就应该选择flatMap方法。

如果还不清楚,可以看看下面这个例子:

梁山新进一批好马,准备给每个马军头领配置一批。于是得到函数以及头领名单如下:
函数 = ( 头领 => 头领 + 好马 )
五虎将 = List(关胜、林冲、秦明、呼延灼、董平 )
八骠骑 = List(花荣、徐宁、杨志、索超、张清、朱仝、史进、穆弘 )
// Map函数的例子
利用map函数,我们可以得到 五虎将马军
五虎将马军 = 五虎将.map( 头领 => 头领 + 好马 )
结果是 List( 关胜 + 马、林冲 + 马、秦明 + 马、呼延灼 + 马、董平 + 马 )
// flatMap函数的例子
但是为了得到统一的马军,则可以用flatMap:
马军头领 = List(五虎将,八骠骑)
马军 = 马军头领.flatMap( 头领 => 头领 + 好马 ) 
结果就是:List( 关胜 + 马、林冲 + 马、秦明 + 马、呼延灼 + 马、董平 + 马,花荣 + 马、徐宁 + 马、杨志 + 马、索超 + 马、张清 + 马、朱仝 + 马、史进 + 马、穆弘 + 马 )

现在大家应该清楚了吧。接下来看看几个FlatMap的实例。

Scala语言的实现

Scala本身对于List类型就有map和flatMap操作。举例如下:

val names = List("Alice","James","Apple")
val strings = names.map(x => x.toUpperCase)
println(strings)
// 输出 List(ALICE, JAMES, APPLE)
val chars = names.flatMap(x=> x.toUpperCase())
println(chars)
// 输出 List(A, L, I, C, E, J, A, M, E, S, A, P, P, L, E)

Flink的例子

以上是scala语言层面的实现。下面我们看看Flink框架是如何使用FlatMap的。

网上常见的一个Flink应用的例子:

//加载数据源
val source = env.fromElements("china is the best country","beijing is the capital of china")
//转化处理数据
val ds = source.flatMap(_.split(" ")).map((_,1)).groupBy(0).sum(1)

Flink源码中的例子

case class WordWithCount(word: String, count: Long)
val text = env.socketTextStream(host, port, '\n')
val windowCounts = text.flatMap { w => w.split("\\s") }
  .map { w => WordWithCount(w, 1) }
  .keyBy("word")
  .timeWindow(Time.seconds(5))
  .sum("count")
windowCounts.print()

0x02 自定义算子(in Flink)

上面提到的都是简单的使用,如果有复杂需求,在Flink中,我们可以通过继承FlatMapFunction和RichFlatMapFunction来自定义算子。

函数类FlatMapFunction

对于不涉及到状态的使用,可以直接继承 FlatMapFunction,其定义如下:

@Public
@FunctionalInterface
public interface FlatMapFunctionextends Function, Serializable {
void flatMap(T value, Collectorout) throws Exception;
}

如何自定义算子呢,这个可以直接看看Flink中的官方例子

// FlatMapFunction that tokenizes a String by whitespace characters and emits all String tokens.
public class Tokenizer implements FlatMapFunction{
  @Override
  public void flatMap(String value, Collectorout) {
    for (String token : value.split("\\W")) {
      out.collect(token);
    }
  }
}
// [...]
DataSettextLines = // [...]
DataSetwords = textLines.flatMap(new Tokenizer());

Rich函数类RichFlatMapFunction

对于涉及到状态的情况,用户可以使用继承 RichFlatMapFunction 类的方式来实现UDF。

RichFlatMapFunction属于Flink的Rich函数类。从名称上来看,这种函数类在普通的函数类上增加了Rich前缀,比如RichMapFunctionRichFlatMapFunctionRichReduceFunction等等。比起普通的函数类,Rich函数类增加了:

  • open()方法:Flink在算子调用前会执行这个方法,可以用来进行一些初始化工作。
  • close()方法:Flink在算子最后一次调用结束后执行这个方法,可以用来释放一些资源。
  • getRuntimeContext方法:获取运行时上下文。每个并行的算子子任务都有一个运行时上下文,上下文记录了这个算子运行过程中的一些信息,包括算子当前的并行度、算子子任务序号、广播数据、累加器、监控数据。最重要的是,我们可以从上下文里获取状态数据。

FlatMap对应的RichFlatMapFunction如下:

@Public
public abstract class RichFlatMapFunctionextends AbstractRichFunction implements FlatMapFunction{
@Override
public abstract void flatMap(IN value, Collectorout) throws Exception;
}

其基类 AbstractRichFunction 如下,可以看到主要是和运行时上下文建立了联系,并且有初始化和退出操作:

@Public
public abstract class AbstractRichFunction implements RichFunction, Serializable {
  
private transient RuntimeContext runtimeContext;
@Override
public void setRuntimeContext(RuntimeContext t) {
this.runtimeContext = t;
}
@Override
public RuntimeContext getRuntimeContext() {
return this.runtimeContext;
}
@Override
public IterationRuntimeContext getIterationRuntimeContext() {
    if (this.runtimeContext instanceof IterationRuntimeContext) {
return (IterationRuntimeContext) this.runtimeContext;

}
@Override
public void open(Configuration parameters) throws Exception {}
@Override
public void close() throws Exception {}
}

如何最好的使用? 当然还是官方文档和例子最靠谱。

因为涉及到状态,所以如果使用,你必须创建一个 StateDescriptor,才能得到对应的状态句柄。 这保存了状态名称(你可以创建多个状态,并且它们必须具有唯一的名称以便可以引用它们),状态所持有值的类型,并且可能包含用户指定的函数,例如ReduceFunction。 根据不同的状态类型,可以创建ValueStateDescriptorListStateDescriptorReducingStateDescriptorFoldingStateDescriptorMapStateDescriptor

状态通过 RuntimeContext 进行访问,因此只能在 rich functions 中使用。 但是我们也会看到一个例子。RichFunctionRuntimeContext 提供如下方法:

  • ValueState getState(ValueStateDescriptor)
  • ReducingState getReducingState(ReducingStateDescriptor)
  • ListState getListState(ListStateDescriptor)
  • AggregatingState getAggregatingState(AggregatingStateDescriptor)
  • FoldingState getFoldingState(FoldingStateDescriptor)
  • MapState getMapState(MapStateDescriptor)

下面是一个 FlatMapFunction 的例子,展示了如何将这些部分组合起来:

class CountWindowAverage extends RichFlatMapFunction[(Long, Long), (Long, Long)] {
  private var sum: ValueState[(Long, Long)] = _
  override def flatMap(input: (Long, Long), out: Collector[(Long, Long)]): Unit = {
    // access the state value
    val tmpCurrentSum = sum.value
    // If it hasn't been used before, it will be null
    val currentSum = if (tmpCurrentSum != null) {
      tmpCurrentSum
    } else {
      (0L, 0L)
    }
    // update the count
    val newSum = (currentSum._1 + 1, currentSum._2 + input._2)
    // update the state
    sum.update(newSum)
    // if the count reaches 2, emit the average and clear the state
    if (newSum._1 >= 2) {
      out.collect((input._1, newSum._2 / newSum._1))
      sum.clear()
    }
  }
  override def open(parameters: Configuration): Unit = {
    sum = getRuntimeContext.getState(
      new ValueStateDescriptor[(Long, Long)]("average", createTypeInformation[(Long, Long)])
    )
  }
}
object ExampleCountWindowAverage extends App {
  val env = StreamExecutionEnvironment.getExecutionEnvironment
  env.fromCollection(List(
    (1L, 3L),
    (1L, 5L),
    (1L, 7L),
    (1L, 4L),
    (1L, 2L)
  )).keyBy(_._1)
    .flatMap(new CountWindowAverage())
    .print()
  // the printed output will be (1,4) and (1,5)
  env.execute("ExampleManagedState")
}

这个例子实现了一个简单的计数窗口。 我们把元组的第一个元素当作 key(在示例中都 key 都是 “1”)。 该函数将出现的次数以及总和存储在 “ValueState” 中。 一旦出现次数达到 2,则将平均值发送到下游,并清除状态重新开始。 请注意,我们会为每个不同的 key(元组中第一个元素)保存一个单独的值。

0x03 从Flink源码入手看FlatMap实现

FlatMap从Flink编程模型角度讲属于一个算子,用来对数据流或者数据集进行转换。从框架角度说,FlatMap是怎么实现的呢?  或者说FlatMap是怎么从用户代码转换到Flink运行时呢 ?

1. DataSet

首先说说 DataSet相关这套系统中FlatMap的实现。

请注意,DataSteam对应的那套系统中,operator名字都是带着Stream的,比如StreamOperator。

DataSet

val ds = source.flatMap(_.split(" ")).map((_,1)).groupBy(0).sum(1) 这段代码调用的就是DataSet中的API。具体如下:

public abstract class DataSet{
  
publicFlatMapOperatorflatMap(FlatMapFunctionflatMapper) {
    
String callLocation = Utils.getCallLocationName();
    
TypeInformationresultType = TypeExtractor.getFlatMapReturnTypes(flatMapper, getType(), callLocation, true);
return new FlatMapOperator<>(this, resultType, clean(flatMapper), callLocation);
}
}

FlatMapOperator

可以看出,flatMap @ DataSet 主要就是生成了一个FlatMapOperator,这个可以理解为是逻辑算子。其定义如下:

public class FlatMapOperatorextends SingleInputUdfOperator {
protected final FlatMapFunctionfunction;
protected final String defaultName;
public FlatMapOperator(DataSetinput, TypeInformationresultType, FlatMapFunctionfunction, String defaultName) {
super(input, resultType);
this.function = function;
this.defaultName = defaultName;
}
@Override
protected FlatMapFunctiongetFunction() {
return function;
}
  // 这个translateToDataFlow就是生成计划(Plan)的关键代码
@Override
protected FlatMapOperatorBase translateToDataFlow(Operatorinput) {
String name = getName() != null ? getName() : "FlatMap at " + defaultName;
// create operator
FlatMapOperatorBase po = new FlatMapOperatorBase(function,
new UnaryOperatorInformation(getInputType(), getResultType()), name);
// set input
po.setInput(input);
// set parallelism
if (this.getParallelism() > 0) {
// use specified parallelism
po.setParallelism(this.getParallelism());
} else {
// if no parallelism has been specified, use parallelism of input operator to enable chaining
po.setParallelism(input.getParallelism());
}
return po;
}
}

FlatMapOperator的基类如下:

public abstract class SingleInputUdfOperator extends SingleInputOperatorimplements UdfOperator{
}
// Base class for operations that operates on a single input data set.
public abstract class SingleInputOperator extends Operator{
   private final DataSetinput;
}

生成计划

DataSet API所编写的批处理程序跟DataStream API所编写的流处理程序在生成作业图(JobGraph)之前的实现差别很大。流处理程序是生成流图(StreamGraph),而批处理程序是生成计划(Plan)并由优化器对其进行优化并生成优化后的计划(OptimizedPlan)。

计划(Plan)以数据流(dataflow)的形式来表示批处理程序,但它只是批处理程序最初的表示,在一个批处理程序生成作业图之前,计划还会被进行优化以产生更高效的方案。Plan不同于流图(StreamGraph),它以sink为入口,因为一个批处理程序可能存在若干个sink,所以Plan采用集合来存储它。另外Plan还封装了批处理作业的一些基本属性:jobId、jobName以及defaultParallelism等。

生成Plan的核心部件是算子翻译器(OperatorTranslation),createProgramPlan方法通过它来”翻译“出计划,核心代码如下

public class OperatorTranslation {
  
   // 接收每个需遍历的DataSink对象,然后将其转换成GenericDataSinkBase对象
   public Plan translateToPlan(List sinks, String jobName) {
       List planSinks = new ArrayList<>();
       //遍历sinks集合
       for (DataSink sink : sinks) {
            //将翻译生成的GenericDataSinkBase加入planSinks集合*,对每个sink进行”翻译“
            planSinks.add(translate(sink));
        }
       //以planSins集合构建Plan对象
       Plan p = new Plan(planSinks);
       p.setJobName(jobName);
       return p;
    }
private  org.apache.flink.api.common.operators.OperatortranslateSingleInputOperator(SingleInputOperator op) {
    //会调用到 FlatMapOperator 的 translateToDataFlow
org.apache.flink.api.common.operators.OperatordataFlowOp = typedOp.translateToDataFlow(input);    
  }
  
}

FlatMapOperatorBase就是生成的plan中的一员。

public class FlatMapOperatorBase extends SingleInputOperator{
@Override
protected ListexecuteOnCollections(Listinput, RuntimeContext ctx, ExecutionConfig executionConfig) throws Exception {
FlatMapFunctionfunction = userFunction.getUserCodeObject();

FunctionUtils.setFunctionRuntimeContext(function, ctx);
FunctionUtils.openFunction(function, parameters);
ArrayListresult = new ArrayList(input.size());
TypeSerializerinSerializer = getOperatorInfo().getInputType().createSerializer(executionConfig);
TypeSerializeroutSerializer = getOperatorInfo().getOutputType().createSerializer(executionConfig);
CopyingListCollectorresultCollector = new CopyingListCollector(result, outSerializer);
for (IN element : input) {
IN inCopy = inSerializer.copy(element);
function.flatMap(inCopy, resultCollector);
}
FunctionUtils.closeFunction(function);
return result;
}
}

而最后优化时候,则FlatMapOperatorBase会被优化成FlatMapNode。

public class GraphCreatingVisitor implements Visitor {
public boolean preVisit(Operator c) {
    else if (c instanceof FlatMapOperatorBase) {
n = new FlatMapNode((FlatMapOperatorBase) c);
}
  }
}

自此,FlatMap就被组合到 DataSet的 OptimizedPlan 中。下一步Flink会依据OptimizedPlan来生成 JobGraph。

作业图(JobGraph)是唯一被Flink的数据流引擎所识别的表述作业的数据结构,也正是这一共同的抽象体现了流处理和批处理在运行时的统一。至此就完成了从用户业务代码到Flink运行系统的转化。

在运行状态下,如果上游有数据流入,则FlatMap这个算子就会发挥作用。

2. DataStream

对于DataStream,则是另外一套体系结构。首先我们找一个使用DataStream的例子看看。

//获取数据: 从socket中获取
val textDataStream = env.socketTextStream("127.0.0.1", 8888, '\n')
val tupDataStream = textDataStream.flatMap(_.split(" ")).map(WordWithCount(_,1))
//groupby: 按照指定的字段聚合
val windowDstram = tupDataStream.keyBy("word").timeWindow(Time.seconds(5),Time.seconds(1))
windowDstram.sum("count").print()

上面例子中,flatMap 调用的是DataStream中的API,具体如下:

public class DataStream{
  
publicSingleOutputStreamOperatorflatMap(FlatMapFunctionflatMapper) {
    //clean函数用来移除FlatMapFunction类对象的外部类部分,这样就可以进行序列化
    //getType用来获取类对象的输出类型
TypeInformationoutType = TypeExtractor.getFlatMapReturnTypes(clean(flatMapper),
getType(), Utils.getCallLocationName(), true);
return flatMap(flatMapper, outType);
}
  
  // 构建了一个StreamFlatMap的Operator
publicSingleOutputStreamOperatorflatMap(FlatMapFunctionflatMapper, TypeInformationoutputType) {
return transform("Flat Map", outputType, new StreamFlatMap<>(clean(flatMapper)));
}  
  
  // 依次调用下去
@PublicEvolving
publicSingleOutputStreamOperatortransform(
String operatorName,
TypeInformationoutTypeInfo,
OneInputStreamOperatoroperator) {
return doTransform(operatorName, outTypeInfo, SimpleOperatorFactory.of(operator));
}
  
protectedSingleOutputStreamOperatordoTransform(
String operatorName,
TypeInformationoutTypeInfo,
StreamOperatorFactoryoperatorFactory) {
// read the output type of the input Transform to coax out errors about MissingTypeInfo
transformation.getOutputType();
    // 构建Transform对象,Transform对象中包含其上游Transform对象,这样上游下游就串成了一个Transform链。
OneInputTransformationresultTransform = new OneInputTransformation<>(
this.transformation,
operatorName,
operatorFactory,
outTypeInfo,
environment.getParallelism());
@SuppressWarnings({"unchecked", "rawtypes"})
SingleOutputStreamOperatorreturnStream = new SingleOutputStreamOperator(environment, resultTransform);
    // 将这Transform对象放入env的transform对象列表。
getExecutionEnvironment().addOperator(resultTransform);
    // 返回流
return returnStream;
}  
}

上面源码中的几个概念需要澄清。

Transformation:首先,FlatMap在FLink编程模型中是算子API,在DataStream中会生成一个Transformation,即逻辑算子。

逻辑算子Transformation最后会对应到物理算子Operator,这个概念对应的就是StreamOperator。

StreamOperator:DataStream 上的每一个 Transformation 都对应了一个 StreamOperator,StreamOperator是运行时的具体实现,会决定UDF(User-Defined Funtion)的调用方式。

processElement()方法也是UDF的逻辑被调用的地方,例如FlatMapFunction里的flatMap()方法。

public class StreamFlatMap
extends AbstractUdfStreamOperator
implements OneInputStreamOperator{
private transient TimestampedCollectorcollector;
public StreamFlatMap(FlatMapFunctionflatMapper) {
super(flatMapper);
chainingStrategy = ChainingStrategy.ALWAYS;
}
@Override
public void open() throws Exception {
super.open();
collector = new TimestampedCollector<>(output);
}
@Override
public void processElement(StreamRecordelement) throws Exception {
collector.setTimestamp(element);
    // 调用用户定义的FlatMap
userFunction.flatMap(element.getValue(), collector);
}
}

我们可以看到,StreamFlatMap继承了AbstractUdfStreamOperator,从而间接继承了StreamOperator。

public abstract class AbstractStreamOperator
implements StreamOperator, SetupableStreamOperator, Serializable {
}

StreamOperator是根接口。对于 Streaming 来说所有的算子都继承自 StreamOperator。继承了StreamOperator的扩展接口则有OneInputStreamOperator,TwoInputStreamOperator。实现了StreamOperator的抽象类有AbstractStreamOperator以及它的子类AbstractUdfStreamOperator。

从 API  到 逻辑算子  Transformation,再到 物理算子Operator,就生成了 StreamGraph。下一步Flink会依据StreamOperator来生成 JobGraph。

作业图(JobGraph)是唯一被Flink的数据流引擎所识别的表述作业的数据结构,也正是这一共同的抽象体现了流处理和批处理在运行时的统一。至此就完成了从用户业务代码到Flink运行系统的转化。

0x04 参考

Flink中richfunction的一点小作用

【浅显易懂】scala中map与flatMap的区别

Working with State

flink简单应用: scala编写wordcount

【Flink】Flink基础之实现WordCount程序(Java与Scala版本)

Flink进阶教程:以flatMap为例,如何进行算子自定义

Flink运行时之批处理程序生成计划


推荐阅读
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 如何用UE4制作2D游戏文档——计算篇
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了如何用UE4制作2D游戏文档——计算篇相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 这篇文章主要介绍了Python拼接字符串的七种方式,包括使用%、format()、join()、f-string等方法。每种方法都有其特点和限制,通过本文的介绍可以帮助读者更好地理解和运用字符串拼接的技巧。 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • 本文介绍了使用Spark实现低配版高斯朴素贝叶斯模型的原因和原理。随着数据量的增大,单机上运行高斯朴素贝叶斯模型会变得很慢,因此考虑使用Spark来加速运行。然而,Spark的MLlib并没有实现高斯朴素贝叶斯模型,因此需要自己动手实现。文章还介绍了朴素贝叶斯的原理和公式,并对具有多个特征和类别的模型进行了讨论。最后,作者总结了实现低配版高斯朴素贝叶斯模型的步骤。 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
  • java drools5_Java Drools5.1 规则流基础【示例】(中)
    五、规则文件及规则流EduInfoRule.drl:packagemyrules;importsample.Employ;ruleBachelorruleflow-group ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • HashMap的扩容知识详解
    本文详细介绍了HashMap的扩容知识,包括扩容的概述、扩容条件以及1.7版本中的扩容方法。通过学习本文,读者可以全面了解HashMap的扩容机制,提升对HashMap的理解和应用能力。 ... [详细]
  • 在本教程中,我们将看到如何使用FLASK制作第一个用于机器学习模型的RESTAPI。我们将从创建机器学习模型开始。然后,我们将看到使用Flask创建AP ... [详细]
author-avatar
牛牛发的
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有