美文网首页
《java in action》2的一些记录

《java in action》2的一些记录

作者: 以梦为马驾驾驾 | 来源:发表于2024-01-14 15:39 被阅读0次

    java迭代的趋势

    更好的并发

    流的引入,lambda,一切都是在为了适应现在的硬件架构: 多核,分布式网络架构。即期望给用户提供“轻松”,“安全”的并发编程接口。 流处理,几乎免费的并行,用户在高层次写逻辑代码,具体的执行由底层的lib来选择最合适的执行方式,比如把计算分布到不同的cpu核上。吸取函数式语言里高阶函数,增加了lambda表达式和函数Function,增强了java语言的抽象能力,表达能力,“把行为作为参数传递给函数”,即高阶函数的能力,又称为“行为参数化“(与此对应的还有”类型参数化“,即泛型),自此函数也成为了java语言的一等公民,但是还有方法和类仍然是二等公民,不过提供了一些设施,将二等公民转化为一等公民。
    基于Stream的并发,很少使用synchronized关键字,因为是不同的指导思想,stream的并发关注数据分块 而不是 协调访问 , 这样就像函数式编程在靠齐,“无共享可变数据” + “高阶函数” ,不使用synchronized,来协调共享数据的访问(互斥与并发),而是将数据拆分,不共享。

    image.png

    流的与集合的一个差异是: 流用于表达计算,集合的元素是计算完之后添加进来或者删除的,但是流是在固定的数据结构上,不能直接删除和增加,按需计算,定义流的时候计算并不发生,且只能遍历一次生成新的流(Java里是这样,scala里面的流可以多次使用)。
    使用流的好处,是能写出具有如下特点的代码:

    • 声明式:
    • 可复合:
      • 抽象度高
    • 免费并行:

    流的定义是: 从支持数据处理的源生成特定的元素序列。定义决定设计


    image.png

    如果自己设计一条流,也应该按照这样的思路来。

    所以,流的使用一般就是三件事:

    • 定义数据源
    • 定义中间操作链 形成一条流水线
    • 终端操作 执行流水线(按需计算)生成结果

    无状态流有状态流的区别
    有:流内部的算子有用户提供的lambda, 或者 方法引用(方法不是纯函数)
    无:没有内部状态,没有用户提供的lambda或者方法引用,没有内部可变状态。
    无状态流对并行友好,无缝切换到parallel,而有状态的流不行,比如求和,如果用外部变量进行累加,则parallel很容易出错,但是如果是利用reduce的分开累加,最终将每个累加结果再累加,就不会有并发问题。

    数值流存在的原因不是流的复杂性,而是 基本类型和对应的对象类型 之间的装箱和拆箱性能。

    流的生成: 万物皆可流

    万物都可以作为流的元素,也可以作为流的源头生成流元素。

    • 数值
    • 集合
    • 文件
    • 空对象
    • 函数 (比如无限流,Stream.iterate(0, n -> n + 2)偶数的无限流, Stream.iterate(new int[]{0,1}, t-> new int[]{t[1], t[0] + t[1]}) 斐波那契流, Stream.generate(Math::random) 随机流

    流收集:最终计算: 归约

    流水线是lazy的数据集的计算迭代器,最终的计算由 terminal action出发,通用的操作即collect,collect接受一个参数Collector来表示最终的流元素去往何处。
    Collectors工具类提供了许多直接的预定义的归约器,也提供了一些高阶方法生成归约器,而这一切都离不开背后的基本归约方法:java.util.stream.Collectors#reducing(U, java.util.function.Function<? super T,? extends U>, java.util.function.BinaryOperator<U>)
    U 归约的初始元素
    Function是将流内元素转化为待归约的元素
    BinaryOperator是待归约元素的计算

    归约计算的一个目的,收集,也可由归约完成。这就涉及到范畴论的理论来,以数组的收集举例:

    reducing(new List<>(),
                        (l, e) -> { l.add(e); return l;}, 
                      (l1, l2) -> { List l = new List(); 
                                        l.addAll(l1);
                                        l.addAll(l2); 
                                        return l;} }
    

    并且上面的归约属于“无状态”,可以轻松的用来做 并行。
    reducing这个方法之所以能够作为基本方法是它提供了两个基本能力:

    1. 元素到范畴的映射
    2. 范畴到范畴的映射

    代码实际实现是Collector类,另外Stream提供了一个collect方法,接受三个参数- supplier, accumulator 和 combiner,来自定义收集,其语义和Collector接口相应方法返回的函数完全相同。

    分组: Collector的连接 : groupingBy( , [ toList toSet]), collectAndThen,

    复杂的归约,可以通过groupingBy以及partitioningBy完成,并且他们之间可以通过多个Collector的连接完成
    如:

    Map<Type, List<String>>  = 
       collect(groupingBy(Dish::getType,
                                  mapping(Dish:getName, toList())));
    

    或者多级分组

    Map<Type, Map<CaloricLevel, List<Dish>>>  = 
    .collect( groupingBy(Dish::getType,
                      groupingBy( dish -> {
                                  if (dish.getCateGory <= 400 ) return CaloricLevel.DIET;
                                  ......
                                  })
                                  ));
    

    或者分组统计

    Map<Dish.Type, Long>  = .collect( groupintBy(Dish::getType, counting() ));
    

    或者分组统计后计算

    Map<Dish.Type, Optional<Dish>> = .collect(groupingBy(Dish::getType, maxBy(comparingInt(Dish::getCalories))));
    
    groupingBy(Dish::getType, collectAndThen( maxBy(comparingInt(Dish::getCalories)), Optional::get));
    
    

    分区 partitioningBy

    将流的元素,利用一个谓词做分类。分区可以理解为产生了两个流,并且由 partitioningBy实现的Map实现(特殊map)更高效,紧凑,因为只包含两个键: true 和 false

    分区和分组一样,可以多级分区,只要 连接 partitioningBy 就可以

    收集器性能对比

    实现自定义的Collector的目的是为了获得 比 ’使用内置工厂方法创建的收集器‘ 更好的性能,但是也可能让性能更拉垮,只能说:提供了设置,让有能力的同学可以自由发挥。
    JMH框架来测试。

    并行

    在Java8之前的并行流,需要主动的拆分数据,分配给不同的线程,然后还要进行协调和同步以避免可能发生的竞争条件,等到每个线程都完成后,再合并结果。并且java7引入了一个 “分支/合并”的框架,可以更稳定,更不容易出错的完成这一件事。
    不过,相对于java8的并行流还是落后了些,Java8的Stream接口让用户免费使用并行,一个指令就可以将顺序流转化为并行流(当然,前提是你的数据能够接受并行处理),控制数据切分的过程主要是: Spliterator (splitable , iterator)

    someStream.parallel() 方法并不会改变实际的流,而是设置了一个flag,表示 parallel之后的所有操作都是可以并行执行的,指需要再调用 sequential 就可以再变回顺序流。

    并行流内部使用的还是:ForJoinPool,默认的个数等于cpu的个数。

    java.util.concurrent.ForkJoinPool. common.parallelism 来修改线程池大小, 缺点是jvm级别的,会修改所有并行流的线程池大小,所以一般不修改,如下所示:
    System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
    

    并行流的思想是:数据分块,任务分块,然后利用Fork/Joinpool, 实现任务的计算。
    任务的分块由Spliterator实现。

    Fork/Join Pool框架

    思想分治,将任务拆分,最后合并。

    • 可拆分的任务抽象ForkJoinTask<T> <= RecursiveTask<T>, 或者 ForkJoinTask<Void> <= RecursiveAction
      • 任务执行: compute, 任务在compute内部再次拆分,不能拆的计算然后返回结果,将之前的拆分都合并。
    • 执行任务的框架: work stealing。由于任务拆分的子任务的计算负载不均衡,导致虽然每个线程的队列都只有两个任务,但是其中一个队列的任务特别简单,瞬间完成,而另一个队列的任务可能特别耗时,第二个任务就在等待第一个完成,不能并发。steal能够让空闲/或者负载低的线程偷取忙碌线程的双端队列的task来帮助执行,这也是为什么 任务 要分细的原因。

    流的拆分: Spliterator

    参考:

    interface Spliterator<T> {
      boolean tryAdvance(COnsumer< ? super T> action);
    
    Spliterator<T> trySplit();
    
    long estimateSize(); // 不是很准确
    
    int characteristics();
    
    }
    

    trySplit会递归调用,调用的时候,它会划分一些元素给它返回的第二个Spliterator,让他们两个并行处理。

    递归的拆分,拆分的过程会影响整个的执行效率,在自定义Spliterator的时候,注意效率,采用物理分割,比如将数据复制一份,显然没有逻辑分割保存原数据引用,修改数据范围来的效率高。另外,拆分的过程收到了 “特性”影响(characteristics方法声明)


    image.png

    如果当前的Spliterator实例X是可分割的,trySplit()方法会分割X产生一个全新的Spliterator实例Y,原来的X所包含的元素(范围)也会收缩,类似于X = [a,b,c,d] => X = [a,b], Y = [c,d];如果当前的Spliterator实例X是不可分割的,此方法会返回NULL),具体的分割算法由实现类决定

    todo: 对于无限流的并发,是怎么split的?

    集合与lambda带来的高效编程和改变

    集合工厂的增强,对于List,Set,map等,SomeCollection.of() 方法会生成 小规模的高性能的不可变集合类,即集合常量 ,虽然生成的数据结构不可变,但是效率更高,就类似于: Arrays.asList() 方法一样,生成的是视图。
    另外,对于大家常用集合类时候,用到的一些常用操作,都增加了相应语义的方法:

    • removeIf: list 和set提供,可以删除满足条件的元素,而不会触发ConcurrentModificationException,不然就要显式的使用迭代器和迭代器的删除方法;


      image.png

      正确的代码如下


      image.png
    • replaceAll: 替换集合的元素,而不用新生成集合。

    • remove: 删除map里面指定的 key和value 对

    • merge: 把两个map对元素进行合并的逻辑

    • computeIfXXXX,compute:高效的计算和填充

    ConcurrentHashMap

    • set视图:一个可以时刻同步map的set视图,xxx.keySet()方法,随时在set里看到map的变化。
    • 基础类型的归约:使用对应的基础类型方法会更快,少去了装箱拆箱的步骤 : reduceValuesToInt、reduce-KeysToLong

    lambda重构设计模式

    新的语言特性常常让现存的编程模式或设计黯然失色,对设计经验的总结陈称为“设计模式”。
    lambda之所以可以重构设计模式,是因为:将行为参数化,传递给高阶函数。而策略模式,模板模式的核心就是封装了不同的行为; 观察者模式,是在发生一些事件之后,触发一些行为的执行;责任链模式本质是对数据多次的操作,完全可以用函数式编程的组合模式完成;工厂模式,也只是某种特定的函数罢了:Function<someParameter, SomeClassInstance>;

    基于lambda的DSL todo

    精读此章节内容后,最好搭配:
    英文版本的: Domain-Specific Languages Martin的书,中文翻译贼拉垮
    DSLs in Action,有引进的话,先看中文看看
    还有 antlr4的两本,因为 martin的书是设计思想,指导原则,用的工具还是 antlr4 做解析。

    Optional,时间 : 非常简单,略

    默认方法和模块系统

    默认方法

    模块系统

    模块系统比较复杂: 搭配:Nicolai Parlog《 The Java Module System 》

    设计的高层次(软件架构层次)设计模式:
    关注点分离(separation of concern,SoC)和信息隐藏(information hiding)

    • 关注点分离 推崇的是将:单体的计算机程序分解为一个个相互独立的特性

    采用关注点分离,可以将软件的功能,作用等划分到名为“模块“的独立组成部分中去,所以,模块是具有“内聚”特质的一组代码,它与其他模块的代码很少耦合;通过模块组织类,可以清晰地描绘出应用程序类与类之间的可见性关系。
    Java的包机制并为从本质上支持模块化,它的粒度太粗。而模块化的粒度更细,且控制检查是编译期的。带来的好处就是:可以使得各项工作独立开展,减少组件之间的依赖,便于团队合作,有利于推动组建重用,系统整体的维护性更好。

    • 信息隐藏: 隐藏信息能够减少局部变更对其他部分程序的影响,从而避免“变更传递”。

    在低层次(代码层次) 的表现就是封装。虽然我们具有private,protected,public 关键字,但是就语言层面而言,Java 9 出现之前, 编译器无法依据语言结构判断某个类或者包仅供某个特定目标访问。

    Java9之前的Java内置模块化的问题:

    1. 有限的可见性控制:三个描述符只能控制包级别的类访问,无法描述 包之间的访问。比如:“希望一个包中的某个类或接口可以被另外一个包中的类或接口访问,那么只能将它声明为 public。这样一来,任何人都可以访问这些类和接口了”。这样就可能让代码被随意使用,给开发者演进自己的代码带来困难。
    2. 类路径的问题:Java编译器把所有的类都打入一个扁平的jar包中,并且把jar包放到class path上,jvm可以动态一句类的路径从中定位并且加载相关的类。然后,这样存在了几个严重的问题:
    • 无法通过路径指定版本。比如如果类路径上存在同一个库的两个版本,会发生什么。而大型项目中很常见,它的不同组件使用同一个库的不同版本:解决方法有
      1. 使用自定义ClassLoader来隔离: http://www.blogjava.net/landon/category/54860.html(值得注意的两点是:公有接口可以由系统类加载器加载,旧的类的实例和class很难被卸载)。比如: elasticsearch中的插件加载机制,实现了自定义的classLoader 。 甚至可以用ClassLoader来实现热部署:https://cloud.tencent.com/developer/article/1915650
      2. 或者是OSGI
      3. sofa-ark是动态热部署和类隔离框架,支付宝开源
    • 类路径不支持显式的依赖:


      image.png

    Java9提供了一个新的单位: 模块。通过 module声明,紧接着的是模块的名字和主体的内容,定义在特殊的文件:module-info.class, 下图可知,module的层级是高于package的。

    image.png
    image.png

    使用命令可以指导哪些目录和类文件会被打包进入生成的JAR文件中:
    javac module-info.java xxx/yyyy/zzz.java -d target
    jar cvfe xxxxx.jar xxx.yyy.zzz -C target
    然后运行执行命令:
    java --module-path xxxxx.jar --module moduleName.xxx.yyy.zzz

    image.png

    并发性

    无论是基于 Future 的异步 API 还是反应式异步 API,被调方法的概念体(conceptual body) 都在另一个线程中执行,调用方很可能已经退出了执行,不在调用异常处理器的作用域内。很明显,这种非常规行为触发的异常需要通过其他的动作来处理。

    CompletableFuture

    和Scala的Future很类似

    反应式编程

    直接看《反应式设计模式》就好了

    函数式

    个人了解较多,忽略。

    相关文章

      网友评论

          本文标题:《java in action》2的一些记录

          本文链接:https://www.haomeiwen.com/subject/ztlzndtx.html