Shared posts

03 Aug 01:50

监控 Linux 系统的 7 个命令行工具

by changqi

深入

关于Linux最棒的一件事之一是你能深入操作系统,来探索它是如何工作的,并寻找机会来微调性能或诊断问题。这里有一些基本的命令行工具,让你能更简单地探索和操作Linux。大多数的这些命令是在你的Linux系统中已经内建的,但假如它们没有的话,就用谷歌搜索命令名和你的发行版名吧,你会找到哪些包需要安装(注意,一些命令是和其它命令捆绑起来打成一个包的,你所找的包可能写的是其它的名字)。如果你知道一些你所使用的其它工具,欢迎评论。

我们怎么开始


须知: 本文中的截图取自一台Debian Linux 8.1 (“Jessie”),其运行在OS X 10.10.3 (“Yosemite”)操作系统下的Oracle VirtualBox 4.3.28中的一台虚拟机里。想要建立你的Debian虚拟机,可以看看我的这篇教程——“如何在 VirtualBox VM 下安装 Debian”。

Top


作为Linux系统监控工具中比较易用的一个,top命令能带我们一览Linux中的几乎每一处。以下这张图是它的默认界面,但是按“z”键可以切换不同的显示颜色。其它热键和命令则有其它的功能,例如显示概要信息和内存信息(第四行第二个),根据各种不一样的条件排序、终止进程任务等等(你可以在这里找到完整的列表)。

htop


相比top,它的替代品Htop则更为精致。维基百科是这样描述的:“用户经常会部署htop以免Unix top不能提供关于系统进程的足够信息,比如说当你在尝试发现应用程序里的一个小的内存泄露问题,Htop一般也能作为一个系统监听器来使用。相比top,它提供了一个更方便的光标控制界面来向进程发送信号。” (想了解更多细节猛戳这里)

Vmstat


Vmstat是一款监控Linux系统性能数据的简易工具,这让它更合适使用在shell脚本中。使出你的正则表达式绝招,用vmstat和cron作业来做一些激动人心的事情吧。“后面的报告给出的是上一次系统重启之后的均值,另外一份报告给出的则是从前一个报告起间隔周期中的信息。其它的进程和内存报告是那个瞬态的情况”(猛戳这里获取更多信息)。

ps


ps命令展现的是正在运行中的进程列表。在这种情况下,我们用“-e”选项来显示每个进程,也就是所有正在运行的进程了(我把列表滚动到了前面,否则列名就看不到了)。这个命令有很多选项允许你去按需格式化输出。只要使用上述一点点的正则表达式技巧,你就能得到一个强大的工具了。猛戳这里获取更多信息。

Pstree


Pstree“以树状图显示正在运行中的进程。这个进程树是以某个 pid 为根节点的,如果pid被省略的话那树是以init为根节点的。如果指定用户名,那所有进程树都会以该用户所属的进程为父进程进行显示。”以树状图来帮你将进程之间的所属关系进行分类,这的确是个很有效的工具(戳这里)。

pmap


在调试过程中,理解一个应用程序如何使用内存是至关重要的,而pmap的作用就是当给出一个进程ID时显示出相关信息。上面的截图展示的是使用“-x”选项所产生的部分输出,你也可以用pmap的“-X”选项来获取更多的细节信息,但是前提是你要有个更宽的终端窗口。

iostat


Linux系统的一个至关重要的性能指标是处理器和存储的使用率,它也是iostat命令所报告的内容。如同ps命令一样,iostat有很多选项允许你选择你需要的输出格式,除此之外还可以在某一段时间范围内的重复采样几次。

监控 Linux 系统的 7 个命令行工具,首发于博客 - 伯乐在线

03 Aug 01:16

Apache Storm 官方文档 —— 基础概念

by 魏 勇

原文链接    译者:魏勇

Storm 系统中包含以下几个基本概念:

  1. 拓扑(Topologies)
  2. 流(Streams)
  3. 数据源(Spouts)
  4. 数据流处理组件(Bolts)
  5. 数据流分组(Stream groupings)
  6. 可靠性(Reliability)
  7. 任务(Tasks)
  8. 工作进程(Workers)

译者注:由于 Storm 的几个基础概念无论是直译还是意译均不够清晰,而且还会让习惯了 Storm 编程模型的读者感到困惑,因此后文在提及这些概念时大多还会以英文原文出现,希望大家能够谅解。


拓扑(Topologies)

Storm 的拓扑是对实时计算应用逻辑的封装,它的作用与 MapReduce 的任务(Job)很相似,区别在于 MapReduce 的一个 Job 在得到结果之后总会结束,而拓扑会一直在集群中运行,直到你手动去终止它。拓扑还可以理解成由一系列通过数据流(Stream Grouping)相互关联的 Spout 和 Bolt 组成的的拓扑结构。Spout 和 Bolt 称为拓扑的组件(Component)。我们会在后文中给出这些概念的解释。

相关资料

数据流(Streams)

数据流(Streams)是 Storm 中最核心的抽象概念。一个数据流指的是在分布式环境中并行创建、处理的一组元组(tuple)的无界序列。数据流可以由一种能够表述数据流中元组的域(fields)的模式来定义。在默认情况下,元组(tuple)包含有整型(Integer)数字、长整型(Long)数字、短整型(Short)数字、字节(Byte)、双精度浮点数(Double)、单精度浮点数(Float)、布尔值以及字节数组等基本类型对象。当然,你也可以通过定义可序列化的对象来实现自定义的元组类型。

在声明数据流的时候需要给数据流定义一个有效的 id。不过,由于在实际应用中使用最多的还是单一数据流的 Spout 与 Bolt,这种场景下不需要使用 id 来区分数据流,因此可以直接使用 OutputFieldsDeclarer来定义“无 id”的数据流。实际上,系统默认会给这种数据流定义一个名为“default”的 id。

相关资料

数据源(Spouts)

数据源(Spout)是拓扑中数据流的来源。一般 Spout 会从一个外部的数据源读取元组然后将他们发送到拓扑中。根据需求的不同,Spout 既可以定义为可靠的数据源,也可以定义为不可靠的数据源。一个可靠的 Spout 能够在它发送的元组处理失败时重新发送该元组,以确保所有的元组都能得到正确的处理;相对应的,不可靠的 Spout 就不会在元组发送之后对元组进行任何其他的处理。

一个 Spout 可以发送多个数据流。为了实现这个功能,可以先通过 OutputFieldsDeclarerdeclareStream 方法来声明定义不同的数据流,然后在发送数据时在 SpoutOutputCollectoremit 方法中将数据流 id 作为参数来实现数据发送的功能。

Spout 中的关键方法是 nextTuple。顾名思义,nextTuple 要么会向拓扑中发送一个新的元组,要么会在没有可发送的元组时直接返回。需要特别注意的是,由于 Storm 是在同一个线程中调用所有的 Spout 方法,nextTuple 不能被 Spout 的任何其他功能方法所阻塞,否则会直接导致数据流的中断(关于这一点,阿里的 JStorm 修改了 Spout 的模型,使用不同的线程来处理消息的发送,这种做法有利有弊,好处在于可以更加灵活地实现 Spout,坏处在于系统的调度模型更加复杂,如何取舍还是要看具体的需求场景吧——译者注)。

Spout 中另外两个关键方法是 ackfail,他们分别用于在 Storm 检测到一个发送过的元组已经被成功处理或处理失败后的进一步处理。注意,ackfail 方法仅仅对上述“可靠的” Spout 有效。

相关资料

数据流处理组件(Bolts)

拓扑中所有的数据处理均是由 Bolt 完成的。通过数据过滤(filtering)、函数处理(functions)、聚合(aggregations)、联结(joins)、数据库交互等功能,Bolt 几乎能够完成任何一种数据处理需求。

一个 Bolt 可以实现简单的数据流转换,而更复杂的数据流变换通常需要使用多个 Bolt 并通过多个步骤完成。例如,将一个微博数据流转换成一个趋势图像的数据流至少包含两个步骤:其中一个 Bolt 用于对每个图片的微博转发进行滚动计数,另一个或多个 Bolt 将数据流输出为“转发最多的图片”结果(相对于使用2个Bolt,如果使用3个 Bolt 你可以让这种转换具有更好的可扩展性)。

与 Spout 相同,Bolt 也可以输出多个数据流。为了实现这个功能,可以先通过 OutputFieldsDeclarerdeclareStream 方法来声明定义不同的数据流,然后在发送数据时在 OutputCollectoremit 方法中将数据流 id 作为参数来实现数据发送的功能。

在定义 Bolt 的输入数据流时,你需要从其他的 Storm 组件中订阅指定的数据流。如果你需要从其他所有的组件中订阅数据流,你就必须要在定义 Bolt 时分别注册每一个组件。对于声明为默认 id(即上文中提到的“default”——译者注)的数据流,InputDeclarer支持订阅此类数据流的语法糖。也就是说,如果需要订阅来自组件“1”的数据流,declarer.shuffleGrouping("1")declarer.shuffleGrouping("1", DEFAULT_STREAM_ID) 两种声明方式是等价的。

Bolt 的关键方法是 execute 方法。execute 方法负责接收一个元组作为输入,并且使用 OutputCollector 对象发送新的元组。如果有消息可靠性保障的需求,Bolt 必须为它所处理的每个元组调用 OutputCollectorack 方法,以便 Storm 能够了解元组是否处理完成(并且最终决定是否可以响应最初的 Spout 输出元组树)。一般情况下,对于每个输入元组,在处理之后可以根据需要选择不发送还是发送多个新元组,然后再响应(ack)输入元组。IBasicBolt 接口能够实现元组的自动应答。

在 Bolt 中启动新线程来进行异步处理是一种非常好的方式,因为 OutputCollector 是线程安全的对象,可以在任意时刻被调用(此处译者保留意见,由于 Storm 的并发设计和集群的弹性扩展机制,在 Bolt 中新建的线程可能存在一定的不可控风险——译者注)。

相关资料

数据流分组(Stream groupings)

为拓扑中的每个 Bolt 的确定输入数据流是定义一个拓扑的重要环节。数据流分组定义了在 Bolt 的不同任务(tasks)中划分数据流的方式。

在 Storm 中有八种内置的数据流分组方式(原文有误,现在已经已经有八种分组模型——译者注),而且你还可以通过CustomStreamGrouping 接口实现自定义的数据流分组模型。这八种分组分时分别为:

  1. 随机分组(Shuffle grouping):这种方式下元组会被尽可能随机地分配到 Bolt 的不同任务(tasks)中,使得每个任务所处理元组数量能够能够保持基本一致,以确保集群的负载均衡。
  2. 域分组(Fields grouping):这种方式下数据流根据定义的“域”来进行分组。例如,如果某个数据流是基于一个名为“user-id”的域进行分组的,那么所有包含相同的“user-id”的元组都会被分配到同一个任务中,这样就可以确保消息处理的一致性。
  3. 部分关键字分组(Partial Key grouping):这种方式与域分组很相似,根据定义的域来对数据流进行分组,不同的是,这种方式会考虑下游 Bolt 数据处理的均衡性问题,在输入数据源关键字不平衡时会有更好的性能1。感兴趣的读者可以参考这篇论文,其中详细解释了这种分组方式的工作原理以及它的优点。
  4. 完全分组(All grouping):这种方式下数据流会被同时发送到 Bolt 的所有任务中(也就是说同一个元组会被复制多份然后被所有的任务处理),使用这种分组方式要特别小心。
  5. 全局分组(Global grouping):这种方式下所有的数据流都会被发送到 Bolt 的同一个任务中,也就是 id 最小的那个任务。
  6. 非分组(None grouping):使用这种方式说明你不关心数据流如何分组。目前这种方式的结果与随机分组完全等效,不过未来 Storm 社区可能会考虑通过非分组方式来让 Bolt 和它所订阅的 Spout 或 Bolt 在同一个线程中执行。
  7. 直接分组(Direct grouping):这是一种特殊的分组方式。使用这种方式意味着元组的发送者可以指定下游的哪个任务可以接收这个元组。只有在数据流被声明为直接数据流时才能够使用直接分组方式。使用直接数据流发送元组需要使用 OutputCollector 的其中一个 emitDirect 方法。Bolt 可以通过 TopologyContext 来获取它的下游消费者的任务 id,也可以通过跟踪 OutputCollectoremit 方法(该方法会返回它所发送元组的目标任务的 id)的数据来获取任务 id。
  8. 本地或随机分组(Local or shuffle grouping):如果在源组件的 worker 进程里目标 Bolt 有一个或更多的任务线程,元组会被随机分配到那些同进程的任务中。换句话说,这与随机分组的方式具有相似的效果。

相关资料

  • TopologyBuilder:使用此类构造拓扑
  • InputDeclarer:在 TopologyBuilder 中调用 setBolt 方法时会返回这个对象的实例,通过该对象就可以定义 Bolt 的输入数据流以及数据流的分组方式
  • CoordinatedBolt:这个 Bolt 主要用于分布式 RPC 拓扑,其中大量使用了直接数据流与直接分组模型

可靠性(Reliability)

Storm 可以通过拓扑来确保每个发送的元组都能得到正确处理。通过跟踪由 Spout 发出的每个元组构成的元组树可以确定元组是否已经完成处理。每个拓扑都有一个“消息延时”参数,如果 Storm 在延时时间内没有检测到元组是否处理完成,就会将该元组标记为处理失败,并会在稍后重新发送该元组。

为了充分利用 Storm 的可靠性机制,你必须在元组树创建新结点的时候以及元组处理完成的时候通知 Storm。这个过程可以在 Bolt 发送元组时通过 OutputCollector 实现:在 emit 方法中实现元组的锚定(Anchoring),同时使用 ack 方法表明你已经完成了元组的处理。

关于可靠性保障的更多内容可以参考这篇文章:消息的可靠性处理

任务(Tasks)

在 Storm 集群中每个 Spout 和 Bolt 都由若干个任务(tasks)来执行。每个任务都与一个执行线程相对应。数据流分组可以决定如何由一组任务向另一组任务发送元组。你可以在 TopologyBuildersetSpout 方法和 setBolt 方法中设置 Spout/Bolt 的并行度。

工作进程(Workers)

拓扑是在一个或多个工作进程(worker processes)中运行的。每个工作进程都是一个实际的 JVM 进程,并且执行拓扑的一个子集。例如,如果拓扑的并行度定义为300,工作进程数定义为50,那么每个工作进程就会执行6个任务(进程内部的线程)。Storm 会在所有的 worker 中分散任务,以便实现集群的负载均衡。

相关资料


1 Partial Key grouping 方式目前仅支持开发版,尚未加入 Storm 的正式发行版,不过可以通过 CustomStreamGrouping间接实现该分组功能,具体的实现可以参考 PartialKeyGrouping 源代码

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Apache Storm 官方文档 —— 基础概念

28 Jul 10:42

文章: OpenJDK修订了Java内存模型

by Monica Beckwith

传统的Java内存模型涵盖了很多Java语言的语义保证。在这篇文章中,我们将重点介绍其中的几个语义,以更深入地了解他们。对于本文中描述的语义,我们还将尝试体会对现有Java内存模型更新的动机。本文中与JMM未来更新相关的讨论,将被称为JMM9。

1. Java内存模型

现有的Java内存模型,如JSR133(以下称为JMM-JSR133)中所定义的,为共享内存指定了一致性模型,并且有助于为开发者提供与JMM-JSR133表述一致的定义。JMM-JSR133规范的目标是确保线程通过内存交互语义的精确定义,以便允许优化并提供清晰的编程模型。JMM-JSR133旨在提供定义和语义,使多线程程序不仅是正确的,而且是高性能的,并对现有代码库的影响微乎其微。

考虑到这一点,我们来过一下JMM-JSR133中,过分指定或者指定不足的语义保证,同时重点放到社区广泛讨论的,关于我们如何在JMM9对其改进的话题上。

2. JMM9 - 顺序一致性 - 数据竞态自由问题

JMM-JSR133谈到了相对于操作的程序执行。结合有序操作的执行,描述了这些操作之间的关系。在这篇文章中,我们将扩展一些这样的顺序和关系,进而讨论一下什么是顺序一致的执行。让我们先从“程序顺序”开始。每个线程的程序顺序是一个总体顺序,表示通过该线程执行的所有操作的顺序。有时候,并不是所有操作都需要按序执行的。因此,有一些关系仅是部分有序的关系。例如,happens-before和synchronized-with两个就是部分有序关系。当一个操作发生在另一个操作之前;第一个操作不仅对第二个操作是可见的,而且其顺序在第二个操作之前。这两个操作之间的关系被称为是happens-before关系。有时,有些特殊操作需要指定顺序,他们被称为“同步操作”。volatile的读取和写入、monitor的锁定和解锁等都是同步操作的例子。一个同步操作会引起该操作的synchronized-with关系。synchronized-with关系是偏序的,这意味着并非所有两两的同步操作都包含这个关系之内。所有同步操作的总体顺序被称为“同步顺序”,每个执行都有一个同步顺序。

现在让我们谈谈顺序一致的执行。当所有的读写操作是总体有序执行时,被认为是顺序一致的(SC)。在SC执行中,读操作总是能看到最后一次写入特定变量的值。当SC执行表现为没有“数据竞态”时,该程序被认为是数据竞态自由(DRF)的。当程序中有两个不具备happens-before关系顺序的访问,他们访问的变量相同且至少其中之一是一个写访问时,就会发生数据竞态。数据竞态自由的顺序一致(SC for DRF)意味着DRF程序的行为是顺序一致的。但是严格支持顺序一致是以牺牲性能为代价的,大多数系统会对内存中的操作重新排序,以提高执行速度,并“隐藏”昂贵操作的延迟。同时,编译器也会对代码重新排序以优化执行。在保证严格顺序的一致性的场景中,不能进行这些内存操作重新排序或代码优化,因此性能会受到影响。JMM-JSR133已经使用底层编译器、高速缓冲存储器的相互作用和对程序不可见的JIT,合并了松散排序限制和任何重新排序。

注:昂贵操作是那些占用大量的CPU周期来完成、阻止执行流水线。

对于JMM9来说,性能是一个重要的考虑因素,而且任何一门编程语言的内存模型,理论上,都应该让开发者可以利用内存模型架构上弱有序(weakly-ordered)的优势。成功的实现和示例是放松严格的顺序,尤其是在弱有序的架构上。

注:弱序是指可以对读取和写入重新排序,并且需要显式的内存屏障遏制这种重新排序的架构。

3. JMM9 - 无中生有问题

JMM-JSR133另一个主要的语义是对“无中生有”(Out-of Thin Air,OoTA)值的禁止。happens-before模型有时会创建变量值并“无中生有”地读取,因为它不包含因果条件。有一点非常重要,由自身引起的关系不会采用数据和控制依赖的概念,我们将在下面正确同步代码的例子看到,非法写入是由写入本身引起的。

注:x和y初始化为0) -

Thread a

Thread b

r1 = x;

r2 = y;

if (r1 != 0)

if (r2 != 0)

y = 42;

x = 42;

这段码是happens-before一致的,但不是真正的顺序一致。例如,如果r1看到为x=42的写入,并且r2看到Y=42的写入,x和y的值都是42,这是一个数据竞态条件的结果。

r1 = x;

y = 42;

r2 = y;

x = 42;

这里,写入变量都在读取变量之前,读取将看到相关的写入,这将导致OoTA结果。

注:数据竞态可能产生推测的结果,这将最终把自己变成自我实现的预言。OoTA保证是关于秉承因果关系的规则。目前的想法是,因果关系可以避免写入推测。JMM9旨在寻找OoTA的原因和改进方法,以避免OoTA。

为了禁止OoTA值,一些写入需要等待他们的读取来避免数据竞态。因此,JMM-JSR133定义的OoTA禁止正式拒绝OoTA读取。这个正式的定义包括内存模型的“执行和因果条件”。基本上,当所有的程序操作提交时,一个良好的执行要满足因果条件。

注:在每次读取可以看到对同一变量的写入时,一个良好的执行遵循在一个线程内、happens-before和synchronization-order一致地执行。

正如你可能已经知道的,JMM-JSR133定义严格定义,不让OoTA值侵袭。JMM9旨在发现和纠正正式的定义,以便允许一些常见的优化。

4. JMM9 非Volatile变量上的Volatile操作

首先,关键字'Volatile'是什么意思呢?Java的volatile保证了线程间的交互,使得当一个线程写入一个volatile变量,不仅这次写入对其他线程可见,而且其他线程可以看到该线程所有的对volatile变量的写入。

那么对于non-volatile变量又发生了什么呢?非volatile变量没有volatile关键字保证交互的好处。因此,编译器可以使用non-volatile变量的缓存值而不是volatile保证,volatile变量将总是从内存中读取。happens-before模型可以用来绑定同步访问到非volatile变量上。

注:声明的任何字段为volatile并不意味着有锁参与。因此volatile比使用锁来同步更便宜。但是着重要注意的是,当方法中有多个volatile字段时,可能比使用锁更昂贵。

5. JMM9 - 读写原子性问题和字分裂问题

JMM-JSR133也有为共享内存并行算法提供的读取和写入的原子性保证(使用异常)。异常是为non-volatile的长整型和双精度浮点型的写入被视为两个独立的写入而定义的。因此,一个64位的值可以分别写入两个32位,一个线程正在执行读的时候,如果其中的一个写入仍未完成,该线程可能会看到只有一半正确的值,从而失去原子性。这是原子性保证依赖于底层硬件和内存子系统的一个例子。例如,底层汇编指令应该能够处理的操作数的大小,以便保证原子性,否则如果读或写操作必须被分成多于一个的操作,最终将破坏原子性(正如例子中的non-volatile的长整型和双精度浮点型的值)。类似地,如果因为实现产生一个以上的内存子系统事务,那么也将破坏原子性。

注:volatile的长整型和双精度浮点型字段和引用始终保证读取和写入的原子性

基于位的设计不是一个理想的解决方案,因为如果64位的异常被删除,那么在32位的体系结构中就会受损。如果在64位架构上行不通,如果期望原子性,那么不得不为长整型和双精度浮点型引入“volatile”,即使底层硬件可以保证原子操作。例如:volatile类型的字段不需要定义为双精度浮点型,因为基础架构,或者ISA、浮点单元会处理好64位宽字段的原子性需求。JMM9的目的是确定硬件提供原子性的保证。

JMM-JSR133写于十多年前;此后处理器位数发生了演变,64位已经成为主流的处理位数。当即强调的是,JMM-JSR133提出了针对64位读写的妥协,尽管64位的值可以由任何架构原子生成,一些架构仍然有必要请求锁。现在,这使得在这些架构上的64位读写操作非常昂贵。在32位x86架构上,如果不能找到一个合理的原子64位操作实现,则原子性将不会改变。

注:在语言设计中潜在一个问题,关键字“volatile”被赋予了过分的含义。运行时很难弄清楚,用户使用volatile是为了恢复原子性(因此它可以在64位平台被剥离出来),还是为了内存排序的目的。

当谈论访问原子性,读写操作的独立性是要着重考虑的。写入一个特定的字段不应该与读取或者写入其他字段有交互。JMM-JSR133的保证意味着,同步不应需要提供顺序一致性。因此,JMM-JSR133保证禁止被称为“字分裂”的问题。基本上,当更新一个操作数希望在比基础架构为所有操作数生成的更低的粒度上操作时,我们将遇到“字撕裂”问题。需要记住的重要一点是,字撕裂问题的原因之一是,64位长整型和双精度浮点型都没有给出原子性保证。字撕裂在JMM-JSR133中是禁止的,在JMM9中继续保持这种方式。

6. JMM9 - final字段问题

与其他字段相比,final字段是不同的。例如,一个线程用final字段x读取一个“完全初始化”的对象;在对象“完全初始化”后,能保证读取了final字段y的初始值,但不能保证“正常”的非final字段nonX。

注:“完全初始化”是指对象的构造函数完成。

鉴于上述情况,有一些简单的事情可以在JMM9中修复。例如:volatile类型字段,volatile字段在构造函数中初始化是不保证可见性的,即使对实例本身是可见的。因此,问题来了,是否final字段应该保证扩大到所有字段,包括初始化volatile字段?此外,如果一个完全初始化对象的“正常”非final字段的值不发生变化,我们是否可以将final字段保证到这个“正常”的字段。

参考文献

我从如下这些网站学到了很多,他们提供了大量的示例编码。本文是一篇介绍性的文章,以下文章更适合深入掌握Java内存模型。

  1. JSR 133: JavaTM Memory Model and Thread Specification Revision
  2. The Java Memory Model
  3. JAVA CONCURRENCY (&C)
  4. The jmm-dev Archives
  5. Threads and Locks
  6. Synchronization and the Java Memory Model
  7. All Accesses Are Atomic
  8. Java Memory Model Pragmatics (transcript)
  9. Memory Barriers: a Hardware View for Software Hackers

特别感谢

感谢Jeremy Manson,帮助我纠正了很多误解,并为我更清楚地解释了那些对于我来说很新的术语。还要感谢Aleksey Shipilev,帮助我减少了本文草稿版本中出现的概念的复杂性。Aleksey还指导我们去他的JMM,语用学文章更深层次的理解,澄清和例子。

关于作者

Monica Beckwith是Java性能顾问。她过去曾经与Oracle/Sun和AMD一起工作,对JVM服务器级系统进行优化。Monica被评为JavaOne 2013的明星演讲者,并且是First Garbage Collector(G1 GC)性能团队的领导者。她的Twitter是@mon_beck。

 

查看英文原文:The OpenJDK Revised Java Memory Model


感谢张龙对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者)。

23 Jul 13:15

Java新手问题集锦

Java是目前最流行的编程语言之一——它可以用来编写Windows程序或者是Web应用,移动应用,网络程序,消费电子产品,机顶盒设备,它无处不在。

有超过30亿的设备是运行在Java之上的。根据Oracle的统计数据,光是使用中的Java Card就有有50亿。

超过900万程序员选择使用Java进行开发,它是最受开发人员欢迎的语言,同时也是最流行的开发平台。

本文为那些准Java程序员们准备了一系列广为流传的Java最佳编程实践:

  1. 优先返回空集合而非null

如果程序要返回一个不包含任何值的集合,确保返回的是空集合而不是null。这能节省大量的"if else"检查。

public class getLocationName {
    return (null==cityName ? "": cityName);
}
  1. 谨慎操作字符串

如果两个字符串在for循环中使用+操作符进行拼接,那么每次循环都会产生一个新的字符串对象。这不仅浪费内存空间同时还会影响性能。类似的,如果初始化字符串对象,尽量不要使用构造方法,而应该直接初始化。比方说:

//Slower Instantiation
String bad = new String("Yet another string object");
     
//Faster Instantiation
String good = "Yet another string object"
  1. 避免无用对象

创建对象是Java中最昂贵的操作之一。因此最好在有需要的时候再进行对象的创建/初始化。如下:

import java.util.ArrayList;
import java.util.List;

public class Employees {

    private List Employees;

    public List getEmployees() {

        //initialize only when required
        if(null == Employees) {
            Employees = new ArrayList();
        }
        return Employees;
    }
}
  1. 数组与ArrayList之争

开发人员经常会发现很难在数组和ArrayList间做选择。它们二者互有优劣。如何选择应该视情况而定。

import java.util.ArrayList;

public class arrayVsArrayList {

    public static void main(String[] args) {
        int[] myArray = new int[6];
        myArray[7]= 10; // ArraysOutOfBoundException

        //Declaration of ArrayList. Add and Remove of elements is easy.
        ArrayList<Integer> myArrayList = new ArrayList<>();
        myArrayList.add(1);
        myArrayList.add(2);
        myArrayList.add(3);
        myArrayList.add(4);
        myArrayList.add(5);
        myArrayList.remove(0);
        
        for(int i = 0; i < myArrayList.size(); i++) {
        System.out.println("Element: " + myArrayList.get(i));
        }
        
        //Multi-dimensional Array 
        int[][][] multiArray = new int [3][3][3]; 
    }
}
  • 数组是定长的,而ArrayList是变长的。由于数组长度是固定的,因此在声明数组时就已经分配好内存了。而数组的操作则会更快一些。另一方面,如果我们不知道数据的大小,那么过多的数据便会导致ArrayOutOfBoundException,而少了又会浪费存储空间。
  • ArrayList在增删元素方面要比数组简单。
  • 数组可以是多维的,但ArrayList只能是一维的。
  1. try块的finally块没有被执行

看下下面这段代码:

public class shutDownHooksDemo {
    public static void main(String[] args) {
        for(int i=0;i<5;i++)
        {
            try {
                if(i==4) {
                    System.out.println("Inside Try Block.Exiting without executing Finally block.");
                    System.exit(0);
                }
            }
            finally {
                System.out.println("Inside Finally Block.");
            }
        }
    }
}

从代码来看,貌似finally块中的println语句应该会被执行5次。但当程序运行后,你会发现finally块只执行了4次。第5次迭代的时候会触发exit函数的调用,于是这第5次的finally便永远也触发不到了。原因便是——System.exit会挂起所有线程的执行,包括当前线程。即便是try语句后的finally块,只要是执行了exit,便也无力回天了。

在调用System.exit时,JVM会在关闭前执行两个结束任务:

首先,它会执行完所有通过Runtime.addShutdownHook注册进来的终止的钩子程序。这一点很关键,因为它会释放JVM外部的资源。

接下来的便是Finalizer了。可能是System.runFinalizersOnExit也可能是Runtime.runFinalizersOnExit。finalizer的使用已经被废弃有很长一段时间了。finalizer可以在存活对象上进行调用,即便是这些对象仍在被其它线程所使用。而这会导致不可预期的结果甚至是死锁。

public class shutDownHooksDemo {

    public static void main(String[] args) {
            for(int i=0;i<5;i++)
            {
                    final int final_i = i;
                    try {
                            Runtime.getRuntime().addShutdownHook(
                                            new Thread() {
                                            public void run() {
                                            if(final_i==4) {
                                            System.out.println("Inside Try Block.Exiting without executing Finally block.");
                                            System.exit(0);
                                            }
                                            }
                                            });
                    }
                    finally {
                            System.out.println("Inside Finally Block.");
                    }

            }
    }
}
  1. 判断奇数

看下这几行代码,看看它们是否能用来准确地判断一个数是奇数?

public boolean oddOrNot(int num) {
    return num % 2 == 1;
}

看似是对的,但是每执行四便会有一个错误的结果(用数据说话)。考虑到负奇数的情况,它除以2的结果就不会是1。因此,返回值是false,而这样是不对的。

代码可以修改成这样:

public boolean oddOrNot(int num) {
    return (num & 1) != 0;
}

这么写不光是负奇数的问题解决了,并且还是经过充分优化过的。因为算术运算和逻辑运行要比乘除运算更高效,计算的结果也会更快。

  1. 单引号与双引号的区别
public class Haha {
    public static void main(String args[]) {
    System.out.print("H" + "a");
    System.out.print('H' + 'a');
    }
}

看起来这段代码会返回"Haha",但实际返回的是Ha169。原因就是用了双引号的时候,字符会被当作字符串处理,而如果是单引号的话,字符值会通过一个叫做基础类型拓宽的操作来转换成整型值。然后再将值相加得到169。

  1. 一些防止内存泄露的小技巧

内存泄露会导致软件的性能降级。由于Java是自动管理内存的,因此开发人员并没有太多办法介入。不过还是有一些方法能够用来防止内存泄露的。

  • 查询完数据后立即释放数据库连接
  • 尽可能使用finally块
  • 释放静态变量中的实例
  1. 避免死锁

死锁出现的原因有很多。避免死锁不是一句话就能解决的。通常来说,当某个同步对象在等待另一个同步对象所拥有的资源上的锁时,便会产生死锁。

试着运行下下面的程序。它会告诉你什么是死锁。这个死锁是由于两个线程都在等待对方所拥有的资源,因此会产生死锁。它们会一直等待,没有谁会先放手。

public class DeadlockDemo {
   public static Object addLock = new Object();
   public static Object subLock = new Object();

   public static void main(String args[]) {

      MyAdditionThread add = new MyAdditionThread();
      MySubtractionThread sub = new MySubtractionThread();
      add.start();
      sub.start();
   }
private static class MyAdditionThread extends Thread {
      public void run() {
         synchronized (addLock) {
        int a = 10, b = 3;
        int c = a + b;
            System.out.println("Addition Thread: " + c);
            System.out.println("Holding First Lock...");
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("Addition Thread: Waiting for AddLock...");
            synchronized (subLock) {
               System.out.println("Threads: Holding Add and Sub Locks...");
            }
         }
      }
   }
   private static class MySubtractionThread extends Thread {
      public void run() {
         synchronized (subLock) {
        int a = 10, b = 3;
        int c = a - b;
            System.out.println("Subtraction Thread: " + c);
            System.out.println("Holding Second Lock...");
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("Subtraction  Thread: Waiting for SubLock...");
            synchronized (addLock) {
               System.out.println("Threads: Holding Add and Sub Locks...");
            }
         }
      }
   }
}

输出:

Addition Thread: 13
Subtraction Thread: 7
Holding First Lock...
Holding Second Lock...
Addition Thread: Waiting for AddLock...
Subtraction  Thread: Waiting for SubLock...

但如果调用的顺序变一下的话,死锁的问题就解决了。

public class DeadlockSolutionDemo {
   public static Object addLock = new Object();
   public static Object subLock = new Object();

   public static void main(String args[]) {

      MyAdditionThread add = new MyAdditionThread();
      MySubtractionThread sub = new MySubtractionThread();
      add.start();
      sub.start();
   }


private static class MyAdditionThread extends Thread {
      public void run() {
         synchronized (addLock) {
        int a = 10, b = 3;
        int c = a + b;
            System.out.println("Addition Thread: " + c);
            System.out.println("Holding First Lock...");
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("Addition Thread: Waiting for AddLock...");
            synchronized (subLock) {
               System.out.println("Threads: Holding Add and Sub Locks...");
            }
         }
      }
   }
   
   private static class MySubtractionThread extends Thread {
      public void run() {
         synchronized (addLock) {
        int a = 10, b = 3;
        int c = a - b;
            System.out.println("Subtraction Thread: " + c);
            System.out.println("Holding Second Lock...");
            try { Thread.sleep(10); }
            catch (InterruptedException e) {}
            System.out.println("Subtraction  Thread: Waiting for SubLock...");
            synchronized (subLock) {
               System.out.println("Threads: Holding Add and Sub Locks...");
            }
         }
      }
   }
}

输出:

Addition Thread: 13
Holding First Lock...
Addition Thread: Waiting for AddLock...
Threads: Holding Add and Sub Locks...
Subtraction Thread: 7
Holding Second Lock...
Subtraction  Thread: Waiting for SubLock...
Threads: Holding Add and Sub Locks...
  1. 替Java省点内存

某些Java程序是CPU密集型的,但它们会需要大量的内存。这类程序通常运行得很缓慢,因为它们对内存的需求很大。为了能提升这类应用的性能,可得给它们多留点内存。因此,假设我们有一台拥有10G内存的Tomcat服务器。在这台机器上,我们可以用如下的这条命令来分配内存:

export JAVA_OPTS="$JAVA_OPTS -Xms5000m -Xmx6000m -XX:PermSize=1024m -XX:MaxPermSize=2048m"
  • Xms = 最小内存分配
  • Xmx = 最大内存分配
  • XX:PermSize = JVM启动时的初始大小
  • XX:MaxPermSize = JVM启动后可分配的最大空间
  1. 如何计算Java中操作的耗时

在Java中进行操作计时有两个标准的方法:System.currentTimeMillis()和System.nanoTime()。问题就在于,什么情况下该用哪个。从本质上来讲,他们的作用都是一样的,但有以下几点不同:

  1. System.currentTimeMillis()的精度在千分之一秒到千分之15秒之间(取决于系统)而System.nanoTime()则能到纳秒级。
  2. System.currentTimeMillis读操作耗时在数个CPU时钟左右。而System.nanoTime()则需要上百个。
  3. System.currentTimeMillis对应的是绝对时间(1970年1 月1日所经历的毫秒数),而System.nanoTime()则不与任何时间点相关。

  4. Float还是double

数据类型 所用字节 有效位数
float 4 7
double 8 15

在对精度要求高的场景下,double类型相对float要更流行一些,理由如下:

大多数处理器在处理float和double上所需的时间都是差不多的。而计算时间一样的前提下,double类型却能提供更高的精度。

  1. 幂运算

Java是通过异或操作来进行幂运算的。Java对于幂运算有两种处理方式:

  • 乘积:
double square = double a * double a;                           // Optimized
double cube = double a * double a * double a;                   // Non-optimized
double cube = double a * double square;                       // Optimized
double quad = double a * double a * double a * double a;          // Non-optimized
double quad = double square * double square;                  // Optimized
  • pow方法:在无法使用乘积的情况下可以使用pow方法。
double cube = Math.pow(base, exponent);

不到万不得已不要使用Math.pow。比方说,当指数是小数的时候。因为Math.pow要比乘积慢300-600倍左右。

  1. 如何处理空指针异常

空指针异常是Java中很常见的异常。当你尝试调用一个null对象上的方法时便会抛出这个异常。比如:

int noOfStudents = school.listStudents().count;

在上述例子中,school为空或者listStudents()为空都可能会抛出了NullPointerException。因此最好检查下对象是否为空以避免类似情况。

private int getListOfStudents(File[] files) {
      if (files == null)
        throw new NullPointerException("File list cannot be null");
    }
  1. JSON编码

JSON是数据存储及传输的一种协议。与XML相比,它更易于使用。由于它非常轻量级以及自身的一些特性,现在JSON在网络上已经是越来越流行了。常见的数据结构都可以编码成JSON然后在各个网页间自由地传输。不过在开始编码前,你得先安装一个JSON解析器。在下面的例子中,我们将使用json.simple库来完成这项工作 (https://code.google.com/p/json-simple/)。

下面是编码成JSON串的一个简单的例子。

import org.json.simple.JSONObject;
import org.json.simple.JSONArray;

public class JsonEncodeDemo {
    
    public static void main(String[] args) {
        
        JSONObject obj = new JSONObject();
        obj.put("Novel Name", "Godaan");
        obj.put("Author", "Munshi Premchand");
 
        JSONArray novelDetails = new JSONArray();
        novelDetails.add("Language: Hindi");
        novelDetails.add("Year of Publication: 1936");
        novelDetails.add("Publisher: Lokmanya Press");
        
        obj.put("Novel Details", novelDetails);
        
        System.out.print(obj);
    }
}

输出:

{"Novel Name":"Godaan","Novel Details":["Language: Hindi","Year of Publication: 1936","Publisher: Lokmanya Press"],"Author":"Munshi Premchand"}
  1. JSON解析

开发人员要想解析JSON串,首先你得知道它的格式。下面例子有助于你来理解这一点:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Iterator;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;

public class JsonParseTest {

    private static final String filePath = "//home//user//Documents//jsonDemoFile.json";
    
    public static void main(String[] args) {

        try {
            // read the json file
            FileReader reader = new FileReader(filePath);
            JSONParser jsonParser = new JSONParser();
            JSONObject jsonObject = (JSONObject)jsonParser.parse(reader);
            
            // get a number from the JSON object
            Long id =  (Long) jsonObject.get("id");
            System.out.println("The id is: " + id);           

            // get a String from the JSON object
            String   type = (String) jsonObject.get("type");
            System.out.println("The type is: " + type);

            // get a String from the JSON object
            String   name = (String) jsonObject.get("name");
            System.out.println("The name is: " + name);

            // get a number from the JSON object
            Double ppu =  (Double) jsonObject.get("ppu");
            System.out.println("The PPU is: " + ppu);
            
            // get an array from the JSON object
            System.out.println("Batters:");
            JSONArray batterArray= (JSONArray) jsonObject.get("batters");
            Iterator i = batterArray.iterator();
            // take each value from the json array separately
            while (i.hasNext()) {
                JSONObject innerObj = (JSONObject) i.next();
                System.out.println("ID "+ innerObj.get("id") + 
                        " type " + innerObj.get("type"));
            }

            // get an array from the JSON object
            System.out.println("Topping:");
            JSONArray toppingArray= (JSONArray) jsonObject.get("topping");
            Iterator j = toppingArray.iterator();
            // take each value from the json array separately
            while (j.hasNext()) {
                JSONObject innerObj = (JSONObject) j.next();
                System.out.println("ID "+ innerObj.get("id") + 
                        " type " + innerObj.get("type"));
            }
            

        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        } catch (ParseException ex) {
            ex.printStackTrace();
        } catch (NullPointerException ex) {
            ex.printStackTrace();
        }

    }

}

jsonDemoFile.json

{
    "id": 0001,
    "type": "donut",
    "name": "Cake",
    "ppu": 0.55,
    "batters":
        [
            { "id": 1001, "type": "Regular" },
            { "id": 1002, "type": "Chocolate" },
            { "id": 1003, "type": "Blueberry" },
            { "id": 1004, "type": "Devil's Food" }
        ],
    "topping":
        [
            { "id": 5001, "type": "None" },
            { "id": 5002, "type": "Glazed" },
            { "id": 5005, "type": "Sugar" },
            { "id": 5007, "type": "Powdered Sugar" },
            { "id": 5006, "type": "Chocolate with Sprinkles" },
            { "id": 5003, "type": "Chocolate" },
            { "id": 5004, "type": "Maple" }
        ]
}
The id is: 1
The type is: donut
The name is: Cake
The PPU is: 0.55
Batters:
ID 1001 type Regular
ID 1002 type Chocolate
ID 1003 type Blueberry
ID 1004 type Devil's Food
Topping:
ID 5001 type None
ID 5002 type Glazed
ID 5005 type Sugar
ID 5007 type Powdered Sugar
ID 5006 type Chocolate with Sprinkles
ID 5003 type Chocolate
ID 5004 type Maple
  1. 简单字符串查找

Java提供了一个库函数叫做indexOf()。这个方法可以用在String对象上,它返回的是要查找的字符串所在的位置序号。如果查找不到则会返回-1。

  1. 列出目录下的文件

你可以用下面的代码来列出目录下的文件。这个程序会遍历某个目录下的所有子目录及文件,并存储到一个数组里,然后通过遍历数组来列出所有文件。

import java.io.*;

public class ListContents {
    public static void main(String[] args) {
        File file = new File("//home//user//Documents/");
        String[] files = file.list();

        System.out.println("Listing contents of " + file.getPath());
        for(int i=0 ; i < files.length ; i++)
        {
            System.out.println(files[i]);
        }
    }
}
  1. 一个简单的IO程序

Java提供了FileInputStream以及FileOutputStream类来进行文件的读写操作。FileInputStream的构造方法会接收输入文件的路径作为入参然后创建出一个文件的输入流。同样的,FileOutputStream的构造方法也会接收一个文件路径作为入参然后创建出文件的输出流。在处理完文件之后,一个很重要的操作就是要记得"close"掉这些流。

import java.io.*;

public class myIODemo {
    public static void main(String args[]) throws IOException {
        FileInputStream in = null;
        FileOutputStream out = null;
        
        try {
            in = new FileInputStream("//home//user//Documents//InputFile.txt");
            out = new FileOutputStream("//home//user//Documents//OutputFile.txt");
            
            int c;
            while((c = in.read()) != -1) {
                out.write(c);
            }
        } finally {
            if(in != null) {
                in.close();
            }
            if(out != null) {
                out.close();
            }
        }
    }
}
  1. 在Java中执行某个shell命令

Java提供了Runtime类来执行shell命令。由于这些是外部的命令,因此异常处理就显得异常重要。在下面的例子中,我们将通过一个简单的例子来演示一下。我们会在shell命令行中打开一个pdf文件。

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;

public class ShellCommandExec {

    public static void main(String[] args) {
        String gnomeOpenCommand = "gnome-open //home//user//Documents//MyDoc.pdf";

        try {
            Runtime rt = Runtime.getRuntime();
            Process processObj = rt.exec(gnomeOpenCommand);

            InputStream stdin = processObj.getErrorStream();
            InputStreamReader isr = new InputStreamReader(stdin);
            BufferedReader br = new BufferedReader(isr);

            String myoutput = "";

            while ((myoutput=br.readLine()) != null) {
                myoutput = myoutput+"\n";
            }
            System.out.println(myoutput);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 使用正则

正则表达式的结构摘录如下(来源: Oracle官网)

字符

x 字符x
\ 反斜杠
\0n 8进制值为0n的字符(0<=n<=7)
\0nn
\0mnn 8进制值为0mnn的字符(0 <= m <= 3, 0<=n<=7)
\xhh 16进制值为0xhh的字符
\uhhhh 16进制值为0xhhhh的字符
\x{h…h} 16进制值为0xh…h的字符(Character.MINCODEPOINT <= 0xh…h <= Character.MAXCODEPOINT)
\t 制表符(‘\u0009′)
\n 换行符(‘\u000A’)
\r 回车(‘\u000D’)
\f 分页符(‘\u000C’)
\a 警告符(‘\u0007′)
\e ESC(‘\u001B’)
\cx ctrl+x

字符分类

[abc] a, b或c
[^abc] abc以外的任意字符
[a-zA-Z] a到z以及A到Z
[a-d[m-p]] a到d或者m到p[a-dm-p]则是取并集
[a-z&&[def]] d,e或f(交集)
[ad-z]
[a-z&&[^bc]] a到z但不包括b和c
[a-z&&[^m-p]] a到z但不包括mp:也就是[a-lq-z]

预定义字符

. 任意字符,有可能包括换行符
\d 0到9的数字
\D 0到9以外的字符
\s 空格符[ \t\n\x0B\f\r]
\S 非空格符[^\s]
\w 字母[a-zA-Z_0-9]
\W 非字母[^\w]

边界匹配

^ 行首
$ 行末
\b 单词边界
\A 输入的起始位置
\G 前一个匹配的末尾
\Z 输入的结束位置,仅用于最后的结束符
\z 输入的结束位置
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexMatches
{
    private static String pattern =  "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
    private static Pattern mypattern = Pattern.compile(pattern);
    
    public static void main( String args[] ){

        String valEmail1 = "testemail@domain.com";
        String invalEmail1 = "....@domain.com";
        String invalEmail2 = ".$$%%@domain.com";
        String valEmail2 = "test.email@domain.com";

        System.out.println("Is Email ID1 valid? "+validateEMailID(valEmail1));
        System.out.println("Is Email ID1 valid? "+validateEMailID(invalEmail1));
        System.out.println("Is Email ID1 valid? "+validateEMailID(invalEmail2));
        System.out.println("Is Email ID1 valid? "+validateEMailID(valEmail2));

    }
    
    public static boolean validateEMailID(String emailID) {
        Matcher mtch = mypattern.matcher(emailID);
        if(mtch.matches()){
            return true;
        }
        return false;
    }    
}
  1. Java Swing的简单示例

有了Java的swing,你便可以编写GUI应用了。Java所提供的javax包中就包含了swing。使用swing来编写GUI程序首先需要继承下JFrame。然后在里面添加Box,然后便可以往里面添加诸如按钮,多选按钮,文本框等控件了。这些Box是放在Container的最外层的。

import java.awt.*; 
import javax.swing.*;  

public class SwingsDemo extends JFrame 
{ 
    public SwingsDemo() 
    {
        String path = "//home//user//Documents//images";
        Container contentPane = getContentPane(); 
        contentPane.setLayout(new FlowLayout());   
        
        Box myHorizontalBox = Box. createHorizontalBox();  
        Box myVerticleBox = Box. createVerticalBox();   
        
        myHorizontalBox.add(new JButton("My Button 1")); 
        myHorizontalBox.add(new JButton("My Button 2")); 
        myHorizontalBox.add(new JButton("My Button 3"));   

        myVerticleBox.add(new JButton(new ImageIcon(path + "//Image1.jpg"))); 
        myVerticleBox.add(new JButton(new ImageIcon(path + "//Image2.jpg"))); 
        myVerticleBox.add(new JButton(new ImageIcon(path + "//Image3.jpg")));   
        
        contentPane.add(myHorizontalBox); 
        contentPane.add(myVerticleBox);   
        
        pack(); 
        setVisible(true);
    } 
    
    public static void main(String args[]) { 
        new SwingsDemo(); 
    }  
}
  1. 使用Java播放音频

在Java中,播放音频是一个很常见的需求,尤其是在游戏开发里面。

下面这个DEMO演示了如何在Java中播放音频。

import java.io.*;
import java.net.URL;
import javax.sound.sampled.*;
import javax.swing.*;

// To play sound using Clip, the process need to be alive.
// Hence, we use a Swing application.
public class playSoundDemo extends JFrame {

   // Constructor
   public playSoundDemo() {
      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      this.setTitle("Play Sound Demo");
      this.setSize(300, 200);
      this.setVisible(true);

      try {
         URL url = this.getClass().getResource("MyAudio.wav");
         AudioInputStream audioIn = AudioSystem.getAudioInputStream(url);
         Clip clip = AudioSystem.getClip();
         clip.open(audioIn);
         clip.start();
      } catch (UnsupportedAudioFileException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      } catch (LineUnavailableException e) {
         e.printStackTrace();
      }
   }

   public static void main(String[] args) {
      new playSoundDemo();
   }
}
  1. 导出PDF文件

将表格导出成pdf也是一个比较常见的需求。通过itextpdf,导出pdf也不是什么难事。

import java.io.FileOutputStream;
import com.itextpdf.text.Document;
import com.itextpdf.text.Paragraph;
import com.itextpdf.text.pdf.PdfPCell;
import com.itextpdf.text.pdf.PdfPTable;
import com.itextpdf.text.pdf.PdfWriter;

public class DrawPdf {

      public static void main(String[] args) throws Exception {
        Document document = new Document();
        PdfWriter.getInstance(document, new FileOutputStream("Employee.pdf"));
        document.open();
        
        Paragraph para = new Paragraph("Employee Table");
        para.setSpacingAfter(20);
        document.add(para);
        
        PdfPTable table = new PdfPTable(3);
        PdfPCell cell = new PdfPCell(new Paragraph("First Name"));

        table.addCell(cell);
        table.addCell("Last Name");
        table.addCell("Gender");
        table.addCell("Ram");
        table.addCell("Kumar");
        table.addCell("Male");
        table.addCell("Lakshmi");
        table.addCell("Devi");
        table.addCell("Female");

        document.add(table);
        
        document.close();
      }
    }
  1. 邮件发送

在Java中发送邮件也很简单。你只需装一下Java Mail这个jar包,放到你的类路径里即可。在下面的代码中,我们设置了几个基础属性,然后便可以发送邮件了:

import java.util.*;
import javax.mail.*;
import javax.mail.internet.*;

public class SendEmail
{
    public static void main(String [] args)
    {    
        String to = "recipient@gmail.com";
        String from = "sender@gmail.com";
        String host = "localhost";

        Properties properties = System.getProperties();
        properties.setProperty("mail.smtp.host", host);
        Session session = Session.getDefaultInstance(properties);

        try{
            MimeMessage message = new MimeMessage(session);
            message.setFrom(new InternetAddress(from));

            message.addRecipient(Message.RecipientType.TO,new InternetAddress(to));

            message.setSubject("My Email Subject");
            message.setText("My Message Body");
            Transport.send(message);
            System.out.println("Sent successfully!");
        }
        catch (MessagingException ex) {
            ex.printStackTrace();
        }
    }
}
  1. 计算时间

许多程序都需要精确的时间计量。Java提供了一个System的静态方法来支持这一功能:

currentTimeMillis():返回当前时间自新纪元时间以来的毫秒值,long类型。

long startTime = System.currentTimeMillis();
long estimatedTime = System.currentTimeMillis() - startTime;

nanoTime():返回系统计时器当前的精确时间,纳秒值,这也是long类型。nanoTime()主要是用于计算相对时间而非绝对时间。

long startTime = System.nanoTime();
long estimatedTime = System.nanoTime() - startTime;
  1. 图片缩放

图片缩放可以通过AffineTransform来完成。首先要生成一个输入图片的图片缓冲,然后通过它来渲染出缩放后的图片。

import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import javax.imageio.ImageIO;

public class RescaleImage {
  public static void main(String[] args) throws Exception {
    BufferedImage imgSource = ImageIO.read(new File("images//Image3.jpg"));
    BufferedImage imgDestination = new BufferedImage(100, 100, BufferedImage.TYPE_INT_RGB);
    Graphics2D g = imgDestination.createGraphics();
    AffineTransform affinetransformation = AffineTransform.getScaleInstance(2, 2);
    g.drawRenderedImage(imgSource, affinetransformation);
    ImageIO.write(imgDestination, "JPG", new File("outImage.jpg"));
  }
}
  1. 捕获鼠标动作

实现了MouseMotionListner接口后,便可以捕获鼠标事件了。 当鼠标进入到某个特定区域时便会触发MouseMoved事件,你便能捕获到这个移动的动作了。通过一个例子来看下:

import java.awt.event.*;
import javax.swing.*;

public class MouseCaptureDemo extends JFrame implements MouseMotionListener
{
    public JLabel mouseHoverStatus;

    public static void main(String args[]) 
    {
        new MouseCaptureDemo();
    }

    MouseCaptureDemo() 
    {
        setSize(500, 500);
        setTitle("Frame displaying Coordinates of Mouse Motion");

        mouseHoverStatus = new JLabel("No Mouse Hover Detected.", JLabel.CENTER);
        add(mouseHoverStatus);
        addMouseMotionListener(this);
        setVisible(true);
    }

    public void mouseMoved(MouseEvent e) 
    {
        mouseHoverStatus.setText("Mo...
21 Jul 05:47

RPC框架的性能比较

by 鸟窝

永源科技做的一个RPC框架的性能测试。
原文:RPC框架性能基本比较测试

gRPC是Google最近公布的开源软件,基于最新的HTTP2.0协议,并支持常见的编程语言。 我们知道HTTP2.0是基于二进制的HTTP协议升级版本,目前各大浏览器都在快马加鞭的加以支持。 我们可以设想一下,未来浏览器支持HTTP2.0,并通过现有开源序列化库比如protobuf等,可以直接和各种语言的服务进行高效交互,这将是多么“美好”的场景!

gPRC的Java实现底层网络库是Netty,而且是用到最新的Netty5.0.0.Alpha3的开发版本,因为最新版本针对HTTP/2做了很多改进。 为了跨语言,gPRC也和其他方案一样,采用了类似古老IDL的接口描述语言,利用自家的Protobuf项目带的protoc编译器来生成框架代码。这和目前最流行的Facebook开源的,现为Apache顶级项目的Thrift原理一致。

我比较好奇,这个新出世的框架的性能怎么样,和现有的RPC开源方案比较如何。就花了一些时间进行简单比较。 我选择了以下五种开源项目进行测试:gRPC, Thrift, Wildfly, Dubbo, JBoss EAP。 为了简化,测试范例都使用项目自带的demo或者sample等进行简单修改,使得跨进程网络调用次数一致。

RPC框架

gRPC

  1. 从Github master主干上获得最新版本,按照说明文件进行编译。如上所述,网络框架是Netty5,基于最新的HTTP/2.
  2. 测试例子为RouteGuideClient
  3. IDL为route_guide.proto
  4. 选择其中getFeature方法,去除不用的语句和屏幕输出,进行10,000次同步调用。
12345678910
TestClientSync client = new TestClientSync("localhost", 8980);try {  final long startTime = System.nanoTime();  for (int i = 0; i < 10000; i++)    client.getFeature(409146138, -746188906);  final long endTime = System.nanoTime();  info("method 1 : " + (endTime - startTime));}
123456789
public void getFeature(int lat, int lon) {  try {    Point request = Point.newBuilder().setLatitude(lat).setLongitude(lon).build();    Feature feature = blockingStub.getFeature(request);  } catch (RuntimeException e) {    logger.log(Level.WARNING, "RPC failed", e);    throw e;  }}

多次执行,记录需要的时间。

gRPC还有一种非阻塞的调用方法,不过因为时间有限,为了简化测试,我只用标准的server启动的方式,asyncStub在大并发访问时出错,用时也较长,故这次测试没有这种方法的结果数据。

Thrift

  1. 从Apache网站获得最新的0.9.2版本,本机编译获得C的编译器和Java运行环境。
  2. 测试例子为JavaClient.java
  3. IDLtutorial.thrift
12345678910
int diff;final long startTime = System.nanoTime();try {  for (int i = 0; i < 10000; i++)     diff = client.calculate(1, work);} catch (InvalidOperation io) {  System.out.println("Invalid operation: " + io.why);}final long endTime = System.nanoTime();System.out.println("method 1 : " + (endTime - startTime));

Wildfly8.2.0

Wildfly是JBossAS改名后的JBoss应用服务器,实现了完整的JavaEE规范。我们知道JavaEE中远程RPC调用是在EJB规范中定义的。我们这里就是要测试Wildlfy中的远程EJB调用能力,

  1. 选用的Wildfly8.2是目前发布的最新稳定版本。这个版本也支持端口多路服用,也就是EJB远程调用是通过HTTP端口复用来进行的,利用HTTP的Upgrade机制做到二进制运行时刻协商升级。尽管不是纯粹的HTTP/2,但也运行机理也相差无几。
  2. 测试例子选用jboss-eap-quickstarts项目中的远程ejb调用例子RemoteEJBClient.java
  3. 纯Java的RPC方案好处是不需要再有IDL文件定义和编译生成代码的过程,只要商议好接口就可以了
123
public interface RemoteCalculator {    int add(int a, int b);}
123456789
int sum=0;final long startTime = System.nanoTime();for (int i = 0; i < 10000; i++) {         sum = statelessRemoteCalculator.add(a, b);}final long endTime = System.nanoTime();System.out.println("method 1 : " + (endTime - startTime));

调用无状态的SessionBean方法10,000次,对应的远程EJB服务是部署在Wildfly应用服务器中的EJB。

Dubbo2.5.4-SNAPSHOT

Dubbo是阿里集团开源的一个RPC框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是及其鲜明的特色。同样的远程接口是基于Java Interface,并且依托于spring框架方便开发。可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致。

  1. 采用github中master主干,目前版本是 2.5.4-SNAPSHOT
  2. 测试例子选用其中的demo进行修改DemoAction.java
123
public interface DemoService {	String sayHello(String name);}
1234567891011
final long startTime = System.nanoTime();for (int i = 0; i < 10000; i ++) {    try {    	String hello = demoService.sayHello("world" + i);    } catch (Exception e) {        e.printStackTrace();    }}final long endTime = System.nanoTime();System.out.println("method 1 : " + (endTime - startTime));

调用完毕后查看输入log文件获得运行时间。

Redhat JBoss EAP6.3.2

EAP是JBossAS的商业版本,实现了完整的JavaEE规范。

  1. EAP6基于AS7.2以后的版本构建,红帽提供商业支持。
  2. AS7在7.2以后,社区版没有再发布,具备能力的企业可以从源码进行编译使用,EAP6.3基于AS7.4分支构建,很快发布的EAP6.4基于AS7.5分支构建,不出意外这个会是最后一个EAP6的minor版本。
  3. AS7还没有像Wildfly完全采用端口复用的方式,短程EJB调用通过独立端口完成,基于JBossRemoting3的网络连接管理能力。
  4. 测试例子依然选用jboss-eap-quickstarts项目中的远程ejb调用例子
123
public interface RemoteCalculator {    int add(int a, int b);}

记录一万次调用后的时长。

测试结果

最终经过4轮测试,不间断运行10,000次远程RPC调用后的结果如下:

我们可以看到Thrift的效率最高,大概领先一个数量级。而其他三个项目的性能数据在同数量级中,由高到低分别为JBossEAP, dubbo, wildfly和gRPC。

需要说明的有以下几点:

  1. 为了简化测试,我并没有选择同样的调用接口,而是顺手用了项目自带的,方便修改的示例程序。其中gRPC和Thrift的接口有对象传递,稍微复杂一些。
  2. 不是严格的性能测试流程,比如没有做预热过程,以及测试都运行在我的桌面用机上,没有完全恢复成“干净”的状态。
  3. 都是简单的服务器单一进程实例,标准示范例子,没有做特别优化和设置多个线程池之类的。而客户端调用也是最简单的阻塞式多次调用压力测试。应该是用多个机器多连接,多个线程,以及异步非阻塞的调用多种环境进行测试更为客观,有机会再继续完善。
  4. 之前没有看到过基于HTTP/2的RPC调用性能比较,理论上是应该低于经典的基于端口的RPC方案的。这个测试结果可以简单印证这个猜想。Thrift的数据遥遥领先.gRPC还在开发之中,基于的Netty还是alpha版本,而且非阻塞的方式还没有最后的数据。我想耐心一些,给gRPC一些时间,它会让我们惊艳的。
  5. Wildfly表现良好,要知道它的服务端可是完整的JavaEE服务器啊。不过有时间的化,我试试看经典RMI连接的效率如何,要是能和thrift一个数量级就更好了。
  6. dubbo性能也很出色,而且协议层可以更换的话,应该还能有更大提升。
  7. 我的测试在一台过时的笔记本上,受条件限制,没有先进的G级网络和多台服务器进行标准化性能测试。如果哪位在互联网或者企业工作的朋友有条件,也愿意充分完成这个测试,请和我联系,我会完整介绍我的测试搭建环境,共享代码,并帮助完成。我想那个结果会更有意义。

补记

最初四个测试时间为2015-03-11,03-21加入EAP6.3.2的测试,为基于JBossRemoting的EJB远程调用测试,性能良好。和thrift进入一个数量级,EJB功能可是很丰富的,带有事务,安全等高级企业级组件特性。

Wildfly8经过配置后使用和EAP类似的远程调用选项,效率和EAP应该是一致的。

21 Jul 04:27

架构学习资料整理(2013)

by 鸟窝

地瓜哥2013攒的架构资料:分享D瓜哥最近攒的资料(架构方面)

以前见过零零散散地介绍一些知名网站架构的分析文章。最近D瓜哥也想研究一下各大知名网站的架构。所以,就搜集了一下这方面资料。限于时间问题,这篇文章分享的文章并没有都看完,所以不保证所有文章的质量。另外,如果有朋友发现更好的文章,欢迎留言告知。再补充进来。

知名网站架构分析


  1. 探索Google App Engine背后的奥秘(1)–Google的核心技术

  2. 探索Google App Engine背后的奥秘(2)–Google的整体架构猜想

  3. 探索Google App Engine背后的奥秘(3)- Google App Engine的简介

  4. 探索Google App Engine背后的奥秘(4)- Google App Engine的架构

  5. 探索Google App Engine背后的奥秘(5)- Datastore的设计

  6. 探索Google App Engine背后的奥秘(6)-总结

  7. Amazon网站架构学习总结

  8. Amazon网站架构学习总结

  9. Amazon 的 Dynamo 架构

  10. eBay 的应用服务器规模

  11. eBay 的数据量

  12. 来自淘宝的架构经验

  13. Yahoo!社区架构

  14. 基于Facebook和Flash平台的应用架构解析(一)

  15. 基于Facebook和Flash平台的应用架构解析(二)

  16. 基于Facebook和Flash平台的应用架构解析(三)

  17. Facebook图片存储架构的学习

  18. facebook图片存储架构技术全解析

  19. Facebook数据仓库揭秘:RCFile高效存储结构

  20. Facebook 架构学习

  21. Facebook 架构学习

  22. 人人网移动开发架构

  23. QQ空间技术架构之深刻揭密

  24. Twitter 的架构扩展: 100 倍性能提升

  25. Twitter网站架构学习笔记

  26. 国内外大型SNS网站后台架构对比

  27. 优酷网架构学习笔记

  28. 优酷网(Youku.com)架构经验

  29. YouTube架构学习体会

  30. YouTube 的架构扩展

  31. Digg网站架构

  32. Digg 网站架构

  33. WikiPedia技术架构学习笔记

  34. Yupoo网站架构学习总结

  35. Flickr 网站架构分析

  36. 学习 Flickr 的 基于 LAMP 的容量规划经验

  37. MySpace 系统架构

  38. 回顾MySpace架构的坎坷之路

  39. 挑战空中加油——1号店B2C电商系统演进之路

  40. PlentyOfFish.com .NET网站的又一传奇

  41. 学习豆瓣好榜样–网站架构

  42. 37Signals 架构

  43. Tailrank 网站架构

  44. SmugMug 的架构介绍

  45. 高并发PHP网站Poppen.de架构学习

  46. Drupal与大型网站架构

  47. 手机之家网站架构–对话高春辉

  48. 手机之家的网站架构设计和演化

  49. 学习 HeroKu 的架构设计

  50. 各大网站架构总结笔记

  51. 花瓣网的架构介绍

  52. 大型网站架构技术专家谈

架构分析与设计


  1. 架构之美–开放环境下的网络架构

  2. 案例分析:基于消息的分布式架构

  3. 基于模式的架构评审

  4. 为不规则应用设计新一代超大型多线程架构

  5. 从简单到复杂:大型Rails与VoIP系统架构与部署实践

  6. 专家视角看IT与架构

  7. 架构腐化之谜

  8. 我眼中的云端架构

  9. 从100PV到1亿级PV网站架构演变

  10. 网站架构设计方案

  11. 浅谈大型网站动态应用系统架构——鉴于某度有胡乱改系统,而且不自动保留原来数据的恶习,建议大家手动备份下来。

  12. 做大的艺术 – 大型网站的架构设计

  13. 平台网站架构设计之我所见

  14. 屌丝程序员如何打造日PV百万的网站架构

  15. 设计 REST 风格的 MVC 框架

  16. 集群架构实践 – 初试Memcached

  17. BigPipe,加速你的页面加载

  18. 一步步构建大型网站架构

  19. 图片存储架构学习:缓存,架构师的美丽小三

  20. 要快速伸缩?重新架构吧!

  21. 大型Rails与VoIP系统架构与部署实践

  22. LAMP网站架构方案分析

  23. 大型网站架构演变和知识体系

  24. 网站架构相关PPT、文章整理

  25. 说说大型高并发高负载网站的系统架构

  26. 软件架构设计思考之一

  27. 视频流服务架构解析

  28. Rails架构简介

  29. 自建CDN防御DDoS(1):知己知彼,建设持久防线

  30. 自建CDN防御DDoS(2):架构设计、成本与部署细节

  31. 自建CDN防御DDoS(3):架构的后续改进

  32. 系统技术非业余研究-资料下载

  33. 中小型应用系统知识体系

  34. 中小型应用系统架构体系

  35. 《架构师》月刊——InfoQ推出来的电子杂志。貌似还不错。

  36. 系统设计说明书(架构、概要、详细)目录结构——D瓜哥不太擅长写文档,所以插播个文档模板。

架构背后的技术


  1. CDN(内容分发网络)技术原理

  2. 高性能网站的十四条黄金法则

  3. 大型网站后台架构的web server与缓存

  4. Facebook性能大提升的秘密:HipHop

  5. 浅谈Squid在图片存储架构中的应用

  6. 揭秘淘宝自主研发的文件系统——TFS

  7. 百万级访问量网站的技术准备工作

  8. 谈谈大型网站的负载均衡器、db proxy和db

  9. 深入浅出node.js游戏服务器开发——Pomelo框架的设计动机与架构介绍

  10. 架构师不可不知的十大可扩展架构

  11. Memcached FAQ(1) 一般性的问题

  12. Memcached FAQ(2) 集群架构方面的问题

  13. Memcached FAQ(3) 性能和客户端库方面的问题

  14. Memcached FAQ(4) 选项、Item过期和命名空间方面的问题

  15. 分享一些资料(侧重Linux)

  16. 用JavaScript阐述MapReduce原理

  17. 分享一些D瓜哥攒的比较好的Web开发资料

数据库架构


  1. 大型高并发高负载web应用系统架构-数据库架构策略

  2. 又拍网架构中的分库设计

  3. Datomic的架构

  4. 低成本和高性能MySQL云数据的架构探索

插件架构


  1. 关于Plugin Framework的关键因素

  2. Plugin Architecture简述

  3. Eclipse(3.1) Plugin Framework(基于OSGI的Plugin Architecture)

  4. OSGI与Plugin Architecture

  5. 思考插件架构体系

  6. 基于Equinox开发系统的总结

  7. Declarative Services――Service-Oriented Component Model

  8. Service-Oriented Component Model(SOCM)

  9. 插件开发框架的思考

  10. 基于Eclipse Equinox的插件框架:TPF

20 Jul 00:55

Java日志终极指南

by Wing

Java日志基础

Java使用了一种自定义的、可扩展的方法来输出日志。虽然Java通过java.util.logging包提供了一套基本的日志处理API,但你可以很轻松的使用一种或者多种其它日志解决方案。这些解决方案尽管使用不同的方法来创建日志数据,但它们的最终目标是一样的,即将日志从你的应用程序输出到目标地址。

在这一节中,我们会探索Java日志背后的原理,并说明如何通过日志来让你成为一个更好的Java开发人员。

Java日志组件

Java日志API由以下三个核心组件组成:

  • Loggers:Logger负责捕捉事件并将其发送给合适的Appender。
  • Appenders:也被称为Handlers,负责将日志事件记录到目标位置。在将日志事件输出之前,Appenders使用Layouts来对事件进行格式化处理。
  • Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的最终形式。

当Logger记录一个事件时,它将事件转发给适当的Appender。然后Appender使用Layout来对日志记录进行格式化,并将其发送给控制台、文件或者其它目标位置。另外,Filters可以让你进一步指定一个Appender是否可以应用在一条特定的日志记录上。在日志配置中,Filters并不是必需的,但可以让你更灵活地控制日志消息的流动。

 

日志框架

在Java中,输出日志需要使用一个或者多个日志框架,这些框架提供了必要的对象、方法和配置来传输消息。Java在java.util.logging包中提供了一个默认的框架。除此之外,还有很多其它第三方框架,包括Log4jLogback以及tinylog。还有其它一些开发包,例如SLF4JApache Commons Logging,它们提供了一些抽象层,对你的代码和日志框架进行解耦,从而允许你在不同的日志框架中进行切换。

如何选择一个日志解决方案,这取决于你的日志需求的复杂度、和其它日志解决方案的兼容性、易用性以及个人喜好。Logback基于Log4j之前的版本开发(版本1),因此它们的功能集合都非常类似。然而,Log4j在最新版本(版本2)中引用了一些改进,例如支持多API,并提升了在用Disruptor库的性能。而tinylog,由于缺少了一些功能,运行特别快,非常适合小项目。

另外一个考虑因素是框架在基于Java的各种不同项目上的支持程度。例如Android程序只能使用Log4jLogback或者第三方包来记录日志, Apache Tomcat可以使用Log4j来记录内部消息,但只能使用版本1的Log4j。

抽象层

诸如SLF4J这样的抽象层,会将你的应用程序从日志框架中解耦。应用程序可以在运行时选择绑定到一个特定的日志框架(例如java.util.logging、Log4j或者Logback),这通过在应用程序的类路径中添加对应的日志框架来实现。如果在类路径中配置的日志框架不可用,抽象层就会立刻取消调用日志的相应逻辑。抽象层可以让我们更加容易地改变项目现有的日志框架,或者集成那些使用了不同日志框架的项目。

配置

尽管所有的Java日志框架都可以通过代码进行配置,但是大部分配置还是通过外部配置文件完成的。这些文件决定了日志消息在何时通过什么方式进行处理,日志框架可以在运行时加载这些文件。在这一节中提供的大部分配置示例都使用了配置文件。

java.util.logging

默认的Java日志框架将其配置存储到一个名为 logging.properties 的文件中。在这个文件中,每行是一个配置项,配置项使用点标记(dot notation)的形式。Java在其安装目录的lib文件夹下面安装了一个全局配置文件,但在启动一个Java程序时,你可以通过指定 java.util.logging.config.file 属性的方式来使用一个单独的日志配置文件,同样也可以在个人项目中创建和存储 logging.properties 文件。

下面的示例描述了如何在全局的logging.properties文件中定义一个Appender:

# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XmlFormatter

Log4j

Log4j版本1使用的语法和 java.util.logging 的语法很类似。使用了Log4j的程序会在项目目录中寻找一个名为 log4j.properties 的文件。默认情况下,Log4j配置会将所有日志消息输出到控制台上。Log4j同样也支持XML格式的配置文件,对应的配置信息会存储到 log4j.xml 文件中。

Log4j版本2支持XML、JSON和YAML格式的配置,这些配置会分别存储到 log4j2.xml、log4j2.json 和 log4j2.yaml 文件中。和版本1类似,版本2也会在工程目录中寻找这些文件。你可以在每个版本的文档中找到相应的配置文件示例。

Logback

对于Logback来说,大部分配置都是在 logback.xml 文件中完成的,这个文件使用了和Log4j类似的XML语法。Logback同时也支持通过Groovy语言的方式来进行配置,配置信息会存储到 logback.groovy 文件中。你可以通过每种类型配置文件的链接找到对应的配置文件示例。

Loggers

Loggers是用来触发日志事件的对象,在我们的Java应用程序中被创建和调用,然后Loggers才会将事件传递给Appender。一个类中可以包含针对不同事件的多个独立的Loggers,你也可以在一个Loggers里面内嵌一个Loggers,从而创建一种Loggers层次结构

创建新Logger

在不同的日志框架下面创建新Logger过程大同小异,尽管调用的具体方法名称可能不同。在使用 java.util.logging 时,你可以通过 Logger.getLogger().getLogger() 方法创建新Logger,这个方法接收一个string参数,用于指定Logger的名字。如果指定名字的Logger已经存在,那么只需要返回已经存在的Logger;否则,程序会创建一个新Logger。通常情况下,一种好的做法是,我们在当前类下使用 class.getName() 作为新Logger的名字。

Logger logger = Logger.getLogger(MyClass.class.getName());

记录日志事件

Logger提供了几种方法来触发日志事件。然而在你记录一个事件之前,你还需要设置级别。日志级别用来确定日志的严重程度,它可以用来过滤日志事件或者将其发送给不同的Appender(想了解更多信息,请参考“日志级别”一节),Logger.log() 方法除了日志消息以外,还需要一个日志级别作为参数:

logger.log(Level.WARNING, “This is a warning!”);

大部分日志框架都针对输出特定级别日志提供了快捷方式。例如,下面语句的作用和上面语句的作用是一样的:

logger.warning(“This is a warning!”);

你还可以阻止Logger输出低于指定日志级别的消息。在下面的示例中,Logger只能输出高于WARNING级别的日志消息,并丢弃日志级别低于WARNING的消息:

logger.setLevel(Level.WARNING);

我们还有另外一些方法可以用来记录额外的信息。logp()(精确日志)可以让你指定每条日志记录的源类(source class)和方法,而 logrb()(使用资源绑定的日志)可以让你指定用于提取日志消息的资源。entering() 和 exiting() 方法可以让你记录方法调用信息,从而追踪程序的执行过程。

Appenders

Appenders将日志消息转发给期望的输出。它负责接收日志事件,使用Layout格式化事件,然后将其发送给对应的目标。对于一个日志事件,我们可以使用多个Appenders来将事件发送到不同的目标位置。例如,我们可以在控制台上显示一个简单的日志事件的同时,将其通过邮件的方式发送给指定的接收者。

请注意,在java.util.logging中,Appenders被称作Handlers。

增加Appender

大部分日志框架的Appender都会执行类似的功能,但在实现方面大相径庭。如果使用 java.util.logging,你可以使用 Logger.addHandler() 方法将Appender添加到Logger中。例如,下面的代码添加了一个新的ConsoleHandler,它会将日志输出到控制台:

logger.addHandler(new ConsoleHandler());

一种更常用的添加Appender的方式是使用配置文件。如果使用 java.util.logging,Appenders会定义一个以逗号隔开的列表,下面的示例将日志事件输出到控制台和文件:

handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler

如果使用基于XML的配置文件,Appenders会被添加到<Appenders>元素下面,如果使用Log4j,我们可以很容易地添加一个新ConsoleAppender来将日志消息发送到System.out:

<Console name="console" target="SYSTEM_OUT">
  <PatternLayout pattern="[%p] %t: %m%n" />
</Console>

Appenders类型

这一节描述了一些更通用的Appenders,以及它们在各种日志框架中是如何实现的。

ConsoleAppender

ConsoleAppender是最常用的Appenders之一,它只是将日志消息显示到控制台上。许多日志框架都将其作为默认的Appender,并且在基本的配置中进行预配置。例如,在Log4j中ConsoleAppender的配置参数如下所示。

参数 描述
filter 用于决定是否需要使用该Appender来处理日志事件
layout 用于决定如何对日志记录进行格式化,默认情况下使用“%m%n”,它会在每一行显示一条日志记录
follow 用于决定Appender是否需要了解输出(system.out或者system.err)的变化,默认情况是不需要跟踪这种变化
name 用于设置Appender的名字
ignoreExceptions 用于决定是否需要记录在日志事件处理过程中出现的异常
target 用于指定输出目标位置,默认情况下使用SYSTEM_OUT,但也可以修改成SYSTEM_ERR

一个完整的Log4j2的配置文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
 <Configuration status="warn" name="MyApp">
   <Appenders>
     <Console name="MyAppender" target="SYSTEM_OUT">
       <PatternLayout pattern="%m%n"/>
     </Console>
   </Appenders>
   <Loggers>
     <Root level="error">
       <AppenderRef ref="MyAppender"/>
     </Root>
   </Loggers>
 </Configuration>

这个配置文件创建了一个名为MyAppender的ConsoleAppender,它使用PatternLayout来对日志事件进行格式化,然后再将其输出到System.out。<Loggers>元素对定义在程序代码中的Loggers进行了配置。在这里,我们只配置了一个LoggerConfig,即名为Root的Logger,它会接收哪些日志级别在ERROR以上的日志消息。如果我们使用logger.error()来记录一个消息,那么它就会出现在控制台上,就像这样:

An unexpected error occurred.

你也可以使用Logback实现完全一样的效果:

<configuration>
  <appender name="MyAppender" class="ch.qos.Logback.core.ConsoleAppender">
    <encoder>
      <pattern>%m%n</pattern>
    </encoder>
  </appender>
  <root level="error">
    <appender-ref ref="MyAppender" />
  </root>
</configuration>

FileAppenders

FileAppenders将日志记录写入到文件中,它负责打开、关闭文件,向文件中追加日志记录,并对文件进行加锁,以免数据被破坏或者覆盖。

在Log4j中,如果想创建一个FileAppender,需要指定目标文件的名字,写入方式是追加还是覆盖,以及是否需要在写入日志时对文件进行加锁:

...
<Appenders>
  <File name="MyFileAppender" fileName="myLog.log" append="true" locking="true">
    <PatternLayout pattern="%m%n"/>
  </File>
</Appenders>
...

这样我们创建了一个名为MyFileAppender的FileAppender,并且在向文件中追加日志时会对文件进行加锁操作。

如果使用Logback,你可以同时启用prudent模式来保证文件的完整性。虽然Prudent模式增加了写入文件所花费的时间,但它可以保证在多个FileAppender甚至多个Java程序向同一个文件写入日志时,文件的完整性。

...
<appender name="FileAppender" class="ch.qos.Logback.core.FileAppender">
  <file>myLog.log</file>
  <append>true</append>
  <prudent>true</prudent>
  <encoder>
    <pattern>%m%n</pattern>
  </encoder>
</appender>
...

SyslogAppender

SyslogAppenders将日志记录发送给本地或者远程系统的日志服务。syslog是一个接收日志事件服务,这些日志事件来自操作系统、进程、其它服务或者其它设备。事件的范围可以从诊断信息到用户登录硬件失败等。syslog的事件按照设备进行分类,它指定了正在记录的事件的类型。例如,auth facility表明这个事件是和安全以及认证有关。

Log4j和Logback都内置支持SyslogAppenders。在Log4j中,我们创建SyslogAppender时,需要指定syslog服务监听的主机号、端口号以及协议。下面的示例演示了如何设定装置:

...
<Appenders>
  <Syslog name="SyslogAppender" host="localhost" port="514" protocol="UDP" facility="Auth" />
</Appenders>
...

在Logback中,我们可以实现同样的效果:

...
<appender name="SyslogAppender" class="ch.qos.Logback.classic.net.SyslogAppender">
  <syslogHost>localhost</syslogHost>
  <port>514</port>
  <facility>Auth</facility>
</appender>
...

其它Appender

我们已经介绍了一些经常用到的Appenders,还有很多其它Appender。它们添加了新功能或者在其它的一些Appender基础上实现了新功能。例如,Log4j中的RollingFileAppender扩展了FileAppender,它可以在满足特定条件时自动创建新的日志文件;SMTPAppender会将日志内容以邮件的形式发送出去;FailoverAppender会在处理日志的过程中,如果一个或者多个Appender失败,自动切换到其他Appender上。

如果想了解更多关于其他Appender的信息,可以查看Log4j Appender参考以及Logback Appender参考

Layouts

Layouts将日志记录的内容从一种数据形式转换成另外一种。日志框架为纯文本、HTML、syslog、XML、JSON、序列化以及其它日志提供了Layouts。

请注意:在java.util.logging中Layouts也被称为Formatters。

例如,java.util.logging提供了两种Layouts:SimpleFormatter和XMLFormatter。默认情况下,ConsoleHandlers使用SimpleFormatter,它输出的纯文本日志记录就像这样:

Mar 31, 2015 10:47:51 AM MyClass main
SEVERE: An exception occurred.

而默认情况下,FileHandlers使用XMLFormatter,它的输出就像这样:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2015-03-31T10:47:51</date>
  <millis>1427903275893</millis>
  <sequence>0</sequence>
  <logger>MyClass</logger>
  <level>SEVERE</level>
  <class>MyClass</class>
  <method>main</method>
  <thread>1</thread>
  <message>An exception occurred.</message>
</record>
</log>

配置Layout

我们通常使用配置文件对Layouts进行配置。从Java 7开始,我们也可以使用system property来配置SimpleFormatter。

例如,在Log4j和Logback中最常用的Layouts是PatternLayout。它可以让你决定日志事件中的哪些部分需要输出,这是通过转换模式(Conversion Pattern)完成的,转换模式在每一条日志事件的数据中扮演了“占位符”的角色。例如,Log4j默认的PatternLayout使用了如下转换模式:

<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>

%d{HH:mm:ss.SSS} 将日期转换成时、分、秒和毫秒的形式,%level显示日志事件的严重程度,%C显示生成日志事件的类的名字,%t显示Logger的当前线程,%m显示时间的消息,最后,%n为下一个日志事件进行了换行。

改变Layouts

如果在java.util.logging中使用一个不同的Layout,需要将Appender的formatter属性设置成你想要的Layout。在代码中,你可以创建一个新的Handler,调用setFormatter方法,然后通过logger.AddHandler()方法将Handler放到Logger上面。下面的示例创建了一个ConsoleAppender,它使用XMLFormatter来对日志进行格式化,而不是使用默认的SimpleFormatter:

Handler ch = new ConsoleHandler();
ch.setFormatter(new XMLFormatter());
logger.addHandler(ch);

这样Logger会将下面的信息输出到控制台上:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2015-03-31T10:47:51</date>
  <millis>1427813271000</millis>
  <sequence>0</sequence>
  <logger>MyClass</logger>
  <level>SEVERE</level>
  <class>MyClass</class>
  <method>main</method>
  <thread>1</thread>
  <message>An exception occurred.</message>
</record>

如果想了解更多信息,你可以查看Log4j Layouts参考以及Logback Layouts参考

使用自定义Layouts

自定义Layouts可以让你指定Appender应该如何输出日志记录。从Java SE 7开始,尽管你可以调整SimpleLogger的输出,但有一个限制,即只能够调整简单的纯文本消息。对于更高级的格式,例如HTML或者JSON,你需要一个自定义Layout或者一个单独的框架。

如果想了解更多使用java.util.logging创建自定义Layouts的信息,你可以查看Jakob Jenkov的Java日志指南中的Java Logging: Formatters章节。

日志级别

日志级别提供了一种方式,我们可以用它来根据严重程度对日志进行分类和识别。java.util.logging 按照严重程度从重到轻,提供了以下级别:

  • SEVERE(最高级别)
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST(最低级别)

另外, 还有两个日志级别:ALL和OFF。ALL会让Logger输出所有消息,而OFF则会关闭日志功能。

设置日志级别

在设定日志级别后,Logger会自动忽略那些低于设定级别的日志消息。例如,下面的语句会让Logger忽略那些低于WARNING级别的日志消息:

logger.setLevel(Level.WARNING);

然后,Logger会记录任何WARNING或者更高级别的日志消息。我们也可以在配置文件中设置Logger的日志级别:

...
<Loggers>
  <Logger name="MyLogger" level="warning">
  ...

转换模式

Log4j和Logback中的PatternLayout类都支持转换模式,它决定了我们如何从每一条日志事件中提取信息以及如何对信息进行格式化。下面显示了这些模式的一个子集,对于Log4j和Logback来说,虽然这些特定的字段都是一样的,但是并不是所有的字段都会使用相同的模式。想要了解更多信息,可以查看Log4jLogback的PatternLayout文档。

字段名称 Log4j/Logback 模式
消息 %m
级别/严重程度 %p
异常 %ex
线程 %t
Logger %c
方法 %M

例如,下面的PatternLayout会在中括号内x显示日志级别,后面是线程名字和日志事件的消息:

[%p] %t: %m

下面是使用了上述转换模式后的日志输出示例:

[INFO] main: initializing worker threads
[DEBUG] worker: listening on port 12222[INFO] worker: received request from 192.168.1.200[ERROR] worker: unknown request ID from 192.168.1.200

记录栈跟踪信息

如果你在Java程序中使用过异常,那么很有可能已经看到过栈跟踪信息。它提供了一个程序中方法调用的快照,让你准确定位程序执行的位置。例如,下面的栈跟踪信息是程序试图打开一个不存在的文件后生成的:

[ERROR] main: Unable to open file! java.io.FileNotFoundException: foo.file (No such file or directory)
  at java.io.FileInputStream.open(Native Method) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:146) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:101) ~[?:1.7.0_79]
  at java.io.FileReader.<init>(FileReader.java:58) ~[?:1.7.0_79]
  at FooClass.main(FooClass.java:47)

这个示例使用了一个名为FooClass的类,它包含一个main方法。在程序第47行,FileReader独享试图打开一个名为foo.file的文件,由于在程序目录下没有名字是foo.file的文件,因此Java虚拟机抛出了一个FileNotFoundException。因为这个方法调用被放到了try-catch语块中,所以我们能够捕获这个异常并记录它,或者至少可以阻止程序崩溃。

使用PatternLayout记录栈跟踪信息

在写本篇文章时最新版本的Log4j和Logback中,如果在Layout中没有和可抛异常相关的信息,那么都会自动将%xEx(这种栈跟踪信息包含了每次方法调用的包信息)添加到PatternLayout中。如果对于普通的日志信息的模式如下:

[%p] %t: %m

它会变为:

[%p] %t: %m%xEx

这样不仅仅错误信息会被记录下来,完整的栈跟踪信息也会被记录:

[ERROR] main: Unable to open file! java.io.FileNotFoundException: foo.file (No such file or directory)
  at java.io.FileInputStream.open(Native Method) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:146) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:101) ~[?:1.7.0_79]
  at java.io.FileReader.<init>(FileReader.java:58) ~[?:1.7.0_79]
  at FooClass.main(FooClass.java:47)

%xEx中的包查询是一个代价昂贵的操作,如果你频繁的记录异常信息,那么可能会碰到性能问题,例如:

  // ...
  } catch (FileNotFoundException ex) {
    logger.error(“Unable to open file!”, ex);
}

一种解决方法是在模式中显式的包含%ex,这样就只会请求异常的栈跟踪信息:

[%p] %t: %m%ex

另外一种方法是通过追加%xEx(none)的方法排除(在Log4j)中所有的异常信息:

[%p] %t: %m%xEx{none}

或者在Logback中使用%nopex:

[%p] %t: %m%nopex

使用结构化布局输出栈跟踪信息

如你在“解析多行栈跟踪信息”一节中所见,对于站跟踪信息来说,使用结构化布局来记录是最合适的方式,例如JSON和XML。 这些布局会自动将栈跟踪信息按照核心组件进行分解,这样我们可以很容易将其导出到其他程序或者日志服务中。对于上述站跟踪信息,如果使用JSON格式,部分信息显示如下:

...
"loggerName" : "FooClass",
  "message" : "Foo, oh no! ",
  "thrown" : {
    "commonElementCount" : 0,
    "localizedMessage" : "foo.file (No such file or directory)",
    "message" : "foo.file (No such file or directory)",
    "name" : "java.io.FileNotFoundException",
    "extendedStackTrace" : [ {
    "class" : "java.io.FileInputStream",
    "method" : "open",
    "file" : "FileInputStream.java",
    ...

记录未捕获异常

通常情况下,我们通过捕获的方式来处理异常。如果一个异常没有被捕获,那么它可能会导致程序终止。如果能够留存任何日志,那么这是一个可以帮助我们调试为什么会发生异常的好办法,这样你就可以找到发生异常的根本原因并解决它。下面来说明我们如何建立一个默认的异常处理器来记录这些错误。

Thread类中有两个方法,我们可以用它来为未捕获的异常指定一个ExceptionHandler:

setDefaultUncaughtExceptionHandler 可以让你在任何线程上处理任何异常。setUncaughtExceptionHandler可以让你针对一个指定的线程设定一个不同的处理方法。而ThreadGroup则允许你设定一个处理方法。大部分人会使用默认的异常处理方法。

下面是一个示例,它设定了一个默认的异常处理方法,来创建一个日志事件。它要求你传入一个UncaughtExceptionHandler:

import java.util.logging.*;
public class ExceptionDemo {
  private static final Logger logger = Logger.getLogger(ExceptionDemo.class);
  public static void main(String[] args) {
    Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
      public void uncaughtException(Thread t, Throwable e) {
        logger.log(Level.SEVERE, t + " ExceptionDemo threw an exception: ", e);
      };
  });
  class adminThread implements Runnable {
    public void run() {
      throw new RuntimeException();
    }
  }
  Thread t = new Thread(new adminThread());
  t.start();
  }
}

下面是一个未处理异常的输出示例:

May 29, 2015 2:21:15 PM ExceptionDemo$1 uncaughtException
SEVERE: Thread[Thread-1,5,main] ExceptionDemo threw an exception:
java.lang.RuntimeException
  at ExceptionDemo$1adminThread.run(ExceptionDemo.java:15)
  at java.lang.Thread.run(Thread.java:745)

JSON

JSON(JavaScript Object Notation)是一种用来存储结构化数据的格式,它将数据存储成键值对的集合,类似于HashMap或者Hashtable。JSON具有的可移植性和通用性,大部分现代语言都内置支持它或者通过已经准备好的第三方类库来支持它。

JSON支持许多基本数据类型,包括字符串、数字、布尔、数组和null。例如,你可以使用下面的JSON格式来表示一个电脑:

{
  "manufacturer": "Dell",
  "model": "Inspiron",
  "hardware": {
    "cpu": "Intel Core i7",
    "ram": 16384,
    “cdrom”: null
  },
  "peripherals": [
    {
      "type": "monitor",
      "manufacturer": "Acer",
      "model": "S231HL"
    }
  ]
}

JSON的可移植性使得它非常适合存储日志记录,使用JSON后,Java日志可以被任何数目的JSON解释器所读取。因为数据已经是结构化的,所以解析JSON日志要远比解析纯文本日志容易。

Java中的JSON

对于Java来说,有大量的JSON实现,其中一个是JSON.simple。JSON.simple是轻量级的、易于使用,并且全部符合JSON标准。

如果想将上面的computer对象转换成可用的Java对象,我们可以从文件中读取JSON内容,将其传递给JSON.simple,然后返回一个Object,接着我们可以将Object转换成JSONObject:

Object computer = JSONValue.parse(new FileReader("computer.json"));
JSONObject computerJSON = (JSONObject)computer;

另外,为了取得键值对的信息,你可以使用任何日志框架来记录一个JSONObject,JSONObject对象包含一个toString()方法, 它可以将JSON转换成文本:

2015-05-06 14:54:32,878 INFO  JSONTest main {"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}

虽然这样做可以很容易的打印JSONObject,但如果你使用结构化的Layouts,例如JSONLayout或者XMLLayout,可能会导致意想不到的结果:

...
"message" : "{"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}",
...

Log4j中的JSONLayout并没有内置支持内嵌JSON对象,但你可以通过创建自定义Layout的方式来添加一个JSONObject字段,这个Layout会继承或者替换JSONLayout。然而,如果你使用一个日志管理系统,需要记住许多日志管理系统会针对某些字段使用预定义的数据类型。如果你创建一个Layout并将JSONObject存储到message字段中,那么它可能会和日志系统中使用的String数据类型相冲突。一种解决办法是将JSON数据存储到一个字段中,然后将字符串类型的日志消息存储到另外一个字段中。

其它JSON库

除了JSON.simple,Java中还有很多其它JSON库。JSON-java是由JSON创建者开发的一个参考实现,它包含了额外的一些功能,可以转换其它数据类型,包括web元素。但是目前JSON-java已经没有人来维护和提供支持了。

如果想将JSON对象转换成Java对象或者逆向转换,Google提供了一个Gson库。使用Gson时,可以很简单使用 toJson() 和 fromJson() 方法来解析JSON,这两个方法分别用来将Java对象转换成JSON字符串以及将JSON字符串转换成Java对象。Gson甚至可以应用在内存对象中,允许你映射到那些没有源代码的对象上。

Jackson

Jackson是一个强大的、流行的、功能丰富的库,它可以在Java中管理JSON对象。有一些框架甚至使用Jackson作为它们的JSONLayouts。尽管它很大并且复杂,但Jackson对于初学者和高级用户来说,是很容易使用的。

Logback通过logback-jackson和logback-json-classic库继承了Jackson,这两个库也是logback-contrib项目的一部分。在集成了Jackson后,你可以将日志以JSON的格式导出到任何Appender中。

Logback Wiki详细解释了如何将JSON添加到logback中,在Wiki页面中的示例使用了LogglyAppender,这里的配置也可以应用到其他Appender上。下面的示例说明了如何将JSON格式化的日志记录写入到名为myLog.json的文件中:

...
<appender name="file" class="ch.qos.Logback.core.FileAppender">
  <file>myLog.json</file>
  <encoder class="ch.qos.Logback.core.encoder.LayoutWrappingEncoder">
  <layout class="ch.qos.Logback.contrib.json.classic.JsonLayout">
    <jsonFormatter class="ch.qos.Logback.contrib.jackson.JacksonJsonFormatter"/>
    </layout>
  </encoder>
</appender>
 ...

你也可以通过FasterXML Wiki找到更多关于Jackson的深度介绍。

了解更多JSON相关信息

你可以通过JSON主页学习更多JSON相关信息,或者通过CodeAcademy来通过学习一个交互式的快速上手教程(请注意这个课程是基于JavaScript的,而不是Java)。有一些在线工具例如JSONLintJSON在线编辑器可以帮助你解析、验证以及格式化JSON代码。

NDC、MDC以及ThreadContext

当处理多线程应用程序,特别是web服务时,跟踪事件可能会变得困难。当针对多个同时存在的多个用户生成日志记录时,你如何区分哪个行为和哪个日志事件有关呢?如何两个用户没有成功打开一个相同的文件,或者在同一时间没有成功登陆,那么怎么处理日志记录?你可能需要一种方式来将日志记录和程序中的唯一标示符关联起来,这些标识符可能是用户ID,会话ID或者设备ID。而这就是NDC、MDC以及ThreadContext的用武之地。

NDC、MDC和ThreadContext通过向单独的日志记录中添加独一无二的数据戳,来创建日志足迹(log trails)。这些数据戳也被称为鱼标记(fish tagging),我们可以通过一个或者多个独一无二的值来区分日志。这些数据戳在每个线程级别上进行管理,并且一直持续到线程结束,或者直到数据戳被删掉。例如,如果你的Web应用程序为每个用户生成一个新的线程,那么你可以使用这个用户的ID来标记日志记录。当你想在一个复杂的系统中跟踪特定的请求、事务或者用户,这是一种非常有用的方法。

嵌套诊断上下文(NDC)

NDC或者嵌套诊断上下文(Nested Diagnostic Context)是基于栈的思想,信息可以被放到栈上或者从栈中移除。而栈中的值可以被Logger访问,并且Logger无需显示想日志方法中传入任何值。

下面的代码示例使用NDC和Log4j来将用户姓名和一条日志记录关联起来。NDC是一个静态类,因此我们可以直接访问它的方法,而无需实例化一个NDC对象。在这个示例中, NDC.oush(username) 和 NDC.push(sessionID) 方法在栈中存储了当前的用户名(admin)和会话ID(1234),而NDC.pop()方法将一些项从栈中移除,NDC.remove()方法让Java回收内存,以免造成内存溢出。

 

import java.io.FileReader;
import org.apache.Log4j.Logger;
import org.apache.Log4j.NDC;
...
String username = "admin";
String sessionID = "1234";
NDC.push(username);
NDC.push(sessionID);
try {
  // tmpFile doesn't exist, causing an exception.
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file.");
}
finally {
  NDC.pop();
  NDC.pop();
  NDC.remove();
}

Log4j的PatternLayout类通过%x转换字符从NDC中提取值。如果一个日志事件被触发,那么完整的NDC栈就被传到Log4j:

<PatternLayout pattern="%x %-5p - %m%n" />

运行程序后,我们可以得出下面的输出:

"admin 1234 ERROR – Unable to open file."

映射诊断上下文(MDC)

MDC或者映射诊断上下文和NDC很相似,不同之处在于MDC将值存储在键值对中,而不是栈中。这样你可以很容易的在Layout中引用一个单独的键。MDC.put(key,value) 方法将一个新的键值对添加到上下文中,而 MDC.remove(key) 方法会移除指定的键值对。

如果想在日志中同样显示用户名和会话ID,我们需要使用 MDC.put() 方法将这两个变量存储成键值对:

import java.io.FileReader;
import org.apache.Log4j.Logger;
import org.apache.Log4j.MDC;
...
MDC.put("username", "admin");
MDC.put("sessionID", "1234");
try {
  // tmpFile doesn't exist, causing an exception.
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file!");
}
finally {
  MDC.clear();
}

这里再一次强调,在不需要使用Context后,我们需要使用 MDC.clear() 方法将所有的键值对从MDC中移除,这样会降低内存的使用量,并阻止MDC在后面试图调用那些已经过期的数据。

在日志框架中访问MDC的值时,也稍微有些区别。对于存储在上下文中的任何键,我们可以使用%X(键)的方式来访问对应的值。这样,我们可以使用 %X(username) 和 %X(sessionID) 来获取对应的用户名和会话ID:

<PatternLayout pattern="%X{username} %X{sessionID} %-5p - %m%n" />
"admin 1234 ERROR – Unable to open file!"

如果我们没有指定任何键,那么MDC上下文就会被以 {(key, value),(key, value)} 的方式传递给Appender。

Logback中的NDC和MDC

和Log4j不同,Logback内置没有实现NDC。但是slf4j-ext包提供了一个NDC实现,它使用MDC作为基础。在Logback内部,你可以使用 MDC.put()、MDC.remove() 和 MDC.clear() 方法来访问和管理MDC:

import org.slf4j.MDC;
...
Logger logger = LoggerFactory.getLogger(MDCLogback.class);
...
MDC.put("username", "admin");
MDC.put("sessionID", "1234");
try {
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file.");
}
finally {
  MDC.clear();
}

在Logback中,你可以在Logback.xml中将如下模式应用到Appender上,它可以输出和上面Log4j相同的结果:

<Pattern>[%X{username}] %X{sessionID} %-5p - %m%n</Pattern>
"[admin] 1234 ERROR - Unable to open file."

针对MDC的访问并不仅仅限制在PatternLayout上,例如,当使用JSONFormatter时,MDC中的所有值都会被导出:

{
"timestamp":"1431970324945",
"level":"ERROR",
"thread":"main",
"mdc":{
"username":"admin",
"sessionID":"1234"
},
"logger":"MyClass",
"message":"Unable to open file.",
"context":"default"
}

ThreadContext

Version 2 of Log4j merged MDC and NDC into a single concept known as the Thread Context. The Thread Context is an evolution of MDC and NDC, presenting them respectively as the Thread Context Map and Thread Context Stack. The Thread Context is managed through the static ThreadContext class, which is implemented similar to Log4j 1’s MDC and NDC classes.

Log4j版本2中将MDC和NDC合并到一个单独的组件中,这个组件被称为线程上下文。线程上下文是针对MDC和NDC的进化,它分别用线程上下文Map映射线程上下文栈来表示MDC和NDC。我们可以通过ThreadContext静态类来管理线程上下文,这个类在实现上类似于Log4j版本1中的MDC和NDC。

When using the Thread Context Stack, data is pushed to and popped from a stack just like with NDC:

当使用线程上下文栈时,我们可以向NDC那样向栈中添加或者删除数据:

import org.apache.logging.Log4j.ThreadContext;
...
ThreadContext.push(username);
ThreadContext.push(sessionID);
// Logging methods go here
ThreadContext.pop();
...

当使用线程上下文映射时,我们可以像MDC那样将值和键结合在一起:

import org.apache.logging.Log4j.ThreadContext;
...
ThreadContext.put(“username”,"admin");
ThreadContext.put("sessionID", "1234");
// Logging methods go here
ThreadContext.clearMap();
...

ThreadContext类提供了一些方法,用于清除栈、清除MDC、清除存储在上下文中的所有值,对应的方法是ThreadContext.clearAll()、ThreadContext.clearMap()和ThreadContext.clearStack()。

和在MDC以及NDC中一样,我们可以使用Layouts在线程上下文中访问这些值。使用PatternLayout时,%x转换模式会从栈中获取值,%X和%X(键)会从图中获取值。

ThreadContext过滤

一些框架允许你基于某些属性对日志进行过滤。例如,Log4j的DynamicThresholdFilter 会在键满足特定条件的情况下,自动调整日志级别。再比如,如果我们想要触发TRACE级别的日志消息,我们可以创建一个名为trace-logging-enabled的键,并向log4j配置文件中添加一个过滤器:

<Configuration name="MyApp">
<DynamicThresholdFilter key="trace-logging-enabled" onMatch="ACCEPT" onMismatch="NEUTRAL">
<KeyValuePair key="true" value="TRACE" />
</DynamicThresholdFilter>
...

如果ThreadContext包含一个名为trace-logging-enabled的键,onMatch 和 onMismatch 会决定如何处理它。关于 onMatch 和 onMismatch,我们有三个可选项:ACCEPT,它会处理过滤器的规则;DENY,它会忽略过滤器的规则;NEUTRAL,它会推迟到下一个过滤器。除了这些,我们还定义一个键值对,当值为true时,我们启用TRACE级别的日志。

现在,当trace-logging-enabled被设置成true时,即使根Logger设置的日志级别高于TRACE,Appender也会记录TRACE级别的消息。

你可能还想过滤一些特定的日志到特定的Appender中,Log4j中提供了ThreadContextMapFilter来实现这一点。如果我们想要限制某个特定的Appender,只记录针对某个用户的TRACE级别的消息,我们可以基于username键添加一个ThreadContextMapFilter:

<Console name="ConsoleAppender" target="SYSTEM_OUT">
<ThreadContextMapFilter onMatch="ACCEPT" onMismatch="DENY">
<KeyValuePair key="username" value="admin" />
</ThreadContextMapFilter>
...

如果想了解更多信息,你可以查看Log4jLogback文档中关于DynamicThresholdFilter部分。

Markers

Markers允许你对单独的日志记录添加一些独一无二的数据。它可以用来对日志记录进行分组,触发一些行为,或者对日志记录进行过滤,并将过滤结果输出到指定的Appender中。你甚至可以将Markers和ThreadContext结合在一起使用,以提高搜索和过滤日志数据的能力。

例如,假设我们有一个可以连接到数据库的类,如果在打开数据库的时候发生了异常,我们需要把异常记录成fatal错误。我们可以创建一个名为DB_ERROR的Marker,然后将其应用到日志事件中:

import org.apache.logging.Log4j.Marker;
import org.apache.logging.Log4j.MarkerManager;
...
final static Marker DB_ERROR = MarkerManager.getMarker("DATABASE_ERROR");
...
logger.fatal(DB_ERROR, "An exception occurred.");

为了在日志输出中显示Marker信息,我们需要在PatternLayout中添加%marker转换模式:

<PatternLayout pattern="%p %marker: %m%n" />
[FATAL] DATABASE_ERROR: An exception occurred.

或者对于JSON和XML格式的Layouts,会自动在输出中包含Marker信息:

...
"thread" : "main",
"level" : "FATAL",
"loggerName" : "DBClass",
"marker" : {
  "name" : "DATABASE_ERROR"
},
"message" : "An exception occurred.",
...

通过对Marker数据进行自动解析和排序,集中式的日志服务可以很容易对日志进行搜索处理。

Markers过滤

Marker过滤器可以让你决定哪些Marker由哪些Logger来处理。marker字段会比较在日志事件里面的Marker名字,如果名字匹配,那么Logger会执行后续的行为。例如,在Log4j中,我们可以配置一个Appender来只显示哪些使用了DB_ERROR Marker的消息,这可以通过log4j2.xml中的Appender添加如下信息来实现:

<MarkerFilter marker="DATABASE_ERROR" onMatch="ACCEPT" onMismatch="DENY" />

如果日志记录中某一条的Marker可以匹配这里的marker字段,那么onMatch会决定如何处理这条记录。如果不能够匹配,或者日志记录中没有Marker信息,那么onMismatch就会决定如何处理这条记录。对于onMatch和onMismatch来说,有3个可选项:ACCEPT,它允许记录事件;DENY,它会阻塞事件;NEUTRAL,它不会对事件进行任何处理。

在Logback中,我们需要更多一些设置。首先,想Appender中添加一个新的EvaluatorFilter,并如上所述指定onMatch和onMismatch行为。然后,添加一个OnMarkerEvaluator并将Marker的名字传递给它:

<filter class="ch.qos.Logback.core.filter.EvaluatorFilter">
  <evaluator class="ch.qos.Logback.classic.boolex.OnMarkerEvaluator">
    <marker>DATABASE_ERROR</marker>
  </evaluator>
  <onMatch>ACCEPT</onMatch>
  <onMismatch>DENY</onMismatch>
</filter>

将Markers和NDC、MDC以及ThreadContext结合使用

Marker的功能和ThreadContext类似,它们都是向日志记录中添加独一无二的数据,这些数据可以被Appender访问。如果把这两者结合使用,可以让你更容易的对日志数据进行索引和搜索。如果能够知道何时使用哪一种技术,会对我们有所帮助。

NDC、MDC和ThreadContext被用于将相关日志记录结合在一起。如果你的应用程序会处理多个同时存在的用户,ThreadContext可以让你将针对某个特定用户的一组日志记录组合在一起。因为ThreadContext针对每个线程都是不一样的,所以你可以使用同样的方法来对相关的日志记录进行自动分组。

另一方面,Marker通常用于标记或者高亮显示某些特殊事件。在上述示例中,我们使用DB_ERROR Marker来标明在方法中发生的SQL相关异常。我们可以使用DB_ERROR Marker来将这些事件的处理过程和其他事件区分开来,例如我们可以使用SMTP Appender来将这些事件通过邮件发送给数据库管理员。

额外资源

指南和教程

  • Java Logging(Jakob Jenkov)——使用Java Logging API进行日志开发教程
  • Java Logging Overview(Oracle)—— Oracle提供的在Java中进行日志开发的指南
  • Log4J Tutorial(Tutorials Point)——使用log4j 版本1进行日志开发的指南

日志抽象层

  • Apache Commons Logging(Apache)——针对Log4j、Avalon LogKit和java.util.logging的抽象层
  • SLF4J(QOS.ch)——一个流程的抽象层,应用在多个日志框架上,包括Log4j、Logback以及java.util.logging

日志框架

  • Java Logging API(Oracle)—— Java默认的日志框架
  • Log4j(Apache)——开源日志框架
  • Logback(Logback Project)——开源项目,被设计成Log4j版本1的后续版本
  • tinylog(tinylog)——轻量级开源logger

 

相关文章

19 Jul 01:25

《Java并发编程的艺术》第一章

by 方 腾飞

封面立体图
作者:方腾飞  本文是样章  购买本书=》  当当 京东 天猫 互动

第1章并发编程的挑战

并发编程的目的是为了让程序运行的更快,但是并不是启动更多的线程,就能让程序最大限度的并发执行。在进行并发编程时,如果希望通过多线程执行任务让程序运行的更快,会面临非常多的挑战,比如上下文切换的问题,死锁的问题,以及受限于硬件和软件的资源限制问题,本章会介绍几种并发编程的挑战,以及解决方案。

1.1     上下文切换

即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停的切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。

CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下个任务,但是在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务的保存到再加载的过程就是一次上下文切换

就像我们同时在读两本书,比如当我们在读一本英文的技术书时,发现某个单词不认识,于是便打开中英文字典,但是在放下英文技术书之前,大脑必需首先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书,这样的切换是会影响读书效率的,同样上下文切换也会影响到多线程的执行速度。

1.1.1    多线程一定快吗?

下面的代码演示串行和并发执行累加操作的时间,请思考下面的代码并发执行一定比串行执行快些吗?

package chapter01;

/**
 * 并发和单线程执行测试
 * @author tengfei.fangtf
 * @version $Id: ConcurrencyTest.java, v 0.1 2014-7-18 下午10:03:31 tengfei.fangtf Exp $
 */
public class ConcurrencyTest {

    /** 执行次数 */
    private static final long count = 10000l;

    public static void main(String[] args) throws InterruptedException {
        //并发计算
        concurrency();
        //单线程计算
        serial();
    }

    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (long i = 0; i < count; i++) {
                    a += 5;
                }
                System.out.println(a);
            }
        });
        thread.start();
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        thread.join();
        System.out.println("concurrency :" + time + "ms,b=" + b);
    }

    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (long i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (long i = 0; i < count; i++) {
            b--;
        }
        long time = System.currentTimeMillis() - start;
        System.out.println("serial:" + time + "ms,b=" + b + ",a=" + a);
    }

}

答案是不一定,测试结果如表1-1所示:

表1-1 测试结果

循环次数 串行执行耗时(单位ms 并发执行耗时 并发比串行快多少
1亿 130 77 约1倍
1千万 18 9 约1倍
1百万 5 5 差不多
10万 4 3
1万 0 1

从表1-1可以发现当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要慢。那么为什么并发执行的速度还比串行慢呢?因为线程有创建和上下文切换的开销。

1.1.2    测试上下文切换次数和时长

下面我们来看看有什么工具可以度量上下文切换带来的消耗。

  • 使用Lmbench3[1]可以测量上下文切换的时长。
  • 使用vmstat可以测量上下文切换的次数。

下面是利用vmstat测量上下文切换次数的示例。

$ vmstat 1

procs -----------memory---------- ---swap-- -----io---- --system-- -----cpu-----

r b   swpd   free   buff cache   si   so   bi   bo   in   cs us sy id wa st

0 0     0 127876 398928 2297092   0   0     0     4   2   2 0 0 99 0 0

0 0     0 127868 398928 2297092   0   0     0     0 595 1171 0 1 99 0 0

0 0     0 127868 398928 2297092   0   0     0     0 590 1180 1 0 100 0 0

0 0     0 127868 398928 2297092   0   0     0     0 567 1135 0 1 99 0 0

CS(Content Switch)表示上下文切换的次数,从上面的测试结果中,我们可以看到其中上下文的每一秒钟切换1000多次。

1.1.3    如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS算法、单线程编程和使用协程。

  • 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据用ID进行Hash算法后分段,不同的线程处理不同段的数据。
  • CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
  • 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。
  • 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。

 

1.1.4    减少上下文切换实战

本节描述通过减少线上大量WAITING的线程,来减少上下文切换次数。

第一步:用jstack命令 dump线程信息,看看pid是3117进程里的线程都在做什么。

sudo -u admin /opt/ifeve/java/bin/jstack 31177 &gt; /home/tengfei.fangtf/dump17

第二步:统计下所有线程分别处于什么状态,发现300多个线程处于WAITING(onobjectmonitor)状态。


[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
39 RUNNABLE
21 TIMED_WAITING(onobjectmonitor)
6 TIMED_WAITING(parking)
51 TIMED_WAITING(sleeping)
305 WAITING(onobjectmonitor)
3 WAITING(parking)

第三步:打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么。发现这些线程基本全是JBOSS的工作线程在await。说明JBOSS线程池里线程接收到的任务太少,大量线程都闲着。

"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in Object.wait() [0x0000000052423000]
 java.lang.Thread.State: WAITING (on object monitor)
 at java.lang.Object.wait(Native Method)
 - waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
 at java.lang.Object.wait(Object.java:485)
 at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)
 - locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
 at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
 at java.lang.Thread.run(Thread.java:662)

第四步:减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降低到100。

<maxThreads="250" maxHttpHeaderSize="8192"
emptySessionPath="false" minSpareThreads="40" maxSpareThreads="75" maxPostSize="512000" protocol="HTTP/1.1"
enableLookups="false" redirectPort="8443" acceptCount="200" bufferSize="16384"
connectionTimeout="15000" disableUploadTimeout="false" useBodyEncodingForURI="true">

第五步:重启JBOSS,再dump线程信息,然后再统计WAITING(onobjectmonitor)的线程,发现减少了175。WAITING的线程少了,系统上下文切换的次数就会少,因为从WAITTING到RUNNABLE会进行一次上下文的切换。读者也可以使用vmstat命令测试下。

[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}' | sort | uniq -c
44 RUNNABLE
22 TIMED_WAITING(onobjectmonitor)
9 TIMED_WAITING(parking)
36 TIMED_WAITING(sleeping)
130 WAITING(onobjectmonitor)
1 WAITING(parking)

1.2 死锁

锁是个非常有用的工具,运用场景非常多,因为其使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,会造成系统功能不可用。让我们先来看一段代码,这段代码会引起死锁,线程t1和t2互相等待对方释放锁。

package chapter01;

/**
 * 死锁例子
 *
 * @author tengfei.fangtf
 * @version $Id: DeadLockDemo.java, v 0.1 2015-7-18 下午10:08:28 tengfei.fangtf Exp $
 */
public class DeadLockDemo {

    /** A锁 */
    private static String A = "A";
    /** B锁 */
    private static String B = "B";

    public static void main(String[] args) {
        new DeadLockDemo().deadLock();
    }

    private void deadLock() {
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (A) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        System.out.println("1");
                    }
                }
            }
        });

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (B) {
                    synchronized (A) {
                        System.out.println("2");
                    }
                }
            }
        });
        t1.start();
        t2.start();
    }

}

这段代码只是演示死锁的场景,在现实中你可能很难会写出这样的代码。但是一些更为复杂的场景中你可能会遇到这样的问题,比如t1拿到锁之后,因为一些异常情况没有释放锁,比如死循环。又或者是t1拿到一个数据库锁,释放锁的时候抛了异常,没释放掉。

一旦出现死锁,业务是可感知的,因为不能继续提供服务了,那么只能通过dump线程看看到底是哪个线程出现了问题,以下线程信息告诉我们是DeadLockDemo类的42行和31号引起的死锁:

"Thread-2" prio=5 tid=7fc0458d1000 nid=0x116c1c000 waiting for monitor entry [116c1b000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.ifeve.book.forkjoin.DeadLockDemo$2.run(DeadLockDemo.java:42)
        - waiting to lock <7fb2f3ec0> (a java.lang.String)
        - locked <7fb2f3ef8> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:695)

"Thread-1" prio=5 tid=7fc0430f6800 nid=0x116b19000 waiting for monitor entry [116b18000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at com.ifeve.book.forkjoin.DeadLockDemo$1.run(DeadLockDemo.java:31)
        - waiting to lock <7fb2f3ef8> (a java.lang.String)
        - locked <7fb2f3ec0> (a java.lang.String)
        at java.lang.Thread.run(Thread.java:695)

现在我们介绍下如何避免死锁的几个常见方法。

  • 避免一个线程同时获取多个锁。
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
  • 尝试使用定时锁,使用tryLock(timeout)来替代使用内部锁机制。
  • 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败。

1.3 资源限制的挑战

(1)什么是资源限制?

资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源的限制。比如服务器的带宽只有2M,某个资源的下载速度是1M每秒,系统启动十个线程下载资源,下载速度不会变成10M每秒,所以在进行并发编程时,要考虑到这些资源的限制。硬件资源限制有带宽的上传下载速度,硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和Sorket连接数等。

(2)资源限制引发的问题

并发编程将代码执行速度加速的原则是将代码中串行执行的部分变成并发执行,但是如果某段串行的代码并发执行,但是因为受限于资源的限制,仍然在串行执行,这时候程序不仅不会执行加快,反而会更慢,因为增加了上下文切换和资源调度的时间。例如,之前看到一段程序使用多线程在办公网并发的下载和处理数据时,导致CPU利用率100%,任务几个小时都不能运行完成,后来修改成单线程,一个小时就执行完成了。

 

(3)如何解决资源限制的问题?

对于硬件资源限制,可以考虑使用集群并行执行程序,既然单机的资源有限制,那么就让程序在多机上运行,比如使用ODPS,hadoop或者自己搭建服务器集群,不同的机器处理不同的数据,比如将数据ID%机器数,得到一个机器编号,然后由对应编号的机器处理这笔数据。

对于软件资源限制,可以考虑使用资源池将资源复用,比如使用连接池将数据库和Sorket连接复用,或者调用对方webservice接口获取数据时,只建立一个连接。

 

(4)在资源限制情况下进行并发编程

那么如何在资源限制的情况下,让程序执行的更快呢?根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源,带宽和硬盘读写速度。有数据库操作时,要数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞住,等待数据库连接。

1.4 本章小结

本章介绍了在进行并发编程的时候,大家可能会遇到的几个挑战,并给出了一些解决建议。有的并发程序写的不严谨,在并发下如果出现问题,定位起来会比较耗时和棘手。所以对于Java开发工程师,笔者强烈建议多使用JDK并发包提供的并发容器和工具类来帮你解决并发问题,因为这些类都已经通过了充分的测试和优化,解决了本章提到的几个挑战。

[1] Lmbench3是一个性能分析工具。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: 《Java并发编程的艺术》第一章

17 Jul 03:48

学习笔记:Twitter核心数据类库团队的Hadoop优化经验

by foreach_break

一、来源

Streaming Hadoop Performance Optimization at Scale, Lessons Learned at Twitter

(Data platform @Twitter)

二、观后感
2.1 概要
此稿介绍了Twitter的核心数据类库团队,在使用Hadoop处理离线任务时,使用的性能分析方法,及由此发现的问题和优化手段,对如何使用JVM/HotSpot profile(-Xprof)分析Hadoop Job的方法调用开销、Hadoop配置对象的高开销、MapReduce阶段的排序中对象序列化/反序列的高开销问题及优化等给出了实际可操作的方案。

其介绍了Apache Parquet这一面向列的存储格式,并成功应用于列投影(column project),配合predicated push-down技术,过滤不需要的列,极大提高了数据压缩比和序列化/反序列化的性能。
纯干货。32个赞!

2.2 优化总结
1) Profile!(-Xprofile)性能优化不能靠猜,而应靠分析!
2) 序列化开销很大,但是Hadoop里有许多序列化(操作)!
3) 根据特定(数据)访问模式,选择不同的存储格式(面向行还是面向列)!
4) 使用column projection。
5) 在Hadoop的MR阶段,排序开销很大,使用Raw Comparators以降低开销。
注:此排序针对如Comparator,其会引发序列化/反序列化操作。
6) I/O并不一定就是瓶颈。必要的时候要多I/O换取更少的CPU计算。

JVM/HotSpot原生profile能力(-Xprof),其优点如下:
1) 低开销(使用Stack sampling)。
2) 能揭示开销最大的方法调用。
3) 使用标准输出(Stdout)将结果直接写入Task Logs。

2.3 Hadoop的配置对象

1) Hadoop的Configuration Object开销出人意料的高。
2) Conf的操作看起来就像一个HashMap的操作。

3) 构造函数:读取+解压+分析一个来自磁盘的XML文件

4) get()调用引起正则表达式计算,变量替换。

5) 如果在循环中对上述等方法进行调用,或者每秒一次调用,开销很高.某些(Hadoop)Jobs有30%的时间花在配置相关的方法上!(的确是出人意料的高开销)

总之,没有profile(-Xprof)技术,不可能获取以上洞察,也不可能轻易找到优化的契机和方向,需要使用profile技术来获知I/O和CPU谁才是真正的瓶颈。

2.4 中间结果的压缩

  • Xprof揭示了spill线程中的压缩和解压缩操作消耗了大量时间。
  • 中间结果是临时的。
  • 使用lz4方法取代lzo level 3,减少了30%多的中间数据,使其能被更快地读取。
  • 并使得某些大型Jobs提速150%。

2.5 对记录的序列化和反序列,会成为Hadoop Job中开销最高的操作!

2.6 对记录的序列化是CPU敏感的,相对比之下,I/O都不算什么了!

2.7 如何消除或者减小序列化/反序列化引起的(CPU)开销
2.7.1 使用Hadoop的Raw Comparator API(来比较元素大小)
开销分析:如下图所示,Hadoop的MR在map和reduce阶段,会反序列化map结果的keys以在此阶段进行排序。

(反序列化操作)开销很大,特别是对于复杂的、非原语的keys,而这些keys又很常用。

Hadoop提供了一个RawComparator API,用于对已序列化的(原始的)数据(字节级)进行比较:

不幸的是,需要亲手实现一个自定义的Comparator。

现在,假设数据已序列化后的字节流,本身是易于比较的:
Scala有个很拉风的API,Scala还有一些宏可以产生这些API,以用于:
Tuples , case classes , thrift objects , primitives , Strings,等等数据结构。

怎么拉风法呢?首先,定义一个密集且易于比较的数据序列化(字节)格式:

其次,生成一个用于比较的方法,以利用这种数据格式的优势:

下图是采用上述优化手段后的比较开销对比:

提速到150%!
接着优化!

2.7.2 使用column projection
不要读取不需要的列:

1) 可使用Apache Parquet(列式文件格式)。

2) 使用特别的反序列化手段可以在面向行的存储中跳过一些不需要的字段。

面向列的存储中,一整列按顺序存储(而不是向面向行的存储那样,列是分开存储的):

可以看到,面向列的存储,使得同类型的字段被顺序排在一起(易于压缩):

采用Lzo + Parquet,文件小了2倍多!

2.7.3 Apache Parquet
1) 按列存储,可以有效地进行列投影(column projection)。
2) 可按需从磁盘上读取列。
3) 更重要的是:可以只反序列化需要的列!

看下效果:

可以看到,列数越少,Parquet的威力越大,到40列时,其效率反而不如Lzo Thrift。

  • 在读取所有列的情况下,Parquet一般比面向行的存储慢。
  • Parquet是种密集格式,其读性能和模式中列的数目相关,空值读取也消耗时间。
  • 而面向行的格式(thrift)是稀疏的,所以其读性能和数据的列数相关,空值读取是不消耗时间的。

跳过不需要的字段,如下所示:

  • 虽然,没有降低I/O开销
  • 但是,可以仅将感兴趣的字段编码进对象中
  • 相对于从磁盘读取 + 略过编码后字节的开销,在解码字符串时所花的CPU时间要高的多!

看下各种列映射方案的对比:

Parquet Thrift还有很多优化空间;Parquet在选取的列数小于13列之前,是更快的;此模式相对平坦,且大多数列都被生成了。

  • 还可以采用Predicate Push-Down策略,使得Parquet可以跳过一些不满足过滤条件的数据记录。
  • Parquet存储了一些统计信息,比如记录的chunks,所以在某些场景下,可以通过对这些统计信息进行读取分析,以跳过整个数据块(chunk)。

注:左图为column projection,中图为predicate push-down过滤,右图为组合效果。可以看到很多字段被跳过了,那绝壁可以优化序列化/反序列化的效率。

下图则展示了push-down过滤 + parquet的优化成效:

2.8 结语
感叹:Twitter真是一家伟大的公司!
上述优化手段,集群越大、Hadoop Job越多,效果越明显!

学习笔记:Twitter核心数据类库团队的Hadoop优化经验,首发于博客 - 伯乐在线

15 Jul 09:20

文章: Docker背后的容器集群管理——从Borg到Kubernetes(一)

by 张磊

2015年4月,传闻许久的Borg论文总算出现在了Google Research的页面上。虽然传言Borg作为G家的“老”项目一直是槽点满满,而且本身的知名度和影响力也应该比不上当年的“三大论文”,但是同很多好奇的小伙伴一样,笔者还是饶有兴趣地把这篇“非典型”论文拜读了一番。

注:本文作者张磊将在8月28日~29日的CNUT全球容器技术峰会上分享题为《从0到1:Kubernetes实战》的演讲,演讲中他将重点剖析Kubernetes的核心原理和实践经验,并分享大规模容器集群管理所面临的问题和解决思路。

1. Borg在讲什么故事

其实,如果这篇论文发表在两三年前,Borg的关注度恐怕真没有今天这么高。作为一篇本质上是关于数据中心利用率的半工程、半研究性的成果,这个领域的关注人群来自大厂的运维部以及系统部的技术同僚可能要占很大的比例。而对于绝大多数研究人员,普通开发者,甚至包括平台开发者而言,Borg论文本身的吸引力应该说都是比较有限的。

不过,一旦我们把Borg放到当前这个时间点上来重新审视,这篇本该平淡的论文就拥有了众多深层意义。当然,这一切绝非偶然,从2013年末以Docker为代表的容器技术的迅速兴起,2014年Google容器管理平台Kubernetes和Container Engine的强势扩张,再到如今由Mesos一手打造的DCOS(数据中心操作系统)概念的炙手可热。容器技术令人咋舌的进化速度很快就将一个曾经并不需要被大多数开发人员关注的问题摆到了台面:

我们应该如何高效地抽象和管理一个颇具规模的服务集群?

这,正是Borg全力阐述的核心问题。

说得更确切一点,当我们逐步接纳了以容器为单位部署和运行应用之后,运维人员终于可以从无休止的包管理,莫名其妙的环境差异,繁杂重复的批处理和任务作业的中稍微回过一点神来,开始重新审视自己手中的物理资源的组织和调度方式:即我们能不能将容器看作传统操作系统的进程,把所有的服务器集群抽象成为统一的CPU、内存、磁盘和网络资源,然后按需分配给任务使用呢?

所以,作为《Docker背后的技术解析》系列文章的特别篇,笔者将和读者一起从Borg出发,结合它的同源项目Kubernetes中尝试探索一下这个问题的答案。

2. Borg的核心概念

同大多数PaaS、云平台类项目宣称的口号一样,Borg最基本的出发点还是“希望能让开发者最大可能地把精力集中在业务开发上”,而不需要关心这些代码制品的部署细节。不过,另一方面,Borg非常强调如何对一个大规模的服务器集群做出更合理的抽象,使得开发者可以像对待一台PC一样方便地管理自己的所有任务。这与Mesos现在主推的观点是一致的,同时也是Borg同PaaS类项目比如Flynn、Deis、Cloud Foundry等区别开来的一个主要特征:即Borg,以及Kubernetes和Mesos等,都不是一个面向应用的产物

什么叫面向应用?
就是以应用为中心。系统原生为用户提交的制品提供一系列的上传、构建、打包、运行、绑定访问域名等接管运维过程的功能。这类系统一般会区分”应用“和”服务“,并且以平台自己定义的方式为”应用“(比如Java程序)提供具体的”服务“(比如MySQL服务)。面向应用是PaaS的一个很重要的特点。

另一方面,Borg强调的是规模二字。文章通篇多次强调了Google内部跑在Borg上的作业数量、以及被Borg托管的机器数量之庞大。比如我们传统认知上的“生产级别集群”在文章中基本上属于Tiny的范畴,而Borg随便一个Medium的计算单元拿出来都是一家中大型企业数据中心的规模(10K个机器)。这也应证了淘宝毕玄老大曾经说过的:“规模绝对是推动技术发展的最关键因素”。

Borg里服务器的划分如下: Site = 一组数据中心(Cluster), Cluster = 一组计算单元(Cell), Cell = 一组机器。 其中计算单元(Cell)是最常用的集群类别。

2.1 Job,Task

既然Borg不关心“应用”和“服务”的区别,也不以应用为中心,那么它需要接管和运行的作业是什么?

Job

Borg文章里对Job的定义很简单,就是多个任务(Task)的集合,而所谓Task就是跑在Linux容器里的应用进程了。这样看起来Job是不是就等同于Kubernetes里的Pod(容器组)呢?

其实不然。Job映射到Kubernetes中的话,其实等同于用户提交的“应用”,至于这个应用运行了几个副本Pod,每个Pod里又运行着哪些容器,用户并不需要关心。用户只知道,我们访问这个服务,应该返回某个结果,就够了。

举个例子,因为高可用等原因,用户常常会在Kubernetes里创建并启动若干个一模一样的Pod(这个功能是通过Kubernetes的Replication Controller实现的)。这些一模一样的Pod“副本”的各项配置和容器内容等都完全相同,他们抽象成一个逻辑上的概念就是Job。

由于Job是一个逻辑上的概念,Borg实际上负责管理和调度的实体就是Task。用户的submit、kill、update操作能够触发Task状态机从Pending到Running再到Dead的的转移,这一点论文里有详细的图解。值得一提的是,作者还强调了Task是通过先SIGTERM,一定时间后后再SIGKILL的方式来被杀死的,所以Task在被杀死前有一定时间来进行“清理,保存状态,结束正在处理的请求并且拒绝新的请求”的工作。

2.2 Alloc

Borg中,真正与Pod对应的概念是Alloc。

Alloc的主要功能,就是在一台机器上“划”一块资源出来,然后一组Task就可以运行在这部分资源上。这样,“超亲密”关系的Task就可以被分在同一个Alloc里,比如一个“Tomcat应用”和它的“logstash服务”。

Kubernetes中Pod的设计与Alloc如出一辙:属于同一个Pod的Docker容器共享Network Namepace和volume,这些容器使用localhost来进行通信,可以共享文件,任何时候都会被当作一个整体来进行调度。

所以,Alloc和Pod的设计其实都是在遵循“一个容器一个进程”的模型。经常有人问,我该如何在Docker容器里跑多个进程?其实,这种需求最好是通过类似Pod这种方法来解决:每个进程都跑在一个单独的容器里,然后这些容器又同属于一个Pod,共享网络和指定的volume。这样既能满足这些进程之间的紧密协作(比如通过localhost互相访问,直接进行文件交换),又能保证每个进程不会挤占其他进程的资源,它们还能作为一个整体进行管理和调度。如果没有Kubernetes的话,Pod可以使用“Docker in Docker”的办法来模拟,即使用一个Docker容器作为Pod,真正需要运行的进程作为Docker容器嵌套运行在这个Pod容器中,这样它们之间互不干涉,又能作为整体进调度。

另外,Kubernetes实际上没有Job这个说法,而是直接以Pod和Task来抽象用户的任务,然后使用相同的Label来标记同质的Pod副本。这很大程度是因为在Borg中Job Task Alloc的做法里,会出现“交叉”的情况,比如属于不同Job的Task可能会因为“超亲密”关系被划分到同一个Alloc中,尽管此时Job只是个逻辑概念,这还是会给系统的管理带来很多不方便。

2.3 Job的分类

Borg中的Job按照其运行特性划分为两类:LRS(Long Running Service)和batch jobs。

上述两种划分在传统的PaaS中也很常见。LRS类服务就像一个“死循环”,比如一个Web服务。它往往需要服务于用户或者其它组件,故对延时敏感。当然论文里Google举的LRS例子就要高大上不少,比如Gmail、Google Docs。

而batch jobs类任务最典型的就是Map-Reduce的job,或者其它类似的计算任务。它们的执行往往需要持续一段时间,但是最终都会停止,用户需要搜集并汇总这些job计算得到的结果或者是job出错的原因。所以Borg在Google内部起到了YARN和Mesos的角色,很多项目通过在Borg之上构建framework来提交并执行任务。Borg里面还指出,batch job对服务器瞬时的性能波动是不敏感的,因为它不会像LRS一样需要立刻响应用户的请求,这一点可以理解。

比较有意思的是,Borg中大多数LRS都会被赋予高优先级并划分为生产环境级别的任务(prod),而batch job则会被赋予低优先级(non-prod)。在实际环境中,prod任务会被分配和占用大部分的CPU和内存资源。正是由于有了这样的划分,Borg的“资源抢占”模型才得以实现,即prod任务可以占用non-prod任务的资源,这一点我们后面会专门说明。

对比Kubernetes,我们可以发现在LRS上定义上是与Borg类似的,但是目前Kubernetes却不能支持batch job:因为对应的Job Controller还没有实现。这意味着当前Kubernetes上一个容器中的任务执行完成退出后,会被Replication Controller无条件重启。Kubernetes尚不能按照用户的需求去搜集和汇总这些任务执行的结果。

2.4 优先级和配额

前面已经提到了Borg任务优先级的存在,这里详细介绍一下优先级的划分。

Borg中把优先级分类为监控级、生产级、批任务级、尽力级(也叫测试级)。其中监控级和生产级的任务就是前面所说的prod任务。为了避免在抢占资源的过程中出现级联的情况触发连锁反应(A抢占B,B抢占C,C再抢占D),Borg规定prod任务不能互相抢占

如果说优先级决定了当前集群里的任务的重要性,配额则决定了任务是否被允许运行在这个集群上。

尽管我们都知道,对于容器来说,CGroup中的配额只是一个限制而并非真正割据的资源量,但是我们必须为集群设定一个标准来保证提交来任务不会向集群索要过分多的资源。Borg中配额的描述方法是:该用户的任务在一段时间内在某一个计算单元上允许请求的最大资源量。需要再次重申,配额一定是任务提交时就需要验证的,它是任务合法性的一部分。

既然是配额,就存在超卖的情况。在Borg中,允许被超卖的是non-prod的任务,即它们在某个计算单元上请求的资源可能超出了允许的额度,但是在允许超卖的情况下它们仍然有可能被系统接受(虽然很可能由于资源不足而暂时进入Pending状态)。而优先级最高的任务则被Borg认为是享有无限配额的。

与Kubernetes类似的是,Borg的配额也是管理员静态分配的。Kubernetes通过用户空间(namespace)来实现了一个简单的多租户模型,然后为每一个用户空间指定一定的配额,比如:

apiVersion: v1beta3
kind: ResourceQuota
metadata:
  name: quota
spec:
  hard:
    cpu: "20"
    memory: 10Gi
    pods: "10"
    replicationcontrollers: "20"
    resourcequotas: "1"
    services: "5"

到这里,我们有必要多说一句。像Borg、Kubernetes以及Mesos这类项目,它们把系统中所有需要对象都抽象成了一种“资源”保存在各自的分布式键值存储中,而管理员则使用如上所示的“资源描述文件”来进行这些对象的创建和更新。这样,整个系统的运行都是围绕着“资源”的增删改查来完成的,各组件的主循环遵循着“检查对象”、“对象变化”、“触发事件”、“处理事件”这样的周期来完成用户的请求。这样的系统有着一个明显的特点就是它们一般都没有引入一个消息系统来进行事件流的协作,而是使用“ectd”或者“Zookeeper”作为事件系统的核心部分。

2.5 名字服务和监控

与Mesos等不同,Borg中使用的是自家的一致性存储项目Chubby来作为分布式协调组件。这其中存储的一个重要内容就是为每一个Task保存了一个DNS名字,这样当Task的信息发生变化时,变更能够通过Chubby及时更新到Task的负载均衡器。这同Kubernetes通过Watch监视etcd中Pod的信息变化来更新服务代理的原理是一样的,但是由于使用了名为“Service”的服务代理机制(Service可以理解为能够自动更新的负载均衡组件),Kubernetes中默认并没有内置名字服务来进行容器间通信(但是提供了插件式的DNS服务供管理员选用)。

在监控方面,Borg中的所有任务都设置了一个健康检查URL,一旦Borg定期访问某个Task的URL时发现返回不符合预期,这个Task就会被重启。这个过程同Kubernetes在Pod中设置health_check是一样的,比如下面这个例子:

apiVersion: v1beta3
kind: Pod
metadata:
  name: pod-with-healthcheck
spec:
  containers:
    - name: nginx
      image: nginx
      # defines the health checking
      livenessProbe:
        # an http probe
        httpGet:
          path: /_status/healthz
          port: 80
        # length of time to wait for a pod to initialize
        # after pod startup, before applying health checking
        initialDelaySeconds: 30
        timeoutSeconds: 1
      ports:
        - containerPort: 80

这种做法的一个小缺点是Task中服务的开发者需要自己定义好这些/healthzURL和对应的响应逻辑。当然,另一种做法是可以在容器里内置一些“探针”来完成很多健康检查工作而做到对用户的开发过程透明。

除了健康检查,Borg对日志的处理也很值得借鉴。Borg中Task的日志会在Task退出后保留一段时间,方便用户进行调试。相比之下目前大多数PaaS或者类似项目的容器退出后日志都会立即被删除(除非用户专门做了日志存储服务)。

最后,Borg轻描淡写地带过了保存event做审计的功能。这其实与Kubernetes的event功能也很类似,比如Kube的一条event的格式类似于:

发生时间 结束时间 重复次数 资源名称 资源类型 子事件 发起原因 发起者 事件日志 

3. Borg的架构与设计

Borg的架构与Kubernetes的相似度很高,在每一个Cell(工作单元)里,运行着少量Master节点和大量Worker节点。其中,Borgmaster负责响应用户请求以及所有资源对象的调度管理;而每个工作节点上运行着一个称为Borglet的Agent,用来处理来自Master的指令。这样的设计与Kubernetes是一致的,Kubernetes这两种节点上的工作进程分别是:

Master:
apiserver, controller-manager, scheduler
Minion:
kube-proxy, kubelet

虽然我们不清楚Borg运行着的工作进程有哪些,但单从功能描述里面我们不难推测到至少在Master节点上两者的工作进程应该是类似的。不过,如果深入到论文中的细节的话,我们会发现Borg在Master节点上的工作要比Kubernetes完善很多。

3.1 Borgmaster

首先,Borgmaster由一个独立的scheduler和主Borgmaster进程组成。其中,主进程负责响应来自客户端的RPC请求,并且将这些请求分为“变更类”和“只读”类。

在这一点上Kubernetes的apiserver处理方法类似,kuber的API服务被分为“读写”(GET,POST,PUT,DELETE)和“只读”(GET)两种,分别由6443和7080两个不同的端口负责响应,并且要求“读写”端口6443只能以HTTPS方式进行访问。同样,Kubernetes的scheduler也是一个单独的进程。

但是,相比Kubernetes的单点Master,Borgmaster是一个由五个副本组成的集群。每一个副本都在内存中都保存了整个Cell的工作状态,并且使用基于Paxos的Chubby项目来保存这些信息和保证信息的一致性。Borgmaster中的Leader是也是集群创建的时候由Paxos选举出来的,一旦这个Leader失败,Chubby将开始新一轮的选举。论文中指出,这个重选举到恢复正常的过程一般耗时10s,但是在比较大的Cell里的集群会由于数据量庞大而延长到一分钟。

更有意思的是,Borgmaster还将某一时刻的状态通过定时做快照的方式保存成了checkpoint文件,以便管理员回滚Borgmaster的状态,从而进行调试或者其他的分析工作。基于上述机制,Borg还设计了一个称为Fauxmaster的组件来加载checkpoint文件,从而直接进入某时刻Borgmaster的历史状态。再加上Fauxmaster本身为kubelet的接口实现了“桩”,所以管理员就可以向这个Fauxmaster发送请求来模拟该历史状态数据下Borgmaster的工作情况,重现当时线上的系统状况。这个对于系统调试来说真的是非常有用。此外,上述Fauxmaster还可以用来做容量规划,测试Borg系统本身的变更等等。这个Fauxmaster也是论文中第一处另我们眼前一亮的地方。

上述些特性使得Borg在Master节点的企业级特性上明显比Kubernetes要成熟得多。当然,值得期待的是Kube的高可用版本的Master也已经进入了最后阶段,应该很快就能发布了。

3.2 Borg的调度机制

用户给Borg新提交的任务会被保存在基于Paxos的一致性存储中并加入到等待队列。Borg的scheduler会异步地扫描这个队列中的任务,并检查当前正在被扫描的这个任务是否可以运行在某台机器上。上述扫描的顺序按照任务优先级从高到低来Round-Robin,这样能够保证高优先级任务的可满足性,避免“线头阻塞”的发生(某个任务一直不能完成调度导致它后面的所有任务都必须进行等待)。每扫描到一个任务,Borg即使用调度算法来考察当前Cell中的所有机器,最终选择一个合适的节点来运行这个任务。

此算法分两阶段:

第一,可行性检查。这个检查每个机器是所有符合任务资源需求和其它约束(比如指定的磁盘类型),所以得到的结果一般是个机器列表。需要注意的是在可行性检查中,一台机器“资源是否够用”会考虑到抢占的情况,这一点我们后面会详细介绍。

第二,打分。这个过程从上述可行的机器列表中通过打分选择出分数最高的一个。

这里重点看打分过程。Borg设计的打分标准有如下几种:

  1. 尽量避免发生低优先级任务的资源被抢占;如果避免不了,则让被抢占的任务数量最少、优先级最低;
  2. 挑选已经安装了任务运行所需依赖的机器;
  3. 使任务尽量分布在不同的高可用域当中;
  4. 混合部署高优先级和低优先级任务,这样在流量峰值突然出现后,高优先级可以抢占低优先级的资源(这一点很有意思)。

此行文本用于列表编号,不因该出现在正文中。

Borg其实曾经使用过E-PVM模型(简单的说就是把所有打分规则按照一定算法综合成一种规则)来进行打分的。但是这种调度的结果是任务最终被平均的分散到了所有机器上,并且每台机器上留出了一定的空闲空间来应对压力峰值。这直接造成了整个集群资源的碎片化。

与上述做法的相反的是另一个极端,即尽量让所有的机器都填满。但是这将导致任务不能很好的应对突发峰值。而且Borg或者用户对于任务所需的资源配额的估计往往不是很准确,尤其是对于batch job来说,它们所请求的资源量默认是很少的(特别是CPU资源)。所以在这种调度策略下batch job会很容易被填充在狭小的资源缝隙中,这时一旦遇到压力峰值,不仅batch job会出问题,与它运行在同一台机器上的LRS也会遭殃。

而Borg采用的是“混部加抢占”的模式,这种做法集成了上述两种模型的优点:兼顾公平性和利用率。这其中,LRS和batch job的混部以及优先级体系的存在为资源抢占提供了基础。这样,Borg在“可行性检查”阶段就可以考虑已经在此机器上运行的任务的资源能被抢占多少。如果算上可以抢占的这部分资源后此机器可以满足待调度任务的需求的话,任务就会被认为“可行”。接下,Borg会按优先级低到高“kill”这台机器上的任务直到满足待运行任务的需求,这就是抢占的具体实施过程。当然,被“kill”的任务会重新进入了调度队列,等待重新调度。

另一方面Borg也指出在任务调度并启动的过程中,安装依赖包的过程会构成80%的启动延时,所以调度器会优先选择已经安装好了这些依赖的机器。这让我想起来以前使用VMware开发的编排系统BOSH时,它的每一个Job都会通过spec描述自己依赖哪些包,比如GCC。所以当时为了节省时间,我们会在部署开始前使用脚本并发地在所有目标机器上安装好通用的依赖,比如Ruby、GCC这些,然后才开始真正的部署过程。 事实上,Borg也有一个类似的包分发的过程,而且使用的是类似BitTorrent的协议。

这时我们回到Kubernetes上来,不难发现它与Borg的调度机制还比较很类似的。这当然也就意味着Kubernetes中没有借鉴传说中的Omega共享状态调度(反倒是Mesos的Roadmap里出现了类似”乐观并发控制“的概念)。

Kubernetes的调度算法也分为两个阶段:

  • “Predicates过程”:筛选出合格的Minion,类似Borg的“可行性检查”。这一阶段Kubernetes主要需要考察一个Minion的条件包括:
  • 容器申请的主机端口是否可用
  • 其资源是否满足Pod里所有容器的需求(仅考虑CPU和Memory,且没有抢占机制)
  • volume是否冲突
  • 是否匹配用户指定的Label
  • 是不是指定的hostname

“Priorities过程”:对通过上述筛选的Minon打分,这个打分的标准目前很简单:

  • 选择资源空闲更多的机器
  • 属于同一个任务的副本Pod尽量分布在不同机器上

从调度算法实现上差异中,我们可以看到Kubernetes与Borg的定位有着明显的不同。Borg的调度算法中资源抢占和任务混部是两个关键点,这应是考虑到了这些策略在Google庞大的机器规模上所能带来的巨大的成本削减。所以Borg在算法的设计上强调了混部状态下对资源分配和任务分布的优化。而Kubernetes明显想把调度过程尽量简化,其两个阶段的调度依据都采用了简单粗暴的硬性资源标准,而没有支持任何抢占策略,也没有优先级的说法。当然,有一部分原因是开源项目的用户一般都喜欢定制自己的调度算法,从这一点上来说确实是“less is more”。总之,最终的结果是尽管保留了Borg的影子(毕竟作者很多都是一伙人),Kubernetes调度器的实现上却完全是另外一条道路,确切的说更像Swarm这种偏向开发者的编排项目。

此外,还有一个非常重要的因素不得不提,那就是Docker的镜像机制。Borg在Google服役期间所使用的Linux容器虽然应用极广且规模庞大,但核心功能还是LXC的变体或者强化版,强调的是隔离功能。这一点从它的开源版项目lmctfy的实现,以及论文里提到需要考虑任务依赖包等细节上我们都可以推断出来。可是Docker的厉害之处就在于直接封装了整个Job的运行环境,这使得Kubernetes在调度时可以不必考虑依赖包的分布情况,并且可以使用Pod这样的“原子容器组”而不是单个容器作为调度单位。当然,这也提示了我们将来进行Docker容器调度时,其实也可以把镜像的分布考虑在内:比如事先在所有工作节点上传基础镜像;在打分阶段优先选择任务所需基础镜像更完备的节点。

如果读者想感受一下没有镜像的Docker容器是什么手感,不妨去试用一下DockerCon上刚刚官宣的runc项目(https://github.com/opencontainers/runc)。runc完全是一个libcontainer的直接封装,提供所有的Docker容器必备功能,但是没有镜像的概念(即用户需要自己指定rootfs环境),这十分贴近lmctfy等仅专注于隔离环境的容器项目。

3.3 Borglet

离开了Borgmaster节点,我们接下来看一下工作节点上的Borglet组件,它的主要工作包括:

启停容器,进行容器失败恢复,通过kernel参数操作和管理OS资源,清理系统日志,收集机器状态供Borgmaster及其他监控方使用。

这个过程中,Borgmaster会通过定期轮询来检查机器的状态。这种主动poll的做法好处是能够大量Borglet主动汇报状态造成流量拥塞,并且能防止“恢复风暴”(比如大量失败后恢复过来的机器会在同段一时间不停地向Borgmaster发送大量的恢复数据和请求,如果没有合理的拥塞控制手段,者很可能会阻塞整个网络或者直接把master拖垮掉)。一旦收到汇报信息后,充当leader的Borgmaster会根据这些信息更新自己持有的Cell状态数据。

这个过程里,集群Borgmaster的“优越性”再次得到了体现。Borgmaster的每个节点维护了一份无状态的“链接分片(link shard)”。每个分片只负责一部分Borglet机器的状态检查,而不是整个Cell。而且这些分片还能够汇集并diif这些状态信息,最后只让leader获知并更新那些发生了变化的数据。这种做法有效地降低了Borgmaster的工作负载。

当然,如果一个Borglet在几个poll周期内都没有回应,他就会被认为宕机了。原本运行在整个节点上的任务容器会进入重调度周期。如果此期间Borglet与master的通信恢复了,那么master会请求杀死那些被重调度的任务容器,以防重复。Borglet的运行并不需要依赖于Borgmaster,及时master全部宕机,任务依然可以正常运行。

与Borg相比,Kubernetes则选择了方向相反的状态汇报策略。当一个kubelet进程启动后,它会主动将自己注册给master节点上的apiserver。接下来,kubelet会定期向apiserver更新自己对应的node的信息,如果一段时间内没有更新,则master就会认为此工作节点已经发生故障。上述汇报信息的收集主要依赖于每个节点上运行的CAdvisor进程,而并非直接与操作系统进行交互。

事实上,不止kubelet进程会这么做。Kubernetes里的所有组件协作,都会采用主动去跟apiServer建立联系,进而通过apiserver来监视、操作etcd的资源来完成相应的功能。

举个例子,用户向apiserver发起请求表示要创建一个Pod,在调度器选择好了某个可用的minion后apiserver并不会直接告诉kubelet说我要在这个机器上创建容器,而是会间接在etcd中创建一个“boundPod”对象(这个对象的意思是我要在某个kubelet机器上绑定并运行某个Pod)。与此同时,kubelet则定时地主动检查有没有跟自己有关的“boundPod”,一旦发现有,它就会按照这个对象保存的信息向Docker Daemon发起创建容器的请求。

这正是Kubernetes设计中“一切皆资源”的体现,即所有实体对象,消息等都是作为etcd里保存起来的一种资源来对待,其他所有协作者要么通过监视这些资源的变化来采取动作,要么就是通过apiserver来对这些资源进行增删改查。

所以,我们可以把Kubernetes的实现方法描述为“面向etcd的编程模式”。这也是Kubernetes与Borg设计上的又一个不同点,说到底还是规模存在的差异:即Kubernetes认为它管理的集群中不会存在那么多机器同时向apiserver发起大量的请求。这也从另一个方面表现出了作者们对etcd响应能力还是比较有信心的。

3.4 可扩展性

这一节里与其说在Borg的可扩展性,倒不如说在讲它如何通过各种优化实现了更高的可扩展性。

首先是对Borgmaster的改进。最初的Borgmaster就是一个同步循环,在循环过程中顺序进行用户请求响应、调度、同Borglet交互等动作。所以Borg的第一个改进就是将调度器独立出来,从而能够同其他动作并行执行。改进后的调度器使用Cell集群状态的缓存数据来不断重复以下操作:

  • 从Borgmaster接受集群的状态变化
  • 更新本地的集群状态缓存数据
  • 对指定的Task执行调度工作
  • 将调度结果告诉Borgmaster

这些操作组成了调度器的完整工作周期。

其次,Borgmaster上负责响应只读请求和同Borglet进行交互的进程也被独立出来,通过职责的单一性来保证各自的执行效率。这些进程会被分配在Borgmaster的不同副本节点上来进一步提高效率(只负责同本副本节点所管理的那部分Worker节点进行交互)。

最后是专门针对调度器的优化。

缓存机器的打分结果。毕竟每次调度都给所有机器重新打一次分确实很无聊。只有当机器信息或者Task发生了变化(比如任务被从这个机器上调度走了)时,调度器缓存的机器分数才会发生更新。而且,Borg会忽略那些不太明显的资源变化,减少缓存的更新次数。

划分Task等价类。Borg的调度算法针对的是一组需求和约束都一样的Task(等价类)而不是单个Task来执行的。

随机选择一组机器来做调度。这是很有意思的一种做法,即Borg调度器并不会把Cell里的所有机器拿过来挨个进行可行性检查,而是不断地随机挑选一个机器来检查可行性,判断是否通过,再挑选下一个,直到通过筛选的机器达到一定的数目。然后再在这些通过筛选的机器集合里进行打分过程。这个策略与著名的Sparrow调度器的做法很类似。

这些优化方法大大提高了Borg的工作效率,作者在论文中指出在上述功能被禁掉,有些原来几百秒完成的调度工作需要几天才能完全完成。

4. 可用性

Borg在提高可用性方面所做的努力与大多数分布式系统的做法相同。比如:

  • 自动重调度失败的任务
  • 将同一Job的不同任务分布在不同的高可用域
  • 在机器或者操作系统升级的过程中限制允许的任务中断的次数和同时中断的任务数量
  • 保证操作的幂等性,这样当客户端失败时它可以放心的发起重试操作
  • 当一台机器失联后,任务重调度的速度会被加以限制,因为Borg不能确定失联的原因是大规模的机器失败(比如断电),还是部分网络错误。
  • 任务失败后,在一段时间内在本地磁盘保留日志及其他关键数据,哪怕对应的任务已经被杀死或者调度到其他地方了

最后也是最重要的,Borglet的运行不依赖于master,所以哪怕控制节点全部宕机,用户提交的任务依然正常运行。

在这一部分,Kubernetes也没有特别的设计。毕竟,在任务都已经容器化的情况下,只要正确地处理好容器的调度和管理工作,任务级别高可用的达成并不算十分困难。

至此,论文的前四章我们就介绍完了。通过与Kubernetes的实现作比较,我们似乎能得到一个“貌合神离”的结论。即Kubernetes与Borg从表面上看非常相似:相同的架构,相似的调度算法,当然还有同一伙开发人员。但是一旦我们去深入一些细节就会发现,在某些重要的设计和实现上,Borg似乎有着和Kubernetes截然不同的认识:比如完全相反的资源汇报方向,复杂度根本不在一个水平上的Master实现(集群VS单点),对batch job的支持(Kubernetes目前不支持batch job),对于任务优先级和资源抢占的看法等等。

这些本来可以照搬的东西,为什么在Kubernetes又被重新设计了一遍呢?在本文的第二部分,我们将一步步带领读者领悟造成这些差异的原因,即:资源回收和利用率优化。敬请关注。

作者简介

张磊,浙江大学博士,科研人员, VLIS lab云计算团队技术负责人、策划人

参考文献

  • http://research.google.com/pubs/pub43438.html
  • https://github.com/googlecloudplatform/kubernetes

感谢郭蕾对本文的审校。

给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入InfoQ读者交流群InfoQ好读者)。

15 Jul 00:51

10行Java代码实现最近被使用(LRU)缓存

by paddx

在最近的面试中,我曾被多次问到,怎么实现一个最近最少使用(LRU)的缓存。缓存可以通过哈希表来实现,然而为这个缓存增加大小限制会变成另一个有意思的问题。现在我们看一下怎么实现。

最近最少使用缓存的回收

为了实现缓存回收,我们需要很容易做到:

  • 查询出最近最晚使用的项
  • 给最近使用的项做一个标记

链表可以实现这两个操作。检测最近最少使用的项只需要返回链表的尾部。标记一项为最近使用的项只需要从当前位置移除,然后将该项放置到头部。比较困难的事情是怎么快速的在链表中找到该项。

哈希表的帮助

看一下我们工具箱中的数据结构,哈希表可以在(消耗)常量的时间内索引到某个对象。如果我们创建一个形如key->链表节点的哈希表,我们就能够在常量时间内找到最近使用的节点。更甚的是,我们也能够在常量时间内判断节点的是否存在(或不存在);

找到这个节点后,我们就能将这个节点移动到链表的最前端,标记为最近使用的项了。

Java的捷径

据我所知,很少有一种编程语言的标准库中有通用的数据结构能提供上述功能的。这是一种混合的数据结构,我们需要在哈希表的基础上建立一个链表。但是Java已经为我们提供了这种形式的数据结构-LinkedHashMap!它甚至提供可覆盖回收策略的方法(见removeEldestEntry文档)。唯一需要我们注意的事情是,改链表的顺序是插入的顺序,而不是访问的顺序。但是,有一个构造函数提供了一个选项,可以使用访问的顺序(见文档)。

无需多说:

import java.util.LinkedHashMap;
import java.util.Map;

public LRUCache<K, V> extends LinkedHashMap<K, V> {
  private int cacheSize;

  public LRUCache(int cacheSize) {
    super(16, 0.75, true);
    this.cacheSize = cacheSize;
  }

  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() >= cacheSize;
  }
}

相关文章

13 Jul 09:43

关于Java集合的小抄

by importnewzz

在尽可能短的篇幅里,将所有集合与并发集合的特征,实现方式,性能捋一遍。适合所有”精通Java”其实还不那么自信的人阅读。

不断更新中,请尽量访问博客原文

List

ArrayList

以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组,因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。

按数组下标访问元素–get(i)/set(i,e) 的性能很高,这是数组的基本优势。

直接在数组末尾加入元素–add(e)的性能也高,但如果按下标插入、删除元素–add(i,e), remove(i), remove(e),则要用System.arraycopy()来移动部分受影响的元素,性能就变差了,这是基本劣势。

LinkedList

以双向链表实现。链表无容量限制,但双向链表本身使用了更多空间,也需要额外的链表指针操作。

按下标访问元素–get(i)/set(i,e) 要悲剧的遍历链表将指针移动到位(如果i>数组大小的一半,会从末尾移起)。

插入、删除元素时修改前后节点的指针即可,但还是要遍历部分链表的指针才能移动到下标所指的位置,只有在链表两头的操作–add(), addFirst(),removeLast()或用iterator()上的remove()能省掉指针的移动。

CopyOnWriteArrayList

并发优化的ArrayList。用CopyOnWrite策略,在修改时先复制一个快照来修改,改完再让内部指针指向新数组。

因为对快照的修改对读操作来说不可见,所以只有写锁没有读锁,加上复制的昂贵成本,典型的适合读多写少的场景。如果更新频率较高,或数组较大时,还是Collections.synchronizedList(list),对所有操作用同一把锁来保证线程安全更好。

增加了addIfAbsent(e)方法,会遍历数组来检查元素是否已存在,性能可想像的不会太好。

补充

无论哪种实现,按值返回下标–contains(e), indexOf(e), remove(e) 都需遍历所有元素进行比较,性能可想像的不会太好。

没有按元素值排序的SortedList,在线程安全类中也没有无锁算法的ConcurrentLinkedList,凑合着用Set与Queue中的等价类时,会缺少一些List特有的方法。

Map

HashMap

以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。

插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),Entry用一个next属性实现多个Entry以单向链表存放,后入桶的Entry将next指向桶当前的Entry。

查找哈希值为17的key时,先定位到第一个哈希桶,然后以链表遍历桶里所有元素,逐个比较其key值。

当Entry数量达到桶数量的75%时(很多文章说使用的桶数量达到了75%,但看代码不是),会成倍扩容桶数组,并重新分配所有原来的Entry,所以这里也最好有个预估值。

取模用位运算(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。

iterator()时顺着哈希桶数组来遍历,看起来是个乱序。

在JDK8里,新增默认为8的閥值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。

LinkedHashMap

扩展HashMap增加双向链表的实现,号称是最占内存的数据结构。支持iterator()时按Entry的插入顺序来排序(但是更新不算, 如果设置accessOrder属性为true,则所有读写访问都算)。

实现上是在Entry上再增加属性before/after指针,插入时把自己加到Header Entry的前面去。如果所有读写访问都要排序,还要把前后Entry的before/after拼接起来以在链表中删除掉自己。

TreeMap

以红黑树实现,篇幅所限详见入门教程。支持iterator()时按Key值排序,可按实现了Comparable接口的Key的升序排序,或由传入的Comparator控制。可想象的,在树上插入/删除元素的代价一定比HashMap的大。

支持SortedMap接口,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段。

ConcurrentHashMap

并发优化的HashMap,默认16把写锁(可以设置更多),有效分散了阻塞的概率,而且没有读锁。
数据结构为Segment[],Segment里面才是哈希桶数组,每个Segment一把锁。Key先算出它在哪个Segment里,再算出它在哪个哈希桶里。

支持ConcurrentMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现CAS的replace(key, oldValue, newValue)。

没有读锁是因为put/remove动作是个原子动作(比如put是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。

ConcurrentSkipListMap

JDK6新增的并发优化的SortedMap,以SkipList实现。SkipList是红黑树的一种简化替代方案,是个流行的有序集合算法,篇幅所限见入门教程。Concurrent包选用它是因为它支持基于CAS的无锁算法,而红黑树则没有好的无锁算法。

很特殊的,它的size()不能随便调,会遍历来统计。

补充

关于null,HashMap和LinkedHashMap是随意的,TreeMap没有设置Comparator时key不能为null;ConcurrentHashMap在JDK7里value不能为null(这是为什么呢?),JDK8里key与value都不能为null;ConcurrentSkipListMap是所有JDK里key与value都不能为null。

Set

Set几乎都是内部用一个Map来实现, 因为Map里的KeySet就是一个Set,而value是假值,全部使用同一个Object。Set的特征也继承了那些内部Map实现的特征。

  • HashSet:内部是HashMap。
  • LinkedHashSet:内部是LinkedHashMap。
  • TreeSet:内部是TreeMap的SortedSet。
  • ConcurrentSkipListSet:内部是ConcurrentSkipListMap的并发优化的SortedSet。
  • CopyOnWriteArraySet:内部是CopyOnWriteArrayList的并发优化的Set,利用其addIfAbsent()方法实现元素去重,如前所述该方法的性能很一般。

补充:好像少了个ConcurrentHashSet,本来也该有一个内部用ConcurrentHashMap的简单实现,但JDK偏偏没提供。Jetty就自己封了一个,Guava则直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 实现。

Queue

Queue是在两端出入的List,所以也可以用数组或链表来实现。

–普通队列–

LinkedList

是的,以双向链表实现的LinkedList既是List,也是Queue。它是唯一一个允许放入null的Queue。

ArrayDeque

以循环数组实现的双向Queue。大小是2的倍数,默认是16。

普通数组只能快速在末尾添加元素,为了支持FIFO,从数组头快速取出元素,就需要使用循环数组:有队头队尾两个下标:弹出元素时,队头下标递增;加入元素时,如果已到数组空间的末尾,则将元素循环赋值到数组[0](如果此时队头下标大于0,说明队头弹出过元素,有空位),同时队尾下标指向0,再插入下一个元素则赋值到数组[1],队尾下标指向1。如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。

PriorityQueue

用二叉堆实现的优先级队列,详见入门教程,不再是FIFO而是按元素实现的Comparable接口或传入Comparator的比较结果来出队,数值越小,优先级越高,越先出队。但是注意其iterator()的返回不会排序。

–线程安全的队列–

ConcurrentLinkedQueue/ConcurrentLinkedDeque

无界的并发优化的Queue,基于链表,实现了依赖于CAS的无锁算法。

ConcurrentLinkedQueue的结构是单向链表和head/tail两个指针,因为入队时需要修改队尾元素的next指针,以及修改tail指向新入队的元素两个CAS动作无法原子,所以需要的特殊的算法,篇幅所限见入门教程

PriorityBlockingQueue

无界的并发优化的PriorityQueue,也是基于二叉堆。使用一把公共的读写锁。虽然实现了BlockingQueue接口,其实没有任何阻塞队列的特征,空间不够时会自动扩容。

DelayQueue

内部包含一个PriorityQueue,同样是无界的。元素需实现Delayed接口,每次调用时需返回当前离触发时间还有多久,小于0表示该触发了。
pull()时会用peek()查看队头的元素,检查是否到达触发时间。ScheduledThreadPoolExecutor用了类似的结构。

–线程安全的阻塞队列–

BlockingQueue的队列长度受限,用以保证生产者与消费者的速度不会相差太远,避免内存耗尽。队列长度设定后不可改变。当入队时队列已满,或出队时队列已空,不同函数的效果见下表:

可能报异常 返回布尔值 可能阻塞等待 可设定等待时间
入队 add(e) offer(e) put(e) offer(e, timeout, unit)
出队 remove() poll() take() poll(timeout, unit)
查看 element() peek()

ArrayBlockingQueue

定长的并发优化的BlockingQueue,基于循环数组实现。有一把公共的读写锁与notFull、notEmpty两个Condition管理队列满或空时的阻塞状态。

LinkedBlockingQueue/LinkedBlockingDeque

可选定长的并发优化的BlockingQueue,基于链表实现,所以可以把长度设为Integer.MAX_VALUE。利用链表的特征,分离了takeLock与putLock两把锁,继续用notEmpty、notFull管理队列满或空时的阻塞状态。

补充

JDK7有个LinkedTransferQueue,transfer(e)方法保证Producer放入的元素,被Consumer取走了再返回,比SynchronousQueue更好,有空要学习下。

可能感兴趣的文章

10 Jul 00:57

Unit testing a TCP stack

10 Jul 00:54

Scala Collections 提示和技巧

by 鸟窝

原文:Scala Collections Tips and Tricks,
作者Pavel Fatin是JetBrains 的一名员工,为神器IntelliJ IDEA开发Scala插件。
受其工作Scala Collections inspections)的启发,他整理了这个关于Java Collections API技巧的列表。
一些技巧只在一些微妙的实现细节中体现,但是大部分技巧都是一般的常识,但是在大部分情况下被忽视了。
性和谐提示和技巧很有价值,可以帮你深入理解Scala Collections,可以是你的代码更快更简洁。

图例 Legend

为了使下面的例子更容易理解,这里列出了一些约定:

  • seq— 一个Seq-based的集合, 比如Seq(1, 2, 3)
  • set— 一个Set实例, 比如Set(1, 2, 3)
  • array— 一个数组, 比如Array(1, 2, 3)
  • option— 一个Option, 比如Some(1)
  • map— 一个Map, 比如Map(1 -> "foo", 2 -> "bar")
  • p— 一个断言predicate函数,类型T => Boolean, 比如_ > 2
  • n— 一个整数
  • i— 一个索引
  • f, g— 简单函数,A => B
  • x, y— 一些字面值(arbitrary values)
  • z— 初始值或者缺省值

组合Composition

记住,尽管这些技巧都是独立和自包含的,我们还是可以将它们组合起来逐步迭代得到一个更高级的表达式,比如下面的例子,在一个Seq中检查一个元素是否存在:

123456789
seq.filter(_ == x).headOption != None// Via seq.filter(p).headOption -> seq.find(p)seq.find(_ == x) != None// Via option != None -> option.isDefinedseq.find(_ == x).isDefined// Via seq.find(p).isDefined -> seq.exists(p)seq.exists(_ == x)// Via seq.exists(_ == x) -> seq.contains(x)seq.contains(x)

我们可以依赖”substitution model of recipe application“ (SICP))来简化复杂的表达式。

副作用 Side effects

”Side effects“是一个基础概念,在函数式编程语言中会有这个概念。 Scala有一个PPT专门介绍:Side-effect checking for Scala
基本上,”Side effects“是这样一个动作, 除了返回一个值外,外部函数或者表达式还观察到此动作还有以下行为之一:

  • 有输入输出操作 (比如文件,网络I/O)
  • 对外部变量有修改
  • 外部对象的状态有改变
  • 抛出异常

当一个函数或者表达式有以上任何一种情况时,我们就说它有副作用(Side effects),否则我们就说它是"纯"的函数或者表达式 (pure)。

side effects有什么大不了的?当有副作用时,计算的顺序不能随便改变。 比如下面两个"纯" (pure)表达式:

12
val x = 1 + 2val y = 2 + 3

因为它们没有副作用,两个表达式可以互换位置,先x后y和先y后x的效果一样。
如果有副作用 (有控制台输出):

12
val x = { print("foo"); 1 + 2 }val y = { print("bar"); 2 + 3 }

两个表达式不能互换位置,因为一旦互换位置,输出结果的顺序变了。
所以副作用的一个影响就是会减少可能的转换的数量(reduces the number of possible transformations),包括可能的简化和优化。
同样的原因可适用用Collection相关的表达式上。看一个外部的builder变量(副作用方法append):

1
seq filter { x => builder.append(x); x > 3 } headOption

原则上seq.filter(p).headOption可以简化为seq.find(p),但是副作用阻止我们这么做. 如果你尝试这么做:

1
seq find { x => builder.append(x); x > 3 }

结果和前一个表达式并不一样。 前一个表达式计算后所有大于3的元素都增加到builder中了, 后一个表达式在找到第一个大于3的元素后就不会再增加了。
两个表达式并不等价。

自动简化是否可能?这里有两条黄金法则,可以用在有副作用的代码上:

  1. 尽可能的避免副作用
  2. 否则将副作用嗲吗从纯代码中分离开

对于上面的例子,我们需要去掉builder或者将它从纯代码中隔离。 考虑到builder是第三方的对象,我们无法去除,那我们通过隔离的方式实现:

12
seq.foreach(builder.append)seq.filter(_ > 3).headOption

这样我们就可以用到本文中的技巧进行替换:

12
seq.foreach(builder.append)seq.find(x > 3)

干的漂亮!自动简化也成为可能,一个额外好处是由于清晰的隔离,代码更容易理解。
一个不太明显的好处是,代码变得更健壮。如上面的例子,副作用针对不同的Seq实现,副作用的结果也不相同, 比如Vector和Stream, 副作用隔离可以让我们避免这种不确定的行为。

Sequence

本节中的技巧针对Seq以及它的子类, 而一些转换可以应用于其它集合类,如Set,Option,Map,甚至Iterator类,因为它们提供了相近的接口。

创建Creation

显示创建集合

12345
// BeforeSeq[T]()// AfterSeq.empty[T]

有时候可以节省内存(重用empty对象)和CPU (length check浪费)。

也可应用于Set,Option,Map,Iterator.

Length

对于数组,优先使用length而不是size。

12345
// Beforearray.size// Afterarray.length

length和size基本是同义词。在Scala 2.11中,Array.size是通过隐式转换实现的。因此每次调用时,一个中间包装类会被创建,除非你允许jvm的escape analysis。这会产生多余的GC对象,影响性能。

不要对检查empty的属性取反

1234567
// Before!seq.isEmpty!seq.nonEmpty// Afterseq.nonEmptyseq.isEmpty

同样适用于Set,Option,Map,Iterator

不要通过计算length来检查empty

123456789
// Beforeseq.length > 0seq.length != 0seq.length == 0// Afterseq.nonEmptyseq.nonEmptyseq.isEmpty

一方面已经有检查empty的方法,另一方面,,比如LinearSeq和子类List,会花费O(n)的时间计算length(IndexedSeq花费O(1))。

同样适用于Set,Map

不要直接使用length来比较

1234567891011
// Beforeseq.length > nseq.length < nseq.length == nseq.length != n// Afterseq.lengthCompare(n) > 0seq.lengthCompare(n) < 0seq.lengthCompare(n) == 0seq.lengthCompare(n) != 0

同上一条,计算length有时候非常昂贵,有可能将花费从O(length)减少到O(length min n)。
对于无限的stream来说,上面的技巧是绝对必要的。

等价 Equality

不要使用==比较数组:

12345
// Beforearray1 == array2// Afterarray1.sameElements(array2)

因为==只是比较实例对象,而不是里面的元素。

同样适用于Iterator

不要检查不同分类(categories)的集合的相等性

12345
// Beforeseq == set// Afterseq.toSet == set

不要使用sameElements来比较有序集合

12345
// Beforeseq1.sameElements(seq2)// Afterseq1 == seq2

不要手工检查相等性

12345
// Beforeseq1.corresponds(seq2)(_ == _)// Afterseq1 == seq2

使用内建的方法。

Indexing

不要使用index得到第一个元素

12345
// Beforeseq(0)// Afterseq.head

不要使用index得到最后一个元素

12345
// Beforeseq(seq.length - 1)// Afterseq.last

不要显式检查index的边界

12345
// Beforeif (i < seq.length) Some(seq(i)) else None// Afterseq.lift(i)

不要仿造headOption

123456
// Beforeif (seq.nonEmpty) Some(seq.head) else Noneseq.lift(0)// Afterseq.headOption

不要仿造lastOption

123456
// Beforeif (seq.nonEmpty) Some(seq.last) else Noneseq.lift(seq.length - 1)// Afterseq.lastOption

小心indexOf和lastIndexOf参数类型

1234567
// BeforeSeq(1, 2, 3).indexOf("1") // compilableSeq(1, 2, 3).lastIndexOf("2") // compilable//  AfterSeq(1, 2, 3).indexOf(1)Seq(1, 2, 3).lastIndexOf(2)

不要构造index的Range

12345
// BeforeRange(0, seq.length)// Afterseq.indices

不要手工使用index来zip集合

12345
// Beforeseq.zip(seq.indices)// Afterseq.zipWithIndex

检查元素的存在 Existence

不要用断言equality predicate 来检查存在

12345
// Beforeseq.exists(_ == x)//  Afterseq.contains(x)

同样应用于Set,Option,Iterator

小心contains参数类型

12345
// BeforeSeq(1, 2, 3).contains("1") // compilable//  AfterSeq(1, 2, 3).contains(1)

不要用断言inequality predicate 来检查不存在

12345
// Beforeseq.forall(_ != x)// After!seq.contains(x)

同样应用于Set,Option,Iterator

不要统计元素的数量来检查存在

123456789
// Beforeseq.count(p) > 0seq.count(p) != 0seq.count(p) == 0//  Afterseq.exists(p)seq.exists(p)!seq.exists(p)

同样应用于Set,Map,Iterator

不要借助filter来检查存在

1234567
// Beforeseq.filter(p).nonEmptyseq.filter(p).isEmpty// Afterseq.exists(p)!seq.exists(p)

同样应用于Set,Option,Map,Iterator

Filtering

不要对断言取反

12345
// Beforeseq.filter(!p)// Afterseq.filterNot(p)

同样应用于Set,Option,Map,Iterator

不要借助filter统计元素数量

12345
// Beforeseq.filter(p).length// Afterseq.count(p)

调用filter会产生一个临时集合,影响GC和性能。

同样应用于Set,Option,Map,Iterator

不要借助filter找到元素的第一个值

12345
// Beforeseq.filter(p).headOption// Afterseq.find(p)

同样应用于Set,Option,Map,Iterator

Sorting

不要手工按一个属性排序

12345
// Beforeseq.sortWith(_.property <  _.property)// Afterseq.sortBy(_.property)

不要手工按照identity排序

1234567
// Beforeseq.sortBy(it => it)seq.sortBy(identity)seq.sortWith(_ < _)// Afterseq.sorted

一步完成排序反转

123456789
// Beforeseq.sorted.reverseseq.sortBy(_.property).reverseseq.sortWith(f(_, _)).reverse// Afterseq.sorted(Ordering[T].reverse)seq.sortBy(_.property)(Ordering[T].reverse)seq.sortWith(!f(_, _))

Reduction

不要手工计算sum

1234567
// Beforeseq.reduce(_ + _)seq.fold(z)(_ + _)// Afterseq.sumseq.sum + z

其它可能用的方法reduceLeft,reduceRight,foldLeft,foldRight

同样应用于Set,Iterator

不要手工计算product

1234567
// Beforeseq.reduce(_ * _)seq.fold(z)(_ * _)// Afterseq.productseq.product * z

同样应用于Set,Iterator

不要手工搜索最小值和最大值

1234567
// Beforeseq.reduce(_ min _)seq.fold(z)(_ min _)// Afterseq.minz min seq.min
1234567
// Beforeseq.reduce(_ max _)seq.fold(z)(_ max _)// Afterseq.maxz max seq.max

同样应用于Set,Iterator

不要仿造forall

123456
// Beforeseq.foldLeft(true)((x, y) => x && p(y))!seq.map(p).contains(false)// Afterseq.forall(p)

同样应用于Set,Option(for the second line),Iterator

不要仿造exists

123456
// Beforeseq.foldLeft(false)((x, y) => x || p(y))seq.map(p).contains(true)// Afterseq.exists(p)

Rewriting

合并连续的filter调用

12345
// Beforeseq.filter(p1).filter(p2)// Afterseq.filter(x => p1(x) && p2(x))

或seq.view.filter(p1).filter(p2).force

同样应用于Set,Option,Map,Iterator

合并连续的map调用

12345
// Beforeseq.map(f).map(g)// Afterseq.map(f.andThen(g))

或seq.view.map(f).map(g).force

同样应用于Set,Option,Map,Iterator

filter完后再排序

12345
// Beforeseq.sorted.filter(p)// Afterseq.filter(p).sorted

在调用map前不要显式调用反转reverse

12345
// Beforeseq.reverse.map(f)// Afterseq.reverseMap(f)

不要显示反转得到迭代器

12345
// Beforeseq.reverse.iterator// Afterseq.reverseIterator

不要通过将集合转成Set得到不重复集合

12345
// Beforeseq.toSet.toSeq// Afterseq.distinct

不要仿造slice

12345
// Beforeseq.drop(x).take(y)// Afterseq.slice(x, x + y)

同样应用于Set,Map,Iterator

不要仿造splitAt

123456
// Beforeval seq1 = seq.take(n)val seq2 = seq.drop(n)// Afterval (seq1, seq2) = seq.spiltAt(n)

不要仿造span

123456
// Beforeval seq1 = seq.takeWhile(p)val seq2 = seq.dropWhile(p)// Afterval (seq1, seq2) = seq.span(p)

不要仿造partition

123456
// Beforeval seq1 = seq.filter(p)val seq2 = seq.filterNot(p)// Afterval (seq1, seq2) = seq.partition(p)

不要仿造takeRight

12345
// Beforeseq.reverse.take(n).reverse// Afterseq.takeRight(n)

不要仿造flatten

123456
// Before (seq: Seq[Seq[T]])seq.flatMap(it => it)seq.flatMap(identity)// Afterseq.flatten

同样应用于Set,Map,Iterator

不要仿造flatMap

12345
// Before (f: A => Seq[B])seq.map(f).flatten// Afterseq.flatMap(f)

同样应用于Set,Option,Iterator

不需要结果时不要用map

12345
// Beforeseq.map(...) // the result is ignored// Afterseq.foreach(...)

同样应用于Set,Option,Map,Iterator

不要产生临时集合

1.使用view

12345
// Beforeseq.map(f).flatMap(g).filter(p).reduce(...)// Afterseq.view.map(f).flatMap(g).filter(p).reduce(...)
  1. 将view转换成一个同样类型的集合
12345
// Beforeseq.map(f).flatMap(g).filter(p)// Afterseq.view.map(f).flatMap(g).filter(p).force

如果中间的转换是filter,还可以

1
seq.withFilter(p).map(f)
  1. 将view转换成另一种集合
12345
// Beforeseq.map(f).flatMap(g).filter(p).toList// Afterseq.view.map(f).flatMap(g).filter(p).toList

还有一种“transformation + conversion”方法:

1
seq.map(f)(collection.breakOut): List[T]

使用赋值操作符

1234567891011
// Beforeseq = seq :+ xseq = x +: seqseq1 = seq1 ++ seq2seq1 = seq2 ++ seq1// Afterseq :+= xseq +:= xseq1 ++= seq2seq1 ++:= seq2

Scala有一个语法糖,自动将x <op>= y转换成x = x <op> y. 如果op以:结尾,则被认为是右结合的操作符。
一些list和stream的语法:

12345678910111213
// Beforelist = x :: listlist1 = list2 ::: liststream = x #:: liststream1 = stream2 #::: stream// Afterlist ::= xlist1 :::= list2stream #::= xstream1 #:::= stream2

同样应用于Set,Map,Iterator

Set

大部分的Seq的技巧也可以应用于Set。另外还有一些只针对Set的技巧。

不要使用sameElements比较未排序的集合

12345
// Beforeset1.sameElements(set2)// Afterset1 == set2

同样应用于Map

不要手工计算交集

123456
// Beforeset1.filter(set2.contains)set1.filter(set2)// Afterset1.intersect(set2) // or set1 & set2

不要手工计算diff

123456
// Beforeset1.filterNot(set2.contains)set1.filterNot(set2)// Afterset1.diff(set2) // or set1 &~ set2

Option

Option并不是集合类,但是它提供了类似的方法和行为。
大部分针对Seq的技巧也适用于Option。这里列出了一些特殊的只针对Option的技巧。

Value

不要使用None和Option比较

1234567
// Beforeoption == Noneoption != None// Afteroption.isEmptyoption.isDefined

不要使用Some和Option比较

1234567
// Beforeoption == Some(v)option != Some(v)// Afteroption.contains(v)!option.contains(v)

不要使用实例类型来检查值的存在性

12345
// Beforeoption.isInstanceOf[Some[_]]// Afteroption.isDefined

不要使用模式匹配来检查值的存在

12345678
// Beforeoption match {    case Some(_) => true    case None => false}// Afteroption.isDefined

同样适用于Seq,Set

对于检查存在性的属性不要取反

123456789
// Before!option.isEmpty!option.isDefined!option.nonEmpty// Afterseq.isDefinedseq.isEmptyseq.isEmpty

不要检查值的存在性再处理值

12345678910
// Beforeif (option.isDefined) {    val v = option.get    ...}// Afteroption.foreach { v =>    ...}

Null

不要通过和null比较来构造Option

12345
// Beforeif (v != null) Some(v) else None// AfterOption(v)

不要显示提供null作为备选值

12345
// Beforeoption.getOrElse(null)// Afteroption.orNull

Rewriting

将mapwithgetOrElse转换成fold

12345
// Beforeoption.map(f).getOrElse(z)// Afteroption.fold(z)(f)

不要仿造exists

12345
// Beforeoption.map(p).getOrElse(false)// Afteroption.exists(p)

不要手工将option转换成sequence

123456
// Beforeoption.map(Seq(_)).getOrElse(Seq.empty)option.getOrElse(Seq.empty) // option: Option[Seq[T]]// Afteroption.toSeq

Map

同上,这里只列出针对map的技巧

不要使用lift替换get

12345
// Beforemap.lift(n)// Aftermap.get(n)

因为没有特别的需要将map值转换成一个Option。

不要分别调用get和getOrElse

12345
// Beforemap.get(k).getOrElse(z)// Aftermap.getOrElse(k, z)

不要手工抽取键集合

123456789
// Beforemap.map(_._1)map.map(_._1).toSetmap.map(_._1).toIterator// Aftermap.keysmap.keySetmap.keysIterator

不要手工抽取值集合

1234567
// Beforemap.map(_._2)map.map(_._2).toIterator// Aftermap.valuesmap.valuesIterator

小心使用filterKeys

12345
// Beforemap.filterKeys(p)// Aftermap.filter(p(_._1))

因为filterKeys包装了原始的集合,并没有复制元素,后续处理得小心。

小心使用mapValues

12345
// Beforemap.mapValues(f)// Aftermap.map(f(_._2))

同上。

不要手工filter 键

12345
// Beforemap.filterKeys(!seq.contains(_))// Aftermap -- seq

使用赋值操作符重新赋值

1234567891011
// Beforemap = map + x -> ymap1 = map1 ++ map2map = map - xmap = map -- seq// Aftermap += x -> ymap1 ++= map2map -= xmap --= seq

补充

除了以上的介绍,建议你看一下官方文档Scala Collections documentation

还有

最后一段是作者的谦虚话,欢迎提供意见和建议。

08 Jul 00:56

MySQL 调优/优化的 100 个建议

by 刘晓鹏

(编注:本文写于 2011 年)

MySQL是一个强大的开源数据库。随着MySQL上的应用越来越多,MySQL逐渐遇到了瓶颈。这里提供 101 条优化 MySQL 的建议。有些技巧适合特定的安装环境,但是思路是相通的。我已经将它们分成了几类以帮助你理解。

MySQL监控

MySQL服务器硬件和OS(操作系统)调优:

1、有足够的物理内存,能将整个InnoDB文件加载到内存里 —— 如果访问的文件在内存里,而不是在磁盘上,InnoDB会快很多。

2、全力避免 Swap 操作 — 交换(swapping)是从磁盘读取数据,所以会很慢。

3、使用电池供电的RAM(Battery-Backed RAM)。

4、使用一个高级磁盘阵列 — 最好是 RAID10 或者更高。

5、避免使用RAID5 — 和校验需要确保完整性,开销很高。

6、将你的操作系统和数据分开,不仅仅是逻辑上要分开,物理上也要分开 — 操作系统的读写开销会影响数据库的性能。

7、将临时文件和复制日志与数据文件分开 — 后台的写操作影响数据库从磁盘文件的读写操作。

8、更多的磁盘空间等于更高的速度。

9、磁盘速度越快越好。

10、SAS优于SATA。

11、小磁盘的速度比大磁盘的更快,尤其是在 RAID 中。

12、使用电池供电的缓存 RAID(Battery-Backed Cache RAID)控制器。

13、避免使用软磁盘阵列。

14. 考虑使用固态IO卡(不是磁盘)来作为数据分区 — 几乎对所有量级数据,这种卡能够支持 2 GBps 的写操作。

15、在 Linux 系统上,设置 swappiness 的值为0 — 没有理由在数据库服务器上缓存文件,这种方式在Web服务器或桌面应用中用的更多。

16、尽可能使用 noatime 和 nodirtime 来挂载文件系统 — 没有必要为每次访问来更新文件的修改时间。

17、使用 XFS 文件系统 — 一个比ext3更快的、更小的文件系统,拥有更多的日志选项,同时,MySQL在ext3上存在双缓冲区的问题。

18、优化你的 XFS 文件系统日志和缓冲区参数 – -为了获取最大的性能基准。

19、在Linux系统中,使用 NOOP 或 DEADLINE IO 调度器 — CFQ 和 ANTICIPATORY 调度器已经被证明比 NOOP 和 DEADLINE 慢。

20、使用 64 位操作系统 — 有更多的内存能用于寻址和 MySQL 使用。

21、将不用的包和后台程序从服务器上删除 — 减少资源占用。

22、将使用 MySQL 的 host 和 MySQL自身的 host 都配置在一个 host 文件中 — 这样没有 DNS 查找。

23、永远不要强制杀死一个MySQL进程 — 你将损坏数据库,并运行备份。

24、让你的服务器只服务于MySQL — 后台处理程序和其他服务会占用数据库的 CPU 时间。

 

MySQL 配置:

25、使用 innodb_flush_method=O_DIRECT 来避免写的时候出现双缓冲区。

26、避免使用 O_DIRECT 和 EXT3 文件系统 — 这会把所有写入的东西序列化。

27、分配足够 innodb_buffer_pool_size ,来将整个InnoDB 文件加载到内存 — 减少从磁盘上读。

28、不要让 innodb_log_file_size 太大,这样能够更快,也有更多的磁盘空间 — 经常刷新有利降低发生故障时的恢复时间。

29、不要同时使用 innodb_thread_concurrency 和 thread_concurrency 变量 — 这两个值不能兼容。

30、为 max_connections 指定一个小的值 — 太多的连接将耗尽你的RAM,导致整个MySQL服务器被锁定。

31、保持 thread_cache 在一个相对较高的数值,大约是 16 — 防止打开连接时候速度下降。

32、使用 skip-name-resolve — 移除 DNS 查找。

33、如果你的查询重复率比较高,并且你的数据不是经常改变,请使用查询缓存 — 但是,在经常改变的数据上使用查询缓存会对性能有负面影响。

34、增加 temp_table_size — 防止磁盘写。

35、增加 max_heap_table_size — 防止磁盘写。

36、不要将 sort_buffer_size 的值设置的太高 — 可能导致连接很快耗尽所有内存。

37、监控 key_read_requests 和 key_reads,以便确定 key_buffer 的值 — key 的读需求应该比 key_reads 的值更高,否则使用 key_buffer 就没有效率了。

38、设置 innodb_flush_log_at_trx_commit = 0 可以提高性能,但是保持默认值(1)的话,能保证数据的完整性,也能保证复制不会滞后。

39、有一个测试环境,便于测试你的配置,可以经常重启,不会影响生产环境。

MySQL Schema 优化:

40、保证你的数据库的整洁性。

41、归档老数据 — 删除查询中检索或返回的多余的行

42、在数据上加上索引。

43、不要过度使用索引,评估你的查询。

44、压缩 text 和 blob 数据类型 — 为了节省空间,减少从磁盘读数据。

45、UTF 8 和 UTF16 比 latin1 慢。

46、有节制的使用触发器。

47、保持数据最小量的冗余 — 不要复制没必要的数据.

48、使用链接表,而不是扩展行。

49、注意你的数据类型,尽可能的使用最小的。

50、如果其他数据需要经常需要查询,而 blob/text 不需要,则将 blob/text 数据域其他数据分离。

51、经常检查和优化表。

52、经常做重写 InnoDB 表的优化。

53、有时,增加列时,先删除索引,之后在加上索引会更快。

54、为不同的需求选择不同的存储引擎。

55、日志表或审计表使用ARCHIVE存储引擎 — 写的效率更高。

56、将 session 数据存储在 memcache 中,而不是 MySQL 中 — memcache 可以设置自动过期,防止MySQL对临时数据高成本的读写操作。

57、如果字符串的长度是可变的,则使用VARCHAR代替CHAR — 节约空间,因为CHAR是固定长度,而VARCHAR不是(utf8 不受这个影响)。

58、逐步对 schema 做修改 — 一个小的变化将产生的巨大的影响。

59、在开发环境测试所有 schema 变动,而不是在生产环境的镜像上去做。

60、不要随意改变你的配置文件,这可能产生非常大的影响。

61、有时候,少量的配置会更好。

62、质疑使用通用的MySQL配置文件。

查询优化:

63、使用慢查询日志,找出执行慢的查询。

64、使用 EXPLAIN 来决定查询功能是否合适。

65、经常测试你的查询,看是否需要做性能优化 — 性能可能会随着时间的变化而变化。

66、避免在整个表上使用count(*) ,它可能会将整个表锁住。

67、保持查询一致,这样后续类似的查询就能使用查询缓存了。

68、如果合适,用 GROUP BY 代替 DISTINCT。

69、在 WHERE、GROUP BY 和 ORDER BY 的列上加上索引。

70、保证索引简单,不要在同一列上加多个索引。

71、有时,MySQL 会选择错误的索引,这种情况使用 USE INDEX。

72、使用 SQL_MODE=STRICT 来检查问题。

73、索引字段少于5个时,UNION 操作用 LIMIT,而不是 OR。

74、使用 INSERT ON DUPLICATE KEY 或 INSERT IGNORE 来代替 UPDATE,避免 UPDATE 前需要先 SELECT。

75、使用索引字段和 ORDER BY 来代替 MAX。

76、避免使用 ORDER BY RAND()。

77、LIMIT M,N 在特定场景下会降低查询效率,有节制使用。

78、使用 UNION 来代替 WHERE 子句中的子查询。

79、对 UPDATE 来说,使用 SHARE MODE 来防止排他锁。

80、重启 MySQL 时,记得预热数据库,确保将数据加载到内存,提高查询效率。

81、使用 DROP TABLE ,然后再 CREATE TABLE ,而不是 DELETE FROM ,以删除表中所有数据。

82、最小化你要查询的数据,只获取你需要的数据,通常来说不要使用 *。

83、考虑持久连接,而不是多次建立连接,已减少资源的消耗。

84、基准查询,包括服务器的负载,有时一个简单的查询会影响其他的查询。

85、当服务器的负载增加时,使用SHOW PROCESSLIST来查看慢的/有问题的查询。

86、在存有生产环境数据副本的开发环境中,测试所有可疑的查询。

MySQL备份过程:

87、在二级复制服务器上进行备份。

88、备份过程中停止数据的复制,以防止出现数据依赖和外键约束的不一致。

89、彻底停止MySQL之后,再从数据文件进行备份。

90、如果使用MySQL dump进行备份,请同时备份二进制日志 — 确保复制过程不被中断。

91、不要信任 LVM 快照的备份 — 可能会创建不一致的数据,将来会因此产生问题。

92、为每个表做一个备份,这样更容易实现单表的恢复 — 如果数据与其他表是相互独立的。

93、使用 mysqldump 时,指定 -opt 参数。

94、备份前检测和优化表。

95、临时禁用外键约束,来提高导入的速度。

96、临时禁用唯一性检查,来提高导入的速度。

97、每次备份完后,计算数据库/表数据和索引的大小,监控其增长。

98、使用定时任务(cron)脚本,来监控从库复制的错误和延迟。

99、定期备份数据。

100、定期测试备份的数据。

MySQL 调优/优化的 100 个建议,首发于博客 - 伯乐在线

05 Jul 00:39

程序员必读的六本书

by 技术小黑屋

作为一名程序员,日常的工作除了上班撸代码就是加班撸代码了。撸码其实不难,无非询问Google,StackOverflow,解决方法和demo一箩筐,可是撸的一手好代码着实不易。无独有偶,码农一抓一大把,优秀的程序员却不易寻觅。优秀的程序员既不可能出自各种天花乱坠的培训机构,更不可能来自挖掘机摇篮山东布鲁斯特,大多数优秀的程序员有一个共同点,那就是自学。

为什么是自学呢?首先大学的教育不可能让你成为专家级别的程序员,其次为了能从团队脱颖而出必然付出更多的努力来学习。自学其实是一种很强有力的能力,一旦掌握,许多技术和问题都可以轻松搞定。

中学物理常客牛顿曾说,”如果我比别人看得更远,那是因为我站在巨人的肩上“。他山之石,可以攻玉。阅读大师巨匠的作品无疑是最有效的自学方式之一。业界知名的Bob大叔是代码整洁和面向对象编程的坚定支持推进者,其在这方面的著作可以称得上权威。Martin Fowler同样名声在外,他的关于重构的著作《重构—-改善既有代码的设计》应该是人手一本。阅读诸如上面两位大家的著作,对技术提到大有裨益。

本文讲列举六本业界牛人的著作,也是编程书籍中经典的经典,这几本书并非简单的教程书籍,而是教给你方法和思想来解决现实遇到的问题,提高编码的技艺和境界。

重构—-改善既有代码的设计

何为重构,一言以蔽之,就是在不改变外部行为的前提下,有条不紊地改善代码。本书虽然使用Java语言书写示例代码,但是其概念与思想同样适合于其他语言。书中,作者以一些平淡无奇,甚至带有坏味道的代码开始,一步一步地修改转变成更加灵活,可重用的代码。通过书中的示例,你会清楚地明白什么才是整洁的代码。重构其实依然成为经验丰富的程序员的必备技能,当你想要改善重构代码时,读一读这本书就会让你有章可循,豁然开朗。
查看详细:亚马逊



代码整洁之道

这是我最喜欢的一本书,不止一次我将它推荐给我的同事,读者还有学生。我认为它可以称得上软件开发与编码方便最好的一本书。Bob大叔我想无需做介绍,他写过一个关于敏捷开发的系列书籍,我的书架上就有他的《代码整洁之道》,《程序员的职业素养》《敏捷软件开发(原则模式与实践)》《敏捷软件开发(原则模式与实践)》《UML for Java For Programmers》, 《Extreme Programming in Practice》等这些书籍。虽然他的这些书有点老旧,但是这些书仍然很有价值,纵使数十年之后,这些书依旧受用,尤其是在面向对象编程方面。 本书不仅仅是告诉你要做什么,还教会你什么不能做。书中有关于代码味道的一个章节,全面列举了大多数程序员遇到的各种错误,其后的章节则详细描述如何纠正这些错误。比如如何将过长的switch声明转换成遵循开放闭合原则的模型,如何利用集成和多态。再次啰嗦一下,这本书确实值得每个程序员拥有。和上本书一样,书中的例子使用Java语言,但依然适合使用其他面向对象编程语言的开发者阅读。想要撸的一手好码,这本书必不可少。
查看详细:亚马逊

代码大全

想必这本书大家都曾阅读过,这就是鼎鼎大名的《代码大全》,从某个角度看,它其实就是C++版的《代码整洁之道》。本书的目标就是帮开发者使用做高质的代码写出更好的软件。同样书中也涉及了编程中常见问题和最佳实践。这本书也可以称得上是必读书籍,尤其是对于C和C++程序员。《代码大全(第2版)》中所论述的技术不仅填补了初级与高级编程实践之间的空白,而且也为程序员们提供了一个有关软件开发技术的信息来源。《代码大全(第2版)》对经验丰富的程序员、技术带头人、自学的程序员及没有太多编程经验的学生都是大有裨益的。可以说,只要你具有一定的编程基础,想成为一名优秀的程序员,阅读《代码大全(第2版)》都不会让你失望。
查看详细:亚马逊

单元测试的艺术

如果非要做一件改善项目,提高开发者水平的事情,我想那就是让开发者掌握单元测试的能力。对于专业的开发者来说,单元测试是一项必备的技能,多数的程序员却不具备TDD(测试驱动开发)的能力。我虽然在不太遵循TDD这种模式,但是也会为自己写的或维护的代码编写单元测试。对于工程来说,开源项目基本都严格遵守执行单元测试,而很多商业的工程则在单元测试方面有所缺失。一个拥有单元测试的项目会变得更加容易维护和更改。本书会介绍成功的项目与失败项目的差别,可维护的代码库与不可维护的代码库之间的区别。本书示例为.NET代码,但这并不会影响你了解单元测试。如果你是一名技术负责人或者项目负责人,这本书可以帮你更好地把控项目代码质量。如果你看Java更舒服的话,也可以看一看这本书《JUnit实战(第2版)》
查看详细:亚马逊

精益软件开发管理之道

《精益软件开发管理之道》是一本软件开发方法学的书。作者从24个不同的视角,在更大的空间、时间、行业、文化背景下,考察了敏捷和精益方法。《精益软件开发管理之道》详细阐述了敏捷和精益开发方法取得成功的深层原因。《精益软件开发管理之道》包括以下内容:系统思考,以适当足够的方式关注客户;技术杰出,介绍了杰出软件开发的基础-低耦合的架构、测试驱动的开发过程等;可靠交付,讨论了工作流和日程计划,以及反馈的重要作用;无情改进,讨论了所有精益组织的基本特点:持续不断、永不满足的改进;卓越的人,卓越的结果来自于卓越的人;一致的领导,讨论在领导团队中达成一致。
查看详细:亚马逊



设计模式 可复用面向对象软件的基础

这本书要么你读过,要么就是听说过,这就是经典的GOF(Gang of Four 中文译为四人帮)设计模式一书。该书作者为四人,分别是Eric Gamma, Richard Helm, Ralph Johnson, 和John Vissides。四位顶尖的面向对象领域专家精心选取了最具价值的设计实践,加以分类整理和命名,并用简洁而易于重用的形式表达出来。本书已经成为面向对象技术人员的圣经和词典,书中定义的23个模式逐渐成为开发界技术交流所必备的基础知识和语汇。使用这些设计模式,我们可以设计出灵活,优雅和可重用的可扩展的设计。
查看详细:亚马逊





颈椎病康复指南

最新更新,原文不包含本书,应读者评论要求,特意加入此书。

写代码不仅是头脑的工作,更是体力的付出。长年累月,身体必然出现问题。脂肪肝,脊椎病等等。希望这本书可以给你脊椎病恢复提供指导。
查看详细:亚马逊

原文参考

01 Jul 00:34

平庸程序员的各种迹象

by 柒柒

二、平庸程序员的迹象

1.无法从集合的角度思考

从命令式编程过度到函数式和声明式编程,会立刻要求你思考将数据集合当作原语来操作,而不是作为标量值。无论你在关系型数据库(并且不是作为一个对象仓库)中使用 SQL,还是设计规模会随多处理器线性变化的程序,亦或是你写的代码必须要在拥有 SIMD 能力的芯片(比如现代显卡和电子游戏机)上执行,都需要这种过渡。

特征

只有在带有声明式或函数式编程特性(程序猿应该知道这些特性)的平台上看到这些特征时,下面列出的才算数。

  1. 在 for 或 foreach 的循环里对集合中的每个元素执行原子操作。
  2. 写的 Map 或 Reduce 函数里包含自定义的循环,在循环里逐一重复执行数据集。
  3. 从服务器抓取大量数据集,并在客户端上计算和,而不是在查询里使用聚集函数。
  4. 函数作用于一个集合中的每个元素,并在函数开头通过执行一次新的数据库查询来抓取一个关联记录。
  5. 写的业务逻辑函数,例如更新一个用户界面或执行文件 I/O,很不幸地为了某种折衷而伴随有副作用。
  6. 在实体类打开专属的数据库连接或文件操作符,并且在每个对象的生命周期里都保持连接状态。

补救措施

非常有趣,想象着一个发牌的人通过手指在牌里翻转把一副牌切成两堆交叉洗牌,就能让大脑联想到集合,以及如何成批地操作集合。激发人联想的其他想象还有:

  • 高速公路上的车流通过一系列收费站(并行处理)。
  • 泉水汇聚成溪流,溪流又汇聚成小河,最后再汇聚成江河(并行分解/聚集函数)。
  • 一个报纸印刷机(协同程序、流水线)。
  • 夹克上的拉链头把拉链齿拉上(简单的联结)。
  • 转移 RNA 加上氨基酸,并在一个核蛋白中加入信使 RNA,就变成了蛋白质(多阶段函数驱动联结,详见 animation)。
  • 在一棵橘子树中,数以亿计的细胞里同时发生着上述的过程,不断地将空气、水和阳光转换成橘子汁(大型分布式集群上的 Map/Reduce)。

如果你正在写一个处理集合的程序,思考一下所有的附加数据和记录,你的函数需要操作它们的每一个元素。并且在 Reduce 函数应用到每对数据上之前,使用 Map 函数把它们成对地联结在一起。

2.缺乏批判性思维

除非你能批判自己的思维并从中找出缺陷,否则你会错过那些可以在敲代码之前就能解决的问题。如果你也无法评判自己曾经写过的代码,那你只能在不断摸索中以龟速学习。这个问题同时来源于思考怠惰和以自我为中心,因此,这个问题的特征似乎也来自两个不同的方向。

特征

  1. 自制“业务规则引擎”。
  2. 静态工具类很冗余且庞大,或者多学科的函数库只用一个命名空间。
  3. 把各种应用糅合在一起,或给当前的应用附加不相关的特性来避免启动新项目的开销。
  4. 程序架构开始需要建立 epicycle 模型。(译者: epicycle 模型是天文学上使用的模型,用来解释天体在运动过程中出现的偏差等异常行为。)
  5. 为了很不相关的数据向表中添加字段(比如:在通讯录的表中放置“# cars owned”字段)。(译者:通讯录的内容要记录是否有车干嘛,确实扯远了。)
  6. 前后矛盾的命名规范。
  7. 处于“拿着锤子看什么都是钉子”的心态,或者改变对问题的定义,这样所有问题都能用某个特定的技术来解决。
  8. 编写程序降低问题的复杂度。
  9. 从病理上冗余地防御式编程(“企业级代码”)。
  10. 用 XML 重新发明 LISP。

补救措施

从Paul 和 Elder 写的《批判性思维 | Critical Thinking》这样的书入手,控制自我意识,在向朋友或同事发表自己的想法以此寻求评论时,练习抵制为自己辩护的冲动。

一旦你习惯了别人来检验你的想法,你就会开始自我审视并练习想象这些想法的结果。另外,你也需要培养起区别轻重缓急的能力(能直觉知道对这种规模的问题,需要花费多少精力比较合适)、用实践验证假设的习惯(这样你就不会高估问题的大小)和面对失败的健康心态(就算艾萨克.牛顿的地心引力说是错的,但我们依然爱他并需要他去尝试)。

最后,你必须自律。意识到计划里有缺陷不会让你更高效,除非你有足够的意志力去改正缺陷,并重建手中正在进行的工作。

3.弹球式编程

如果你把面板倾斜得刚刚好,把曲柄拉回到刚好的距离,并且以正确的顺序击中那些凸起的按钮,那么程序就会像弹球一样运行无误:随着指令的执行流程,从条件语句返回,跳过未选中的指令,转向下一次的状态转换。

特征

  1. 用一个 try-catch 代码块包围 Main() 的整个函数体,并在 Catch 分句中重置整个程序(像弹球地沟,掉下去以后重新开始游戏)。
  2. 在强类型的语言中,用字符串或整型来存储那些拥有(可以用)更合适封装类型的值。
  3. 把复杂数据打包成带分隔符的字符串,然后在使用它的每个函数里解析一遍。
  4. 对输入有歧义的函数,不会用断言(assertion)或方法协定(method contract)。
  5. 使用 Sleep() 来等待另一个线程完成任务。
  6. 对非枚举类型的值使用 switch 语句,而且分支语句中没有“Otherwise”分句。
  7. 用 Automethods 或 Reflection 来调用在非法的用户输入中提到的方法。
  8. 在函数里通过设置全局变量来返回多个值。
  9. 类里有一个方法和几个字段,通过设置字段来为方法传递参数。
  10. 不用事务来更新多行数据库内容。
  11. 孤注一掷(比如,试图不用事务和 ROLLBACK 来恢复数据库的状态)。

补救措施

把程序的输入想象成水。它即将流过每一个缝隙,灌满每一个容器。那么你要想一想,如果它流过的地方并没有明确创建任何东西去呈接它的话,会造成什么后果。

你要让自己熟悉平台的机制,这有助于写出健壮且易扩展的程序。共有三种基础机制:

  1. 当某种意外发生时,能在产生任何破坏之前停止程序,然后帮助你识别出是哪里出错了(类型体系、断言、异常等)。
  2. 将程序的执行导向处理意外最佳的代码块( try-catch 模块、多重分发、基于事件驱动编程等)。
  3. 暂停线程直到一切就绪(WaitUntil 命令、互斥锁和信号量、同步锁等)。

还有第四条,单元测试,你可以在设计阶段使用。

使用这些机制应该成为你的第二天性,就像在句子里用逗号和句号一样。为了做到这些,每次浏览一遍上面介绍的机制(括号里提到的那些),并重构你的旧程序,把提到的这些机制塞到任何能塞的地方,就算最后发现这么做并不合适(尤其是在它们看似不合适的时候,至少那时你也开始明白其中的缘由)。

4.不熟悉安全原则

如果要说下述特征并不很严重,但它们几乎是大部分程序都存在的整体质量问题。意思是说,这些特征不会让你成为一名很糟糕的程序猿,只是意味着你不应该从事网络程序或安全系统的工作,直到你已经在这方面做了一些功课。

特征

  1. 以明文形式存储可利用信息(名字、卡号、密码等)。
  2. 用低效的加密术存储可利用信息(将密码编译在程序中的对称加密算法;简单密码;任何“解码环( decoder-ring )”、自创加密算法、专有的或未验证的加密算法)。
  3. 在接受网络连接或解释来自非置信源的输入信息之前,程序或设备没有限制它们的权限。
  4. 不进行边界检查或输入合法性验证,尤其是在使用非托管类的语言时。
  5. 把不合法或非转义的输入串接到字符串上来构建SQL查询。
  6. 调用用户输入中指定的程序。
  7. 试图通过搜索已知漏洞的签名(signature)来阻止漏洞被利用。
  8. 用不加盐的哈希值(unsalted hash)存储信用卡卡号或密码。

补救措施

下面只涵盖了基本原则,但遵照这些原则会避免绝大多数臭名昭著的错误,那些错误可以让整个系统大打折扣。对于任何处理或存储有价值信息的系统,无论是向你还是其用户,或是控制一个贵重资源的系统,它们通常都有一个安全专家来审查系统的设计与实现。

从审查程序开始,找出用数组或其他配置内存的容器来存储输入的代码,确保这部分代码检查了输入的大小不会超出分配给它的内存大小。没有其他类型的 bug 能比缓冲区溢出更能导致可利用的安全漏洞。从某个层面来说,在写网络通信程序或任何安全第一的场合下,你应该认真考虑使用某种内存托管型的编程语言。

下一步,审查数据库查询操作。审查那些将未修改输入串接到 SQL 查询内容中的查询操作,并且,如果平台支持的话就切换为使用参数化查询,如果不支持就对输入进行过滤或转义。这么做是为了防止 SQL 注入攻击。

在你清除了这两类最臭名昭著的安全 bug 之后,你应该继续将所有的的程序输入视为完全不可靠,或是有潜在恶意。按有效的验证规则来定义程序的输入很重要,而且除非输入能通过验证,否则程序应该拒绝它,这样你就能够通过修复验证方法并使其更加明确来修复可利用的漏洞,而不是通过扫描已知漏洞的签名来修复漏洞。

进一步说,你应该总是在开始设计程序之前,思考程序需要执行的操作以及这些操作需要从 host 获得什么样的权限,因为这个时候是想出怎么样能尽可能使用最少权限的最佳时机。这条建议背后的原则是,如果在你的代码中找到一个可利用的 bug ,限制这个bug可能对系统其他部分造成的损害。换言之:在你学会不信任输入之后,你也应该学会不要信任自己写的程序。

最后你要学会的是数据加密基础,从《Kerckhoff’s principle》开始。这一点亦可表达为“安全第一”,从中还衍伸出了一些有趣之处。

原则一,永远不要信任一个密码或其他加密原语,除非它已经被公开发表,并且已经由更高级别的安全社区对其进行了全面的分析和测试。从密码学的发展来看,模糊晦涩的加密法、专有的加密法或是新出现的加密法都毫无安全可言。即使是可信的加密原语,其实现中也会存在缺陷,因此,对于你不能确定其已经得到全面审查的加密算法(包括自己实现的版本),要避免使用。所有的新型加密系统都要经过一系列的详细审查,这个过程可能长达十年之久,或更长,而你只要关注那些最后经受住了审查并且所有已知错误都已修复的加密系统。

原则二,如果密钥容易破解或存储失当,那这和完全不加密一样糟糕。如果程序要对数据加密,但不需要解密或很少需要解密,那就考虑只把对称加密密钥对的公钥给它,并让解密阶段和私钥分开运行,用户必须每次输入一个好的口令来确保密钥的安全。

越是处于危险之中,你需要做的功课越多,并且必须在程序的设计阶段投入更多精力。这都是因为一旦你的程序部署下去,就会有成堆、有时候可能是成千上万的不速之客试图去破坏它的安全性。

绝大部分可追溯到代码问题的安全故障都归因于一些很愚蠢的错误,其中大部分错误可以通过筛选输入、谨慎使用资源、利用常识、想清楚再写代码等方式来避免。

5. 代码一塌糊涂

特征

  1. 不遵循一贯的命名规范。
  2. 不使用缩进,或缩进不一致。
  3. 不使用空格,例如在方法之间不加空格(或表达式里不加空格,看“ANDY=NO”)。
  4. 有一大堆被注释掉的代码。

补救措施

程序猿在匆忙之下(或特殊情况下)犯了上述所有毛病的话,会在之后返回来清理,但一个糟糕的程序猿真的就只是粗心大意。有时,利用可通过快捷键来修复缩进和空格(“美观的格式”)的 IDE 是很帮助的,但我发现程序猿总是把代码搞得一团糟,极大地违背 Visual Studio 对适当缩进的坚持。

平庸程序员的各种迹象,首发于博客 - 伯乐在线

29 Jun 00:55

How Rust Achieves Thread Safety

29 Jun 00:46

标准模板库(STL)使用入门(上)

by 柒柒

或许你已经把 C++ 作为主要的编程语言用来解决 TopCoder 上的问题。这意味着你已经简单使用过了 STL,因为数组和字符串都是作为 STL 对象传递给函数。也许你已经注意到了,很多程序员写代码比你快得多,也更简洁。

或许你还不是但想成为一名 C++ 程序猿,因为这种编程语言功能很强大还有丰富的库(也许是因为在 TopCoder 的练习室里和竞赛中看到了很多非常精简的解决方案)。

无论过去如何,这篇文章都会有所帮助。在这里,我们将回顾标准模板库(Standard Template Library—STL,一个非常有用的工具,有时甚至能在算法竞赛中为你节省大量时间)的一些强大特性。

要熟悉 STL,最简单的方式就是从容器开始。

容器

无论何时需要操作大量元素,都会用到某种容器。C语言只有一种内置容器:数组。

问题不在于数组有局限性(例如,不可能在运行时确定数组大小)。相反,问题主要在于很多任务需要功能更强大的容器。

例如,我们可能需要一个或多个下列操作:

  • 向容器添加某种字符串
  • 从容器中移除一个字符串
  • 确定容器中是否存在某个字符串
  • 从容器中返回一些互不相同的元素
  • 对容器进行循环遍历,以某种顺序获取一个附加字符串列表。

当然,我们可以在一个普通数组上实现这些功能。但是,这些琐碎的实现会非常低效。你可以创建树结构或哈希结构来快速解决问题,但是想想:这种容器的实现是取决于即将存储的元素类型吗?例如,我们要存储平面上的点而不是字符串的话,是不是要重写这个模块才能实现功能?

如果不是,那我们可以一劳永逸地为这种容器开发出接口,然后对任何数据类型都能使用。简言之,这就是 STL 容器的思想。

前言

程序要使用 STL 时,应包含(#include)适当的标准头文件。对大部分容器来说,标准头文件的名称和容器名一致,且不需扩展名。比如说,如果你要用栈(stack),只要在程序最开头添加下面这行代码:

#include <stack>

容器类型(还有算法、运算符和所有 STL也一样)并不是定义在全局命名空间,而是定义在一个叫“std”的特殊命名空间里。在包含完所有头文件之后,写代码之前添加下面这一行:

using namespace std;

还有另一个很重要的事情要记住:容器类型也是模板参数。在代码中用“尖括号”(‘<’/’>’)指明模板参数。比如:

vector<int> N;

如果要进行嵌套式的构造,确保“方括号”之间不是紧挨着——留出一个空格的位置。(译者:C++11新特性支持两个尖括号之间紧挨着,不再需要加空格)

vector< vector<int> > CorrectDefinition;

vector<vector<int>> WrongDefinition; // Wrong: compiler may be confused by 'operator >>'

Vector

最简单的 STL 容器就是 vector。Vector 只是一个拥有扩展功能的数组。顺便说一下,vector 是唯一向后兼容 C 代码的容器——这意味着 vector 实际上就是数组,只是拥有一些额外特性。

vector<int> v(10);

 for(int i = 0; i < 10; i++) {

      v[i] = (i+1)*(i+1);

 }

 for(int i = 9; i > 0; i--) {

      v[i] -= v[i-1];

 }

实际上,当你敲下

 vector<int> v;

就创建了一个空 vector。注意这样的构造方式:

 vector<int> v[10];

这里我们把’V’声明成一个存放了 10 个 vector<int> 类型元素的数组,初始化为空。大部分情况下,这不是我们想要的。在这里用圆括号代替方括号。Vector 最常使用的特性就是获取容器大小。

 int elements_count = v.size();

有两点要注意:首先,size() 函数返回的值是无符号的,这点有时会引起一些问题。因此,我经常定义宏,有点像 sz(C) (把C 的大小作为一个普通的带符号整型返回)这样的。其次,如果你想知道容器是否为空,把 vector 的 size() 返回值和0比较不是一个好的做法。你最好使用 empty() 函数:

bool is_nonempty_notgood = (v.size() >= 0); // Try to avoid this
bool is_nonempty_ok = !v.empty();

这是因为,不是所有容器都能在常量时间内返回自己的大小,而且你绝不应该为了确定链表中至少包含一个节点元素就对一条双链表中的所有元素计数。

另一个 vector 中经常使用的函数是 push_back。Push_back 函数向 vector 尾部添加一个元素,容器长度加 1。思考下面这个例子:

 vector<int> v;

 for(int i = 1; i < 1000000; i *= 2) {

      v.push_back(i);

 }

 int elements_count = v.size();

别担心内存分配问题——vector 不会一次只分配一个元素的空间。相反,每次用 push_back 添加新元素时,vector 分配的内存空间总是比它实际需要的更多。你应该担心的唯一一件事情是内存使用情况,但在 TopCoder 上这点可能不是问题。(后面再进一步探讨 vector 的内存策略)

当你需要重新改变 vector 的大小时,使用 resize() 函数:

 vector<int> v(20);

 for(int i = 0; i < 20; i++) {

      v[i] = i+1;

 }

 v.resize(25);

 for(int i = 20; i < 25; i++) {

      v[i] = i*2;
 }

Resize() 函数让 vector 只存储所需个数的元素。如果你需要的元素个数少于 vector 当前存储的个数,剩余那些元素就会被删除。如果你要求 vector 变大,使用这个函数也会扩大它的长度,并用 0 填充新创建的元素。

注意,如果在使用了 resize() 后又用了 push_back(),那新添加的元素就会位于新分配内存的后面,而不是被放入新分配的内存当中。上面的例子得到的 vector 大小是25,如果在第二个循环中使用 push_back(),那vector 的大小最后会是30。

 vector<int> v(20);

 for(int i = 0; i < 20; i++) {

      v[i] = i+1;

 }
 v.resize(25);

 for(int i = 20; i < 25; i++) {
      v.push_back(i*2); //把下标值写入元素 [25..30), not [20..25) ! <
 }

使用 clear() 函数来清空 vector。这个函数使 vector 包含 0 个元素。它并不是让所有元素的值为0——注意——它是完全删除所有元素,成为空容器。

有很多种方式初始化 vector。你也许用另一个 vector 来创建新的 vector:

vector<int> v1;
 // ...
 vector<int> v2 = v1;
 vector<int> v3(v1);

上面的例子中,v2 和 v3 的初始化过程一样。如果你想创建指定大小的 vector,使用下面的构造函数:

 vector<int> Data(1000);

上面的例子中,变量 data 创建后将包含1,000 个0值元素。记得使用圆括号,而不是方括号。如果你想用其他东西来初始化 vector,你可以这么写:

 vector<string> names(20, “Unknown”);

记住,你可以创建任何类型的 vector。多维数组很重要。通过 vector 创建二维数组,最简单的方式就是创建一个存储 vector 元素的 vector。

 vector< vector<int> > Matrix;

你现在应该清楚如何创建一个给定大小的二维 vector:

 int N, N;

 // ...

 vector< vector<int> > Matrix(N, vector<int>(M, -1));

这里,我们创建了一个 N*M 的矩阵,并用 -1 填充所有位置上的值。向 vector 添加数据的最简单方式是使用 push_back()。但是,万一我们想在除了尾部以外的地方添加数据呢?Insert() 函数可以实现这个目的。同时还有 erase() 函数来删除元素。但我们得先讲讲迭代器。

你还应该记住另一个非常重要的事情:当 vector 作为参数传给某个函数时,实际上是复制了这个 vector(也就是值传递)。在不需要这么做的时候创建新的 vector 可能会消耗大量时间和内存。实际上,很难找到一个任务需要在传递 vector 为参数时对其进行复制。因此,永远不要这么写:

 void some_function(vector<int> v) { // Never do it unless you’re sure what you do!

      // ...
 }

相反,使用下面的构造方法(引用传递):

 void some_function(const vector<int>& v) { // OK

      // ...
 }

如果在函数里要改变 vector 中的元素值,那就去掉‘const’修饰符。

 int modify_vector(vector<int>& v) { // Correct

      V[0]++;
 }

键值对

在讨论迭代器之前,先说说键值对(pairs)。STL 中广泛使用键值对。一些简单的问题,像 TopCoder SRM 250 和 500 分值的简单题,通常需要一些简单的数据结构,它们都非常适合用 pair 来构造。STL 中的 std::pair 就是一个元素对。最简单的形式如下:

 template<typename T1, typename T2> struct pair {

      T1 first;

      T2 second;

 };

普通的 pair<int,int> 就是一对整型值。来点更复杂的,pair<string,pair<int,int>> 就是一个字符串和两个整型组成的值对。第二种情况也许能这么用:

 pair<string, pair<int,int> > P;
 string s = P.first; // extract string
 int x = P.second.first; // extract first int
 int y = P.second.second; // extract second int

键值对的最大优势就在于它们有内置操作来比较 pair 对象。键值对优先对比第一个元素值,再比较第二个元素。如果第一个元素不相等,那结果就只取决于第一个元素之间的比较;只有在第一个元素相等时才比较第二个元素。使用 STL 的内置函数,可以轻易地对数组(或 vector)对进行排序。

例如,如果要对存放整型值坐标点的数组排序,使得这些点排列成一个多边形,一种很好的思路就是把点放入 vector<pair<double, pair<int, int>>>,其中每个元素表示成 {polar angle,{x, y}}(点的极角和点的坐标值)。调用 STL 的排序函数可以按你的期望对点进行排序。

关联容器中也广泛使用 pair,这点会在文章后面提及。

迭代器

什么是迭代器?STL 迭代器是访问容器数据的最普通的方式。思考这个简单的问题:将包含 N 个整型(int)的数组 A 倒置。从类 C 语言的方案开始:

 void reverse_array_simple(int *A, int N) {

      int first = 0, last = N-1; // First and last indices of elements to be swapped

      While(first < last) { // Loop while there is something to swap

           swap(A[first], A[last]); // swap(a,b) is the standard STL function

           first++; // Move first index forward

           last--; // Move last index back
      }
 }

对你来说这些代码应该一目了然。很容易用指针来重写:

void reverse_array(int *A, int N) {

      int *first = A, *last = A+N-1;

      while(first < last) {

           Swap(*first, *last);

           first++;

           last--;

      }

 }

看看这个代码的主循环,它对指针‘first’和‘last’只用了四种不同的操作:

  • 比较指针(first < last),
  • 通过指针取值(*first,*last),
  • 指针自增,以及
  • 指针自减

现在,想象你正面临第二个问题:将一个双链表翻转,或部分翻转。第一个程序使用了下标,肯定不行。至少效率不够,因为不可能在常数时间内通过下标获取双链表中的元素值,必须花费 O(N) 的时间复杂度,所以整个算法的时间复杂度是 O(N^2)。

但是你看:第二个程序对任何类似指针(pointer-like)的对象都能奏效。唯一的要求是,对象能够执行上面所列出的四种操作:取值(一元运算符 *),对比(<),和自增/自减(++/–)。拥有这些属性并和容器相关联的对象就叫迭代器。任何 STL 容器都可以通过迭代器遍历。尽管 vector 不常用,但对其他类型的容器很重要。

那么,我们现在讨论的这个东西是什么?一个语法上很像指针的对象。为迭代器定义如下操作:

  • 从迭代器取值,int x = *it;
  • 让迭代器自增和自减 it1++,it2–;
  • 通过‘!=’和‘<’来比较迭代器大小;
  • 向迭代器添加一个常量值 it += 20;(向前移动了 20 个元素位置)
  • 获取两个迭代器之间的差值,int n = it2 – it1;

和指针不同,迭代器提供了许多更强大的功能。它们不仅能操作任何类型的容器,还能执行范围检查并分析容器的使用。

当然,迭代器的最大优势就是极大地增加了代码重用性:基于迭代器写的算法在大部分的容器上都能使用,而且,自己写的容器要是提供了迭代器,就能作为参数传给各种各样的标准函数。

不是所有类型的迭代器都会提供所有潜在的功能。实际上,存在所谓的“常规迭代器”和“随机存取迭代器”两种分类。简单地说,常规迭代器可以用‘==’和‘!=’来做比较运算,而且还能自增和自减。它们不能做减法,也不能在常规迭代器上做加法。基本上来说,不可能对所有类型的容器都在常数时间范围内实现以上描述的操作。尽管如此,翻转数组的函数应该这么写:

 template<typename T> void reverse_array(T *first, T *last) {

      if(first != last) {

           while(true) {

                swap(*first, *last);

                first++;

                if(first == last) {

                     break;

                }

                last--;

                if(first == last) {

                     break;

                }

           }

      }

 }

这个程序和前面一个程序的主要差别在于,我们没有在迭代器上进行“<”比较,只用了“==”比较。再次强调,如果你对函数原型感到惊讶(发现函数原型和实际不同),不要慌张:模板只是声明函数的一种方式,对任何恰当的参数类型都是有效的。

对指向任意对象类型的指针和所有常规迭代器来说,这个函数应该都能完美运行。

还是回到 STL 上吧。STL 算法常常使用两个迭代器,称为“begin”和“end”。尾部迭代器不指向最后一个对象,而是指向第一个无效对象,或是紧跟在最后一个对象后面的对象。这一对迭代器使用起来通常很方便。

每一个 STL 容器都有 begin() 和 end() 两个成员函数,分别返回容器的初始迭代器和尾部迭代器。

基于这些原理,只有容器 c 为空时,“c.begin() == c.end()”才成立,而“c.end() – c.begin()”总是会等于 c.size()。(后一句只有在迭代器可以做减法运算时才有效,例如,begin() 和 end() 都返回随机存取迭代器,但不是所有容器的这两个函数都这样。见前面的双向链表示例。)

兼容 STL 的翻转函数应该这么写:

template<typename T> void reverse_array_stl_compliant(T *begin, T *end) {

      // We should at first decrement 'end'

      // But only for non-empty range

      if(begin != end)

      {

           end--;

           if(begin != end) {

                while(true) {

                     swap(*begin, *end);

                     begin++;

                     If(begin == end) {

                          break;

                     }

                     end--;

                     if(begin == end) {

                          break;

                     }

                }

           }

      }
 }

注意,这个函数和标准函数 std::reverse(T begin, T end) 的功能一样,这个标准函数可以在算法模块找到(头文件要包含 #include <algorithm>)。

另外,只要对象定义了足够的功能函数,任何对象都可以作为迭代器传递给 STL 算法和函数。这些就是模板的强大来源。看下面的例子:

vector<int> v;

 // ...

 vector<int> v2(v);

 vector<int> v3(v.begin(), v.end()); // v3 equals to v2

 int data[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 };
 vector<int> primes(data, data+(sizeof(data) / sizeof(data[0])));

最后一行代码用一个普通数组 C 构造了一个 vector。不带下标的‘data’作为一个指向数组头的指针。‘data + N’指向第 N 个元素,因此,当 N 表示数组大小时,‘data + N’就指向第一个不在数组内的元素,那么‘data + length of data’可以作为数组‘data’的尾部迭代器。表达式‘sizeof(data)/sizeof(data[0])’返回数组 data 的大小,但只在少数情况下才成立。因此,除非是用这种方法构造的容器,否则不要在任何其他情况下使用这个表达式来获取容器大小。

此外,我们甚至可以像下面这样构造容器:

vector<int> v;

 // ...
 vector<int> v2(v.begin(), v.begin() + (v.size()/2));

构造的vector容器 v2 等于v 的前半部分。下面是翻转函数 reverse() 的示例:

 int data[10] = { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 };
 reverse(data+2, data+6); // the range { 5, 7, 9, 11 } is now { 11, 9, 7, 5 };

每个容器都有 rbegin()/rend() 函数,它们返回反向迭代器(和正常迭代器的指向相反)。反向迭代器用来从后往前地遍历容器。因此:

vector<int> v;
vector<int> v2(v.rbegin()+(v.size()/2), v.rend());

上面用 v 的前半部分来构造 v2,但顺序上前后颠倒。要创建一个迭代器对象,必须指定类型。在容器的类型后面加上“::iterator”、“::const_iterator”、“::reverse_iterator”或“::const_reverse_iterator”就可以构建迭代器的类型。因此,可以这样遍历 vector:

vector<int> v;

 // ...

 // Traverse all container, from begin() to end()

 for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {

      *it++; // Increment the value iterator is pointing to
 }

我推荐使用‘!=’而不是‘<’,使用‘empty()’而不要用‘size() != 0’——对于某些容器类型来说,无法高效地确定迭代器的前后顺序。

现在你了解了 STL 算法 reverse()。很多 STL 算法的声明方式相同:得到一对迭代器(一个范围的初始迭代器和尾部迭代器),并返回一个迭代器。

Find() 算法在一个区间内寻找合适的元素。如果找到了合适的元素,就返回指向第一个匹配元素的迭代器。否则,返回的值指向区间的尾部。看代码:

vector<int> v;

 for(int i = 1; i < 100; i++) {

      v.push_back(i*i);

 }

 if(find(v.begin(), v.end(), 49) != v.end()) {

      // ...
 }

要得到被找到元素的下标,必须用 find() 返回的结果减去初始迭代器:

int i = (find(v.begin(), v.end(), 49) - v.begin();

 if(i < v.size()) {

      // ...
 }

使用 STL 算法时,记得在源码中加上 #include <algorithm>。

Min_element 和 max_element 算法分别返回指向最小值元素和最大值元素的迭代器。要得到最小/最大值元素的值,就像在函数 find() 中一样,用 *min_element(…) 和 *max_elment(…),在数组中减去一个容器或范围的初始迭代器来取得下标值:

int data[5] = { 1, 5, 2, 4, 3 };
 vector<int> X(data, data+5);
 int v1 = *max_element(X.begin(), X.end()); // Returns value of max element in vector
 int i1 = min_element(X.begin(), X.end()) – X.begin; // Returns index of min element in vector
 int v2 = *max_element(data, data+5); // Returns value of max element in array
 int i3 = min_element(data, data+5) – data; // Returns index of min element in array

现在,你可以看到一个有效的宏定义如下:

 #define all(c) c.begin(), c.end()

不要将宏定义中的右边部分全部放到圆括号中去——那是错的!

另一个很好的算法是 sort(),使用很简单。思考下面的示例:

 vector<int> X;
 // ...
 sort(X.begin(), X.end()); // Sort array in ascending order
 sort(all(X)); // Sort array in ascending order, use our #define
 sort(X.rbegin(), X.rend()); // Sort array in descending order using with reverse iterators

编译 STL 程序

在这里有必要指出 STL 的错误信息。由于 STL 分布在源代码中,那编译器就必须创建有效的可执行文件,而 STL 的一个特性就是错误信息不可读。例如,如果你把一个 vector<int> 作为常引用参数(当你应该这么做的时候)传递给某个函数:

 void f(const vector<int>& v) {

      for(

           vector<int>::iterator it = v.begin(); // hm... where’s the error?..
           // ...
      // ...
 }

这里的错误是,你正试图对一个定义了 begin() 成员函数的常量对象创建非常量迭代器(因为识别这种错误比实际更正它更难)。正确的代码是这样:

void f(const vector<int>& v) {

      int r = 0;

      // Traverse the vector using const_iterator

      for(vector<int>::const_iterator it = v.begin(); it != v.end(); it++) {

           r += (*it)*(*it);
      }
      return r;
 }

尽管如此,还是来说说‘typeof’,它是 GNU C++ 非常重要的特性。在编译过程中,这个运算符会被替换成表达式的类型。思考下面的示例:

 typeof(a+b) x = (a+b);

这句代码创建了变量 x,它的类型和表达式 (a + b)的类型一致。注意,对任何类型的 STL 容器来说,typeof(v.size()) 得到的值都是无符号的。但在Topcoder 上,typeof 最重要的应用是遍历容器。思考下列宏定义:

 #define tr(container, it) 

      for(typeof(container.begin()) it = container.begin(); it != container.end(); it++)

使用这些宏,我们可以遍历每一种容器而不仅仅是 vector。这些宏会为常量对象生成 const_iterator,为非常量对象生成常规迭代器,而你永远不会在这里出错。

void f(const vector<int>& v) {

      int r = 0;

      tr(v, it) {

           r += (*it)*(*it);

      }

      return r;
 }

注意:为了提高可读性,在 #define 这一行我并没有添加额外的圆括号。阅读文章的后续部分得到更多关于 #define 的正确表述,你可以在练习室里面自己试试。

Vector 不需要真的遍历宏定义,但对于更复杂的数据类型(不支持下标,迭代器是获取数据的唯一方式)来说很方便。我们稍后会在文章中谈及这一点。

Vector 中的数据操作

可以用 insert() 函数往 vector 中插入一个元素:

 vector<int> v;
 // ...
 v.insert(1, 42); // Insert value 42 after the first

从第二个(下标为1的元素)往后的所有元素都要右移一位,从而空出一个位置给新插入的元素。如果你打算添加很多元素,那多次右移并不可取——明智的做法是单次调用 insert()。因此,insert() 有一种区间形式:

vector<int> v;
 vector<int> v2;
 // ..
 // Shift all elements from second to last to the appropriate number of elements.
 // Then copy the contents of v2 into v.
 v.insert(1, all(v2));

Vector 还有一个成员函数 erase,它有两种形式。猜猜都是什么:

erase(iterator);

erase(begin iterator, end iterator);

第一个例子删除 vector 中的单个元素,第二个例子用两个迭代器指定区间并从vector 中删除整个区间内的元素。

字符串(string)

这是一个操纵字符串的特殊容器。这个字符串容器稍微不同于 vector<char>。绝大部分的不同在于字符串控制函数和内存管理策略。字符串有不支持迭代器的子串函数 substring(),只支持下标:

string s = "hello";

string s1 = s.substr(0, 3), // "hel"
       s2 = s.substr(1, 3), // "ell"
       s3 = s.substr(0, s.length()-1), "hell"
       s4 = s.substr(1); // "ello"

谨防对空串执行(s.length() – 1),因为 s.length() 的返回值不带符号,而 unsigned(0) – 1 得到的结果绝对不是你想的那样。

Set

总是很难决定要先描述哪种容器——set 还是 map。我的观点是,如果读者了解一些算法的基本知识,从‘set’开始会更容易理解。

思考我们需要一个拥有下列特性的容器:

  • 添加一个元素,但不允许和已有元素重复[复制?]
  • 移除元素
  • 获取元素个数(不同元素的个数)
  • 检查集合中是否存在某个元素

这个操作的使用相当频繁。STL 为此提供了特殊容器——set。Set 可以在 O(log N)(其中 N 是 set 中对象的个数)的时间复杂度下添加、移除元素,并检查特定元素是否存在。向 set 添加元素时,如果和已有元素值重复,那新添加的元素就会被抛弃。在常数时间复杂度 O(1) 下返回 set 的元素个数。我们将在后面讨论 set 和 map 的算法实现——现在,我们研究一下函数接口:

set<int> s;
 for(int i = 1; i <= 100; i++) {
      s.insert(i); // Insert 100 elements, [1..100]
 }

 s.insert(42); // does nothing, 42 already exists in set

 for(int i = 2; i <= 100; i += 2) {
      s.erase(i); // Erase even values
 }
 int n = int(s.size()); // n will be 50

Set 不使用 push_back() 成员函数。这样是有道理的:因为 set 中元素的添加顺序并不重要,因此这里用不上 push_back()。

由于 set 不是线性容器,不可能用下标获取 set 中的元素。因此,遍历 set 元素的唯一方法就是使用迭代器。

// Calculate the sum of elements in set

 set<int> S;
 // ...

 int r = 0;

 for(set<int>::const_iterator it = S.begin(); it != S.end(); it++) {
      r += *it;
 }

在这里使用遍历宏会更简洁。为什么?想象一下你有这样的容器 set<pair<string,pair<int,vector<int>>>>,怎么遍历呢?写迭代器的类型名称?天呐,还是用我们为遍历迭代器类型而定义的宏吧。

set< pair<string, pair< int, vector<int> > > SS;

 int total = 0;

 tr(SS, it) {

      total += it->second.first;

 }

注意这样的语法‘it->second.first’。由于‘it’是一个迭代器,所以我们必须在运算前从‘it’得到对象。因此,正确的语法是‘(*it).second.first’。无论如何,写‘something->’总是比写‘(*something)’更容易。完整的解释会很长——只要记住,对迭代器而言两种语法都允许。

使用‘find()’成员函数确定集合 set 中是否存在某个元素。不要搞混了,因为 STL 中有很多‘find()’。有一个全局算法‘find()’,输入两个迭代器和一个元素,它能工作在 O(N) 的线性时间复杂度下。你可能会用它来搜索 set 中的元素,但是明明存在一个 O(log N) 时间复杂度的算法,为何要用一个 O(N) 的算法呢?在 set 和 map (还包括 multiset/multimap、hash_map/hash_set等容器)中搜索元素时,不要使用全局的搜索函数 find() ——反而应该使用成员函数‘set::find()’。作为‘顺序的’find函数,set::find 会返回一个迭代器,不论这个迭代器指向被找到的元素,还是指向‘end()’。因此,像这样检查元素是否存在:

set<int> s;

 // ...

 if(s.find(42) != s.end()) {
      // 42 presents in set
 }
 else {
      // 42 not presents in set
 }

作为成员函数被调用时,另一个工作在 O(log N) 时间复杂度下的算法是计数函数 count。有的人认为这样

if(s.count(42) != 0) {

      // …
 }

或者甚至这样

if(s.count(42)) {

      // …
 }

写更方便。个人来说,我不这么想。在 set/map 中使用 count() 没有意义:元素要么存在,要么不存在。对我来说,我更愿意使用下面两个宏:

#define present(container, element) (container.find(element) != container.end())

#define cpresent(container, element) (find(all(container),element) != container.end())

(记住 all(c) 代表“c.begin(), c.end()”)

这里,‘present()’用成员函数‘find()’ (比如 set/map 等等)来返回容器中是否存在某个元素,而‘cpresent’则是为 vector 定义的。

使用 erase() 函数从 set 中删除一个元素。

set<int> s;

 // …
 s.insert(54);
 s.erase(29);

Erase() 函数也有区间操作形式:

set<int> s;

 // ..

 set<int>::iterator it1, it2;

 it1 = s.find(10);

 it2 = s.find(100);
 // Will work if it1 and it2 are valid iterators, i.e. values 10 and 100 present in set.
 s.erase(it1, it2); // Note that 10 will be deleted, but 100 will remain in the container

Set 有一个区间构造函数:

int data[5] = { 5, 1, 4, 2, 3 };
set<int> S(data, data+5);

这样可以轻松避免 vector 中的重复元素,然后排序:

vector<int> v;

 // …

 set<int> s(all(v));
 vector<int> v2(all(s));

这里,‘v2’将和‘v’包含相同元素,但以升序排列,并且移除了重复元素。任何可比较的元素都可以存储在 set中。这个在后面解释。

Map

Map 有两种解释。简单版本如下:

map<string, int> M;

 M["Top"] = 1;

 M["Coder"] = 2;

 M["SRM"] = 10;

 int x = M["Top"] + M["Coder"];

 if(M.find("SRM") != M.end()) {

      M.erase(M.find("SRM")); // or even M.erase("SRM")
 }

很简单,对吧?

实际上,map 非常像 set,除了一点——它包含的不只是值而是键值对 pair<key, value>。Map 保证最多只有一个键值对拥有指定键。另一个很讨喜的地方是, map 定义了下标运算符 []。

用宏‘tr()’可以轻易遍历 map。注意,迭代器是键值对 std::pair。因此,用 it->second 来取值,示例如下:

 map<string, int> M;

 // …

 int r = 0;

 tr(M, it) {

      r += it->second;

 }

不要通过迭代器来更改 map 元素的键,因为这可能破坏 map 内部数据结构的完整性(见下面的解释)。

在 map::find() 和 map::operator [] 之间有一个重要的区别。Map::find() 永远不会改变 map 的内容,而操作符 [] 则会在元素不存在时创建一个新元素。有时这样做很方便,但当你不想添加新元素时,在循环中多次使用操作符 [] 绝对不是好主意。这就是为什么把 map 作为常引用参数传递给某个函数时,可能不用操作符 [] 的原因:

void f(const map<string, int>& M) {

      if(M["the meaning"] == 42) { // Error! Cannot use [] on const map objects!

      }

      if(M.find("the meaning") != M.end() && M.find("the meaning")->second == 42) { // Correct

           cout << "Don't Panic!" << endl;
      }
 }

关于 Map 和 Set 的注意事项

从内部看,map 和 set 几乎都是以红黑树的结构存储。我们确实不必担忧内部结构,要记住的是,遍历容器时 map 和 set 的元素总是按升序排列。而这也是为何在遍历 map 或 set时,极力不推荐改变键值的原因:如果所做的修改破坏了元素间的顺序,这至少会导致容器的算法失效。

但在解决 TopCoder 的问题时,几乎都会用上 map 和 set 的元素总是有序这个事实。

另一件重要的事情是,map 和 set 的迭代器都定义了运算符 ++ 和 –。因此,如果 set 里存在值 42,而它不是第一个也不是最后一个元素,那下列代码会奏效:

set<int> S;

 // ...

 set<int>::iterator it = S.find(42);

 set<int>::iterator it1 = it, it2 = it;
 it1--;
 it2++;
 int a = *it1, b = *it2;

这里的‘a’包含 42 左边的第一个相邻元素,而‘b’则包含右边的第一个相邻元素。

进一步讨论算法

是时候稍微深入探讨算法。大部分算法都声明在标准头文件 #include <algorithm> 中。首先,STL 提供了三种很简单的算法:min(a, b)、max(a, b)、swap(a, b)。这里,min(a, b) 和 max(a, b) 分别返回两个元素间的最小值和最大值,而 swap(a, b) 则交换两个元素的值。

算法 sort() 的使用也很普遍。调用 sort(begin, end) 按升序对一个区间的元素进行排序。注意,sort() 需要随机存取迭代器,因此它不能作用在所有类型的容器上。无论如何,你很可能永远都不会对已然有序的 set 调用 sort()。

你已经了解了算法 find()。调用 find(begin, end, element) 返回‘element’首次出现时对应的迭代器,如果找不到则返回 end。和 find(…) 相反,count(begin, end, element) 返回一个元素在容器或容器的某个范围内出现的次数。记住,set 和 map 都有成员函数 find() 和 count(),它们的时间复杂度是 O(log N),而 std::find() 和 std::count() 的时间复杂度是 O(N)。

其他有用的算法还有 next_permutation() 和 prev_permutation()。先说说 next_permutation。调用 next_permutation(begin, end) 令区间 [begin, end) 保存区间元素的下一个全排列顺序,如果当前顺序已是最后一种全排列则返回 false。当然, next_permutation 使得许多任务变得相当简单。如果你想验证所有的全排列方式,只要这么写:

 vector<int> v;

 for(int i = 0; i < 10; i++) {

      v.push_back(i);

 }

 do {

      Solve(..., v);

 } while(next_permutation(all(v));

在第一次调用 next_permutation(…) 之前,别忘了确保容器中的元素已排序。元素的初始状态应该形成第一个全排列状态;否则,某些全排列状态会被遗漏,得不到验证。

字符串流

你常常需要进行一些字符串的处理、输入或输出,C++ 为此提供了两个有趣的对象:‘istringstream’和‘ostringstream’。这两个对象都声明在标准头文件 #include <sstream> 中。

对象 istringstream 允许你从一个字符串读入,就像从一个标准输入读数据一样。直接看源码:

void f(const string& s) 
{
      // Construct an object to parse strings

      istringstream is(s);

      // Vector to store data

      vector<int> v;

      // Read integer while possible and add it to the vector

      int tmp;

      while(is >> tmp) 
      {
           v.push_back(tmp);
      }
 }

对象 ostringstream 用来格式化输出。代码如下:

string f(const vector<int>& v) 
{

      // Constucvt an object to do formatted output

      ostringstream os;
      // Copy all elements from vector<int> to string stream as text

      tr(v, it) 
      {

           os << ' ' << *it;

      }
      // Get string from string stream

      string s = os.str();
      // Remove first space character

      if(!s.empty()) 
      { // Beware of empty string here
           s = s.substr(1);
      }
      return s;
}

总结

为了继续探讨 STL,我将总结后面会用到的模板列表。这会简化代码示例的阅读,并且希望能提高你的 TopCoder 技巧。模板和宏的简短列表如下:

typedef vector<int> vi;

 typedef vector<vi> vvi;

 typedef pair<int,int> ii;

 #define sz(a) int((a).size())

 #define pb push_back

 #defile all(c) (c).begin(),(c).end()

 #define tr(c,i) for(typeof((c).begin() i = (c).begin(); i != (c).end(); i++)

 #define present(c,x) ((c).find(x) != (c).end())

 #define cpresent(c,x) (find(all(c),x) != (c).end())

由于容器 vector<int> 的使用相当普遍,因此在列表中一并列出。实际上我发现,给许多容器(尤其是 vector<string>、vector<ii>、vector<pair<double, ii>>等等)定义简短的别称非常方便。但上面的列表只给出了理解后文所需的宏。还有一点要牢记:当 #define 左侧的符号出现在右侧时,为了避免很多棘手的问题,应该在上面加上一对圆括号。

标准模板库(STL)使用入门(上),首发于博客 - 伯乐在线

24 Jun 00:46

10种简单的Java性能优化

by 一直在路上

你是否正打算优化hashCode()方法?是否想要绕开正则表达式?Lukas Eder介绍了很多简单方便的性能优化小贴士以及扩展程序性能的技巧。

最近“全网域(Web Scale)”一词被炒得火热,人们也正在通过扩展他们的应用程序架构来使他们的系统变得更加“全网域”。但是究竟什么是全网域?或者说如何确保全网域?

扩展的不同方面

全网域被炒作的最多的是扩展负载(Scaling load),比如支持单个用户访问的系统也可以支持10 个、100个、甚至100万个用户访问。在理想情况下,我们的系统应该保持尽可能的“无状态化(stateless)”。即使必须存在状态,也可以在网络的不同处理终端上转化并进行传输。当负载成为瓶颈时候,可能就不会出现延迟。所以对于单个请求来说,耗费50到100毫秒也是可以接受的。这就是所谓的横向扩展(Scaling out)。

扩展在全网域优化中的表现则完全不同,比如确保成功处理一条数据的算法也可成功处理10条、100条甚至100万条数据。无论这种度量类型是是否可行,事件复杂度(大O符号)是最佳描述。延迟是性能扩展杀手。你会想尽办法将所有的运算处理在同一台机器上进行。这就是所谓的纵向扩展(Scaling up)。

如果天上能掉馅饼的话(当然这是不可能的),我们或许能把横向扩展和纵向扩展组合起来。但是,今天我们只打算介绍下面几条提升效率的简单方法。

大O符号

Java 7的 ForkJoinPool 和Java8 的并行数据流(parallel Stream) 都对并行处理有所帮助。当在多核处理器上部署Java程序时表现尤为明显,因所有的处理器都可以访问相同的内存。

所以,这种并行处理较之在跨网络的不同机器上进行扩展,根本的好处是几乎可以完全消除延迟。

但不要被并行处理的效果所迷惑!请谨记下面两点:

  • 并行处理会吃光处理器资源。并行处理为批处理带来了极大的好处,但同时也是非同步服务器(如HTTP)的噩梦。有很多原因可以解释,为什么在过去的几十年中我们一直在使用单线程的Servlet模型。并行处理仅在纵向扩展时才能带来实际的好处。
  • 并行处理对算法复杂度没有影响。如果你的算法的时间复杂度为 O(nlogn),让算法在 c 个处理器上运行,事件复杂度仍然为 O(nlogn/c), 因为 c 只是算法中的一个无关紧要的常量。你节省的仅仅是时钟时间(wall-clock time),实际的算法复杂度并没有降低。

降低算法复杂度毫无疑问是改善性能最行之有效的办法。比如对于一个 HashMap 实例的 lookup() 方法来说,事件复杂度 O(1) 或者空间复杂度 O(1) 是最快的。但这种情况往往是不可能的,更别提轻易地实现。

如果你不能降低算法的复杂度,也可以通过找到算法中的关键点并加以改善的方法,来起到改善性能的作用。假设我们有下面这样的算法示意图:

该算法的整体时间复杂度为 O(N3),如果按照单独访问顺序计算也可得出复杂度为 O(N x O x P)。但是不管怎样,在我们分析这段代码时会发现一些奇怪的场景:

  • 在开发环境中,通过测试数据可以看到:左分支(N->M->Heavy operation)的时间复杂度 M 的值要大于右边的 O 和 P,所以在我们的分析器中仅仅看到了左分支。
  • 在生产环境中,你的维护团队可能会通过 AppDynamicsDynaTrace 或其它小工具发现,真正导致问题的罪魁祸首是右分支(N -> O -> P -> Easy operation or also N.O.P.E.)。

在没有生产数据参照的情况下,我们可能会轻易的得出要优化“高开销操作”的结论。但我们做出的优化对交付的产品没有起到任何效果。

优化的金科玉律不外乎以下内容:

  • 良好的设计将会使优化变得更加容易。
  • 过早的优化并不能解决多有的性能问题,但是不良的设计将会导致优化难度的增加。

理论就先谈到这里。假设我们已经发现了问题出现在了右分支上,很有可能是因产品中的简单处理因耗费了大量的时间而失去响应(假设N、O和 P 的值非常大), 请注意文章中提及的左分支的时间复杂度为 O(N3)。这里所做出的努力并不能扩展,但可以为用户节省时间,将困难的性能改善推迟到后面再进行。

这里有10条改善Java性能的小建议:

1、使用StringBuilder

StingBuilder 应该是在我们的Java代码中默认使用的,应该避免使用 + 操作符。或许你会对 StringBuilder 的语法糖(syntax sugar)持有不同意见,比如:

String x = "a" + args.length + "b";

将会被编译为:

0  new java.lang.StringBuilder [16]
 3  dup
 4  ldc <String "a"> [18]
 6  invokespecial java.lang.StringBuilder(java.lang.String) [20]
 9  aload_0 [args]
10  arraylength
11  invokevirtual java.lang.StringBuilder.append(int) : java.lang.StringBuilder [23]
14  ldc <String "b"> [27]
16  invokevirtual java.lang.StringBuilder.append(java.lang.String) : java.lang.StringBuilder [29]
19  invokevirtual java.lang.StringBuilder.toString() : java.lang.String [32]
22  astore_1 [x]

但究竟发生了什么?接下来是否需要用下面的部分来对 String 进行改善呢?

String x = "a" + args.length + "b";

if (args.length == 1)
    x = x + args[0];

现在使用到了第二个 StringBuilder,而且这个 StringBuilder 不会消耗堆中额外的内存,但却给 GC 带来了压力。

StringBuilder x = new StringBuilder("a");
x.append(args.length);
x.append("b");

if (args.length == 1);
    x.append(args[0]);

小结

在上面的样例中,如果你是依靠Java编译器来隐式生成实例的话,那么编译的效果几乎和是否使用了 StringBuilder 实例毫无关系。请记住:在  N.O.P.E 分支中,每次CPU的循环的时间到白白的耗费在GC或者为 StringBuilder 分配默认空间上了,我们是在浪费 N x O x P 时间。

一般来说,使用 StringBuilder 的效果要优于使用 + 操作符。如果可能的话请在需要跨多个方法传递引用的情况下选择 StringBuilder,因为 String 要消耗额外的资源。JOOQ在生成复杂的SQL语句便使用了这样的方式。在整个抽象语法树AST Abstract Syntax Tree)SQL传递过程中仅使用了一个 StringBuilder 。

更加悲剧的是,如果你仍在使用 StringBuffer 的话,那么用 StringBuilder 代替 StringBuffer 吧,毕竟需要同步字符串的情况真的不多。

2、避免使用正则表达式

正则表达式给人的印象是快捷简便。但是在 N.O.P.E 分支中使用正则表达式将是最糟糕的决定。如果万不得已非要在计算密集型代码中使用正则表达式的话,至少要将 Pattern 缓存下来,避免反复编译Pattern。

static final Pattern HEAVY_REGEX =
    Pattern.compile("(((X)*Y)*Z)*");

如果仅使用到了如下这样简单的正则表达式的话:

String[] parts = ipAddress.split("\\.");

这是最好还是用普通的 char[] 数组或者是基于索引的操作。比如下面这段可读性比较差的代码其实起到了相同的作用。

int length = ipAddress.length();
int offset = 0;
int part = 0;
for (int i = 0; i < length; i++) {
    if (i == length - 1 ||
            ipAddress.charAt(i + 1) == '.') {
        parts[part] =
            ipAddress.substring(offset, i + 1);
        part++;
        offset = i + 2;
    }
}

上面的代码同时表明了过早的优化是没有意义的。虽然与 split() 方法相比较,这段代码的可维护性比较差。

挑战:聪明的小伙伴能想出更快的算法吗?

小结

正则表达式是十分有用,但是在使用时也要付出代价。尤其是在 N.O.P.E 分支深处时,要不惜一切代码避免使用正则表达式。还要小心各种使用到正则表达式的JDK字符串方法,比如 String.replaceAll()String.split()。可以选择用比较流行的开发库,比如 Apache Commons Lang 来进行字符串操作。

3、不要使用iterator()方法

这条建议不适用于一般的场合,仅适用于在 N.O.P.E 分支深处的场景。尽管如此也应该有所了解。Java 5格式的循环写法非常的方便,以至于我们可以忘记内部的循环方法,比如:

for (String value : strings) {
    // Do something useful here
}

当每次代码运行到这个循环时,如果 strings 变量是一个 Iterable 的话,代码将会自动创建一个Iterator 的实例。如果使用的是 ArrayList 的话,虚拟机会自动在堆上为对象分配3个整数类型大小的内存。

private class Itr implements Iterator<E> {
    int cursor;
    int lastRet = -1;
    int expectedModCount = modCount;
    // ...

也可以用下面等价的循环方式来替代上面的 for 循环,仅仅是在栈上“浪费”了区区一个整形,相当划算。

int size = strings.size();
for (int i = 0; i < size; i++) {
    String value : strings.get(i);
    // Do something useful here
}

如果循环中字符串的值是不怎么变化,也可用数组来实现循环。

for (String value : stringArray) {
    // Do something useful here
}

小结

无论是从易读写的角度来说,还是从API设计的角度来说迭代器、Iterable接口和 foreach 循环都是非常好用的。但代价是,使用它们时是会额外在堆上为每个循环子创建一个对象。如果循环要执行很多很多遍,请注意避免生成无意义的实例,最好用基本的指针循环方式来代替上述迭代器、Iterable接口和 foreach 循环。

讨论

一些与上述内容持反对意见的看法(尤其是用指针操作替代迭代器)详见Reddit上的讨论

4、不要调用高开销方法

有些方法的开销很大。以 N.O.P.E 分支为例,我们没有提到叶子的相关方法,不过这个可以有。假设我们的JDBC驱动需要排除万难去计算 ResultSet.wasNull() 方法的返回值。我们自己实现的SQL框架可能像下面这样:

if (type == Integer.class) {
    result = (T) wasNull(rs,
        Integer.valueOf(rs.getInt(index)));
}

// And then...
static final <T> T wasNull(ResultSet rs, T value)
throws SQLException {
    return rs.wasNull() ? null : value;
}

在上面的逻辑中,每次从结果集中取得 int 值时都要调用 ResultSet.wasNull() 方法,但是 getInt() 的方法定义为:

返回类型:变量值;如果SQL查询结果为NULL,则返回0。

所以一个简单有效的改善方法如下:

static final <T extends Number> T wasNull(
    ResultSet rs, T value
)
throws SQLException {
    return (value == null ||
           (value.intValue() == 0 && rs.wasNull()))
        ? null : value;
}

这是轻而易举的事情。

小结

将方法调用缓存起来替代在叶子节点的高开销方法,或者在方法约定允许的情况下避免调用高开销方法。

5、使用原始类型和栈

上面介绍了来自 jOOQ的例子中使用了大量的泛型,导致的结果是使用了 byte、 short、 int 和 long 的包装类。但至少泛型在Java 10或者Valhalla项目中被专门化之前,不应该成为代码的限制。因为可以通过下面的方法来进行替换:

//存储在堆上
Integer i = 817598;

……如果这样写的话:

// 存储在栈上
int i = 817598;

在使用数组时情况可能会变得更加糟糕:

//在堆上生成了三个对象
Integer[] i = { 1337, 424242 };

……如果这样写的话:

// 仅在堆上生成了一个对象
int[] i = { 1337, 424242 };

小结

当我们处于 N.O.P.E. 分支的深处时,应该极力避免使用包装类。这样做的坏处是给GC带来了很大的压力。GC将会为清除包装类生成的对象而忙得不可开交。

所以一个有效的优化方法是使用基本数据类型、定长数组,并用一系列分割变量来标识对象在数组中所处的位置。

遵循LGPL协议的 trove4j 是一个Java集合类库,它为我们提供了优于整形数组 int[] 更好的性能实现。

例外

下面的情况对这条规则例外:因为 boolean 和 byte 类型不足以让JDK为其提供缓存方法。我们可以这样写:

Boolean a1 = true; // ... syntax sugar for:
Boolean a2 = Boolean.valueOf(true);

Byte b1 = (byte) 123; // ... syntax sugar for:
Byte b2 = Byte.valueOf((byte) 123);

其它整数基本类型也有类似情况,比如 char、short、int、long。

不要在调用构造方法时将这些整型基本类型自动装箱或者调用 TheType.valueOf() 方法。

也不要在包装类上调用构造方法,除非你想得到一个不在堆上创建的实例。这样做的好处是为你为同事献上一个巨坑的愚人节笑话

非堆存储

当然了,如果你还想体验下堆外函数库的话,尽管这可能参杂着不少战略决策,而并非最乐观的本地方案。一篇由Peter Lawrey和 Ben Cotton撰写的关于非堆存储的很有意思文章请点击: OpenJDK与HashMap——让老手安全地掌握(非堆存储!)新技巧

6、避免递归

现在,类似Scala这样的函数式编程语言都鼓励使用递归。因为递归通常意味着能分解到单独个体优化的尾递归(tail-recursing)。如果你使用的编程语言能够支持那是再好不过。不过即使如此,也要注意对算法的细微调整将会使尾递归变为普通递归。

希望编译器能自动探测到这一点,否则本来我们将为只需使用几个本地变量就能搞定的事情而白白浪费大量的堆栈框架(stack frames)。

小结

这节中没什么好说的,除了在 N.O.P.E 分支尽量使用迭代来代替递归。

7、使用entrySet()

当我们想遍历一个用键值对形式保存的 Map 时,必须要为下面的代码找到一个很好的理由:

for (K key : map.keySet()) {
    V value : map.get(key);
}

更不用说下面的写法:

for (Entry<K, V> entry : map.entrySet()) {
    K key = entry.getKey();
    V value = entry.getValue();
}

在我们使用 N.O.P.E. 分支应该慎用map。因为很多看似时间复杂度为 O(1) 的访问操作其实是由一系列的操作组成的。而且访问本身也不是免费的。至少,如果不得不使用map的话,那么要用 entrySet() 方法去迭代!这样的话,我们要访问的就仅仅是Map.Entry的实例。

小结

在需要迭代键值对形式的Map时一定要用 entrySet() 方法。

9、使用EnumSet或EnumMap

在某些情况下,比如在使用配置map时,我们可能会预先知道保存在map中键值。如果这个键值非常小,我们就应该考虑使用 EnumSet 或 EnumMap,而并非使用我们常用的 HashSet 或 HashMap。下面的代码给出了很清楚的解释:

private transient Object[] vals;

public V put(K key, V value) {
    // ...
    int index = key.ordinal();
    vals[index] = maskNull(value);
    // ...
}

上段代码的关键实现在于,我们用数组代替了哈希表。尤其是向map中插入新值时,所要做的仅仅是获得一个由编译器为每个枚举类型生成的常量序列号。如果有一个全局的map配置(例如只有一个实例),在增加访问速度的压力下,EnumMap 会获得比 HashMap 更加杰出的表现。原因在于 EnumMap 使用的堆内存比 HashMap 要少 一位(bit),而且 HashMap 要在每个键值上都要调用 hashCode() 方法和 equals() 方法。

小结

Enum 和 EnumMap 是亲密的小伙伴。在我们用到类似枚举(enum-like)结构的键值时,就应该考虑将这些键值用声明为枚举类型,并将之作为 EnumMap 键。

9、优化自定义hasCode()方法和equals()方法

在不能使用EnumMap的情况下,至少也要优化 hashCode() 和 equals() 方法。一个好的 hashCode() 方法是很有必要的,因为它能防止对高开销 equals() 方法多余的调用。

在每个类的继承结构中,需要容易接受的简单对象。让我们看一下jOOQ的 org.jooq.Table 是如何实现的?

最简单、快速的 hashCode() 实现方法如下:

// AbstractTable一个通用Table的基础实现:

@Override
public int hashCode() {

    // [#1938] 与标准的QueryParts相比,这是一个更加高效的hashCode()实现
    return name.hashCode();
}

name即为表名。我们甚至不需要考虑schema或者其它表属性,因为表名在数据库中通常是唯一的。并且变量 name 是一个字符串,它本身早就已经缓存了一个 hashCode() 值。

这段代码中注释十分重要,因继承自 AbstractQueryPart 的 AbstractTable 是任意抽象语法树元素的基本实现。普通抽象语法树元素并没有任何属性,所以不能对优化 hashCode() 方法实现抱有任何幻想。覆盖后的 hashCode() 方法如下:

// AbstractQueryPart一个通用抽象语法树基础实现:

@Override
public int hashCode() {
    // 这是一个可工作的默认实现。
    // 具体实现的子类应当覆盖此方法以提高性能。
    return create().renderInlined(this).hashCode();
}

换句话说,要触发整个SQL渲染工作流程(rendering workflow)来计算一个普通抽象语法树元素的hash代码。

equals() 方法则更加有趣:

// AbstractTable通用表的基础实现:

@Override
public boolean equals(Object that) {
    if (this == that) {
        return true;
    }

    // [#2144] 在调用高开销的AbstractQueryPart.equals()方法前,
    // 可以及早知道对象是否不相等。
    if (that instanceof AbstractTable) {
        if (StringUtils.equals(name,
            (((AbstractTable<?>) that).name))) {
            return super.equals(that);
        }

        return false;
    }

    return false;
}

首先,不要过早使用 equals() 方法(不仅在N.O.P.E.中),如果:

  • this == argument
  • this“不兼容:参数

注意:如果我们过早使用 instanceof 来检验兼容类型的话,后面的条件其实包含了argument == null。我在以前的博客中已经对这一点进行了说明,请参考10个精妙的Java编码最佳实践

在我们对以上几种情况的比较结束后,应该能得出部分结论。比如jOOQ的 Table.equals() 方法说明是,用来比较两张表是否相同。不论具体实现类型如何,它们必须要有相同的字段名。比如下面两个元素是不可能相同的:

  • com.example.generated.Tables.MY_TABLE
  • DSL.tableByName(“MY_OTHER_TABLE”)

如果我们能方便地判断传入参数是否等于实例本身(this),就可以在返回结果为 false 的情况下放弃操作。如果返回结果为 true,我们还可以进一步对父类(super)实现进行判断。在比较过的大多数对象都不等的情况下,我们可以尽早结束方法来节省CPU的执行时间。

一些对象的相似度比其它对象更高。

在jOOQ中,大多数的表实例是由jOOQ的代码生成器生成的,这些实例的 equals() 方法都经过了深度优化。而数十种其它的表类型(衍生表 (derived tables)、表值函数(table-valued functions)、数组表(array tables)、连接表(joined tables)、数据透视表(pivot tables)、公用表表达式(common table expressions)等,则保持 equals() 方法的基本实现。

10、考虑使用set而并非单个元素

最后,还有一种情况可以适用于所有语言而并非仅仅同Java有关。除此以外,我们以前研究的 N.O.P.E. 分支也会对了解从 O(N3) 到 O(n log n)有所帮助。

不幸的是,很多程序员的用简单的、本地算法来考虑问题。他们习惯按部就班地解决问题。这是命令式(imperative)的“是/或”形式的函数式编程风格。这种编程风格在由纯粹命令式编程向面对象式编程向函数式编程转换时,很容易将“更大的场景(bigger picture)”模型化,但是这些风格都缺少了只有在SQL和R语言中存在的:

声明式编程。

在SQL中,我们可以在不考虑算法影响下声明要求数据库得到的效果。数据库可以根据数据类型,比如约束(constraints)、键(key)、索引(indexes)等不同来采取最佳的算法。

在理论上,我们最初在SQL和关系演算(relational calculus)后就有了基本的想法。在实践中,SQL的供应商们在过去的几十年中已经实现了基于开销的高效优化器CBOs (Cost-Based Optimisers) 。然后到了2010版,我们才终于将SQL的所有潜力全部挖掘出来。

但是我们还不需要用set方式来实现SQL。所有的语言和库都支持Sets、collections、bags、lists。使用set的主要好处是能使我们的代码变的简洁明了。比如下面的写法:

SomeSet INTERSECT SomeOtherSet

而不是

// Java 8以前的写法
Set result = new HashSet();
for (Object candidate : someSet)
    if (someOtherSet.contains(candidate))
        result.add(candidate);

// 即使采用Java 8也没有很大帮助
someSet.stream()
       .filter(someOtherSet::contains)
       .collect(Collectors.toSet());

有些人可能会对函数式编程和Java 8能帮助我们写出更加简单、简洁的算法持有不同的意见。但这种看法不一定是对的。我们可以把命令式的Java 7循环转换成Java 8的Stream collection,但是我们还是采用了相同的算法。但SQL风格的表达式则是不同的:

SomeSet INTERSECT SomeOtherSet

上面的代码在不同的引擎上可以有1000种不同的实现。我们今天所研究的是,在调用 INTERSECT 操作之前,更加智能地将两个set自动的转化为 EnumSet 。甚至我们可以在不需要调用底层的 Stream.parallel() 方法的情况下进行并行 INTERSECT 操作。

总结

在这篇文章中,我们讨论了关于N.O.P.E.分支的优化。比如深入高复杂性的算法。作为jOOQ的开发者,我们很乐于对SQL的生成进行优化。

  • 每条查询都用唯一的StringBuilder来生成。
  • 模板引擎实际上处理的是字符而并非正则表达式。
  • 选择尽可能的使用数组,尤其是在对监听器进行迭代时。
  • 对JDBC的方法敬而远之。
  • 等等。

jOOQ处在“食物链的底端”,因为它是在离开JVM进入到DBMS时,被我们电脑程序所调用的最后一个API。位于食物链的底端意味着任何一条线路在jOOQ中被执行时都需要 N x O x P 的时间,所以我要尽早进行优化。

我们的业务逻辑可能没有N.O.P.E.分支那么复杂。但是基础框架有可能十分复杂(本地SQL框架、本地库等)。所以需要按照我们今天提到的原则,用Java Mission Control 或其它工具进行复查,确认是否有需要优化的地方。

相关文章

21 Jun 12:05

深入理解 Java 垃圾回收机制

by importnewzz

一、垃圾回收机制的意义

Java语言中一个显著的特点就是引入了垃圾回收机制,使c++程序员最头疼的内存管理的问题迎刃而解,它使得Java程序员在编写程序的时候不再需要考虑内存管理。由于有个垃圾回收机制,Java中的对象不再有“作用域”的概念,只有对象的引用才有“作用域”。垃圾回收可以有效的防止内存泄露,有效的使用空闲的内存。

ps:内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。

二、垃圾回收机制中的算法

Java语言规范没有明确地说明JVM使用哪种垃圾回收算法,但是任何一种垃圾回收算法一般要做2件基本的事情:(1)发现无用信息对象;(2)回收被无用对象占用的内存空间,使该空间可被程序再次使用。

1.引用计数法(Reference Counting Collector)

1.1算法分析

引用计数是垃圾收集器中的早期策略。在这种方法中,堆中每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个变量,该变量计数设置为1。当任何其它变量被赋值为这个对象的引用时,计数加1(a = b,则b引用的对象实例的计数器+1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数器减1。任何引用计数器为0的对象实例可以被当作垃圾收集。当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器减1。

1.2优缺点

优点:

引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:

无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0.

1.3引用计数算法无法解决循环引用问题,例如:

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
         
        object1.object = object2;
        object2.object = object1;
         
        object1 = null;
        object2 = null;
    }
}

最后面两句将object1和object2赋值为null,也就是说object1和object2指向的对象已经不可能再被访问,但是由于它们互相引用对方,导致它们的引用计数器都不为0,那么垃圾收集器就永远不会回收它们。

2.tracing算法(Tracing Collector) 或 标记-清除算法(mark and sweep)

2.1根搜索算法

根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,从一个节点GC ROOT开始,寻找对应的引用节点,找到这个节点以后,继续寻找这个节点的引用节点,当所有的引用节点寻找完毕之后,剩余的节点则被认为是没有被引用到的节点,即无用的节点。

java中可作为GC Root的对象有

1.虚拟机栈中引用的对象(本地变量表)

2.方法区中静态属性引用的对象

3. 方法区中常量引用的对象

4.本地方法栈中引用的对象(Native对象)

2.2tracing算法的示意图

2.3标记-清除算法分析

标记-清除算法采用从根集合进行扫描,对存活的对象对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如上图所示。标记-清除算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,因此会造成内存碎片。

3.compacting算法 或 标记-整理算法

标记-整理算法采用标记-清除算法一样的方式进行对象的标记,但在清除时不同,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

4.copying算法(Compacting Collector)

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收。它开始时把堆分成 一个对象 面和多个空闲面, 程序从对象面为对象分配空间,当对象满了,基于copying算法的垃圾 收集就从根集中扫描活动对象,并将每个 活动对象复制到空闲面(使得活动对象所占的内存之间没有空闲洞),这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。一种典型的基于coping算法的垃圾回收是stop-and-copy算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。

5.generation算法(Generational Collector)

分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。

年轻代(Young Generation)

1.所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。

2.新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。一个Eden区,两个 Survivor区(一般而言)。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。

3.当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收

4.新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)

年老代(Old Generation)

1.在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。

2.内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。

持久代(Permanent Generation)

用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。

三.GC(垃圾收集器)

新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge

老年代收集器使用的收集器:Serial Old、Parallel Old、CMS

Serial收集器(复制算法)

新生代单线程收集器,标记和清理都是单线程,优点是简单高效。

Serial Old收集器(标记-整理算法)

老年代单线程收集器,Serial收集器的老年代版本。

ParNew收集器(停止-复制算法) 

新生代收集器,可以认为是Serial收集器的多线程版本,在多核CPU环境下有着比Serial更好的表现。

Parallel Scavenge收集器(停止-复制算法)

并行收集器,追求高吞吐量,高效利用CPU。吞吐量一般为99%, 吞吐量= 用户线程时间/(用户线程时间+GC线程时间)。适合后台应用等对交互相应要求不高的场景。

Parallel Old收集器(停止-复制算法)

Parallel Scavenge收集器的老年代版本,并行收集器,吞吐量优先

CMS(Concurrent Mark Sweep)收集器(标记-清理算法)

高并发、低停顿,追求最短GC回收停顿时间,cpu占用比较高,响应时间快,停顿时间短,多核cpu 追求高响应时间的选择

四、GC的执行机制

由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。

Scavenge GC

一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。

Full GC

对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC:

1.年老代(Tenured)被写满

2.持久代(Perm)被写满

3.System.gc()被显示调用

4.上一次GC之后Heap的各域分配策略动态变化

五、Java有了GC同样会出现内存泄露问题

1.静态集合类像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,所有的对象Object也不能被释放,因为他们也将一直被Vector等应用着。

Static Vector v = new Vector(); 
for (int i = 1; i<100; i++) 
{ 
    Object o = new Object(); 
    v.add(o); 
    o = null; 
}

在这个例子中,代码栈中存在Vector 对象的引用 v 和 Object 对象的引用 o 。在 For 循环中,我们不断的生成新的对象,然后将其添加到 Vector 对象中,之后将 o 引用置空。问题是当 o 引用被置空后,如果发生 GC,我们创建的 Object 对象是否能够被 GC 回收呢?答案是否定的。因为, GC 在跟踪代码栈中的引用时,会发现 v 引用,而继续往下跟踪,就会发现 v 引用指向的内存空间中又存在指向 Object 对象的引用。也就是说尽管o 引用已经被置空,但是 Object 对象仍然存在其他的引用,是可以被访问到的,所以 GC 无法将其释放掉。如果在此循环之后, Object 对象对程序已经没有任何作用,那么我们就认为此 Java 程序发生了内存泄漏。

2.各种连接,数据库连接,网络连接,IO连接等没有显示调用close关闭,不被GC回收导致内存泄露。

3.监听器的使用,在释放对象的同时没有相应删除监听器的时候也可能导致内存泄露。

相关文章

21 Jun 01:32

Java I/O : Java中的进制详解

by 泥瓦匠BYSocket

作者:李强强

上一篇,泥瓦匠基础地讲了下Java I/O : Bit Operation 位运算。这一讲,泥瓦匠带你走进Java中的进制详解。

一、引子

在Java世界里,99%的工作都是处理这高层。那么二进制,字节码这些会在哪里用到呢?

自问自答:在跨平台的时候,就凸显神功了。比如说文件读写数据通信,还有Java编译后的字节码文件。下面会有个数据通信的例子哦。

Java对对象实现Serializablle接口,就可以将其转化为一系列字节,而在通信中,不必要关系数据如何在不同机器表示和字节的顺序。这里泥瓦匠对Serializablle接口,不做详细讲解,以后单独详解。

 

二、Java进制转换

首先认识下Java中的数据类型

1、Int整型:byte(8位,-128~127)、short(16位)、int(32位)、long(64位)

2、Float型:float(32位)、double(64位)

2、char字符:unicode字符(16位)

也就是说,一个int等价于长度为4的字节数组。

Java中进制如何转换呢?

在Java中,Int整形以及char字符型被包装的类中提供了一系列的操作方法。比如 java.lang.Integer api如图所示

QQ截图20150613210316

下面泥瓦匠写个demo,验证下。

package javaBasic.oi.byteoper;

public class IntegerOper
{
	public static void main(String[] args)
	{
		System.out.println("17的十六进制: " + Integer.toHexString(17));
		System.out.println("17的八进制:     " 	+ Integer.toOctalString(17));
		System.out.println("17的二进制:     " 	+ Integer.toBinaryString(17));

		System.out.println(Integer.valueOf("11", 16));
		System.out.println(Integer.valueOf("21", 8));
		System.out.println(Integer.valueOf("00010001", 2));
	}
}

右键Run一下,我们可以在控制台中看到如下输出:

17的十六进制: 11
17的八进制:   21
17的二进制:   10001
17
17
17

补充:如果值太大,则需要调用 java.lang.Long 提供的方法。

 

三、Java基本类型和字节神奇转换

这里泥瓦匠想到了自己是个学生,典型的OO思想。那学号:1206010035是整型,怎么转成字节呢,上面说的拥有字节码的对象能通信。所以,学校关于学号这个都是这样的方式通信的。因此,要将学号转成字节码才行。

泥瓦匠就写了个工具类 IntegerConvert.java:

package javaBasic.oi.byteoper;

public class IntegerConvert
{
	/**
	 * Int转字节数组
	 */
	public static byte[] int2Bytes(int inta)
	{
		// 32位Int可存于长度为4的字节数组 
		byte[] bytes = new byte[4];
		for (int i = 0; i < bytes.length; i++)
			bytes[i] = (byte)(int)((inta >> i * 8) & 0xff);// 移位和清零
		
		return bytes;
	}
	
	/**
	 * 字节数组转Int
	 */
	public static int bytes2Int(byte[] bytes)
	{
		int inta = 0;
		for (int i = 0; i < bytes.length; i++)
			inta += (int)((bytes[i] & 0xff) << i * 8);// 移位和清零
		
		return inta;
	}
	
	public static void main(String[] args)
	{
		// 将我的学号转换成字节码
		byte[] bytes = IntegerConvert.int2Bytes(1206010035);
		System.out.println(bytes[0] + " " + bytes[1] + " " + bytes[2] + " " + bytes[3]);
		// 字节码就可以转换回学号
		System.out.println(IntegerConvert.bytes2Int(bytes));
	}

}

跑一下,右键Run,可以看到以下输出:

-77 64 -30 71
1206010035

代码详细解释如下:

1、(inta >> i * 8) & 0xff

移位 清零从左往右,按8位获取1字节

2、这里使用的是小端法。地位字节放在内存低地址端,即该值的起始地址。补充:32位中分大端模式(PPC)和小段端模式(x86)。

 

自然,Long也有其转换方法,如下:

public class LongConvert
{
	/**
	 * long 转 byte数组
	 */
	public static byte[] long2Bytes(long longa)
	{
		byte[] bytes = new byte[8];
		for (int i = 0; i < bytes.length; i++)
			bytes[i] = (byte)(long)(((longa) >> i * 8) & 0xff); // 移位和清零
		
		return bytes;
	}
	
	/**
	 * byte数组 转 long
	 */
	public static long bytes2Long(byte[] bytes)
	{
		long longa = 0;
		for (int i = 0; i < bytes.length; i++)
			longa += (long)((bytes[i] & 0xff) << i * 8); // 移位和清零
		
		return longa;
	}
	
}

 

那字符串,字符数组呢?比如泥瓦匠的名字:李强强

Java也提供了一系列的方法,其实 java.lang.String 封装了char[],其中本质还是对char数组的操作。代码如下:

package javaBasic.oi.byteoper;

public class StringConvert
{
	public static void main(String[] args)
	{
		String str = "李强强";
		byte[] bytes = str.getBytes();
		// 打印字节数组
		System.out.println("'李强强'的字节数组为:");
		for (int i = 0; i < bytes.length; i++)
			System.out.print("\t" + bytes[i]);
	}
}

右键Run一下,可以看到以下输出:

'李强强'的字节数组为:
	-64	-18	-57	-65	-57	-65

论证:这里我们论证了一个中文,需要两个字节表示,也就是说一个中文是16位

 

四、浅谈Java通信中的数据

下面简单把泥瓦匠学生的故事延续。socket

如图,库表中一个学生对象,有个属性是学号。这时候客户端要向服务端发送这个对象。过程如下:

1、对象实现Serializable接口。

实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。

2、其学号属性值 1206010035,由客户端转换为字节码。

3、字节码传输至服务端

4、服务端接收并转换为对象属性值。

 

五、总结

此文讲的点有点多,泥瓦匠就想把这块用到的知识点串起来,然后慢慢每个讲解。总结如下:

1、Java中进制转换是什么?

2、Java中进制转换的作用?

 

 

Writer      :BYSocket(泥沙砖瓦浆木匠)

微         博:BYSocket

豆         瓣:BYSocket

FaceBook:BYSocket

Twitter    :BYSocket

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: Java I/O : Java中的进制详解

21 Jun 01:31

JAVA面试题100问第一部分

by Borg

原文地址 译者:Borg

译者注:由于原文太长,这只是大概三分一的部分,即翻译至第五页倒数第三个问题。

以下是面试时常问到的JAVA面试题,能让你对JAVA面试有基本的了解。根据我个人的经验,一个好的面试官在面试的时候是不会事先准备一列问题清单,一般来说都从关于JAVA最基本的概念开始,然后再根据你的回答继续深入讨论。

问题:关于JAVA 你知道什么?

回答:JAVA是一种高级程序设计语言,由詹姆斯·高斯林发明并于1995年发布。JAVA可以在多种平台上运行,如Windows、Mac OS、各种版本的UNIX。

 

问题:JAVA编程语言支持什么平台?

回答:JAVA可以在多种平台上运行,如Windows、Mac OS、以及各种版本的UNIX/Linux操作系统如HP-Unix, Sun Solaris, Redhat Linux, Ubuntu, CentOS 等。

 

问题:请列举出JAVA至少5种特性。

回答:JAVA特性包括面向对象、平台独立、健壮性、解释性、多线程等。

 

问题:为什么JAVA是结构中立的?

回答:JAVA编译器会生成一种具备结构中立性的目标文件格式。编译后的程序码可以在提供Java运行系统的多种不同处理器上面执行。

 

问题:JAVA如何保证高效性?

回答:JAVA使用即时编译器来保证高效性。即时编译器能把Java字节码转换成可以直接发送给处理器的指令。

 

问题:为什么JAVA是动态的?

回答:JAVA能适应不断升级的环境。JAVA程序带有大量扩展的运行时信息,这些信息可以用来实时验证处理对对象的引用。

 

问题:什么是JAVA虚拟机?它对JAVA的平台独立性有什么作用?

回答:当JAVA代码被编译的时候,它并不是被编译成针对某一特定平台的机器码,而是转换成平台独立的字节码。这些字节码通过网络传送到不同的机器上,不管这些机器运行的是什么平台,JAVA虚拟机都可以解释字节码。

 

问题:列举两个JAVA集成编译环境。

回答:Netbeans、Eclipse

 

问题:列举几个JAVA与C/C++不同的关键词。

回答:import、super、finally等

 

问题:什么是对象?

回答:对象是在运行时的实体,它的状态储存于域中,它的行为通过方法实现。方法能够操作对象内部的数据,也是对象间通信的主要途径。

 

问题:请给定义类。

回答:类是一张蓝图,对象根据这张蓝图被创建。一个类可以包括数据域和方法,方法可以描述对象的行为。

 

问题:一个类可以包含什么类型的变量?

回答:包括居于变量,实例变量和类变量。

 

问题:什么是局域变量?

回答:在方法内、构造函数内或者代码块内定义的变量是局域变量。局域变量在函数内声明和初始化,当函数执行结束时局域变量会被销毁。

 

问题:什么是实例变量?

回答:实例变量是在类中但在任何函数之外的变量。实例变量只有当类实例化后才能被引用。

 

问题:什么是类变量?

回答:在类中并且在任何函数之外,使用static关键词声明的变量是类变量。

 

问题:什么是类的单例模式?

回答:单例类控制对象的生成,一次只能存在一个对象但兼具灵活性,当条件改变时允许创建多个变量。

 

问题:什么是构造函数?

回答:当一个新的对象被创建的时候会自动调用构造函数。每个类都有构造函数。如果不显性声明构造函数,JAVA编译器会调用默认构造函数。

 

问题:列举为类创建对象的过程(三步)。

回答:首先声明一个对象,然后实例化,再对其初始化。

 

问题:JAVA中字节数据类型的默认值是多少?

回答:0

 

问题:JAVA中float和double的默认值是多少?

回答:float型和double型的默认值与C/C++相同,float是0.0f,double是0.0d。

 

问题:byte类型在什么时候使用?

回答:byte类型用来在较大的数组中节省储存空间。使用byte类型替代int型可以节省三倍的空间。

 

问题:什么是静态变量?

回答:类变量也叫静态变量,使用关键词static,在类里,但在方法、构造函数和代码块之外。

 

问题:什么是访问控制修饰符?

回答:JAVA提供访问控制修饰符来修饰类、变量、方法和构造函数的访问控制属性。当不写出访问控制修饰符时,成员具有默认的访问权限或者包访问控制权限。

 

问题:什么是受保护访问控制修饰符?

回答:变量、方法和构造函数如果在父类中被声明为受保护,那么它们只能在其它包的子类中或者在该父类的包中被访问。

 

问题:什么是同步修饰符?

回答:JAVA在访问控制修饰符之外还提供同步修饰符,同步修饰符用来限制方法,使方法一次只能被一个线程调用。

 

问题:在JAVA运算符优先级中,哪个运算符的优先级最高?

回答:圆括号()和下标运算符[]具有最高的优先级。

 

问题:在switch语句中能使用的数据类型包括哪些?

回答:switch中使用的变量只能是字节型、短整型、整型或字符型。

 

问题:parseInt()函数在什么时候使用到?

回答:parseInt()函数用于解析字符串返回整数。

 

问题:为什么说String类是不可变的?

回答:String对象一旦被创建就不可改变,因此可以在多个线程中被安全地引用,这对于多线程的编程来说十分重要。

 

问题:为什么说StringBuffer类是可变的?

回答:String类是不可变的,因此一个String对象一旦被创建就不可改变。如果需要对字符串进行频繁的修改,那么就应该使用StringBuffer。

 

问题:StringBuffer和StringBuilder类的区别在哪?

回答:尽可能使用StringBuilder,因为它运行时比StringBuffer快。但如果需要强调线程安全,那就应该使用StringBuffer。

 

问题:那个包使用正则表达式来实现模式匹配?

回答:包java.util.regex。

 

问题:java.util.regex包括那些类?

回答:包括三个类:Pattern类、Matcher类和PatternSyntaxException类。

 

问题:什么是finalize()方法?

回答:finalize()方法在一个对象被垃圾回收机制销毁前调用,可以保证对象被彻底地销毁。

 

问题:什么是Exception?

回答:Exception是当程序运行过程中遇到的异常。异常由方法调用栈中的Handler捕获。

 

问题:什么是受检查的异常?

回答:受检查异常通常为用户操作的异常,不能被程序员预见。例如,在打开一个文件的时候,无法找到文件就发生了异常,这样的异常在编译的时候不能被跳过。

问题:什么是运行时间异常?

回答:运行时间异常在编程时可以预见并避免。与受检查异常不同,运行时间异常在编译时可以被跳过。

 

问题:Exception类的两个子类是什么?

回答:Exception类有两个主要的子类:IOException类和RuntimeException类。

 

问题:关键词throws在什么情况下使用?

回答:如果一个方法没有使用handler处理异常,那么这个方法必须使用throws关键词声明。关键词throws用于方法签名之后。

 

问题:关键词throw在什么时候使用?

回答:不论是新实例化的异常还是刚捕获的异常,都可以通过使用关键词throw抛出异常。

 

问题:finally关键词在异常处理中如何使用?

回答:关键词finally用来在try代码块后再加一个代码块,不管有没有发生异常,finally代码块内的语句都会被执行。

 

问题:当自己创建异常类的时候应该注意什么?

回答:当创建异常类时应该注意:

  • 所有的异常类都应该是Throwable类的子类。

2.写一个遵循处理与声明规则的受检查异常类时,应该加上extend Exception。

3.写一个运行时间异常类,需要加上extend RuntimeException。

 

问题:什么是继承?

回答:继承指的是一个类获得另一个类的属性的过程。数据属性和行为按从父类到子类的顺序继承,便于管理。

 

问题:关键词super什么时候使用?

回答:如果一个方法重写了父类的方法,被重写的父类方法可以通过关键词super调用。super也可以用来调用被隐藏的域。

 

问题:什么是多态?

回答:多态是一个对象同时具有多种形态的能力。在面向对象编程中,多态最常见的应用是将一个父类指针指向一个子类的对象。

 

问题:什么是抽象?

回答:抽象是在面向对象编程中将一个类抽象化的过程。抽象能够降低复杂性,提高系统可维护性。

 

问题:什么是抽象类?

回答:抽象类不能被实例化,可以被部分实现接口。抽象方法是没有函数体的方法声明,抽象类含有一个或多个抽象方法。

 

问题:抽象方法什么时候被使用?

回答:当想要让类包含一个方法,但想要让方法的具体实现由子类决定,那么就可以在父类中声明抽象方法。

 

问题:什么是封装?

回答:封装是将数据域设为私有,提供公共方法来处理私有的数据域。如果一个域被设为私有,那么在类外就不能访问该数据域,这样就把数据域隐藏在类内部。因此封装也被称为数据隐藏。

 

问题:封装最主要的优点是什么?

回答:最主要的优点是允许修改实现的借口,同时不用修改他人使用接口的代码。封装使得代码具可维护性、灵活性和扩展性。

 

问题:什么是接口?

回答:接口是一系列抽象方法的集合,一个类实现接口,也就从接口中继承了这些抽象方法。

 

问题:接口具有什么特性?

回答:1.接口不能被实例化

2.接口不含任何构造函数

3.接口中的函数都是抽象函数

 

问题:什么是包?

回答:包包含相关的类型(类、接口、枚举和注释),提供访问保护和命名空间管理。

 

问题:为什么要使用包?

回答:JAVA中使用包可以避免命名空间矛盾,控制访问权限,便于定位和使用类、接口、枚举和注释等。

 

问题:什么是多线程编程?

回答:多线程编程包含两个或者更多个可以同时运行的部分,这些部分被叫做线程。每个线程有独立的执行路径。

 

问题:创建线程的两种方式是什么?

回答:通过实现Runnable接口或者继承Thread类。

 

问题:什么是applet?

回答:applet是一个运行在Web浏览器上的JAVA程序。由于它带有整个JAVA的API,因此可以实现JAVA提供的所有功能。

 

问题:applet继承哪个类?

回答:applet继承了java.applet.Applet 类。

 

问题:解释下JAVA的垃圾回收机制。

回答:JAVA垃圾回收机制能够清理不再被引用的对象,从而来释放内存。

 

问题:什么是不可变对象?

回答:不可变对象一旦被创建就不能再改变。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com

本文链接地址: JAVA面试题100问第一部分

19 Jun 00:53

Redis re-implemented in Rust

19 Jun 00:43

尝试spark

by suchasplus

某个线上服务,访问量每天N亿, output种类异常丰富,依赖内部服务众多,出现问题的概率相对较大,故搞了某准实时分析系统,  用于分析性能和定(bu)位(bei)问(hei)题(guo)。

作为最接近DSL的优秀的prototype language, 我们开始是用PHP写了个多进程模型来跑, kafka传输数据,每分钟计算一次做归并, 速度基本可以满足需求。

跟广告算法团队沟通后, 某同学用scala重写了一遍,之后决定尝试下spark,然后悲催的发现在公司集群上的速度居然没有单机spark快, CPU稍微好点的机器上的PHP多进程也比它快....另外在公司集群上被高优先级任务干掉也是经常出现的...

讨论了下原因,似乎很简单。原始数据每分钟就没多少量,这个场景下并不是那么的合适,考虑了下数据分发并开始执行计算任务的时间,单机就计算的差不多了, 或许只有在做CF的时候才适合用hadoop/spark

我所了解的, spark作为目前被鼓吹的银弹,本质上是由于hadoop对计算的抽象不够,每一步都要落地到磁盘io上,导致在机器学习(比如ctr预估)这种需要多轮迭代的场景上,spark提供的RDD模型从理论上可以有效加速。但是实际工程上,作为一个工程技术人员,我们应该有足够的理智去看待一个所谓的灵丹妙药,技术选型的时候要看有那些feature但是更要了解disadvantage, 毕竟选型的本质是均衡和妥协。

以微博用户的量级做CF, hadoop和spark的scale out解决方案是必然, 但并不是优势。从spark的角度来看, MLLib目前还是属于占坑阶段, 典型的Berkeley风格, 比如线性代数的BLAS居然是fortran翻译的Java。另外,spark本身的参数调优也很重要,据说可以差一个数量级,尝试了不同版本的spark,速度明显有差异。

更为坑爹的是RDD,我们在尝试的时候经常就跑挂了,因为估算RDD几轮计算之后会变多大,spark在这块本质上是个糊涂账。

另外就是最近很火热的borg论文,离线和实时计算混跑的话题。spark在这块直接用的yarn,分配多少资源,task大小,分配速度,完成速度,这些只能靠经验来参数调优。这种QOS的事情spark直接回避掉了。而大家在优化参数的时候尽可能的会去压榨机器性能,但是CPU用满了,加上jvm的gc,会导致系统线程运行不流畅,甚至能触发heartbeat timeout的程度,最后触发fault safe,丢弃已经算了一半的任务,丢弃还能用的资源,还要再分配资源去重新去计算,给系统造成了更大的压力, 很多跑到最后跑挂掉的问题都是这样导致的。

结论: 作为一个scale out方案,如何保证效率,稳定性和可控才是最重要的, 对我们来说,spark还需要大量的人肉调优,算法参数和系统参数,在目前可预知的应用场景上,我团队用C++变相scale up来解决性能问题...至于spark, 作为缺乏调优经验的我们,还是让等算法团队吃螃蟹吧...

btw: 隔壁team在类似问题上原型是PHP单进程(sigh),后来改用golang来解决的计算性能问题,我觉得这个选择其实是为了玩golang (逃

btw2: 好像databrick在尝试把计算过程的内存管理不再交给jvm而是像c一样管理,期望这个做好了能解决gc的麻烦

18 Jun 06:34

浅析Apache Storm 0.10.0-beta发布:剑指Heron

by 投稿 (guest)

  在浅析Storm的发布版本之前,先吐槽一下Storm的版本号。我是从0.8.0版本开始接触Storm的,那时候Storm还是推特的开源项目,作为一个Storm的半个老鸟,看到Storm又推出了一个新版本,当然是有些想法的。从2013年,Apache接手Storm之后版本号的发布继续延续了以前的风格。说白了就是众人期望了无数年,版本依然没有过“1”。

  浅析Storm 0.10.0-beta

  //顺序按照官网的要点顺序,翻译+个人见解

  一、集群安全性以及多用户调度部署。

  我们先把官网列的点列出:

  •   Kerberos Authentication with Automatic Credential Push and Renewal

  •   Pluggable Authorization and ACLs

  •   Multi-Tenant Scheduling with Per-User isolation and configurable resource limits.

  •   User Impersonation

  •   SSL Support for Storm UI, Log Viewer, and DRPC (Distributed Remote Procedure Call)

  •   Secure integration with other Hadoop Projects (such as ZooKeeper, HDFS, HBase, etc.)

  •   User isolation (Storm topologies run as the user who submitted them)

  在早期的Storm设计中,并没有考虑系统安全性的问题。

  其实大部分开源软件的发展过程都差不多,早期的发展肯定是以功能以及性能为主,在发展到一定阶段之后,系统安全才会被加以考虑。如今,随着Storm的发展,以及当前实时业务需求的上涨,实际的集群部署也越来越多,安全性的需求也越来越高。

  这次在安全性的支持方面,除了Storm的社区,雅虎、赛门铁克还有Hortonworks作出了不小的贡献。

  主要改动点:

  •   (1)自动更新Kerberos身份验证机制;

  •   (2)可插拔的访问授权以及访问控制机制;

  •   (3)支持多用户调度,并且对于不同用户可灵活配置资源;//这一点在向资源集中调度方向靠拢

  •   (4)模拟多用户调度;

  •   (5)UI方面的改进,支持SSL协议的日志查看器、DRPC页面监控;

  •   (6)优化了与Hadoop生态的集成,主要是安全性方面,包括Zookeeper、Hbase以及HDFS等等;//随着大数据平台一体化,Storm的推广,越来越多的Hadoop生态组件会集成进去

  •   (7)多用户的隔离,即每个用户中允许操作自己提交的拓扑任务;//其实这点跟在引入多用户的时候,必然是需要支持的

  总结:

  安全性方面,不多说,博客虫(微信ID:blogchong)感觉这个点在一般的企业来说,其实优先级是不高的;

  重点是对多用户调度的支持,以及资源管理方面的优化,自从Hadoop2.0以后,资源的集中管理一直是一个热门方向,所以,Storm在这方面的优化还是值得期待的。

  二、版本升级以及持续改进的优化

  在过去,升级Storm往往会出现很多问题,涉及到拓扑的变更、拓扑的重新部署等等。究其因,不同版本之间的数据结构往往是有所出入的。所以,升级的过程不是一个完全向下兼容的过程。

  从storm 0.10.0开始,版本的升级方面将有很大的优化,甚至是可以在不停止拓扑任务的情况下进行版本升级,整个过程也可以实现自动化。

  总结:

  随着Storm的实际部署节点越来越多,必然会面临版本升级的情况,所以这一点的改进是很重要的,因为其对于用户后续的持续使用是一个巨大的考验。

  三、任务以及拓扑部署上的改进优化

  熟悉Storm的朋友可能知道,对于拓扑任务的部署,往往有任何的拓扑改动,我们都需要进行代码的重新编译。如果部署的拓扑规模较大,这将影响很大。

  在新改进的方向中,希望通过外部配置文件的形式,去定义拓扑的布局以及相关的配置。

  先列上官网改进点:

  •   Easily configure and deploy Storm topologies (Both Storm core and Micro-batch API) without embedding configuration in your topology code

  •   Support for existing topology code

  •   Define Storm Core API (Spouts/Bolts) using a flexible YAML DSL

  •   YAML DSL support for most Storm components (storm-kafka, storm-hdfs, storm-hbase, etc.)

  •   Convenient support for multi-lang components

  •   External property substitution/filtering for easily switching between configurations/environments (similar to Maven-style ${variable.name} substitution)

  主要改动点:

  •   (1)优化拓扑的配置以及部署,在代码中将拓扑结构相关的东西剥离;

  •   (2)依然支持现有的拓扑编码;//向下兼容

  •   (3)使用灵活的YAML DSL去定义Spout以及Bolt;//说白了,还是在拓扑构建方面进行优化

  •   (4)依然支持现有的一些接口,比如storm-kafka、storm-hbase、storm-hdfs;//依然是向下兼容,不多说

  •   (5)多语言组件的支持;//以前虽然号称支持多语言,其实除了python以及java以外的语言,开发难度还是挺大的

  •   (6)支持配置环境之间的切换,类似于Maven中${变量名}代替;

  总结:

  向下兼容的一些东西,咱就不多说了,这是版本升级的理所应当支持的功能;

  重点在于拓扑结构的革命性改变,其实我们可以发现,Storm 0.10.0在灵活性使用方面做了很多的该进,包括了这个拓扑结构

  四、提供新的分组策略:局部关键词分组

  现有的Storm分组策略,在0.8版本到现在一直没有改变,如今引入了一个新的分组策略:局部关键词分组策略。

  局部关键词分组与按字段分组(Grouping)一样,不同之处在于,其考虑了下游Bolt的负载情况,更好的利用了剩余资源,尽量达到了节点间的负载均衡。

  总结:

  从这点我们可以发现,Storm的革命性改进除了在灵活性方面,在资源调度方面也是不余余力啊。

  五、优化了日志框架

  调试分布式程序是一件很困难的事,通常我们会通过一个主要的信息来源去分析,那就是程序产生的日志文件。

  但是这同样会产生矛盾,Storm是实时级别的架构系统,对于日志的记录层级难以掌控:多了,容易刷爆磁盘;少了,难以发现问题。

  在Storm 0.10.0中,使用了log4j v2,这是一个具有更高的吞吐量以及更低的延迟的日志架构。并且更有效的利用资源,对于业务逻辑,我们可以轻松的追踪到。

  同样,列上E文关键点:

  •   Rolling log files with size, duration, and date-based triggers that are composable

  •   Dynamic log configuration updates without dropping log messages

  •   Remote log monitoring and (re)configuration via JMX

  •   A Syslog/RFC-5424-compliant appender.

  •   Integration with log aggregators such as syslog-ng

  主要关键点:

  •   (1)通过日志文件大小、时间日期组合的方式,触发日志的滚动;

  •   (2)动态的修改日志配置,不会丢失日志数据;

  •   (3)支持日志的远程监控,以及JMX的远程配置;

  •   (4)支持RFC-5424协议;

  •   (5)Syslog-ng的整合以及支持日志聚合;//未来很有可能使用syslog-ng完全替代syslog,不过需要时间验证

  总结:

  日志这一块的话,其实没什么多说的地方,现有日志其实也足够用了,问题不是很大,当然如果有更好的支持优化,肯定是大赞的。

  六、支持Hive数据的接入

  这个特性将会在0.13中引入,提供数据从Hive接入Storm的API,以及数据Hive落得的API,让数据从Storm到Hive的流程更加的合理以及方便,一旦数据在源头被提交,在Hive即可查询。

  实现Storm到Hive的一体化,在微批处理以及事务处理中支持Hive。

  总结:

  依然是大数据平台一体化的改进,Storm与Hadoop生态的进一步“合体”。

  七、Microsoft Azure Event Hubs的整合

  在Azure云计算平台上支持Storm的部署执行,这个不多说。只能说Storm的影响力越来越大了,很多云平台已经主动和Storm“联姻”了。

  八、对于Redis的支持

  除了Hadoop生态,对于大行其道的Nosql,Storm也开始整合。跟其他组件的支持类似,提供了一些使用案例,同样是Storm大融合的趋势。

  九、JDBC/RDBMS的整合集成

  Storm 0.10.0支持高灵活可定制的集成,几乎兼容任何类型的JDBC。甚至允许拓扑元组数据与数据库数据在拓扑进行灵活的交互。

  总结:

  在Hadoop盛行之前,其实Storm的数据落地方式很单调,其中将Storm处理过后的数据存入数据库中是一种主流,所以,这一方面的优化可以说解决了很多历史遗留问题。

  十、依赖冲突的优化

  直接总结吧:在以往的Storm历史版本中,其实经常会出现依赖版本冲突的现象。在Storm 0.10.0中,对这方面作了优化。好吧,又是一个历史遗留问题,说明Storm在进一步规范化。

  我们来梳理一下总结

  在推特发布Heron后没几天,Apache就发布了Storm的0.10.0,不可谓不及时。

  也难怪,Heron号称颠覆性的设计,虽然它暂时没有开源,虽然Storm已经占据了数据实时处理领域的半壁江山,但是依然危机感十足啊。

  OK,还是回到正题,说说我的感想:

  (1)大数据平台统一融合趋势

  我在《DT时代变革的反思》一文中,曾经说到:支撑大数据得以发展的核心是数据平台。

  在Hadoop大规模离线数据处理技术日渐成熟的今天,实时业务的需求逐渐在上升,这也是Storm得以快速发展的原因之一。

  实时处理平台以及我们现在盛行的Hadoop生态平台,是两个不同的方向,逻辑上是分离的。

  随着大数据处理的需求多样化,数据在不同平台上流通的需求很迫切。

  如今,不管是现在版本Storm对Hive的支持、对redis的支持,还是以往中对HDFS、Hbase、Zookeeper的支持等,这都是Storm对于数据平台一体化做的努力。

  在大数据平台方面,无论是实时处理,还是以Hadoop为代表的批量离线处理或者Nosql存储等几个方面,平台的统一融合将会是一个趋势。

  (2)资源的集中管理

  Storm在资源拓扑任务资源分配方面,一直以来都是一个硬伤。随着Hadoop2.0之后,Hadoop的资源统一交给Yarn管理,在分布式方面基本算是掀起了一股资源管理优化的风潮。

  PS:翻译以及个人见解若有所误差,欢迎指正。

  来源:博客虫投稿,原文链接

评论《浅析Apache Storm 0.10.0-beta发布:剑指Heron》的内容...

相关文章:


微博:新浪微博 - 微信公众号:williamlonginfo
月光博客投稿信箱:williamlong.info(at)gmail.com
Created by William Long www.williamlong.info
月光博客
18 Jun 06:32

5亿整数的大文件,怎么排?

by changqi

问题

给你1个文件bigdata,大小4663M,5亿个数,文件中的数据随机,如下一行一个整数:

6196302
3557681
6121580
2039345
2095006
1746773
7934312
2016371
7123302
8790171
2966901
...
7005375

现在要对这个文件进行排序,怎么搞?

内部排序

先尝试内排,选2种排序方式:

3路快排:

private final int cutoff = 8;
public <T> void perform(Comparable<T>[] a) {
 perform(a,0,a.length - 1);
 }
private <T> int median3(Comparable<T>[] a,int x,int y,int z) {
 if(lessThan(a[x],a[y])) {
 if(lessThan(a[y],a[z])) {
 return y;
 }
 else if(lessThan(a[x],a[z])) {
 return z;
 }else {
 return x;
 }
 }else {
 if(lessThan(a[z],a[y])){
 return y;
 }else if(lessThan(a[z],a[x])) {
 return z;
 }else {
 return x;
 }
 }
 }
private <T> void perform(Comparable<T>[] a,int low,int high) {
 int n = high - low + 1;
 //当序列非常小,用插入排序
 if(n <= cutoff) {
 InsertionSort insertionSort = SortFactory.createInsertionSort();
 insertionSort.perform(a,low,high);
 //当序列中小时,使用median3
 }else if(n <= 100) {
 int m = median3(a,low,low + (n >>> 1),high);
 exchange(a,m,low);
 //当序列比较大时,使用ninther
 }else {
 int gap = n >>> 3;
 int m = low + (n >>> 1);
 int m1 = median3(a,low,low + gap,low + (gap << 1));
 int m2 = median3(a,m - gap,m,m + gap);
 int m3 = median3(a,high - (gap << 1),high - gap,high);
 int ninther = median3(a,m1,m2,m3);
 exchange(a,ninther,low);
 }
if(high <= low)
 return;
 //lessThan
 int lt = low;
 //greaterThan
 int gt = high;
 //中心点
 Comparable<T> pivot = a[low];
 int i = low + 1;
/*
 * 不变式:
 * a[low..lt-1] 小于pivot -> 前部(first)
 * a[lt..i-1] 等于 pivot -> 中部(middle)
 * a[gt+1..n-1] 大于 pivot -> 后部(final)
 *
 * a[i..gt] 待考察区域
 */
while (i <= gt) {
 if(lessThan(a[i],pivot)) {
 //i-> ,lt ->
 exchange(a,lt++,i++);
 }else if(lessThan(pivot,a[i])) {
 exchange(a,i,gt--);
 }else{
 i++;
 }
 }
// a[low..lt-1] < v = a[lt..gt] < a[gt+1..high].
 perform(a,low,lt - 1);
 perform(a,gt + 1,high);
 }

 

归并排序:

/**
 * 小于等于这个值的时候,交给插入排序
 */
 private final int cutoff = 8;
/**
 * 对给定的元素序列进行排序
 *
 * @param a 给定元素序列
 */
 @Override
 public <T> void perform(Comparable<T>[] a) {
 Comparable<T>[] b = a.clone();
 perform(b, a, 0, a.length - 1);
 }
private <T> void perform(Comparable<T>[] src,Comparable<T>[] dest,int low,int high) {
 if(low >= high)
 return;

 //小于等于cutoff的时候,交给插入排序
 if(high - low <= cutoff) {
 SortFactory.createInsertionSort().perform(dest,low,high);
 return;
 }
int mid = low + ((high - low) >>> 1);
 perform(dest,src,low,mid);
 perform(dest,src,mid + 1,high);
//考虑局部有序 src[mid] <= src[mid+1]
 if(lessThanOrEqual(src[mid],src[mid+1])) {
 System.arraycopy(src,low,dest,low,high - low + 1);
 }
//src[low .. mid] + src[mid+1 .. high] -> dest[low .. high]
 merge(src,dest,low,mid,high);
 }

 private <T> void merge(Comparable<T>[] src,Comparable<T>[] dest,int low,int mid,int high) {
for(int i = low,v = low,w = mid + 1; i <= high; i++) {
 if(w > high || v <= mid && lessThanOrEqual(src[v],src[w])) {
 dest[i] = src[v++];
 }else {
 dest[i] = src[w++];
 }
 }
 }

 

数据太多,递归太深 ->栈溢出?加大Xss?
数据太多,数组太长 -> OOM?加大Xmx?

耐心不足,没跑出来.而且要将这么大的文件读入内存,在堆中维护这么大个数据量,还有内排中不断的拷贝,对栈和堆都是很大的压力,不具备通用性。

sort命令来跑

sort -n bigdata -o bigdata.sorted

跑了多久呢?24分钟.

为什么这么慢?

粗略的看下我们的资源:

内存
jvm-heap/stack,native-heap/stack,page-cache,block-buffer
外存
swap + 磁盘
数据量很大,函数调用很多,系统调用很多,内核/用户缓冲区拷贝很多,脏页回写很多,io-wait很高,io很繁忙,堆栈数据不断交换至swap,线程切换很多,每个环节的锁也很多.

总之,内存吃紧,问磁盘要空间,脏数据持久化过多导致cache频繁失效,引发大量回写,回写线程高,导致cpu大量时间用于上下文切换,一切,都很糟糕,所以24分钟不细看了,无法忍受.

位图法

private BitSet bits;
public void perform(
 String largeFileName,
 int total,
 String destLargeFileName,
 Castor<Integer> castor,
 int readerBufferSize,
 int writerBufferSize,
 boolean asc) throws IOException {
System.out.println("BitmapSort Started.");
 long start = System.currentTimeMillis();
 bits = new BitSet(total);
 InputPart<Integer> largeIn = PartFactory.createCharBufferedInputPart(largeFileName, readerBufferSize);
 OutputPart<Integer> largeOut = PartFactory.createCharBufferedOutputPart(destLargeFileName, writerBufferSize);
 largeOut.delete();
Integer data;
 int off = 0;
 try {
 while (true) {
 data = largeIn.read();
 if (data == null)
 break;
 int v = data;
 set(v);
 off++;
 }
 largeIn.close();
 int size = bits.size();
 System.out.println(String.format("lines : %d ,bits : %d", off, size));
if(asc) {
 for (int i = 0; i < size; i++) {
 if (get(i)) {
 largeOut.write(i);
 }
 }
 }else {
 for (int i = size - 1; i >= 0; i--) {
 if (get(i)) {
 largeOut.write(i);
 }
 }
 }
largeOut.close();
 long stop = System.currentTimeMillis();
 long elapsed = stop - start;
 System.out.println(String.format("BitmapSort Completed.elapsed : %dms",elapsed));
 }finally {
 largeIn.close();
 largeOut.close();
 }
 }
private void set(int i) {
 bits.set(i);
 }
private boolean get(int v) {
 return bits.get(v);
 }

 

nice!跑了190秒,3分来钟.
以核心内存4663M/32大小的空间跑出这么个结果,而且大量时间在用于I/O,不错.

问题是,如果这个时候突然内存条坏了1、2根,或者只有极少的内存空间怎么搞?

外部排序

该外部排序上场了.
外部排序干嘛的?

内存极少的情况下,利用分治策略,利用外存保存中间结果,再用多路归并来排序;
map-reduce的嫡系.

1.分
内存中维护一个极小的核心缓冲区memBuffer,将大文件bigdata按行读入,搜集到memBuffer满或者大文件读完时,对memBuffer中的数据调用内排进行排序,排序后将有序结果写入磁盘文件bigdata.xxx.part.sorted.
循环利用memBuffer直到大文件处理完毕,得到n个有序的磁盘文件:

2.合
现在有了n个有序的小文件,怎么合并成1个有序的大文件?
把所有小文件读入内存,然后内排?
(⊙o⊙)…
no!

利用如下原理进行归并排序:

这里写图片描述

我们举个简单的例子:

文件1:3,6,9
文件2:2,4,8
文件3:1,5,7

第一回合:
文件1的最小值:3 , 排在文件1的第1行
文件2的最小值:2,排在文件2的第1行
文件3的最小值:1,排在文件3的第1行
那么,这3个文件中的最小值是:min(1,2,3) = 1
也就是说,最终大文件的当前最小值,是文件1、2、3的当前最小值的最小值,绕么?
上面拿出了最小值1,写入大文件.

第二回合:
文件1的最小值:3 , 排在文件1的第1行
文件2的最小值:2,排在文件2的第1行
文件3的最小值:5,排在文件3的第2行
那么,这3个文件中的最小值是:min(5,2,3) = 2
将2写入大文件.

也就是说,最小值属于哪个文件,那么就从哪个文件当中取下一行数据.(因为小文件内部有序,下一行数据代表了它当前的最小值)

最终的时间,跑了771秒,13分钟左右.

less bigdata.sorted.text
...
9999966
9999967
9999968
9999969
9999970
9999971
9999972
9999973
9999974
9999975
9999976
9999977
9999978
...

5亿整数的大文件,怎么排?,首发于博客 - 伯乐在线

16 Jun 00:36

Twitter已经用Heron替换了Storm

by Abel Avram

Twitter已经用Heron替换了Storm。此举将吞吐量最高提升了14倍,单词计数拓扑时间延迟最低降到了原来的1/10,所需的硬件减少了2/3。

Twitter使用Storm实时分析海量数据已经有好几年了,并在2011年将其开源。该项目稍后开始在Apache基金会孵化,并在去年秋天成为顶级项目。Storm以季度为发布周期,现在已经达到了0.9.5版本,并且正在向着人们期望的1.0稳定版前进。但一直以来,Twitter都在致力于开发替代方案Heron,因为Storm无法满足他们的实时处理需求。

Twitter的新实时处理需求包括:“每分钟数十亿的事件;大规模处理具有次秒级延迟和可预见的行为;在故障情况下,具有很高的数据准确性;具有很好的弹性,可以应对临时流量峰值和管道阻塞;易于调试;易于在共享基础设施中部署。”Karthik Ramasamy是Twitter Storm/Heron团队的负责人。据他介绍,为满足这些需求,他们已经考虑了多个选项:增强Storm、使用一种不同的开源解决方案或者创建一个新的解决方案。增强Storm需要花费很长时间,也没有其它的系统能够满足他们在扩展性、吞吐量和延迟方面的需求。而且,其它系统也不兼容Storm的API,需要重写所有拓扑。所以,最终的决定是创建Heron,但保持其外部接口与Storm的接口兼容。

拓扑部署在一个Aurora调度器上,而后者将它们作为一个由多个容器(cgroups)组成的任务来执行:一个Topology Master、一个Stream Manager、一个Metrics Manager(用于性能监控)和多个Heron 实例(spouts和bolts)。拓扑的元数据保存在ZooKeeper中。处理流程通过一种反压机制实现调整,从而控制流经拓扑的数据量。除Aurora外,Heron还可以使用其它服务调度器,如YARN或Mesos。实例运行用户编写的Java代码,每个实例一个JVM。Heron通过协议缓冲处理彼此间的通信,一台机器上可以有多个容器。(要了解更多关于Heron内部架构的细节信息,请阅读论文《Twitter Heron:大规模流处理》。)

Twitter已经用Heron完全替换了Storm。前者现在每天处理“数10TB的数据,生成数10亿输出元组”,在一个标准的单词计数测试中,“吞吐量提升了6到14倍,元组延迟降低到了原来的五到十分之一”,硬件减少了2/3。

当被问到Twitter是否会开源Heron时,Ramasamy说“在短时间内不会,但长期来看可能。”

15 Jun 05:09

JAVA 面向对象和集合知识点总结

by 蓝枫紫叶

在 Android 编程或者面试中经常会遇到 JAVA 面向对象和集合的知识点。自己结合实际的编程以及阅读网上资料总结一下。

The post JAVA 面向对象和集合知识点总结 appeared first on 头条 - 伯乐在线.

14 Jun 11:55

关于 Java 对象序列化您不知道的 5 件事

by importnewzz

数年前,当和一个软件团队一起用 Java 语言编写一个应用程序时,我体会到比一般程序员多知道一点关于 Java 对象序列化的知识所带来的好处。

关于本系列

您觉得自己懂 Java 编程?事实上,大多数程序员对于 Java 平台都是浅尝则止,只学习了足以完成手头上任务的知识而已。在本 系列 中,Ted Neward 深入挖掘 Java 平台的核心功能,揭示一些鲜为人知的事实,帮助您解决最棘手的编程挑战。

大约一年前,一个负责管理应用程序所有用户设置的开发人员,决定将用户设置存储在一个 Hashtable中,然后将这个 Hashtable 序列化到磁盘,以便持久化。当用户更改设置时,便重新将 Hashtable 写到磁盘。

这是一个优雅的、开放式的设置系统,但是,当团队决定从 Hashtable 迁移到 Java Collections 库中的HashMap 时,这个系统便面临崩溃。

Hashtable 和 HashMap 在磁盘上的格式是不相同、不兼容的。除非对每个持久化的用户设置运行某种类型的数据转换实用程序(极其庞大的任务),否则以后似乎只能一直用Hashtable 作为应用程序的存储格式。

团队感到陷入僵局,但这只是因为他们不知道关于 Java 序列化的一个重要事实:Java 序列化允许随着时间的推移而改变类型。当我向他们展示如何自动进行序列化替换后,他们终于按计划完成了向 HashMap 的转变。

本文是本系列的第一篇文章,这个系列专门揭示关于 Java 平台的一些有用的小知识 — 这些小知识不易理解,但对于解决 Java 编程挑战迟早有用。

将 Java 对象序列化 API 作为开端是一个不错的选择,因为它从一开始就存在于 JDK 1.1 中。本文介绍的关于序列化的 5 件事情将说服您重新审视那些标准 Java API。

Java 序列化简介

Java 对象序列化是 JDK 1.1 中引入的一组开创性特性之一,用于作为一种将 Java 对象的状态转换为字节数组,以便存储或传输的机制,以后,仍可以将字节数组转换回 Java 对象原有的状态。

实际上,序列化的思想是 “冻结” 对象状态,传输对象状态(写到磁盘、通过网络传输等等),然后 “解冻” 状态,重新获得可用的 Java 对象。所有这些事情的发生有点像是魔术,这要归功于 ObjectInputStream/ObjectOutputStream 类、完全保真的元数据以及程序员愿意用Serializable 标识接口标记他们的类,从而 “参与” 这个过程。

清单 1 显示一个实现 Serializable 的 Person 类。

清单 1. Serializable Person
package com.tedneward;

public class Person
    implements java.io.Serializable
{
    public Person(String fn, String ln, int a)
    {
        this.firstName = fn; this.lastName = ln; this.age = a;
    }

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
    public Person getSpouse() { return spouse; }

    public void setFirstName(String value) { firstName = value; }
    public void setLastName(String value) { lastName = value; }
    public void setAge(int value) { age = value; }
    public void setSpouse(Person value) { spouse = value; }

    public String toString()
    {
        return "[Person: firstName=" + firstName + 
            " lastName=" + lastName +
            " age=" + age +
            " spouse=" + spouse.getFirstName() +
            "]";
    }    

    private String firstName;
    private String lastName;
    private int age;
    private Person spouse;

}

将 Person 序列化后,很容易将对象状态写到磁盘,然后重新读出它,下面的 JUnit 4 单元测试对此做了演示。

清单 2. 对 Person 进行反序列化
public class SerTest
{
    @Test public void serializeToDisk()
    {
        try
        {
            com.tedneward.Person ted = new com.tedneward.Person("Ted", "Neward", 39);
            com.tedneward.Person charl = new com.tedneward.Person("Charlotte",
                "Neward", 38);

            ted.setSpouse(charl); charl.setSpouse(ted);

            FileOutputStream fos = new FileOutputStream("tempdata.ser");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(ted);
            oos.close();
        }
        catch (Exception ex)
        {
            fail("Exception thrown during test: " + ex.toString());
        }
        
        try
        {
            FileInputStream fis = new FileInputStream("tempdata.ser");
            ObjectInputStream ois = new ObjectInputStream(fis);
            com.tedneward.Person ted = (com.tedneward.Person) ois.readObject();
            ois.close();
            
            assertEquals(ted.getFirstName(), "Ted");
            assertEquals(ted.getSpouse().getFirstName(), "Charlotte");

            // Clean up the file
            new File("tempdata.ser").delete();
        }
        catch (Exception ex)
        {
            fail("Exception thrown during test: " + ex.toString());
        }
    }
}

到现在为止,还没有看到什么新鲜的或令人兴奋的事情,但是这是一个很好的出发点。我们将使用 Person 来发现您可能 知道的关于 Java 对象序列化 的 5 件事。

1. 序列化允许重构

序列化允许一定数量的类变种,甚至重构之后也是如此,ObjectInputStream 仍可以很好地将其读出来。

Java Object Serialization 规范可以自动管理的关键任务是:

  • 将新字段添加到类中
  • 将字段从 static 改为非 static
  • 将字段从 transient 改为非 transient

取决于所需的向后兼容程度,转换字段形式(从非 static 转换为 static 或从非 transient 转换为 transient)或者删除字段需要额外的消息传递。

重构序列化类

既然已经知道序列化允许重构,我们来看看当把新字段添加到 Person 类中时,会发生什么事情。

如清单 3 所示,PersonV2 在原先 Person 类的基础上引入一个表示性别的新字段。

清单 3. 将新字段添加到序列化的 Person 中
enum Gender
{
    MALE, FEMALE
}

public class Person
    implements java.io.Serializable
{
    public Person(String fn, String ln, int a, Gender g)
    {
        this.firstName = fn; this.lastName = ln; this.age = a; this.gender = g;
    }
  
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public Gender getGender() { return gender; }
    public int getAge() { return age; }
    public Person getSpouse() { return spouse; }

    public void setFirstName(String value) { firstName = value; }
    public void setLastName(String value) { lastName = value; }
    public void setGender(Gender value) { gender = value; }
    public void setAge(int value) { age = value; }
    public void setSpouse(Person value) { spouse = value; }

    public String toString()
    {
        return "[Person: firstName=" + firstName + 
            " lastName=" + lastName +
            " gender=" + gender +
            " age=" + age +
            " spouse=" + spouse.getFirstName() +
            "]";
    }    

    private String firstName;
    private String lastName;
    private int age;
    private Person spouse;
    private Gender gender;
}

序列化使用一个 hash,该 hash 是根据给定源文件中几乎所有东西 — 方法名称、字段名称、字段类型、访问修改方法等 — 计算出来的,序列化将该 hash 值与序列化流中的 hash 值相比较。

为了使 Java 运行时相信两种类型实际上是一样的,第二版和随后版本的 Person 必须与第一版有相同的序列化版本 hash(存储为 private static final serialVersionUID 字段)。因此,我们需要 serialVersionUID 字段,它是通过对原始(或 V1)版本的 Person 类运行 JDK serialver命令计算出的。

一旦有了 Person 的 serialVersionUID,不仅可以从原始对象 Person 的序列化数据创建 PersonV2 对象(当出现新字段时,新字段被设为缺省值,最常见的是“null”),还可以反过来做:即从 PersonV2 的数据通过反序列化得到 Person,这毫不奇怪。

2. 序列化并不安全

让 Java 开发人员诧异并感到不快的是,序列化二进制格式完全编写在文档中,并且完全可逆。实际上,只需将二进制序列化流的内容转储到控制台,就足以看清类是什么样子,以及它包含什么内容。

这对于安全性有着不良影响。例如,当通过 RMI 进行远程方法调用时,通过连接发送的对象中的任何 private 字段几乎都是以明文的方式出现在套接字流中,这显然容易招致哪怕最简单的安全问题。

幸运的是,序列化允许 “hook” 序列化过程,并在序列化之前和反序列化之后保护(或模糊化)字段数据。可以通过在 Serializable 对象上提供一个 writeObject 方法来做到这一点。

模糊化序列化数据

假设 Person 类中的敏感数据是 age 字段。毕竟,女士忌谈年龄。 我们可以在序列化之前模糊化该数据,将数位循环左移一位,然后在反序列化之后复位。(您可以开发更安全的算法,当前这个算法只是作为一个例子。)

为了 “hook” 序列化过程,我们将在 Person 上实现一个 writeObject 方法;为了 “hook” 反序列化过程,我们将在同一个类上实现一个readObject 方法。重要的是这两个方法的细节要正确 — 如果访问修改方法、参数或名称不同于清单 4 中的内容,那么代码将不被察觉地失败,Person 的 age 将暴露。

清单 4. 模糊化序列化数据
public class Person
    implements java.io.Serializable
{
    public Person(String fn, String ln, int a)
    {
        this.firstName = fn; this.lastName = ln; this.age = a;
    }

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
    public Person getSpouse() { return spouse; }
    
    public void setFirstName(String value) { firstName = value; }
    public void setLastName(String value) { lastName = value; }
    public void setAge(int value) { age = value; }
    public void setSpouse(Person value) { spouse = value; }

    private void writeObject(java.io.ObjectOutputStream stream)
        throws java.io.IOException
    {
        // "Encrypt"/obscure the sensitive data
        age = age << 2;
        stream.defaultWriteObject();
    }

    private void readObject(java.io.ObjectInputStream stream)
        throws java.io.IOException, ClassNotFoundException
    {
        stream.defaultReadObject();

        // "Decrypt"/de-obscure the sensitive data
        age = age << 2;
    }
    
    public String toString()
    {
        return "[Person: firstName=" + firstName + 
            " lastName=" + lastName +
            " age=" + age +
            " spouse=" + (spouse!=null ? spouse.getFirstName() : "[null]") +
            "]";
    }      

    private String firstName;
    private String lastName;
    private int age;
    private Person spouse;
}

如果需要查看被模糊化的数据,总是可以查看序列化数据流/文件。而且,由于该格式被完全文档化,即使不能访问类本身,也仍可以读取序列化流中的内容。

3. 序列化的数据可以被签名和密封

上一个技巧假设您想模糊化序列化数据,而不是对其加密或者确保它不被修改。当然,通过使用 writeObject 和 readObject 可以实现密码加密和签名管理,但其实还有更好的方式。

如果需要对整个对象进行加密和签名,最简单的是将它放在一个 javax.crypto.SealedObject 和/或 java.security.SignedObject 包装器中。两者都是可序列化的,所以将对象包装在 SealedObject 中可以围绕原对象创建一种 “包装盒”。必须有对称密钥才能解密,而且密钥必须单独管理。同样,也可以将 SignedObject 用于数据验证,并且对称密钥也必须单独管理。

结合使用这两种对象,便可以轻松地对序列化数据进行密封和签名,而不必强调关于数字签名验证或加密的细节。很简洁,是吧?

4. 序列化允许将代理放在流中

很多情况下,类中包含一个核心数据元素,通过它可以派生或找到类中的其他字段。在此情况下,没有必要序列化整个对象。可以将字段标记为 transient,但是每当有方法访问一个字段时,类仍然必须显式地产生代码来检查它是否被初始化。

如果首要问题是序列化,那么最好指定一个 flyweight 或代理放在流中。为原始 Person 提供一个 writeReplace 方法,可以序列化不同类型的对象来代替它。类似地,如果反序列化期间发现一个 readResolve 方法,那么将调用该方法,将替代对象提供给调用者。

打包和解包代理

writeReplace 和 readResolve 方法使 Person 类可以将它的所有数据(或其中的核心数据)打包到一个 PersonProxy 中,将它放入到一个流中,然后在反序列化时再进行解包。

清单 5. 你完整了我,我代替了你
class PersonProxy
    implements java.io.Serializable
{
    public PersonProxy(Person orig)
    {
        data = orig.getFirstName() + "," + orig.getLastName() + "," + orig.getAge();
        if (orig.getSpouse() != null)
        {
            Person spouse = orig.getSpouse();
            data = data + "," + spouse.getFirstName() + "," + spouse.getLastName() + ","  
              + spouse.getAge();
        }
    }

    public String data;
    private Object readResolve()
        throws java.io.ObjectStreamException
    {
        String[] pieces = data.split(",");
        Person result = new Person(pieces[0], pieces[1], Integer.parseInt(pieces[2]));
        if (pieces.length > 3)
        {
            result.setSpouse(new Person(pieces[3], pieces[4], Integer.parseInt
              (pieces[5])));
            result.getSpouse().setSpouse(result);
        }
        return result;
    }
}

public class Person
    implements java.io.Serializable
{
    public Person(String fn, String ln, int a)
    {
        this.firstName = fn; this.lastName = ln; this.age = a;
    }

    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
    public Person getSpouse() { return spouse; }

    private Object writeReplace()
        throws java.io.ObjectStreamException
    {
        return new PersonProxy(this);
    }
    
    public void setFirstName(String value) { firstName = value; }
    public void setLastName(String value) { lastName = value; }
    public void setAge(int value) { age = value; }
    public void setSpouse(Person value) { spouse = value; }   

    public String toString()
    {
        return "[Person: firstName=" + firstName + 
            " lastName=" + lastName +
            " age=" + age +
            " spouse=" + spouse.getFirstName() +
            "]";
    }    
    
    private String firstName;
    private String lastName;
    private int age;
    private Person spouse;
}

注意,PersonProxy 必须跟踪 Person 的所有数据。这通常意味着代理需要是 Person 的一个内部类,以便能访问 private 字段。有时候,代理还需要追踪其他对象引用并手动序列化它们,例如 Person 的 spouse。

这种技巧是少数几种不需要读/写平衡的技巧之一。例如,一个类被重构成另一种类型后的版本可以提供一个 readResolve 方法,以便静默地将被序列化的对象转换成新类型。类似地,它可以采用 writeReplace 方法将旧类序列化成新版本。

5. 信任,但要验证

认为序列化流中的数据总是与最初写到流中的数据一致,这没有问题。但是,正如一位美国前总统所说的,“信任,但要验证”。

对于序列化的对象,这意味着验证字段,以确保在反序列化之后它们仍具有正确的值,“以防万一”。为此,可以实现 ObjectInputValidation接口,并覆盖 validateObject() 方法。如果调用该方法时发现某处有错误,则抛出一个 InvalidObjectException

结束语

Java 对象序列化比大多数 Java 开发人员想象的更灵活,这使我们有更多的机会解决棘手的情况。

幸运的是,像这样的编程妙招在 JVM 中随处可见。关键是要知道它们,在遇到难题的时候能用上它们。

5 件事 系列下期预告:Java Collections。在此之前,好好享受按自己的想法调整序列化吧!

相关文章