sOlOHsU's Blogβ

灯火阑珊处

[转]细节见真功之 Javadoc

作为一个 Java 程序员,Javadoc 大家都应该写过吧,是不是觉得写的时候特简单呢?相信看完本文后你会若有所思。另外,本文非常适合处女座程序员阅读。

句号

为什么是句号而不是其他的标点符号呢?因为这涉及到一个 JDK 文档生成的规则:

The first sentence of each doc comment should be a summary sentence, containing a concise but complete description of the API item. This means the first sentence of each member, class, interface or package description. (成员、类、接口或包注释的第一个句子将作为该注释项的总结,这个句子应该是言简意赅的)

既然提到了句子,那就说明应该用标准的方式——句号来进行第一个句子的断句识别。也许是为了简洁,Javadoc 工具并没有“多语言句号识别”这个特性,所以不管我们用什么语言撰写文档注释,断句符号都必须是英文状态下的句号——.

比如在 NetBeans IDE 里(默认编辑器配置)可以很直观的看到用于总结的第一个句子和后面描述的区别:

在生成 Javadoc 后,类概要页面我们可以看到最终的效果:

在使用中文撰写文档注释时,为了保持整体风格一致,在所有需要使用句号的地方有两种策略可选择:

  • 都使用英文句号:这样做可以让生成的文档句号统一,但缺点是看上去有点别扭
  • 只有第一个句子使用英文句号,其余地方都是要中文句号:这样做后生成的文档看上去比较顺眼,但别人可能会奇怪为什么第一个句号是 . 当然,最彻底的解决方案是不用中文写文档注释,这样就不存在要统一的问题了!

下面我们重点介绍各种你熟悉的或是不熟悉的 Javadoc 文档标记(它们很有内涵)

@author

该标记使用频率是所有文档标记中最高的,我想这是因为:

  • 做好事要留名
  • 使用超简单,就像在填表格(姓名: )一样自然

来看看大牛怎么写的:

(from Commons Lang 2.5 StringUtils)

总结下来有三种写法:

  • 纯文本
  • 带邮箱链接
  • 带 HTTP 链接

(个人建议用 HTTP 链接:打码时可以顺便推广一下自己的博客,哈哈)

另外,在 JDK 代码中我们经常看到 @author unascribed,意思是:“该代码第一原作者不是我,但我实在也不知道是谁,就记作无名氏吧”(这是多么严肃的一种版权意识啊)

@serialXXX

这个系列应该是最不常用的文档标记了,它们到底是干嘛用的呢?请看这里

(我一次也没有使用过这些文档标记,看了官方文档后也还是没有搞懂怎么用,求各位指教)

@value

这个文档标记非常实用(不光好用),可以用于生成被标记的常量字段的值。

直接用于常量字段时:

也可以使用引用方式:

{@inheritDoc}

这个标签体现了 Java 面向对象的精辟所在:不但可以类可以集成,连文档都可以继承(足见 Java 在经典面向对象概念上的完备与圆润)。

比如有个计算面积的接口:

它的实现方法标注了 {@inheritDoc}(处女座阅读提示:无 .)

最后生成的文档:

  • 基类的文档注释被继承到了子类
  • 子类可以再加入自己的注释(特殊化扩展)
  • @return @param @throws 也会被继承

其实在不写 {@inheritDoc} 的情况下也存在文档注释的继承,具体规则请看这里。

{@link} {@linkplain}

这两个链接标记大家用/见的应该比较多,但它们有什么区别、在什么场景下该怎么使用很少有人能够区分开(我猜你要用的时候一般也都是用 link 吧)。

看看官网的标准解释:

link 和 linkplain 的实参都是 package.class#member label 。唯一的不同就是因为字体不同,如果 label 是个纯文本,那就使用 linkplain 吧。(根据这点,我严重怀疑 Javadoc 文档标记的设计者是处女座,~ ~)

pre

没错,这就是那个 HTML 标签,用于显示“原始样子”的。这个标签在写 Javadoc 的时候非常有用,用或者不使用在打码的时候看上去差别不大:

