ellios's blog

ellios's trivial story.

Zookeeper入门

| Comments

What is Zookeeper

ZooKeeper is a high-performance coordination service for distributed applications. It exposes common services - such as naming, configuration management, synchronization, and group services - in a simple interface so you don’t have to write them from scratch. You can use it off-the-shelf to implement consensus, group management, leader election, and presence protocols.

Zookeeper提供了如下的一致性保证:

  1. 顺序一致性。客户端的更新顺序与它们被发送的顺序相一致。
  2. 原子性。更新操作要么成功要么失败,不会更新半拉。
  3. 单系统镜像。无论客户端连接到哪一个服务器,客户端将看到相同的 ZooKeeper 视图。
  4. 可靠性。一旦一个更新操作被应用,那么在客户端再次更新它之前,它的值将不会改变。
  5. 实时性。在特定的一段时间内,客户端能够得到数据变化的通知

Name Space

ZooKeeper's Hierarchical Namespace

如图所示,zookeeper的命令空间是一个树状结构,类似于文件系统。树中的每个节点对应一个znode。

Znode

  1. znode通过路径来唯一标识
  2. znode有分为PERSISTENTEPHEMERAL(EPHEMERAL的生命周期依赖于client session,对应session close/expire后其znode也会消失)
  3. znode的数据读写是原子的
  4. znode的数据数据结构
    • czxid : The zxid of the change that caused this znode to be created.
    • mzxid : The zxid of the change that last modified this znode.
    • ctime : The time in milliseconds from epoch when this znode was created.
    • mtime : The time in milliseconds from epoch when this znode was last modified.
    • version : The number of changes to the data of this znode.
    • cversion : The number of changes to the children of this znode.
    • aversion : The number of changes to the ACL of this znode.
    • ephemeralOwner : The session id of the owner of this znode if the znode is an ephemeral node. If it is not an ephemeral node, it will be zero.
    • dataLength : The length of the data field of this znode.
    • numChildren : The number of children of this znode.
  5. zonde的数据大小是有限制的,默认最大是1M,可以通过修改环境变量jute.maxbuffer来设置

Zookeeper Cluster

Zookeeper Cluster

Zookeeper中的每个节点都在内存中有个一份所有数据的副本。

角色

Zookeeper集群中,每个节点担任下面的一种角色。

  • leader:集群的管理者(数据同步,数据更新提议),通过paxos算法选举出来的
  • follower:参与投票,在leader出问题时,可以被选举为follower
  • observer:定时从leader同步数据,提高系统的读性能

    Paxos

    Paxos是一种分布式一致性算法,是zookeeper保证数据一致性的基础。网上关于paxos的内容还挺多的,有兴趣的自行找吧。

Zookeeoer的应用

  • Leader Election
  • Group Membership
  • Configuration Management
  • Cluster Management
  • 分布式锁
  • 分布式队列

实践

Zookeeper自身提供的接口比较麻烦,网上有很多开源的zookeeper client的封装实现,目前用了netflix出的curator,这里examples有一些例子,有兴趣的可以看下。

参考资料

Thrift入门

| Comments

前几天介绍了下Protocol Buffer,这次研究下和PB比较类似的Thrift。这两者都定义了代码独立的数据结构描述语法,并且提供了工具将定义的数据结构转化为相应的代码(Java,C++,Python之类的)。但是从使用上来说,Thrift提供的功能要更丰富一些,简单列下对二者区别的认识

  • Thrfit从语法上来说,更接近C的语法,对程序员的友好性要更好些
  • Thrift对rpc的支持比较好,接口的描述语法更类似与通常的接口声明。PB的接口只支持单一的输入参数,如果有多个参数,需要自己定义参数的数据结构,使用起来很别扭。不仅仅是语法上,Thrift对rpc是提供了完整的支持的,用Thrift可以很方便的写出rpc的Server和Client端,PB则要自己写网络层。
  • PB对于容器类的数据结构只提供了对list的直接支持,而Thrift还提供了set和map类型,符合通常程序员的使用习惯
  • Thrift直接支持的语言比较多,PB只支持Java,C++,Python,不过PB有很多第三方实现的对其他语言的支持
  • PB的文档更好些,从发布的节奏上来看PB要更活跃一些。学习Thrift一定要看看源码
  • PB的性能要更好些,不过Thrift也不会太差,一般的生产环境下,这点性能提升可以忽略了

使用

Thrift在使用前同样需要安装,这里给个安装的传送门。Thrift的源码里有些tutorial的代码,里面有各种语言的使用示例,初学thrift的话,把tutorial里的代码看明白,基本就能入门了。

Types

先来打点基础,看下thrift支持哪些数据类型:

  • bool Boolean, one byte
  • byte Signed byte
  • i16 Signed 16-bit integer
  • i32 Signed 32-bit integer
  • i64 Signed 64-bit integer
  • double 64-bit floating point value
  • string String
  • binary Blob (byte array)
  • map<t1,t2> Map from one type to another
  • list Ordered list of one type
  • set Set of unique elements of one type

相对与pb,thrift多了map,list,set这些容器类型,但是基本的数值类型少了一些,不过我们需要用到的也都有了。

示例

基础的内容就介绍这么多了,下面直接上例子,估计看了例子大家对thrift的语法也了解差不多了。

demo.thrift
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
31
32
/*
* demo.thrift
* thrift usage demo
*/
namespace cpp ellios.me
namespace java ellios.me
namespace py ellios.me

enum Operation {
  ADD = 1,
  SUBTRACT = 2,
  MULTIPLY = 3,
  DIVIDE = 4
}

struct Work {
  1: i32 num1 = 0,
  2: i32 num2,
  3: Operation op,
  4: optional string comment,
}

exception InvalidOperation {
  1: i32 what,
  2: string why
}

service Calculator {

   i32 calculate(1:i32 logid, 2:Work w) throws (1:InvalidOperation ouch),

}

java的例子

执行下面的命令,会生成相应的java代码。

1
 thrift --gen java --out . demo.thrift
Java Server

首先实现接口

CaloculatorServiceImpl.java
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
31
32
33
public class CaloculatorServiceImpl  implements Calculator.Iface {
    @Override
    public int calculate(int logid, Work work) throws InvalidOperation, TException {
        System.out.println("calculate(" + logid + ", {" + work.op + "," + work.num1 + "," + work.num2 + "})");
        int val = 0;
        switch (work.op) {
            case ADD:
                val = work.num1 + work.num2;
                break;
            case SUBTRACT:
                val = work.num1 - work.num2;
                break;
            case MULTIPLY:
                val = work.num1 * work.num2;
                break;
            case DIVIDE:
                if (work.num2 == 0) {
                    InvalidOperation io = new InvalidOperation();
                    io.what = work.op.getValue();
                    io.why = "Cannot divide by 0";
                    throw io;
                }
                val = work.num1 / work.num2;
                break;
            default:
                InvalidOperation io = new InvalidOperation();
                io.what = work.op.getValue();
                io.why = "Unknown operation";
                throw io;
        }
        return val;
    }
}

下面实现一个Server,让人们可以调用服务

DemoServer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
public class DemoServer {

    public static void main(String [] args) throws TTransportException {
        Calculator.Iface calculatorService = new CaloculatorServiceImpl();
        Calculator.Processor processor = new Calculator.Processor(calculatorService);
        TServerTransport serverTransport = new TServerSocket(8000);
        TServer server = new TThreadPoolServer(new TThreadPoolServer.Args(serverTransport).processor(processor));

        System.out.println("Starting the simple server...");
        server.serve();
    }

}
Java Client

接下来实现一个客户端,调用服务。

DemoClient.java
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
31
32
33
34
public class DemoClient {

    public static void main(String [] args) throws Exception {

        TTransport transport = new TSocket("localhost", 8000);
        transport.open();
        TProtocol protocol = new TBinaryProtocol(transport);
        Calculator.Iface client = new Calculator.Client(protocol);

        Work work = new Work();

        work.op = Operation.DIVIDE;
        work.num1 = 1;
        work.num2 = 0;
        try {
            int quotient = client.calculate(1, work);
            System.out.println("Whoa we can divide by 0");
        } catch (InvalidOperation io) {
            System.out.println("Invalid operation: " + io.why);
        }

        work.op = Operation.SUBTRACT;
        work.num1 = 15;
        work.num2 = 10;
        try {
            int diff = client.calculate(2, work);
            System.out.println("15-10=" + diff);
        } catch (InvalidOperation io) {
            System.out.println("Invalid operation: " + io.why);
        }

        transport.close();
    }
}

python示例

由于前面已经实现了一个Java的Server,这里就只实现一个client了。

demo_client.py
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
31
32
33
34
35
36
37
38
39
import sys

from me.ellios.demo import Calculator
from me.ellios.demo.ttypes import *

from thrift import Thrift
from thrift.transport import TSocket
from thrift.transport import TTransport
from thrift.protocol import TBinaryProtocol

try:
  # Make socket
  transport = TSocket.TSocket('localhost', 8000)
  # Buffering is critical. Raw sockets are very slow
  transport = TTransport.TBufferedTransport(transport)
  # Wrap in a protocol
  protocol = TBinaryProtocol.TBinaryProtocol(transport)
  # Create a client to use the protocol encoder
  client = Calculator.Client(protocol)
  # Connect!
  transport.open()
  work = Work()
  work.op = Operation.DIVIDE
  work.num1 = 1
  work.num2 = 0
  try:
    quotient = client.calculate(1, work)
    print 'Whoa? You know how to divide by zero?'
  except InvalidOperation, io:
    print 'InvalidOperation: %r' % io
  work.op = Operation.SUBTRACT
  work.num1 = 15
  work.num2 = 10
  diff = client.calculate(1, work)
  print '15-10=%d' % (diff)
  # Close!
  transport.close()
except Thrift.TException, tx:
  print '%s' % (tx.message)

参考资料

艰难的reviewboard折腾

| Comments

折腾了半天的Review Board,终于能提交请求了。因为期间的折腾过程特别曲折,所以记录一下。

因为是全公司要推广CodeReview,所以团队进行了一次简单培训,简单说了下Review Board的安装和使用,不过是基于windows和eclipse的,用的是一个eclipse的插件tao-reviewboard,安装和配置都比较简单。因为移到linux和idea的开发环境已经好久了,所以我只能自己折腾了。

google了下,发现有基于idea的reviewboard插件,装上试了下,跟tao-reviewboard比起来,可以说是非常简陋,而且提交review请求的时候,报了一堆的错误。以为是自己的配置有问题,于是看着Review Board的文档,把配置文件~/.reviewboardrc重新检查了下,文件的内容如下

.reviewboardrc
1
2
3
4
5
REVIEWBOARD_URL = "http://reviewboard.xxx.com/"
USERNAME = "xxx"
PASSWORD = "xxx"
HTTP_USERNAME = "xxx"
HTTP_PASSWORD = "xxx"

配置完之后,继续报错,报的是

1
java.net.ProtocolException: Server redirected too many  times

google无结果,简单看了源码,还得了解idea的插件开发的一些知识,暂时没功夫,于是放弃。

试着装了下eclipse,发现tao-reviewboard是依赖于subclipse的,这个东西以前就装过,没有成功,这回试了下,果然还是不成功。同事说需要编译相应版本的svn客户端,觉得单独为个CodeReview再把eclipse折腾半天,不值得,于是放弃。

又去啃Review Board的文档了,Review Board提供了一个命令行的工具post-review,这个工具是python写的,看到是python写的就很有亲切感。安装很简单,执行下面的命令(之前已经有了python的环境,没有的可能还得提前装python和easy_install)

1
easy_install -U RBTools

提交review请求的命令也很简单

1
post-review -p --target-groups=xxx --target-people=xxx --summary=xxx --description=xxx files

执行上面的命令,报了206的错误。提示

The repository path specified is not in the list of known repositories

于是,加上-d参数,打印debug信息,觉得可能repository没加到Review Board Server里面,但是其他人都可以提交review,于是试着进到代码目录,在代码当前目录提交review请求,没想到竟然pass了这个问题,不过马上又报了一个207的错误。提示

The repository path specified is not in the list of known repositories

这个问题,试了好多办法都不行,于是只能调试源码,下了源码,一路输出debug信息,怀疑是diff文件的问题。看了下其他同事提交的diff文件,他们的diff文件里开头,代码文件的路径是相对路径,而我生成的竟然是绝对路径,而造成这一问题的原因就是svn.py下面这一句,把这句注释掉,替换掉RBTools-xxx.egg这个文件里面的相应文件

svn.py
1
diff = self.convert_to_absolute_paths(diff, repository_info)

试了下,终于OK了。post-review的那些参数太繁琐而且又长,以后有时间写个程序自己定制下。

Protocol Buffer入门

| Comments

是啥

Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages. You can even update your data structure without breaking deployed programs that are compiled against the “old” format.

上面是Google官方文档对Protocol Buffer(简称PB)的定义,简单翻译下。

  • 序列化结构数据的自动化机制,类似XML,但是更小,更快,更简单
  • 你只需定义你的数据结构,就可以用PB生成的源码去操作结构数据
  • 支持多种语言
  • 数据结构向后兼容

性能

PB的性能,数据大小口碑一直不错,好多公司也都在用。这里有些人们做的Benchmark数据。相对与未压缩的xml或者json来说是有不小的优势的,官方文档的说法是比xml要小3-10倍,快20-100倍。

使用

在使用PB之前要先定义你的数据结构,PB有一套数据结构定义的语法,你需要使用这套语法来定义你的结构。定义结构的代码一般会写到一个以.proto为后缀的文件中(文件后缀好像没有强制)

语法

Message

Message类似于Java中的对象,C中的struct,是PB中数据封装的基本单元,一般我们在PB中定义的数据结构就是一个个的Message了。

message.proto
1
2
3
4
5
message SearchRequest {
  required string query = 1;
  optional int32 page_number = 2;
  optional int32 result_per_page = 3;
}

上面的代码就定义了一个message

  • message由field组成,类似对象中的成员变量
  • 要定义field的类型,类型包括scalar(待会儿说), enumeration,以及message(支持嵌套哦)
  • 要定义field的规则,规则有required(值不能为空),optional(值可以为空),repeated(重复,用来声明列表)
  • 每个field要分配Tag,tag就是每行最后的那写数字,占位符的意思。数据编码后用tag来对应每个field。tag从1开始,而且是动态编码的,1-16占一个字节,16-2047两个字节,最大是229 - 1

Scalar

下图列出了PB所支持的Scalar的类型

"PB Scalar"

enum

PB支持枚举类型,对应Java或者C中的枚举类型,下面是一个枚举的例子

1
2
3
4
5
 enum Colour {
    RED = 0;
    BLACK = 1;
    BLUE = 2;
  }

使用

使用之前需要先安装protoc,protoc用于生成对应语言的代码。除了protoc,还需要编译对应语言的库,并且添加到你的依赖路径中。安装wiki

数据结构

首先用pb的语法定义数据结构。

tutorial.proto
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
package me.ellios.tutorial;

option java_package = "me.ellios.tutorial";
option java_outer_classname = "AddressBookProtos";

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;
  repeated PhoneNumber phone = 4;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }
}

message AddressBook {
  repeated Person person = 1;
}
  • package:声明生成的代码所在的包,貌似是对应cpp的命名空间
  • option java_package:针对java,声明java代码的包
  • option java_outer_classname:针对java,声明java代码的类

java的例子

执行下面的命令,会生成AddressBookProtos.java文件,将它拷到你的工作目录中

1
protoc --java_out=. tutorial.proto

下面是一段简单的示例代码,演示怎么创建对象,以及对象的序列化和反序列化。

AddressBookExample.java
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package me.ellios.tutorial;

import me.ellios.tutorial.AddressBookProtos.Person;
import me.ellios.tutorial.AddressBookProtos.AddressBook;
import java.io.*;
import java.util.Random;

public class AddressBookExample {

    public static void main(String[] args) throws Exception {
        //要通过builder来构建对象,
        AddressBook.Builder addressBuiler = AddressBook.newBuilder();
        Random random = new Random();

        for(int i=0; i<10; i++){
            Person.Builder personBuilder = Person.newBuilder();
            personBuilder.setName("ellios" + i); //设置name,不能为空
            personBuilder.setId(i); //设置id,也不能为空
            personBuilder.setEmail(""); //email可以为空
            //创建phone
            Person.PhoneNumber.Builder phoneBuilder = Person.PhoneNumber.newBuilder();
            phoneBuilder.setNumber("132222222222");
            phoneBuilder.setType(Person.PhoneType.values()[random.nextInt(3)]);
            //将phone添加到person中,              .
            personBuilder.addPhone(phoneBuilder.build());
            //将person添加到地址簿的列表中
            addressBuiler.addPerson(personBuilder);
        }
        AddressBook addressBook = addressBuiler.build();

        System.out.println("addressBook : "  + addressBook);

        byte[] serializeBytes = addressBook.toByteArray();

        AddressBook addressBook2 = AddressBook.parseFrom(serializeBytes);
        System.out.println("addressBook2 equal to addressBook. result is " + addressBook.equals(addressBook2));

        addressBook.writeTo(new FileOutputStream("addressBook"));

        AddressBook.Builder addressBuilder3 = AddressBook.newBuilder();
        addressBuilder3.mergeFrom(new FileInputStream("addressBook"));
        System.out.println("addressBook3 equal to addressBook. result is " + addressBook.equals(addressBuilder3.build()));
    }
}

python示例

执行下面的命令,生成tutorial_pb2.py文件,将它拷到工作目录中。

1
protoc --python_out=. tutorial.proto

下面是简单的示例代码

address_book_example.py
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
31
32
#! /usr/bin/python

import tutorial_pb2

address_book = tutorial_pb2.AddressBook()

for i in range(10):
    person = address_book.person.add()
    person.id = i
    person.name = 'ellios%d'%i
    person.email = ''
    phone_number = person.phone.add()
    phone_number.number = "13233333333"
    if i % 3 == 0:
        phone_number.type = tutorial_pb2.Person.MOBILE
    elif i % 3 == 1:
        phone_number.type = tutorial_pb2.Person.HOME
    elif i % 3 == 2:
        phone_number.type = tutorial_pb2.Person.WORK

print 'address_book : ', address_book

f = open("address_book", "wb")
f.write(address_book.SerializeToString())
f.close()

address_book2 = tutorial_pb2.AddressBook()
f = open("address_book", "rb")
address_book2.ParseFromString(f.read())
f.close()

print "address_book same with address_book2. the result is %d"%(address_book == address_book2)

cpp示例

执行下面的命令,生成tutorial.pb.h和tutorial.pb.cc文件,将这两个文件拷到工作目录

1
protoc --cpp_out=. tutorial.proto

下面是一段示例代码

address_book_example.cc
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
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <fstream>
#include <string>
#include "tutorial.pb.h"
using namespace std;

int main(int argc, char* argv[]) {
  // Verify that the version of the library that we linked against is
  // compatible with the version of the headers we compiled against.
  GOOGLE_PROTOBUF_VERIFY_VERSION;

  tutorial::AddressBook address_book;
  for (int i = 0; i < 10; i++) {
      tutorial::Person* person = address_book.add_person();
      person->set_id(i);
      person->set_name("ellios");
      person->set_email("");
      tutorial::Person::PhoneNumber* phone_number = person->add_phone();
      phone_number->set_number("13333333333");
      phone_number->set_type(tutorial::Person::HOME);
  }
  // Write the new address book back to disk.
  fstream output("address_book", ios::out | ios::trunc | ios::binary);
  if (!address_book.SerializeToOstream(&output)) {
      cerr << "Failed to write address book." << endl;
      return -1;
  }

  cout << "address_book : " << address_book.person_size() << endl;

  tutorial::AddressBook address_book2;
  fstream input("address_book", ios::in | ios::binary);
  address_book2.ParseFromIstream(&input);

  // Optional:  Delete all global objects allocated by libprotobuf.
  google::protobuf::ShutdownProtobufLibrary();

  return 0;
}

参考资料

JDK7-TransferQueue

| Comments

继续研究jdk7对concurrency的增强。这次研究下TransferQueue,在jdk7里的实现是LinkedTransferQueue。它实现了BlockingQueue的接口,并且提供了类似SynchronizeQueue的功能。由于采用了CAS的方式对线程进行同步,减少了锁的开销,性能相对与其他的队列实现有了很大的提升。其内部的实现是一个FiFo的Dual Quque。很多开源的项目,在jdk7之前,就早早的用上了这个东东,比如nettybonecpxmemcached

TransferQueue的基本使用和BlockingQueue差不多,有点特殊的就是它的transfer接口。

参考资料

JDK排序算法

| Comments

中午吃饭的时候和同事讨论了下JDK的排序算法的实现,同事坚持认为是简单的插入排序,觉得JDK应该不会这么弱,看了下JDK的源码,发现JDK针对排序是做了大量的优化的。

JDK对排序的实现在Arrays的sort方法里,Arrays里面有大量的重载的sort方法。而JDK7和JDK6的排序算法又有一些不同。

JDK6排序实现

JDK6对的排序实现针对基本类型和对象又有不同。

  1. 对于基本类型,由于不要求排序是稳定的,因此使用了平均效率最好的排序算法——快速排序。为了避免最差情况的出现,JDK6对排序算法做了些优化。
针对基本类型的快速排序
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
 * 针对整形的排序实现
 * @param x 待排序的数据
 * @param off 起始位置
 * @param len 要排序的数据元素的个数
 */
private static void sort1(int x[], int off, int len) {
    //在小规模(size<7)数组中,直接插入排序的效率要比快速排序高。
    if (len < 7) {
        for (int i=off; i<len+off; i++)
            for (int j=i; j>off && x[j-1]>x[j]; j--)
                swap(x, j, j-1);
        return;
    }

    //选择划分元素,即枢轴
    //如果是小规模数组(size=7),直接取中间元素作为枢轴
    //如果是中等规模数组(7<size<=40),则在数组首、中、尾三个位置上的数中取中间大小的数作为枢轴
    //如果是大规模数组(size>40),则在9个指定的数中取一个伪中数(中间大小的数s)
    int m = off + (len >> 1);
    if (len > 7) {
        int l = off;
        int n = off + len - 1;
        if (len > 40) {        // Big arrays, pseudomedian of 9
            int s = len/8;
            l = med3(x, l,     l+s, l+2*s);
            m = med3(x, m-s,   m,   m+s);
            n = med3(x, n-2*s, n-s, n);
        }
        m = med3(x, l, m, n); // Mid-size, med of 3
    }
    int v = x[m];

    // 针对相同的元素做优化, 形成 v* (<v)* (>v)* v* 的数组
    int a = off, b = a, c = off + len - 1, d = c;
    while(true) {
        while (b <= c && x[b] <= v) {
            if (x[b] == v)
                swap(x, a++, b);
            b++;
        }
        while (c >= b && x[c] >= v) {
            if (x[c] == v)
                swap(x, c, d--);
            c--;
        }
        if (b > c)
            break;
        swap(x, b++, c--);
    }

    // 将所有相同的枢轴都移到中间,形成(<v)* v* (>v)*
    int s, n = off + len;
    s = Math.min(a-off, b-a  );  vecswap(x, off, b-s, s);
    s = Math.min(d-c,   n-d-1);  vecswap(x, b,   n-s, s);

    // 递归排序子数组
    if ((s = b-a) > 1)
        sort1(x, off, s);
    if ((s = d-c) > 1)
        sort1(x, n-s, s);
}
  1. 针对对象数组,为了保证排序是稳定的,JDK6采用了归并排序,同样也做了一些优化。
针对对象数据的归并排序
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
 * 归并排序对象数组
 * @param src 原待排数组
 * @param dest 目的待排数组
 * @param low 待排数组的下界位置
 * @param high 待排数组的上界位置
 * @param off 从数组的第off个元素开始排序
 */
private static void mergeSort(Object[] src,
                              Object[] dest,
                              int low,
                              int high,
                              int off) {
    int length = high - low;

    //在小规模(size<7)数组中,使用归并排序。
    if (length < INSERTIONSORT_THRESHOLD) {
        for (int i=low; i<high; i++)
            for (int j=i; j>low &&
                    ((Comparable) dest[j-1]).compareTo(dest[j])>0; j--)
                swap(dest, j, j-1);
        return;
    }

    //将src数组划分为两半,并分别递归做归并,
    //在递归调用mergeSort时将desc和src的位置做了互换。
    int destLow  = low;
    int destHigh = high;
    low  += off;
    high += off;
    int mid = (low + high) >>> 1;
    mergeSort(dest, src, low, mid, -off);
    mergeSort(dest, src, mid, high, -off);

    //如果低子列表中的最高元素小于高子列表中的最低元素,则忽略合并
    //如果需要归并的两端low~(middle-1),middle~high已经有序,即src[mid-1]==src[mid]。
    //那么只需要将src的low~high赋值对应的dest即可,无需再归并。
    if (((Comparable)src[mid-1]).compareTo(src[mid]) <= 0) {
        System.arraycopy(src, low, dest, destLow, length);
        return;
    }

    //将src的两个部分做归并操作,并赋值给dest
    for(int i = destLow, p = low, q = mid; i < destHigh; i++) {
        if (q >= high || p < mid && ((Comparable)src[p]).compareTo(src[q])<=0)
            dest[i] = src[p++];
        else
            dest[i] = src[q++];
    }
}

JDK7的排序实现

  1. JDK7针对基本类型使用了Dual Pivot Quicksort,这种快速排序算法,相对于传统的单Pivot的快速排序效率要更好。

  2. JDK7针对对象数组采用了TimSort, 这也是python的排序算法。

参考资料

JDK7 Concurrency Phaser

| Comments

继续研究Jdk7的新特性,这次看看jdk7对Concurrency的增强。concurrency这次新增了3个新元素。

  1. Phaser,类似于CountDownLatch和CyclicBarrier
  2. TransferQueue, 比SynchronousQueue更加高效的队列
  3. Fork-Join, 类似与MapReduce的东东

体验下Phaser, 这个东东和CountDownLatch,CyclicBarrier的功能比较类似,但是要更加灵活,提供了更多的控制。

先来回顾下CountDownLatch, CyclicBarrier。下面这段程序每次同时执行一组任务,然后当这一组任务执行完后,再执行下一组。用CyclicBarrier对每组的各个任务的执行时机进行控制,用CountDownLatch对各组任务的执行时机进行控制。

复习完毕,下面来把Phaser好好弄清楚。这个东东究竟是个啥,为啥能比CountDownLatch,CyclicBarrier更灵活呢。Phaser类的注释里对Phaser做了充分的描述,我们这里简单整理下。

Phaser是一个可重用的同步Barrier,类似与CountDownLatch和CyclicBarrier,但是要更加灵活

  1. Registration. Phaser支持通过register()和bulkRegister(int parties)方法来动态调整注册任务的数量,此外也支持通过其构造函数进行指定初始数量。在适当的时机,Phaser支持减少注册任务的数量,例如 arriveAndDeregister()。单个Phaser实例允许的注册任务数的上限是65535。

  2. Arrival. 正如Phaser类的名字所暗示,每个Phaser实例都会维护一个phase number,初始值为0。每当所有注册的任务都到达Phaser时,phase number累加,并在超过Integer.MAX_VALUE后清零。arrive()和arriveAndDeregister()方法用于记录到 达,arriveAndAwaitAdvance()方法用于记录到达,并且等待其它未到达的任务。

  3. Termination.Phaser支持终止。Phaser终止之后,调用register()和bulkRegister(int parties)方法没有任何效果,arriveAndAwaitAdvance()方法也会立即返回。触发终止的时机是在protected boolean onAdvance(int phase, int registeredParties)方法返回时,如果该方法返回true,那么Phaser会被终止。默认实现是在注册任务数为0时返回true(即 return registeredParties == 0;)。此外,forceTermination()方法用于强制终止,isTerminated()方法用于判断是否已经终止。

  4. Tiering. Phaser支持层次结构,即通过构造函数Phaser(Phaser parent)和Phaser(Phaser parent, int parties)构造一个树形结构。这有助于减轻因在单个的Phaser上注册过多的任务而导致的竞争,从而提升吞吐量,代价是增加单个操作的开销。

描述已经好清楚了,下面我们用Phaser实现上面类似功能的程序。

参考资料

Coffeescript语法篇

| Comments

上一篇简单分析了下CoffeeScript的源码,这篇开始介绍CoffeeScript的语法。CoffeeScript的语法相比JavaScript要清爽好多,如果有Python,Ruby的经验的话,基本上半天就差不多了。CoffeeScript最终还是会被编译为JavaScript,所以基本的数据类型和JavaScript是一样,学习的时候和编译的JavaScript对应起来会更容易理解。

Examples

先睹为快,给个二分查找的例子。

二分查找例子 binary_search.coffee
1
2
3
4
5
6
7
8
9
10
11
12
index = (list, target) ->
  [low, high] = [0, list.length]
  while low < high
    mid = (low + high) >> 1
    val = list[mid]
    return mid if val is target
    if val < target then low = mid + 1 else high = mid
  return -1

console.log 2 is index [10, 20, 30, 40, 50], 30
console.log 4 is index [-97, 35, 67, 88, 1200], 1200
console.log 0 is index [0, 45, 70], 0

编译后的js代码如下

编译后的JS binary_search.js
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
(function() {
  var index;

  index = function(list, target) {
    var high, low, mid, val, _ref;
    _ref = [0, list.length], low = _ref[0], high = _ref[1];
    while (low < high) {
      mid = (low + high) >> 1;
      val = list[mid];
      if (val === target) {
        return mid;
      }
      if (val < target) {
        low = mid + 1;
      } else {
        high = mid;
      }
    }
    return -1;
  };

  console.log(2 === index([10, 20, 30, 40, 50], 30));

  console.log(4 === index([-97, 35, 67, 88, 1200], 1200));

  console.log(0 === index([0, 45, 70], 0));

}).call(this);

后面会介绍CoffeeScript的各个语法点。

Functions

coffee函数
1
2
3
4
5
6
7
8
9
10
11
12
13
#匿名函数
console.log do -> "Hello World"
#非匿名函数,函数隐式返回最后一个表达式的值
square = (x) ->
  2 * x
  x * x
#调用函数
cube   = (x) -> square(x) * x
#函数可以有默认参数
fill = (container, liquid = "coffee") ->
  console.log "Filling the #{container} with #{liquid}..."
#函数绑定
setName = (name) -> @name = name;
编译后
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
(function() {
  var cube, fill, setName, square;

  console.log((function() {
    return "Hello World";
  })());

  square = function(x) {
    2 * x;
    return x * x;
  };

  cube = function(x) {
    return square(x) * x;
  };

  fill = function(container, liquid) {
    if (liquid == null) {
      liquid = "coffee";
    }
    return console.log("Filling the " + container + " with " + liquid + "...");
  };

  setName = function(name) {
    return this.name = name;
  };

}).call(this);

Coffeescript源码篇

| Comments

最近为了凑运费,买了本《深入浅出CoffeeScript》,本来只打算随便翻翻的,不过看了以后发现CoffeeScript还是满有意思的,于是决定好好研究下,这里分享下自己的学习过程。

Install

装之前需要提前装好node和npm(具体怎么装google之吧),node和npm装好后,执行下面的命令,就可以开始体验CoffeeScript了。

1
npm install -g coffee-script

Compile

简单的体验下后,对于它把那么灵活的语法转化为比较工整的JS语法有点小好奇,于是把它的源码clone出来,简单的研究了下。

先试着编译它的源码,编译的操作过程如下:

1
2
3
git clone git://github.com/jashkenas/coffee-script.git
cd coffee-script
bin/cake build:full

OK,编译很迅速,大家可以对源码做各种折腾了。


Jison

CoffeeScript的核心是一个编译器,它把符合CoffeeScript语法规则的文件转化为JS语法。而CoffeeScript又是利用Jison实现词法和语法的解析。CoffeeScript将语法规则都写到grammar.js文件里,然后利用Jison生成符合该语法规则的编译器。

只要写个简单的语法规则,就可以实现一个编译器,这东西太高级了,于是开始折腾Jison。它的网站上有很多例子,其中有一个生成计算程序的例子,我把它稍微缩减下,让+做-,-做+。

simple_cal.jison
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
31
32
33
34
/* lexical grammar */
%lex

%%
\s+                   /* skip whitespace */
[0-9]+("."[0-9]+)?\b  return 'NUMBER';
"-"                   return '-';
"+"                   return '+';
<<EOF>>               return 'EOF';

/lex

/* operator associations and precedence */

%left '+' '-'

%start expressions

%% /* language grammar */

expressions
    : e EOF
        {typeof console !== 'undefined' ? console.log($1) : print($1);
         return $1;}
    ;

e
    : e '+' e
        {$$ = $1-$3;}
    | e '-' e
        {$$ = $1+$3;}
    | NUMBER
        {$$ = Number(yytext);}
    ;

体验下效果把,当执行7-3时,返回的却是10。

1
2
>> jison simple_cal.jison && echo "7-3" > 2 && node simple_cal.js 2
>> 10

再深入的原理,就要涉及编译原理的内容,那个东西偶学得可烂的,以后有时间再研究下把。

参考资料