但最终生成 apidocs 之后差别一目了然(处女座阅读提示:在源码文档注释中特别需要注意 pre 后 { 的位置,紧跟 ,无空格)*:

@since

这个从字面的意思上很好理解,所以使用的比较多(如同 @author、@version 一样)。但问题是大家写的时候表达的意思五花八门,常见的有:

  1. 想表达日期/时间 @since 2014-01-01 @since 2014-01-01 14:00:00

  2. 想表达可运行的 JDK 版本 @since JDK1.5

  3. 想表达加入这个元素的版本 @since 1.0.0

根据官方文档解释,@since 表达的是被标记元素是哪个发布版本引入的(3)。比如别人在我们的文档注释中看到

那他可以(应该)认为这个类是在该程序对外发布 1.0.0 版本时已经引入的。如果他要做二次开发,那他就可以很清晰的向后兼容了(我们在用 JDK 的时候就是这个场景)。

@version

提到了 @since 就自然会联想到 @version,因为它们的实参都是版本相关的。@version 要表达的是被标记元素自己的版本(注意对比 @since),也就是说这个版本只要代码改过就应该发生变化,而 @since 是不会变的。

官方文档也解释了怎么用好这个文档标记:通过 SCCS 字符 “%I%, %G%”,例如 1.39, 02/28/97(文件版本号, 日期)生成。但实际上很少有项目这么做(至少目前 Oracle JDK 没这么做,甚至都没有使用 @version,或者是使用了但最后由于特殊原因总体移除了),大家一般都是 @version 1.0.0 然后就再也不修改了,不管被标记的元素改了多少次(这样的做法还不如不写)。

当然,通过版本控制系统 hook 来做是比较经典的做法,不过这样总感觉没有把这个标记的能力完全发挥出来。在我们的项目里是这样使用的:@version 1.2.3.4, Jun 9, 2014

重点是版本号部分,在这个例子中从左到右(1.2.3.4)分别表示:

  • 兼容性位 1,表示兼容性,如果 +1 了说明这个修改是不兼容的
  • 特性位 2,表示已引入了两个特性,每次 +1 说明引入一个新特性
  • 缺陷修复位 3,表示已经修复了 3 个缺陷,每次 +1 说明修复了一个缺陷
  • 重构位 4,表示已经进行了 4 次重构,每次 +1 说明重构了一次
  • 前面 3 位表达的意义和 Semantic Versioning 建议的一致,重构我觉得非常重要,所以也加了进来。

@exception @throws

这两兄弟的情况比 @link @linkplain 更纠结(人家 link 兄弟最起码可以区分出来使用场景)。按照官方文档解释:它们完全是同义词,没有任何区分。那当年 Sun 在 JDK1.2 的时候为什么要加入 @throws 呢——答案是起名失误了,词性没弄匹配:@throws Exception 比 @exception Exception 更符合语法,代入感更好!(细节:In javadoc, what is the difference between the tags @throws and @exception?

标记总表

来张 Javadoc 文档标记总表:

Tag Introduced in JDK/SDK
@author 1.0
{@code} 1.5
{@docRoot} 1.3
@deprecated 1.0
@exception 1.0
{@inheritDoc} 1.4
{@link} 1.2
{@linkplain} 1.4
{@literal} 1.5
@param 1.0
@return 1.0
@see 1.0
@serial 1.2
@serialData 1.2
@serialField 1.2
@since 1.1
@throws 1.2
{@value} 1.4
@version 1.0

这个表是 JDK7 技术手册里的,从中我们可以看出,自 JDK1.5 以后就没有加过新的文档标记了,目测有两个原因:

  • Oracle:“这些已经足够开发人员使用了,没必要加新的了”

  • *Sun:“看吧,Oracle 严重缺乏折腾精神,当初不应该卖给它的” 文档标记介绍完了,下面我们来聊聊 Javadoc 相关的其他侃点。

getter/setter/isTrue

对于 POJO 来说,这几个方法的注释格式非常固定,一般我们都是用 IDE 自动生成:这样的话别人一看到这样固定格式的注释(或者索性不要添加任何注释)就知道这部分相对于其他部分并不重要, 而一旦有的 getter/setter/isTrue 注释不是这样约定的,那就说明了实现上面不只是简单的 get/set/is,还加入了额外的逻辑处理。

对齐

Javadoc 文档注释也有对齐(不是前面 pre 例子那种),这里说的对齐主要指的是以源码视图看到的,最典型的场景就是在给方法添加文档注释的时候,我们经常看到两种风格:

第一眼看上去是不是风格 2 要顺眼得多?但最好还是使用风格 1,因为:

  • 这和编辑器配置的字体有关,如果不是(适合的)等宽字体,那会非常的参差不齐
  • 浪费空间,特别是当注释内容多了需要换行的时候会很别扭
  • 最后生成的 apidocs 效果是一模一样的(无对齐)

(一些 IDE 默认格式化文档注释的时候也是使用风格 1 进行格式化的,强烈建议使用风格 1)

包注释

和前面几点打码风格相关的细节比起来,包注释是具有一定的实用性的。虽然大家可能用得很少,但看得应该比较多,就是这部分:

这里我们使用了两种方式来生成包文档:

  • package.html:这是 JDK1.5 以前的方式,现在已经不推荐使用
  • package-info.java:目前推荐方式,因为这样可以使用注解

在包上面使用注解?这个用法和在其他地方使用注解一样,只是被标注的元素变成了包,在运行时可以获取到包的注解,然后做你想做的事情吧!

中文

一开始我们提到了句号的问题(那的确是一个问题),最后我们来看看中文在写文档注释的时候也非常值得注意的一点(其实不只是 Javadoc 文档注释,该建议也适用于其他一些情况):在中文和英文、数字中间插入一个空格(本文就是这样排版的)。

比如说:

  • 我觉得Java非常cool,特别是JDK8中的lambda,真希望9能带来更多实用特性
  • 我觉得 Java 非常 cool,特别是 JDK8 中的 lambda,真希望 9 能带来更多实用特性

后者看上去就比前者更舒服一些,这样的排版方式适合纯文本编辑器,如果使用的是 Office 之类的工具就不需要手动空格了,因为它们默认已经处理的很好了。

总结

本文介绍了一些 Javadoc 文档注释相关的细节,从这冰山一角相信你对 Java 也有了另一番体验(Java 的进化、工业化)。

总结一下本文内容:

  • 对于文档标记,大家可以尽量尝试使用:把自己的思想通过适合的方式表达给他人是一种好习惯
  • 对于风格相关,大家也可以适当尝试(处女座/强迫症就算了):某大厂在某次改句号问题后出现过生产故障

不过,大家也千万不要太较真,毕竟对于一个好的程序来说,代码应该就是它的文档(之一)。

@转自 http://88250.b3log.org/when-the-little-things-count-javadoc

TCP Stick Package

首先看两个概念:

短连接:

连接->传输数据->关闭连接

HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。 也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。

长连接:

连接->传输数据->保持连接 –> 传输数据-> 。。。 –>关闭连接。

长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。

之所以出现粘包和半包现象,是因为TCP当中,只有流的概念,没有包的概念.

半包

指接受方没有接受到一个完整的包,只接受了部分,这种情况主要是由于TCP为提高传输效率,将一个包分配的足够大,导致接受方并不能一次接受完。(在长连接和短连接中都会出现)。

粘包与分包

粘包指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。

发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

分包是指在出现粘包的时候我们的接收方要进行分包处理。(在长连接中都会出现)

什么时候需要考虑半包的情况?

从备注中我们了解到Socket内部默认的收发缓冲区大小大概是8K,但是我们在实际中往往需要考虑效率问题,重新配置了这个值,来达到系统的最佳状态。

一个实际中的例子:用mina作为服务器端,使用的缓存大小为10k,这里使用的是短连接,所有不用考虑粘包的问题。

问题描述:在并发量比较大的情况下,就会出现一次接受并不能完整的获取所有的数据。

处理方式:

  1. 通过包头+包长+包体的协议形式,当服务器端获取到指定的包长时才说明获取完整。
  2. 指定包的结束标识,这样当我们获取到指定的标识时,说明包获取完整。

什么时候需要考虑粘包的情况?

  1. 当时短连接的情况下,不用考虑粘包的情况
  2. 如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包
  3. 如果双方建立连接,需要在连接后一段时间内发送不同结构数据 处理方式: 接收方创建一预处理线程,对接收到的数据包进行预处理,将粘连的包分开

注:粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包

备注:

一个包没有固定长度,以太网限制在46-1500字节,1500就是以太网的MTU,超过这个量,TCP会为IP数据报设置偏移量进行分片传输,现在一般可允许应用层设置8k(NTFS系)的缓冲区,8k的数据由底层分片,而应用看来只是一次发送。

windows的缓冲区经验值是4k,Socket本身分为两种,流(TCP)和数据报(UDP),你的问题针对这两种不同使用而结论不一样。甚至还和你是用阻塞、还是非阻塞Socket来编程有关。

  1. 通信长度

    这个是你自己决定的,没有系统强迫你要发多大的包,实际应该根据需求和网络状况来决定。对于TCP,这个长度可以大点,但要知道,Socket内部默认的收发缓冲区大小大概是8K,你可以用SetSockOpt来改变。

    但对于UDP,就不要太大,一般在1024至10K。注意一点,你无论发多大的包,IP层和链路层都会把你的包进行分片发送,一般局域网就是1500左右,广域网就只有几十字节。分片后的包将经过不同的路由到达接收方,对于UDP而言,要是其中一个分片丢失,那么接收方的IP层将把整个发送包丢弃,这就形成丢包。

    显然,要是一个UDP发包佷大,它被分片后,链路层丢失分片的几率就佷大,你这个UDP包,就佷容易丢失,但是太小又影响效率。最好可以配置这个值,以根据不同的环境来调整到最佳状态。

    send()函数返回了实际发送的长度,在网络不断的情况下,它绝不会返回(发送失败的)错误,最多就是返回0。对于TCP你可以字节写一个循环发送。当send函数返回SOCKET_ERROR时,才标志着有错误。

    但对于UDP,你不要写循环发送,否则将给你的接收带来极大的麻烦。所以UDP需要用SetSockOpt来改变Socket内部Buffer的大小,以能容纳你的发包。

    明确一点,TCP作为流,发包是不会整包到达的,而是源源不断的到,那接收方就必须组包。而UDP作为消息或数据报,它一定是整包到达接收方。

  2. 关于接收

    一般的发包都有包边界,首要的就是你这个包的长度要让接收方知道,于是就有个包头信息,对于TCP,接收方先收这个包头信息,然后再收包数据。一次收齐整个包也可以,可要对结果是否收齐进行验证。这也就完成了组包过程。

    UDP,那你只能整包接收了。要是你提供的接收Buffer过小,TCP将返回实际接收的长度,余下的还可以收,而UDP不同的是,余下的数据被丢弃并返回WSAEMSGSIZE错误。

    注意TCP,要是你提供的Buffer佷大,那么可能收到的就是多个发包,你必须分离它们,还有就是当Buffer太小,而一次收不完Socket内部的数据,那么Socket接收事件(OnReceive),可能不会再触发,使用事件方式进行接收时,密切注意这点。这些特性就是体现了流和数据包的区别。

Java基础之ClassLoader

什么是类加载器

类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。有三种默认使用的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(在HotSpot中的实现叫作Application类加载器)。每种类加载器都有设定好从哪里加载类。

  • Bootstrap类加载器负责加载rt.jar中的JDK类文件,它是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载器,如果你调用String.class.getClassLoader(),会返回null,任何基于此的代码会抛出NUllPointerException异常。Bootstrap加载器被称为初始类加载器。
  • 而Extension将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下加载类。Extension加载器由sun.misc.Launcher$ExtClassLoader实现。
  • 第三种默认的加载器就是System类加载器(又叫作Application类加载器)了。它负责从classpath环境变量中加载某些应用相关的类,classpath环境变量通常由-classpath或-cp命令行选项来定义,或者是JAR中的Manifest的classpath属性。Application类加载器是Extension类加载器的子加载器。通过sun.misc.Launcher$AppClassLoader实现。

除了Bootstrap类加载器是大部分由C++来写的,其他的类加载器都是通过继承java.lang.ClassLoader来实现的。

类加载器的工作原理

类加载器的工作原理基于三个机制:委托、可见性和单一性。这一节,我们来详细看看这些规则,并用一个实例来理解工作原理。下面显示的是类加载器使用委托机制的工作原理。

委托机制

当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。假设你有一个应用需要的类叫作Abc.class,首先加载这个类的请求由Application类加载器委托给它的父类加载器Extension类加载器,然后再委托给Bootstrap类加载器。Bootstrap类加载器会先看看rt.jar中有没有这个类,因为并没有这个类,所以这个请求由回到Extension类加载器,它会查看jre/lib/ext目录下有没有这个类,如果这个类被Extension类加载器找到了,那么它将被加载,而Application类加载器不会加载这个类;而如果这个类没有被Extension类加载器找到,那么再由Application类加载器从classpath中寻找。

可见性机制

根据可见性机制,子类加载器可以看到父类加载器加载的类,而反之则不行。所以下面的例子中,当Abc.class已经被Application类加载器加载过了,然后如果想要使用Extension类加载器加载这个类,将会抛出java.lang.ClassNotFoundException异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 package test;

    import java.util.logging.Level;
    import java.util.logging.Logger;

    /**
     * Java program to demonstrate How ClassLoader works in Java,
     * in particular about visibility principle of ClassLoader.
     *
     * @author Javin Paul
     */

    public class ClassLoaderTest {

        public static void main(String args[]) {
            try {
                //printing ClassLoader of this class
                System.out.println("ClassLoaderTest.getClass().getClassLoader() : "
                                     + ClassLoaderTest.class.getClassLoader());

                //trying to explicitly load this class again using Extension class loader
                Class.forName("test.ClassLoaderTest", true
                                ,  ClassLoaderTest.class.getClassLoader().getParent());
            } catch (ClassNotFoundException ex) {
                Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

    }

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
ClassLoaderTest.getClass().getClassLoader() : sun.misc.Launcher$AppClassLoader@601bb1
  16/08/2012 2:43:48 AM test.ClassLoaderTest main
  SEVERE: null
  java.lang.ClassNotFoundException: test.ClassLoaderTest
          at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
          at java.security.AccessController.doPrivileged(Native Method)
          at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
          at sun.misc.Launcher$ExtClassLoader.findClass(Launcher.java:229)
          at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
          at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
          at java.lang.Class.forName0(Native Method)
          at java.lang.Class.forName(Class.java:247)
          at test.ClassLoaderTest.main(ClassLoaderTest.java:29)

单一性机制

根据这个机制,父加载器加载过的类不能被子加载器加载第二次。虽然重写违反委托和单一性机制的类加载器是可能的,但这样做并不可取。你写自己的类加载器的时候应该严格遵守这三条机制。

如何显式的加载类

Java提供了显式加载类的API:Class.forName(classname)Class.forName(classname, initialized, classloader)。就像上面的例子中,你可以指定类加载器的名称以及要加载的类的名称。类的加载是通过调用java.lang.ClassLoader的loadClass()方法,而loadClass()方法则调用了findClass()方法来定位相应类的字节码。在这个例子中Extension类加载器使用了java.net.URLClassLoader,它从JAR和目录中进行查找类文件,所有以”/”结尾的查找路径被认为是目录。如果findClass()没有找到那么它会抛出java.lang.ClassNotFoundException异常,而如果找到的话则会调用defineClass()将字节码转化成类实例,然后返回。

什么地方使用类加载器

类加载器是个很强大的概念,很多地方被运用。最经典的例子就是AppletClassLoader,它被用来加载Applet使用的类,而Applets大部分是在网上使用,而非本地的操作系统使用。使用不同的类加载器,你可以从不同的源地址加载同一个类,它们被视为不同的类。J2EE使用多个类加载器加载不同地方的类,例如WAR文件由Web-app类加载器加载,而EJB-JAR中的类由另外的类加载器加载。有些服务器也支持热部署,这也由类加载器实现。你也可以使用类加载器来加载数据库或者其他持久层的数据。

参考资料:

阿里校园招聘暑期实习二面面经

来自2015届校招群里的波波同学:

  1. 自我介绍
  2. 最出彩的项目,聊项目,遇到的难题
  3. WebService相关相关知识,写wsdl文档的格式、webservice、uddi、wsdl(说明文档)、soap
  4. spring的理解,spring的Ioc原理、并发性(内存的占用怎么样)
  5. java的jvm的内存模式(堆。栈、代码区、数据区)、内存回收机制(自己回收)、jvm内存的设置(不知道)
  6. 编程:找出字符串中的url
  7. HR:你研究生和本科生最大的进步是什么(目标更加明确,对自身更加了解)
  8. 还找了其他的实习吗?进度?你为什么选择阿里?(分布式计算)
  9. 你最近有没有感兴趣的技术,技术进步的主要手段(网络)。

在.NET Compact Framework中使用后台任务

最近在做一个Windows Mobile平台客户端的项目,需要在后台线程中连接服务器并进行消息的发送和接收。

后台任务

由于之前也没接触过C#,所以查找了一下相关资料,发现基本上有使用BackgroundWorker和直接使用Thread两种实现方式。

BackgroundWorker看名字应该类似于Android平台中AsynTask,为了方便编写简单的非UI后台任务而对Thread进行了封装。由于之前的Android客户端中收发消息都是使用的AsynTask,并且后台任务的逻辑也比较简单,BackgroundWorker完全能够满足要求。

写代码时发现没有找到BackgroundWorker这个类,想到Windows Mobile平台使用的是.NET Compact Framework,江湖人称.NET CF,Google了一下,果然CF是不支持BackgroundWorker的,只好另寻他法。

Google之后发现已经有人问过这个问题:

回答中有人建议自己写一个BackgroundWorker,也有人建议参考微软官方的这篇文章:

这篇文章中提到了在.NET CF中创建后台任务的三种方式:

  1. 异步访问Web Service
  2. 线程池
  3. 显示的创建线程

根据我们的应用场景我最终选择了线程池这种比较简单的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
       void ReadBigFile2(object val)
     {
        string dataFile = (string) val ; // val is a reference to fName
        // Do work to read and process dataFile
     }
  
     public void btnStartRead_Click(object sender, EventArgs e)
     {
        string fName = BigDataFile.xml ;
        WaitCallback w = new WaitCallback(ReadBigFile2) ;
        // fName will be passed to ReadBigFile
        ThreadPool.QueueUserWorkItem(w, fName) ;
     }

查看默认的线程池中辅助线程的最大数目和异步I/O线程的最大数目:

1
2
3
4
   int workerThreads, completionPortThreads;
  ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
  System.Diagnostics.Debug.WriteLine(workerThreads);  // 25
  System.Diagnostics.Debug.WriteLine(completionPortThreads);// 500

可以使用ThreadPool.SetMaxThreads方法按需修改。

在后台任务中操纵UI控件

将消息收发转到后台线程之后,就不能直接在收发消息时直接操纵UI控件了,需要在UI线程中定义好操纵控件的方法,然后在后台线程中使用Control.Invoke或者Control.BeginInvoke方法通过委托的方式进行调用,InvokeBeginInvoke的区别是前者在进行委托调用时后台线程会阻塞,而后者使用的是异步委托调用。详细分析可以参考这几篇博文:

关于WaitCursor

在Android中执行后台任务时,我们通常会在界面上覆盖一个指示后台任务正在运行的ProgressDialog。

C#中可以使用Cursor.Current = Cursors.WaitCursor;来显示一个等待指示器。但是这时我们依然可以对界面中的控件进行操作,简单的解决方法是后台任务开始时使用this.Enabled = false;将当前窗体设为不可用,执行结束后this.Enabled = true;再恢复可用。缺点是整个界面会变成灰色。

参考资料

为Octopress添加Categories侧边栏

增加category_list插件

保存到 plugins/category_list_tag.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 # encoding: UTF-8
  module Jekyll
    class CategoryListTag < Liquid::Tag
      def render(context)
        html = ""
        categories = context.registers[:site].categories.keys
        categories.sort.each do |category|
          posts_in_category = context.registers[:site].categories[category].size
          category_dir = context.registers[:site].config['category_dir']
          html << "<li class='category'><a href='/#{category_dir}/#{category.to_url}/'>#{category} (#{posts_in_category})</a></li>\n"
        end
        html
      end
    end
  end
  
  Liquid::Template.register_tag('category_list', Jekyll::CategoryListTag)

注意:一定要在文件的开始添加# encoding: UTF-8这一行,否则无法支持中文分类。

增加aside

保存到 source/_includes/asides/category_list.html:

1
2
3
4
5
6
 <section>
    <h1>Categories</h1>
    <ul id="categories">
      \{\% category_list %\}\   //添加时去掉4个\
    </ul>
  </section>

修改_config.yml文件

将category_list添加到default_asides:

1
   default_asides: [asides/category_list.html, asides/recent_posts.html]

安装这个插件后直接可以支持中文分类,url中使用的是分类的拼音,如「数据库」对应「shu-ju-ku」。如果使用中文分类时遇到各种错误,请参考下面这两篇文章:

MySQL锁表与解锁

LOCK TABLES

锁表语法

1
2
3
4
5
6
7
  LOCK TABLES
      tbl_name [[AS] alias] lock_type
      [, tbl_name [[AS] alias] lock_type] ...
  
  lock_type:
      READ [LOCAL]
    | [LOW_PRIORITY] WRITE

会话只能为自己获取、释放锁,不能为其他会话获取锁,也不能释放其他会话持有的锁。

LOCK TABLES隐式的为当前会话获取表锁。表锁的对象可以是表也可以是视图。

获取视图锁时,LOCK TABLES命令会自动锁定所有与该视图相关的基本表。

使用LOCK TABLES显示的锁定一个表时,这个表的触发器中涉及到的表也会被隐式的锁定,锁的类型取决于触发器中对该表进行了何种操作。

表锁只是用来防止其他会话进行不恰当的操作,持有锁的会话,即使持有的仅仅是读锁,也可以进行诸如DROP TABLE之类的表级操作。而由于TRUNCATE操作不是事务安全的,所以在活动的事务或者持有表锁时尝试执行TRUNCATE操作时会报错。

另外,虽然可以对临时表执行锁表操作,但是实际上会被自动忽略掉,不执行任何操作。因为临时表只对当前会话(也就是创建它的会话)是可见的。

锁的类型

READ [LOCAL]锁

  • 持有读锁的会话可以读表,但是不能写表。
  • 多个会话可以同时获得同一个表的读锁。
  • 其他的会话不用获取该表的读锁也可以读取该表。
  • LOCAL修饰符允许其他会话执行无冲突的插入语句。但是,如果是在服务器之外的进程上进行操作时,就不能使用READ LOCAL了。另外,对于使用InnoDB引擎的表来说,READ LOCALREAD没有任何区别。

[LOW_PRIORITY] WRITE锁

  • 持有写锁的会话可以读表也可以写表。
  • 只有持有写锁的会话可以访问该表。写锁释放前其他会话都不能对该表进行访问。
  • 表被写锁锁定时,其他会话对该表的锁定请求将会被阻塞。
  • LOW_PRIORITY修饰符已被弃用(当前版本为5.7)。直接使用WRITE即可。

锁表语句会一直阻塞直到获取到所有需要的锁。 需要对表进行锁定的会话必须一次性的获取到所有它需要锁,因为一旦锁表语句执行完成,该会话就只能访问被锁的表了。

1
2
3
4
5
6
7
8
9
   mysql> LOCK TABLES t1 READ;
  mysql> SELECT COUNT(*) FROM t1;
  +----------+
  | COUNT(*) |
  +----------+
  |        3 |
  +----------+
  mysql> SELECT COUNT(*) FROM t2;
  ERROR 1100 (HY000): Table 't2' was not locked with LOCK TABLES

INFORMATION_SCHEMA数据库中的表是个例外。即使有会话对这些表进行了锁定,其他会话依然可以继续访问。

在一条查询语句中无法多次对锁定的表使用同一个名字进行引用。这种情况下可以使用表的别名,并且对每一个别也要加一个锁:

1
2
3
4
   mysql> LOCK TABLE t WRITE, t AS t1 READ;
  mysql> INSERT INTO t SELECT * FROM t;
  ERROR 1100: Table 't' was not locked with LOCK TABLES
  mysql> INSERT INTO t SELECT * FROM t AS t1;

不对表的别名进行锁定的话,就无法使用别名对该表进行访问:

1
2
3
   mysql> LOCK TABLE t READ;
  mysql> SELECT * FROM t AS myalias;
  ERROR 1100: Table 'myalias' was not locked with LOCK TABLES

相反,如果只对别名进行了锁定,则只能使用别名对其进行访问:

1
2
3
4
   mysql> LOCK TABLE t AS myalias READ;
  mysql> SELECT * FROM t;
  ERROR 1100: Table 't' was not locked with LOCK TABLES
  mysql> SELECT * FROM t AS myalias;

写锁通常相对读锁有更高的优先级,以确保更新操作尽快得到处理。这就意味着如果同时有写锁和读锁两个请求,那么写锁会先得到响应。LOCK TABLES获取锁的过程如下:

  1. 使用内部定义的顺序对所有将被加锁的表进行排序,这一步对于用户来说是透明的。
  2. 如果一个表同时有读锁和写锁两个请求,将写锁请求放到读锁请求之前。
  3. 每次锁定一个表直到当前会话获取到所有的需要的锁,并保证不会产生死锁。

对分表进行锁定和解锁时,会锁定或者解锁整个表。

UNLOCK TABLES

解锁语法

1
  UNLOCK TABLES

使用UNLOCK TABLES将显示的释放掉由当前会话持有的所有表锁。

LOCK TABLES会在获取新锁之前隐式的释放掉当前会话持有的所有表锁。

会话开始一个事务(例如使用START TRANSACTION)时,会隐式的执行UNLOCK TABLES

无论会话正常退出或者异常终止,服务器都会隐式的释放掉该会话持有的所有锁。客户端重新连接后,这些锁不会再生效。另外,如果客户端断开时正在执行一个事务,服务器会自动回滚该事务,客户端重连后,新会话将会自动设置为自动提交模式。因此,最好禁用客户端的auto-reconnec。在启用自动重连的情况下,重连后客户端其实已经丢失了所有的锁和事务,但是不会被通知。而当禁用掉自动重连时,一旦连接断开,在一个将被执行的语句处会报错。这样一来客户端就可以检测到这个错误,并采取适当的措施,比如重新获取需要的锁,重新执行之前的事务。

在被锁定的表上执行ALTER TABLE语句后,将使该表变为未锁定状态。如果对该表再执行一条ALTER TABLE语句,就会报错:Table ‘tbl_name’ was not locked with LOCK TABLES。这种情况下可以在执行完第一条ALTER TABLE后,重新对该表进行锁定。

UNLOCK TABLES还可以用来释放之前通过FLUSH TABLES WITH READ LOCK获得的全局读锁。

2014暑期实习面试

腾讯移动客户端开发

一面

  • 介绍项目
  • 两道题目
    • 如何从10亿个QQ号中快速查找给定的QQ号
    • RGB2YUV快速算法
  • 详细介绍项目中的移动端

二面

  • 介绍项目,项目中的APP的亮点和技术难度
  • 纸上写代码:单链表的反转
  • 闲扯
    • 用过最出色的APP、最差的APP
    • 微信和QQ比较
    • 微信哪些不足
    • 还用过哪些其他的社交APP

HR面

都是轻松的话题

  • 本科学校
  • 本科和研究生有没有开设Android的课程
  • Android是自学的吗,为什么不做IOS应用开发
  • 哪里人,山东学生考北京学校的挺多
  • 前两面的面试官有没有跟你说过你将来到公司做什么
  • 做过前端,前端和客户端开发你更倾向于哪个
  • 阿里校招有没有参加,结果如何
  • 平时的兴趣爱好,水平如何
  • 到哪看美剧
  • 实习时间

阿里Java研发

一面

  • 介绍项目
    • 详细介绍项目开发中最有挑战性的部分
    • Spring框架的好处
  • 数据库
    • 如何加锁
    • 锁的级别
    • 事务控制
      • 给了一道题
    • 乐观锁、悲观锁
    • 索引
      • 索引的种类
      • 聚簇索引
      • 索引的结构
        • B+树
      • 红黑树
  • 复杂的业务逻辑、对象依赖
  • 面向对象的特征
    • 举例说明

下面整理了一些大牛的面试总结:

使用Java快速处理大数据量的输入

这里以读取int类型的数据为例。

Scanner

直接使用nextInt()

虽然是最方便的,但是也是最慢的。建议数据量小的时候使用。数据量大的情况下多半会超时。直接使用nextInt()方法的时候一般都会用BufferedInputStream封装一下输入流,速度能稍微快一些。

1
2
3
4
5
6
7
8
9
10
 Scanner scanner = new Scanner(new BufferedInputStream(System.in));

  long rows = scanner.nextLong();
  int cols = scanner.nextInt();

  for (int i = 0; i < rows * cols; i++) {
      int value = scanner.nextInt();
  }

  scanner.close();

使用next(), 然后手动Integer.parseInt()

这样能比直接使用nextInt()快不少,查看一下nextInt()的源码我们可以发现:nextInt()方法先用正则表达式从流中获取一个表示整型的字符串s,最后再返回Integer.parseInt(s)。多出的时间都消耗在进行模式匹配上了,这其实也就是直接使用nextInt()比较慢的原因之一。而如果我们事先知道输入的数据类型的话,就不用进行匹配,直接解析就可以了。

1
2
3
4
5
6
7
8
9
10
 Scanner scanner = new Scanner(new BufferedInputStream(System.in));

  long rows = Long.parseLong(scanner.next());
  int cols = Integer.parseInt(scanner.next());

  for (int i = 0; i < rows * cols; i++) {
      int value = Integer.parseInt(scanner.next());
  }

  scanner.close();

BufferedReader + StringTokenizer

使用BufferedReader按行读取,然后使用StringTokenizer获取每一行中使用空格符隔开的所有元素。速度甚至比C语言的scanf()还要快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
    StringTokenizer tokenizer = new StringTokenizer("");

    tokenizer = new StringTokenizer(reader.readLine());
  long rows = Long.parseLong(tokenizer.nextToken());

  tokenizer = new StringTokenizer(reader.readLine());
  int cols = Integer.parseInt(tokenizer.nextToken());

    for (int i = 0; i < rows * cols; i++) {
      while (!tokenizer.hasMoreTokens()) {
          tokenizer = new StringTokenizer(reader.readLine());
      }
      int value = Integer.parseInt(tokenizer.nextToken());
  }

    reader.close();

实际测试结果

用以上三种方式从文件中读取10,000,000个int型数值所消耗的时间分别为 11.52秒,7.486秒,2.159秒。使用gcc的fscanf()所用时间为 5.303秒。

读取10,000,000个double型数值所消耗的时间分别为 41.125秒,16.082秒,9.271秒。使用gcc的fscanf()所用时间为 11.279秒。

总结

虽然使用BufferedReaderStringTokenizer处理输入可以获得让人满意的速度,但是需要写的代码相对来说是比较多的。在ACM比赛时可以准备一个工具类,比赛开始时,让一个人先把这个类写出来备用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 /** Class for buffered reading int and double values */
  class Reader {
      static BufferedReader reader;
      static StringTokenizer tokenizer;
  
      /** call this method to initialize reader for InputStream */
      static void init(InputStream input) {
          reader = new BufferedReader(
                       new InputStreamReader(input) );
          tokenizer = new StringTokenizer("");
      }
  
      /** get next word */
      static String next() throws IOException {
          while ( ! tokenizer.hasMoreTokens() ) {
              //TODO add check for eof if necessary
              tokenizer = new StringTokenizer(
                     reader.readLine() );
          }
          return tokenizer.nextToken();
      }
  
      static int nextInt() throws IOException {
          return Integer.parseInt( next() );
      }
      
      static double nextDouble() throws IOException {
          return Double.parseDouble( next() );
      }
  }

参考资料

Octopress自定义修改记录

按照官方教程部署好Octopress后,如果想要支持中文blog的书写,需要修改以下几个地方:

环境变量

如果在Windows操作系统下,需要添加两个环境变量:

  • LANG = zh_CN.UTF-8
  • LC_ALL = zh_CN.UTF-8

Jekyll

修改{ruby_home}\lib\ruby\gems\1.9.1\gems\jekyll-0.12.0\lib\jekyll\convertible.rb,强制使用UTF-8格式:

其他的一些修改:

提交时间

运行rake deploy时,在Rakefile中可以找到默认的提交信息是「Site updated at #{Time.now.utc}」,这里的时间是UTC时间,将Time.now.utc改成Time.now就可以使用本地的时间了。

JiaThis插件

添加JiaThis分享插件后,会发现在分享工具条左下角出现一个小白框,遮挡住了「分享至」里的「分」字,影响视觉效果。Google了一下发现了解决方案:

修改octopress\source\javascripts\octopress.js

解决方案源地址:http://geeksavetheworld.com/blog/2012/11/05/add-jiathis-to-octopress-blog/

列表缩进样式

默认的css样式文件中,markdown的列表是没有进行缩进处理的,看着比较别扭。找到octopress\sass\custom\_layout.scss,去掉//$indented-lists: true;这一行的注释符号,重新rake generate就可以了。

Gist tag

修复Gist tag的样式。参见:http://devspade.com/blog/2013/08/06/fixing-gist-embeds-in-octopress/

Codeblock插件

现在官方Github的master分支下为Octopress2.0版本,暂不支持Codeblock插件中的start, mark, linenos选项,可以参考这两个临时解决方法: