Shared posts

27 Nov 10:39

聊聊多线程程序的load balance

by 七伤

说起load balance,一般比较容易想到的是大型服务在多个replica之间的load balance、和kernal的load balance。前者一般只是在流量入口做一下流量分配,逻辑相对简单;而后者则比较复杂,需要不断发现正在运行的各个进程之间的imbalance,然后通过将进程在CPU之间进行迁移,使得各个CPU都被充分利用起来。

而本文想要讨论的load balance有别于以上两种,它是多线程(多进程)server程序内部,各个worker线程(进程)之间的load balance。
考虑一种常用的server模型:一个receiver线程负责接收请求,后面有一个线程池装了一堆worker线程,收到的请求被分派给这些worker进行处理。receiver与worker之间通过pthread_cond+request_queue来进行通信。一般的做法是:receiver将收到的请求放入queue,然后signal一下cond,就OK了。具体哪个worker会被唤醒,那是kernel的事情(实际上kernel会遵循先来后到原则,唤醒先进入等待的进程,参阅《linux futex浅析》)。通常情况下这样做就足够了,receiver唤醒worker不需要涉及load balance的逻辑。但是有时候我们还是可以做一些load balance的工作,来提高server的性能。

kernel load balance概述

由于这里的load balance跟kernel的load balance息息相关,所以我们有必要先看看kernel的load balance都做了些什么。详细的内容请参阅《linux内核SMP负载均衡浅析》,这里只做一些简要的概括。

说白了,kernel的load balance就做一件事情: 让系统中RUNNING状态的进程尽可能的被分摊,在每一个调度域上看都是balance的  。怎么理解呢?现在CPU的结构一般有:物理CPU、core、超线程、这么几个层次。”在每一个调度域上看都balance”可以理解为在每一个层次上都balance:每个物理CPU上的总load相当、每个core上的总load相当、每个超线程上的load也相当。

我们在系统中看到的”CPU”都是最底层的超线程这个层次,我们可能会直观的认为把RUNNING状态的进程分摊到每一个”CPU”上就行了,但是实际上kernel的load balance还有更高的要求。假设我们的机器有2个物理CPU、每个物理CPU有2个core、每个core有2个超线程,共8个”CPU”。如果现在有8个RUNNING状态的进程(假设优先级都相同),每个”CPU”各分摊一个进程,那么自然就是balance的。但是如果现在只有4个RUNNING状态的进程(假设优先级都相同),真正的balance并不仅仅是每个进程各自落到一个”CPU”上就行了,而是进一步要求每个物理CPU上跑两个进程、每个core上跑一个进程。
为什么要有这样的强约束呢?因为尽管各个”CPU”逻辑上是独立的(不存在主从关系之类),但它们并非孤立存在。相同物理CPU下的”CPU”会共享cache、相同core下的”CPU”会共享计算资源(所谓的超线程也就是一套流水线跑两个线程)。而共享也就意味着争抢。所以,在RUNNING状态的进程并非正好均摊给每一个”CPU”的情况下,需要考虑更高层次的CPU是否被均摊,以避免cache和CPU流水线的争抢(当然,除了性能,这也体现了kernel的公平性)。

最后再多提一点,kernel的load balance是异步的。为避免占用过多资源,kernel肯定不可能实时监控各个”CPU”的情况,然后面对变化实时的做出反应(当然,实时进程除外,但这不在我们讨论范围内)。

server的load balance考虑

有了kernel的load balance作为铺垫,看看我们server上的receiver线程能做些什么吧。

首先是worker线程的数量问题。如果worker数量过多会发生什么情况?还是假设我们的机器有上述的8个”CPU”,假设我们开了80个worker,再假设这80个线程被平均分派到每一个”CPU”上,等待处理任务。当一堆请求陆续到来的时候,由于我们的receiver没有任何load balance的策略,被唤醒的worker出现在哪个”CPU”上可以说是随机的。你想想,”同时”到来的8个请求正好落到8个不同”CPU”上的概率是多少?是:(70*60*50*40*30*20*10)/(79*78*77*76*75*74*73)=0.34%。也就是说几乎肯定会出现某些”CPU”要处理多个请求、某些”CPU”却闲着没事干的情况,系统的性能可想而知。而等到后知后觉的kernel load balance将这些请求balance到每一个”CPU”上时,可能请求已经处理得差不多了,等到下一批请求到来时,load又还是凌乱的。因为刚刚已经balance好的那些worker线程又被放回到了cond等待队列的尾部,而优先响应新请求的则是那些位于队列头部的未曾被balance过的worker。
那么会不会经历几轮请求之后就能达到balance了呢?如果请求真的是一轮一轮的过来,并且每个请求的处理时间完全相同,那么有可能会达到balance,但是实际情况肯定相差甚远。
解决办法是什么呢?将cond先进先出的队列式等待逻辑改为后进先出的栈式逻辑,或许可以解决问题,但是更好的办法应该是限制worker的数目等于或者略小于”CPU”数目,这样很自然的就balance了。

第二个问题,既然我们承认kernel在各个调度域上的load balance的有意义的,我们server中的receiver线程是不是也可以通过类似的办法来获得收益呢?现在我们吸取了之前的教训,只开了8个worker线程。依靠kernel load balance的作用,这8个线程基本会固定在每一个”CPU”上。假设现在一下子来了4个请求,它们会落到4个不同的”CPU”上,如果运气好,这4个”CPU”分别属于不同的core,那么处理请求的过程就不会涉及CPU资源的争抢;反之,可能形成2个core非常忙、2个core闲着的局面。
要解决这个问题需要做到两点,继续以我们之前的server程序为例。首先,receiver线程要知道各个worker线程都落在哪一个”CPU”上;然后在分派任务时还需要有balance的眼光。要做到第一点,最好是借助sched_affinity功能将线程固定在某个”CPU”上,避免kernel load balance把问题搞复杂了。既然前面我们已经得出了工作线程数等于或略小于CPU数的结论,现在每个线程固定在一个CPU上就是可行的。第二点,我们需要在现有pthread_cond的基础上做一些改进,给进入等待状态的worker线程赋一个优先级,比如每个core的第一个超线程作为第一优先级,第二个超线程为第二优先级。那么在cond唤醒工作线程的时候,我们就可以尽量让worker线程不落到同一个core上。实现上可以利用futex的bitset系列功能,通过bitset来标识优先级,以便在唤醒指定的worker线程。(参阅《linux futex浅析》。)

例子

好了,纸上谈兵讲了这么多,得来点实际的例子验证一下。为了简单,就不写什么server程序了,只需要一个生产者线程和若干消费者线程。生产者线程生成一些任务,通过cond+queue将其传递给消费者线程。为了观察在不同任务负载下的程序表现,我们需要控制任务负载。消费者线程在完成任务后通过另一组cond+queue把任务应答给生产者线程,于是生产者就知道当前有多少个任务正在处理中,以便控制生产新任务的节奏。最后,我们通过观察在不同条件下完成一批任务的时间来体会程序的性能。

这里面比较关键的是任务本身的处理逻辑,既然我们讨论的是CPU的负载,任务肯定应该是CPU密集型的任务。然后,单个任务的处理时间不宜太短,否则可能调度过程会成为程序的瓶颈,体现不出CPU的负载问题;另一方面,单个任务的处理时间也不宜太长,否则后知后觉的kernel load balance也能解决问题,体现不出我们主动做load balance的好处(比如任务处理时间是10秒,kernel load balance花费几十毫秒来解决balance问题其实也无伤大雅)。

代码贴在文章最后,编译出来的bin文件是这样的:

$g++ cond.cpp -pthread -O2
$./a.out
usage: ./a.out -j job_kind=shm|calc [-t thread_count=1] 
[-o job_load=1] [-c job_count=10] [-a affinity=0] [-l] 
[-f filename="./TEST" -n filelength=128M]
  • 代码里面准备了两种任务逻辑,”-j shm”是mmap一个文件,然后读取上面的数据做一些运算(文件及其长度由-f和-n参数来限定);”-j calc”是做一些算术运算;
  • “-t”参数指定工作线程的线程数;
  • “-o”指定任务负载;
  • “-c”指定单个线程处理任务的个数;
  • “-a”指定是否设置sched_affinity,并且指明跳几个”CPU”放一个worker线程。比如”-a 1″表示把worker线程顺序固定在1、2、3、……号”CPU”上,而”-a 2″表示固定在2、4、6、……号”CPU”上,以此类推。需要注意的是,邻近的”CPU”号并不表示”CPU”在物理上是邻近的,比如在我测试用的机器上,共24个”CPU”,0~11号是每个core的第一个超线程、12~23是第二个超线程。这个细节需要读/proc/cpuinfo来确定。
  • “-l”参数指定启用我们增强版的分级cond,启用的话会将0~11号worker作为第一优先级,12~23作为第二优先级(当然,需要配合”-a”参数才有实际意义,否则也不确定这些worker都落在哪些”CPU”上);

首先来看worker线程过多所带来的问题(以下case各运行5次取时间最小值)。

case-1,启240个worker线程,24个任务负载:
$./a.out -j calc -t 240 -o 24
total cost: 23790
$./a.out -j shm -t 240 -o 24
total cost: 16827

case-2,启24个worker线程,24个任务负载:
$./a.out -j calc -t 24 -o 24
total cost: 23210
$./a.out -j shm -t 24 -o 24
total cost: 16121

case-2效果明显要好略一些。并且在运行过程中如果用top观察的话,你会发现case-1只能压到2200%左右的CPU,而case-2几乎能达到2400%。

在case-1的基础上,如果禁止kernel load balance会怎样?加affinity试试看:

case-3,启240个worker线程,24个任务负载,加affinity:
$./a.out -j calc -t 240 -o 24 -a 1
total cost: 27170
$./a.out -j shm -t 240 -o 24 -a 1
total cost: 15351

calc任务比较符合预期,没有kernel load balance的情况下,性能继续下降。
而shm任务则让人大跌眼镜,性能居然提升了!其实这个任务除了CPU之外还很依赖于内存,因为所有任务都工作在同一个文件的mmap上,”CPU”挨得近反而更能发挥内存cache。(可见在这种情况下,kernel load balance其实是帮了倒忙。)
那么,我们将工作线程再调回24,是不是应该更理想?

case-3'
$./a.out -j shm -t 24 -o 24 -a 1
total cost: 15133

再来看第二个问题,worker线程站位不均所带来的影响。

case-4,启24个worker线程,12个任务负载:
$./a.out -j calc -t 24 -o 12
total cost: 14686
$./a.out -j shm -t 24 -o 12
total cost: 13265

case-5,启24个worker线程,12个任务负载,加affinity,启用分级cond:
$./a.out -j calc -t 24 -o 12 -a 1 -l
total cost: 12206
$./a.out -j shm -t 24 -o 12 -a 1 -l
total cost: 12376

效果还是不错的。改一下”-a”参数,让同一个core的两个超线程都分在同一优先级呢?

case-5'
$./a.out -j calc -t 24 -o 12 -a 2 -l
total cost: 23510
$./a.out -j shm -t 24 -o 12 -a 2 -l
total cost: 15063

由于争抢CPU资源,calc任务性能变得很差,几乎减半。而shm任务由于cache复用所带来的好处,情况还好(比case-3还略好一些)。

这里的任务只是举了calc和shm两个例子,实际情况可能是很复杂的。尽管load balance的问题肯定存在,但是任务会因共享cache而得利、还是因争抢cache而失利?争抢CPU流水线又会造成多大的损失?这些都只能具体问题具体分析。kernel的load balance将负载尽量均摊到离得远的”CPU”上,大多数情况下没有问题。不过我们也看到shm任务中cache共享的收益还是很大的,如果例子更极端一点,肯定会出现承受负载的CPU离得越近,反而效果越好的情况。
另一方面,争抢CPU流水线会有多大损失,也可以简单的分析一下。超线程相当于两个线程共用一套CPU流水线,如果单个线程的代码上下文依赖很严重,指令基本上只能串行工作,无法充分利用流水线,那么流水线的空余能力就可以留给第二个线程使用。反之如果一个线程就能把流水线填满,硬塞两个线程进来肯定就只能有50%的性能(上述calc的例子就差不多是这样)。
为了说明这个问题,我们给calc任务加了一个SERIAL_CALC的宏开关,让它的运算逻辑变成上下文强依赖。然后重跑case-5中的两个命令,我们会看到其实在这种情况下承受负载的CPU离得近一些似乎也问题不大:

case-6,采用SERIAL_CALC运算逻辑,重跑case-5中的calc任务
$g++ cond.cpp -pthread -O2 -DSERIAL_CALC
$./a.out -j calc -t 24 -o 12 -a 1 -l
total cost: 51269
$./a.out -j calc -t 24 -o 12 -a 2 -l
total cost: 56753

最后是代码,有兴趣你还可以尝试更多的case,have fun!

#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sched.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <linux/futex.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <math.h>
#include <sys/syscall.h>

#define CPUS    24
#define FUTEX_WAIT_BITSET   9
#define FUTEX_WAKE_BITSET   10

struct Job
{
    long _input;
    long _output;
};

class JobRunner
{
public:
    virtual void run(Job* job) = 0;
};

class ShmJobRunner : public JobRunner
{
public:
    ShmJobRunner(const char* filepath, size_t length)
            : _length(length) {
        int fd = open(filepath, O_RDONLY);
        _base = (long*)mmap(NULL, _length*sizeof(long),
                PROT_READ, MAP_SHARED|MAP_POPULATE, fd, 0);
        if (_base == MAP_FAILED) {
            printf("FATAL: mmap %s(%lu) failed!\n",
                    filepath, _length*sizeof(long));
            abort();
        }
        close(fd);
    }
    virtual void run(Job* job) {
        long i = job->_input % _length;
        long j = i + _length - 1;
        const int step = 4;
        while (i + step < j) {
            if (_base[i%_length] * _base[j%_length] > 0) {
                j -= step;
            }
            else {
                i += step;
            }
        }
        job->_output = _base[i%_length];
    }
private:
    const long* _base;
    size_t _length;
};

class CalcJobRunner : public JobRunner
{
public:
    virtual void run(Job* job) {
        long v1 = 1;
        long v2 = 1;
        long v3 = 1;
        for (int i = 0; i < job->_input; i++) {
#ifndef SERIAL_CALC
            v1 += v2 + v3;
            v2 *= 3;
            v3 *= 5;
#else
            v1 += v2 + v3;
            v2 = v1 * 5 + v2 * v3;
            v3 = v1 * 3 + v1 * v2;
#endif
        }
        job->_output = v1;
    }
};

class JobRunnerCreator
{
public:
    static JobRunner* create(const char* name,
            const char* filepath, size_t filelength) {
        if (strcmp(name, "shm") == 0) {
            printf("share memory job\n");
            return new ShmJobRunner(filepath, filelength);
        }
        else if (strcmp(name, "calc") == 0) {
            printf("caculation job\n");
            return new CalcJobRunner();
        }
        printf("unknown job '%s'\n", name);
        return NULL;
    }
};

class Cond
{
public:
    virtual void lock() = 0;
    virtual void unlock() = 0;
    virtual void wait(size_t) = 0;
    virtual void wake() = 0;
};

class NormalCond : public Cond
{
public:
    NormalCond() {
        pthread_mutex_init(&_mutex, NULL);
        pthread_cond_init(&_cond, NULL);
    }
    ~NormalCond() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }
    void lock() { pthread_mutex_lock(&_mutex); }
    void unlock() { pthread_mutex_unlock(&_mutex); }
    void wait(size_t) { pthread_cond_wait(&_cond, &_mutex); }
    void wake() { pthread_cond_signal(&_cond); }
private:
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

class LayeredCond : public Cond
{
public:
    LayeredCond(size_t layers = 1) : _value(0), _layers(layers) {
        pthread_mutex_init(&_mutex, NULL);
        if (_layers > sizeof(int)*8) {
            printf("FATAL: cannot support such layer %u (max %u)\n",
                    _layers, sizeof(int)*8);
            abort();
        }
        _waiters = new size_t[_layers];
        memset(_waiters, 0, sizeof(size_t)*_layers);
    }
    ~LayeredCond() {
        pthread_mutex_destroy(&_mutex);
        delete _waiters;
        _waiters = NULL;
    }
    void lock() {
        pthread_mutex_lock(&_mutex);
    }
    void unlock() {
        pthread_mutex_unlock(&_mutex);
    }
    void wait(size_t layer) {
        if (layer >= _layers) {
            printf("FATAL: layer overflow (%u/%u)\n", layer, _layers);
            abort();
        }
        _waiters[layer]++;
        while (_value == 0) {
            int value = _value;
            unlock();
            syscall(__NR_futex, &_value, FUTEX_WAIT_BITSET, value,
                    NULL, NULL, layer2mask(layer));
            lock();
        }
        _waiters[layer]--;
        _value--;
    }
    void wake() {
        int mask = ~0;
        lock();
        for (size_t i = 0; i < _layers; i++) {
            if (_waiters[i] > 0) {
                mask = layer2mask(i);
                break;
            }
        }
        _value++;
        unlock();
        syscall(__NR_futex, &_value, FUTEX_WAKE_BITSET, 1,
                NULL, NULL, mask);
    }
private:
    int layer2mask(size_t layer) {
        return 1 << layer;
    }
private:
    pthread_mutex_t _mutex;
    int _value;
    size_t* _waiters;
    size_t _layers;
};

template<class T>
class Stack
{
public:
    Stack(size_t size, size_t cond_layers = 0) : _size(size), _sp(0) {
        _buf = new T*[_size];
        _cond = (cond_layers > 0) ?
            (Cond*)new LayeredCond(cond_layers) : (Cond*)new NormalCond();
    }
    ~Stack() {
        delete []_buf;
        delete _cond;
    }
    T* pop(size_t layer = 0) {
        T* ret = NULL;
        _cond->lock();
        do {
            if (_sp > 0) {
                ret = _buf[--_sp];
            }
            else {
                _cond->wait(layer);
            }
        } while (ret == NULL);
        _cond->unlock();
        return ret;
    }
    void push(T* obj) {
        _cond->lock();
        if (_sp >= _size) {
            printf("FATAL: stack overflow\n");
            abort();
        }
        _buf[_sp++] = obj;
        _cond->unlock();
        _cond->wake();
    }
private:
    const size_t _size;
    size_t _sp;
    T** _buf;
    Cond* _cond;
};

inline struct timeval cost_begin()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv;
}

inline long cost_end(struct timeval &tv)
{
    struct timeval tv2;
    gettimeofday(&tv2, NULL);
    tv2.tv_sec -= tv.tv_sec;
    tv2.tv_usec -= tv.tv_usec;
    return tv2.tv_sec*1000+tv2.tv_usec/1000;
}

struct ThreadParam
{
    size_t layer;
    Stack<Job>* inputQ;
    Stack<Job>* outputQ;
    JobRunner* runner;
};

void* thread_func(void *data)
{
    size_t layer = ((ThreadParam*)data)->layer;
    Stack<Job>* inputQ = ((ThreadParam*)data)->inputQ;
    Stack<Job>* outputQ = ((ThreadParam*)data)->outputQ;
    JobRunner* runner = ((ThreadParam*)data)->runner;

    while (1) {
        Job* job = inputQ->pop(layer);
        runner->run(job);
        outputQ->push(job);
    }
    return NULL;
}

void force_cpu(pthread_t t, int n)
{
    cpu_set_t cpus;
    CPU_ZERO(&cpus);
    CPU_SET(n, &cpus);
    if (pthread_setaffinity_np(t, sizeof(cpus), &cpus) != 0) {
        printf("FATAL: force cpu %d failed: %s\n", n, strerror(errno));
        abort();
    }
}

void usage(const char* bin)
{
    printf("usage: %s -j job_kind=shm|calc "
        "[-t thread_count=1] [-o job_load=1] [-c job_count=10] "
        "[-a affinity=0] [-l] "
        "[-f filename=\"./TEST\" -n filelength=128M]\n", bin);
    abort();
}

int main(int argc, char* const* argv)
{
    int THREAD_COUNT = 1;
    int JOB_LOAD = 1;
    int JOB_COUNT = 10;
    int AFFINITY = 0;
    int LAYER = 0;
    char JOB_KIND[16] = "";
    char FILEPATH[1024] = "./TEST";
    size_t LENGTH = 128*1024*1024;
    for (int i = EOF;
        (i = getopt(argc, argv, "t:o:c:a:j:lf:n:")) != EOF;) {
        switch (i) {
        case 't': THREAD_COUNT = atoi(optarg); break;
        case 'o': JOB_LOAD = atoi(optarg); break;
        case 'c': JOB_COUNT = atoi(optarg); break;
        case 'a': AFFINITY = atoi(optarg); break;
        case 'l': LAYER = 2; break;
        case 'j': strncpy(JOB_KIND, optarg, sizeof(JOB_KIND)-1); break;
        case 'f': strncpy(FILEPATH, optarg, sizeof(FILEPATH)-1); break;
        case 'n': LENGTH = atoi(optarg); break;
        default: usage(argv[0]); break;
        }
    }
    JobRunner* runner = JobRunnerCreator::create(
            JOB_KIND, FILEPATH, LENGTH);
    if (!runner) {
        usage(argv[0]);
    }

    srand(0);
    Job jobs[JOB_LOAD];

#ifdef TEST_LOAD
    for (int i = 0; i < JOB_LOAD; i++) {
        jobs[i]._input = rand();
        struct timeval tv = cost_begin();
        runner->run(&jobs[i]);
        long cost = cost_end(tv);
        printf("job[%d](%ld)=(%ld) costs: %ld\n",
                i, jobs[i]._input, jobs[i]._output, cost);
    }
    delete runner;
    return 0;
#endif

    printf("use layer %d\n", LAYER);
    Stack<Job> inputQ(JOB_LOAD, LAYER);
    Stack<Job> outputQ(JOB_LOAD, LAYER);

    pthread_t t;
    ThreadParam param[THREAD_COUNT];

    printf("thread init: ");
    for (int i = 0; i < THREAD_COUNT; i++) {
        int cpu = AFFINITY ? (i/AFFINITY+i%AFFINITY*CPUS/2)%CPUS : -1;
        size_t layer = !!(LAYER && i % CPUS >= CPUS/2);
        param[i].inputQ = &inputQ;
        param[i].outputQ = &outputQ;
        param[i].runner = runner;
        param[i].layer = layer;
        pthread_create(&t, NULL, thread_func, (void*)&param[i]);
        if (cpu >= 0) {
            printf("%d(%d|%d),", i, cpu, layer);
            force_cpu(t, cpu);
        }
        else {
            printf("%d(*|%d),", i, layer);
        }
        usleep(1000);
    }
    printf("\n");

    struct timeval tv = cost_begin();
    for (int i = 0; i < JOB_LOAD; i++) {
        jobs[i]._input = rand();
        inputQ.push(&jobs[i]);
    }
    for (int i = 0; i < JOB_LOAD*JOB_COUNT; i++) {
        Job* job = outputQ.pop();
        job->_input = rand();
        inputQ.push(job);
    }
    for (int i = 0; i < JOB_LOAD; i++) {
        outputQ.pop();
    }
    long cost = cost_end(tv);
    printf("total cost: %ld\n", cost);

    delete runner;
    return 0;
}
27 Nov 00:44

Linux kernel coding style

27 Nov 00:28

DeepSort:三星开发出每秒3.7TB数据的排序算法,打破了每秒1.5TB的排序纪录

by aoi

根据 SortBenchmark 的一篇新报告(PDF),三星美国研究院的云研究实验室的 Zheng Li 和 Juhan Lee 设计一个分布式排序引擎,在 1 分钟中可排序 3.7 TB 数据,打破之前的 1.5 TB/min 纪录。

The post DeepSort:三星开发出每秒3.7TB数据的排序算法,打破了每秒1.5TB的排序纪录 appeared first on 头条 - 伯乐在线.

25 Nov 09:53

老码农冒死揭开行业黑幕:如何编写无法维护的代码

by 老码农

如何编写无法维护的代码

 

让自己稳拿铁饭碗 ;-)

– Roedy Green(翻译版略有删节)

 

简介

永远不要(把自己遇到的问题)归因于(他人的)恶意,这恰恰说明了(你自己的)无能。 — 拿破仑

为了造福大众,在Java编程领域创造就业机会,兄弟我在此传授大师们的秘籍。这些大师写的代码极其难以维护,后继者就是想对它做最简单的修改都需要花上数年时间。而且,如果你能对照秘籍潜心修炼,你甚至可以给自己弄个铁饭碗,因为除了你之外,没人能维护你写的代码。再而且,如果你能练就秘籍中的全部招式,那么连你自己都无法维护你的代码了!

(伯乐在线配图)

你不想练功过度走火入魔吧。那就不要让你的代码一眼看去就完全无法维护,只要它实质上是那样就行了。否则,你的代码就有被重写或重构的风险!

 

总体原则

Quidquid latine dictum sit, altum sonatur.
(随便用拉丁文写点啥都会显得高大上。)

想挫败维护代码的程序员,你必须先明白他的思维方式。他接手了你的庞大程序,没有时间把它全部读一遍,更别说理解它了。他无非是想快速找到修改代码的位置、改代码、编译,然后就能交差,并希望他的修改不会出现意外的副作用。

他查看你的代码不过是管中窥豹,一次只能看到一小段而已。你要确保他永远看不到全貌。要尽量让他难以找到他想找的代码。但更重要的是,要让他不能有把握忽略任何东西。

程序员都被编程惯例洗脑了,还为此自鸣得意。每一次你处心积虑地违背编程惯例,都会迫使他必须用放大镜去仔细阅读你的每一行代码。

你可能会觉得每个语言特性都可以用来让代码难以维护,其实不然。你必须精心地误用它们才行。

 

命名

“当我使用一个单词的时候” Humpty Dumpty 曾经用一种轻蔑的口气说, “它就是我想表达的意思,不多也不少。“
– Lewis Carroll — 《爱丽丝魔镜之旅》, 第6章

编写无法维护代码的技巧的重中之重是变量和方法命名的艺术。如何命名是和编译器无关的。这就让你有巨大的自由度去利用它们迷惑维护代码的程序员。

妙用 宝宝起名大全

      买本宝宝起名大全,你就永远不缺变量名了。比如 Fred 就是个好名字,而且键盘输入它也省事。如果你就想找一些容易输入的变量名,可以试试 adsf 或者 aoeu之类。

单字母变量名

      如果你给变量起名为a,b,c,用简单的文本编辑器就没法搜索它们的引用。而且,没人能猜到它们的含义。

创造性的拼写错误

      如果你必须使用描述性的变量和函数名,那就把它们都拼错。还可以把某些函数和变量名拼错,再把其他的拼对(例如 SetPintleOpening 和 SetPintalClosing) ,我们就能有效地将grep或IDE搜索技术玩弄于股掌之上。这招超级管用。还可以混淆不同语言(比如colour — 英国英语,和 color — 美国英语)。

抽象

      在命名函数和变量的时候,充分利用抽象单词,例如 it, everything, data, handle, stuff, do, routine, perform 和数字,像这样命名的好例子有 routineX48, PerformDataFunction, DoIt, HandleStuff还有 do_args_method。

首字母大写的缩写

      用首字母大写缩写(比如GNU 代表 GNU’s Not Unix) 使代码简洁难懂。真正的汉子(无论男女)从来不说明这种缩写的含义,他们生下来就懂。

辞典大轮换

      为了打破沉闷的编程气氛,你可以用一本辞典来查找尽量多的同义词。例如 display, show, present。在注释里含糊其辞地暗示这些命名之间有细微的差别,其实根本没有。不过,如果有两个命名相似的函数真的有重大差别,那倒是一定要确保它们用相同的单词来命名(例如,对于 “写入文件”, “在纸上书写” 和 “屏幕显示” 都用 print 来命名)。 在任何情况下都不要屈服于编写明确的项目词汇表这种无理要求。你可以辩解说,这种要求是一种不专业的行为,它违反了结构化设计的信息隐藏原则。

首字母大写

      随机地把单词中间某个音节的首字母大写。例如 ComputeReSult()。

重用命名

      在语言规则允许的地方,尽量把类、构造器、方法、成员变量、参数和局部变量都命名成一样。更高级的技巧是在{}块中重用局部变量。这样做的目的是迫使维护代码的程序员认真检查每个实例的作用域。特别是在Java代码中,可以把普通方法伪装成构造器。

使用非英语字母

          在命名中偷偷使用不易察觉的非英语字母,例如
typedef struct { int i; } ínt;

看上去没啥不对是吧?嘿嘿嘿…这里的第二个 ínt 的 í 实际上是东北欧字母,并不是英语中的 i 。在简单的文本编辑器里,想看出这一点点区别几乎是不可能的。

巧妙利用编译器对于命名长度的限制

      如果编译器只区分命名的前几位,比如前8位,那么就把后面的字母写得不一样。比如,其实是同一个变量,有时候写成 var_unit_update() ,有时候又写成 var_unit_setup(),看起来是两个不同的函数调用。而在编译的时候,它们其实是同一个变量 var_unit。

下划线,真正的朋友

      可以拿 _ 和 __ 作为标示符。

混合多语言

      随机地混用两种语言(人类语言或计算机语言都行)。如果老板要求使用他指定的语言,你就告诉他你用自己的语言更有利于组织你的思路,万一这招不管用,就去控诉这是语言歧视,并威胁起诉老板要求巨额精神损失赔偿。

扩展 ASCII 字符

        扩展 ASCII 字符用于变量命名是完全合法的,包括 ß, Ð, 和 ñ  等。在简单的文本编辑器里,除了拷贝/粘贴,基本上没法输入。

其他语言的命名

      使用外语字典作为变量名的来源。例如,可以用德语单词 punkt 代替 point。除非维护代码的程序员也像你一样熟练掌握了德语. 不然他就只能尽情地在代码中享受异域风情了。

数学命名

        用数学操作符的单词来命名变量。例如:
openParen = (slash + asterix) / equals;
    (左圆括号 = (斜杠 + 星号)/等号;)

令人眩晕的命名

        用带有完全不相关的感情色彩的单词来命名变量。例如:
marypoppins = (superman + starship) / god;
    (欢乐满人间 = (超人 + 星河战队)/上帝;)
    这一招可以让阅读代码的人陷入迷惑之中,因为他们在试图想清楚这些命名的逻辑时,会不自觉地联系到不同的感情场景里而无法自拔。

何时使用 i

      永远不要把 i 用作最内层的循环变量。 用什么命名都行,就是别用i。把 i 用在其他地方就随便了,用作非整数变量尤其好。

惯例 — 明修栈道,暗度陈仓

忽视 Java 编码惯例,Sun 自己就是这样做的。幸运的是,你违反了它编译器也不会打小报告。这一招的目的是搞出一些在某些特殊情况下有细微差别的名字来。如果你被强迫遵循驼峰法命名,你还是可以在某些模棱两可的情况下颠覆它。例如,inputFilename 和 inputfileName 两个命名都可以合法使用。在此基础上自己发明一套复杂到变态的命名惯例,然后就可以痛扁其他人,说他们违反了惯例。

小写的 l 看上去很像数字 1

      用小写字母 l 标识 long 常数。例如 10l 更容易被误认为是 101 而不是 10L 。 禁用所有能让人准确区分 uvw wW gq9 2z 5s il17|!j oO08 `’” ;,. m nn rn {[()]} 的字体。要做个有创造力的人。

把全局命名重用为私有

      在A 模块里声明一个全局数组,然后在B 模块的头文件里再声明一个同名的私有数组,这样看起来你在B 模块里引用的是那个全局数组,其实却不是。不要在注释里提到这个重复的情况。

误导性的命名

    让每个方法都和它的名字蕴含的功能有一些差异。例如,一个叫 isValid(x)的方法在判断完参数x的合法性之后,还顺带着把它转换成二进制并保存到数据库里。

 

伪装

当一个bug需要越长的时间才会暴露,它就越难被发现。- Roedy Green(本文作者)

编写无法维护代码的另一大秘诀就是伪装的艺术,即隐藏它或者让它看起来像其他东西。很多招式有赖于这样一个事实:编译器比肉眼或文本编辑器更有分辨能力。下面是一些伪装的最佳招式。

 

把代码伪装成注释,反之亦然

          下面包括了一些被注释掉的代码,但是一眼看去却像是正常代码。
for(j=0; j<array_len; j+ =8)
{ 
     total += array[j+0 ]; 
     total += array[j+1 ]; 
     total += array[j+2 ]; /* Main body of 
     total += array[j+3]; * loop is unrolled 
     total += array[j+4]; * for greater speed. 
     total += array[j+5]; */ 
     total += array[j+6 ]; 
     total += array[j+7 ]; 
}

如果不是用绿色标出来,你能注意到这三行代码被注释掉了么?

用连接符隐藏变量

          对于下面的定义
#define local_var xy_z

可以把 “xy_z” 打散到两行里:

#define local_var xy\
_z // local_var OK

这样全局搜索 xy_z 的操作在这个文件里就一无所获了。 对于 C 预处理器来说,第一行最后的 “\” 表示继续拼接下一行的内容。

 

 

文档

任何傻瓜都能说真话,而要把谎编圆则需要相当的智慧。- Samuel Butler (1835 – 1902)

不正确的文档往往比没有文档还糟糕。- Bertrand Meyer

既然计算机是忽略注释和文档的,你就可以在里边堂而皇之地编织弥天大谎,让可怜的维护代码的程序员彻底迷失。

 

在注释中撒谎

      实际上你不需要主动地撒谎,只要没有及时保持注释和代码更新的一致性就可以了。

只记录显而易见的东西

        往代码里掺进去类似于
 /* 给 i 加 1 */
      这样的注释,但是永远不要记录包或者方法的整体设计这样的干货。

记录 How 而不是 Why

      只解释一个程序功能的细节,而不是它要完成的任务是什么。这样的话,如果出现了一个bug,修复者就搞不清这里的代码应有的功能。

该写的别写

      比如你在开发一套航班预定系统,那就要精心设计,让它在增加另一个航空公司的时候至少有25处代码需要修改。永远不要在文档里说明要修改的位置。后来的开发人员要想修改你的代码?门都没有,除非他们能把每一行代码都读懂。

计量单位

      永远不要在文档中说明任何变量、输入、输出或参数的计量单位,如英尺、米、加仑等。计量单位对数豆子不是太重要,但在工程领域就相当重要了。同理,永远不要说明任何转换常量的计量单位,或者是它的取值如何获得。要想让代码更乱的话,你还可以在注释里写上错误的计量单位,这是赤裸裸的欺骗,但是非常有效。如果你想做一个恶贯满盈的人,不妨自己发明一套计量单位,用自己或某个小人物的名字命名这套计量单位,但不要给出定义。万一有人挑刺儿,你就告诉他们,你这么做是为了把浮点数运算凑成整数运算而进行的转换。

      永远不要记录代码中的坑。如果你怀疑某个类里可能有bug,天知地知你知就好。如果你想到了重构或重写代码的思路,看在老天爷的份上,千万别写出来。切记电影《小鹿斑比》里那句台词 “如果你不能说好听的话,那就什么也不要说。”。万一这段代码的原作者看到你的注释怎么办?万一老板看到了怎么办?万一客户看到了怎么办?搞不好最后你自己被解雇了。一句”这里需要修改“的匿名注释就好多了,尤其是当看不清这句注释指的是哪里需要修改的情况下。切记“难得糊涂”四个字,这样大家都不会感觉受到了批评。

说明变量

      永远不要对变量声明加注释。有关变量使用的方式、边界值、合法值、小数点后的位数、计量单位、显示格式、数据录入规则等等,后继者完全可以自己从程序代码中去理解和整理嘛。如果老板强迫你写注释,就在方法体里胡乱多写点,但绝对不要对变量声明写注释,即使是临时变量!

在注释里挑拨离间

        为了阻挠任何雇佣外部维护承包商的倾向,可以在代码中散布针对其他同行软件公司的攻击和抹黑,特别是可能接替你工作的其中任何一家。例如:
/* 优化后的内层循环
这套技巧对于SSI软件服务公司的那帮蠢材来说太高深了,他们只会
用 <math.h> 里的笨例程,消耗50倍的内存和处理时间。
*/ 
class clever_SSInc
{ 
.. . 
}
    可能的话,除了注释之外,这些攻击抹黑的内容也要掺到代码里的重要语义部分,这样如果管理层想清理掉这些攻击性的言论然后发给外部承包商去维护,就会破坏代码结构。

程序设计

编写无法维护代码的基本规则就是:在尽可能多的地方,以尽可能多的方式表述每一个事实。- Roedy Green

    编写可维护代码的关键因素是只在一个地方表述应用里的一个事实。如果你的想法变了,你也只在一个地方修改,这样就能保证整个程序正常工作。所以,编写无法维护代码的关键因素就是反复地表述同一个事实,在尽可能多的地方,以尽可能多的方式进行。令人高兴的是,像Java这样的语言让编写这种无法维护代码变得非常容易。例如,改变一个被引用很多的变量的类型几乎是不可能的,因为所有造型和转换功能都会出错,而且关联的临时变量的类型也不合适了。而且,如果变量值要在屏幕上显示,那么所有相关的显示和数据录入代码都必须一一找到并手工进行修改。类似的还有很多,比如由C和Java组成的Algol语言系列,Abundance甚至Smalltalk对于数组等结构的处理,都是大有可为的。

Java 造型

      Java的造型机制是上帝的礼物。你可以问心无愧地使用它,因为Java语言本身就需要它。每次你从一个Collection 里获取一个对象,你都必须把它造型为原始类型。这样这个变量的类型就必须在无数地方表述。如果后来类型变了,所有的造型都要修改才能匹配。如果倒霉的维护代码的程序员没有找全(或者修改太多),编译器能不能检测到也不好说。类似的,如果变量类型从short 变成 int,所有匹配的造型也都要从(short) 改成 (int)。

利用Java的冗余

        Java要求你给每个变量的类型写两次表述。 Java 程序员已经习惯了这种冗余,他们不会注意到你的两次表述有细微的差别,例如
Bubblegum b = new Bubblegom();
        不幸的是 ++ 操作符的盛行让下面这种伪冗余代码得手的难度变大了:
swimmer = swimner + 1;

永远不做校验

      永远不要对输入数据做任何的正确性或差异性检查。这样能表现你对公司设备的绝对信任,以及你是一位信任所有项目伙伴和系统管理员的团队合作者。总是返回合理的值,即使数据输入有问题或者错误。

有礼貌,无断言

      避免使用 assert() 机制,因为它可能把三天的debug盛宴变成10分钟的快餐。

避免封装

      为了提高效率,不要使用封装。方法的调用者需要所有能得到的外部信息,以便了解方法的内部是如何工作的。

复制粘贴修改

      以效率的名义,使用 复制+粘贴+修改。这样比写成小型可复用模块效率高得多。在用代码行数衡量你的进度的小作坊里,这招尤其管用。

使用静态数组

      如果一个库里的模块需要一个数组来存放图片,就定义一个静态数组。没人会有比512 X 512 更大的图片,所以固定大小的数组就可以了。为了最佳精度,就把它定义成 double 类型的数组。

傻瓜接口

      编写一个名为 “WrittenByMe” 之类的空接口,然后让你的所有类都实现它。然后给所有你用到的Java 内置类编写包装类。这里的思想是确保你程序里的每个对象都实现这个接口。最后,编写所有的方法,让它们的参数和返回类型都是这个 WrittenByMe。这样就几乎不可能搞清楚某个方法的功能是什么,并且所有类型都需要好玩的造型方法。更出格的玩法是,让每个团队成员编写它们自己的接口(例如 WrittenByJoe),程序员用到的任何类都要实现他自己的接口。这样你就可以在大量无意义接口中随便找一个来引用对象了。

巨型监听器

      永远不要为每个组件创建分开的监听器。对所有按钮总是用同一个监听器,只要用大量的if…else 来判断是哪一个按钮被点击就行了。

好事成堆TM

        狂野地使用封装和OO思想。例如
myPanel.add( getMyButton() ); 
private JButton getMyButton()
{ 
     return myButton; 
}
      这段很可能看起来不怎么好笑。别担心,只是时候未到而已。

友好的朋友

      在C++ 里尽量多使用friend声明。再把创建类的指针传递给已创建类。现在你不用浪费时间去考虑接口了。另外,你应该用上关键字private 和 protected 来表明你的类封装得很好。

使用三维数组

      大量使用它们。用扭曲的方式在数组之间移动数据,比如,用arrayA里的行去填充arrayB的列。这么做的时候,不管三七二十一再加上1的偏移值,这样很灵。让维护代码的程序员抓狂去吧。

混合与匹配

      存取方法和公共变量神马的都要给他用上。这样的话,你无需调用存取器的开销就可以修改一个对象的变量,还能宣称这个类是个”Java Bean”。对于那些试图添加日志函数来找出改变值的源头的维护代码的程序员,用这一招来迷惑他尤其有效。

没有秘密!

      把每个方法和变量都声明为 public。毕竟某个人某天可能会需要用到它。一旦方法被声明为public 了,就很难缩回去。对不?这样任何它覆盖到的代码都很难修改了。它还有个令人愉快的副作用,就是让你看不清类的作用是什么。如果老板质问你是不是疯了,你就告诉他你遵循的是经典的透明接口原则。

全堆一块

      把你所有的没用的和过时的方法和变量都留在代码里。毕竟说起来,既然你在1976年用过一次,谁知道你啥时候会需要再用到呢?当然程序是改了,但它也可能会改回来嘛,你”不想要重新发明轮子”(领导们都会喜欢这样的口气)。如果你还原封不动地留着这些方法和变量的注释,而且注释写得又高深莫测,甭管维护代码的是谁,恐怕都不敢对它轻举妄动。

就是 Final

      把你所有的叶子类都声明为 final。毕竟说起来,你在项目里的活儿都干完了,显然不会有其他人会通过扩展你的类来改进你的代码。这种情况甚至可能有安全漏洞。 java.lang.String 被定义成 final 也许就是这个原因吧?如果项目组其他程序员有意见,告诉他们这样做能够提高运行速度。

避免布局

      永远不要用到布局。当维护代码的程序员想增加一个字段,他必须手工调整屏幕上显示所有内容的绝对坐标值。如果老板强迫你使用布局,那就写一个巨型的 GridBagLayout 并在里面用绝对坐标进行硬编码。

全局变量,怎么强调都不过分

      如果上帝不愿意我们使用全局变量,他就不会发明出这个东西。不要让上帝失望,尽量多使用全局变量。每个函数最起码都要使用和设置其中的两个,即使没有理由也要这么做。毕竟,任何优秀的维护代码的程序员都会很快搞清楚这是一种侦探工作测试,有利于让他们从笨蛋中脱颖而出。

再一次说说全局变量

      全局变量让你可以省去在函数里描述参数的麻烦。充分利用这一点。在全局变量中选那么几个来表示对其他全局变量进行操作的类型。

局部变量

      永远不要用局部变量。在你感觉想要用的时候,把它改成一个实例或者静态变量,并无私地和其他方法分享它。这样做的好处是,你以后在其他方法里写类似声明的时候会节省时间。C++程序员可以百尺竿头更进一步,把所有变量都弄成全局的。

配置文件

      配置文件通常是以 关键字 = 值 的形式出现。在加载时这些值被放入 Java 变量中。最明显的迷惑技术就是把有细微差别的名字用于关键字和Java 变量.甚至可以在配置文件里定义运行时根本不会改变的常量。参数文件变量和简单变量比,维护它的代码量起码是后者的5倍。

子类

    对于编写无法维护代码的任务来说,面向对象编程的思想简直是天赐之宝。如果你有一个类,里边有10个属性(成员/方法),可以考虑写一个基类,里面只有一个属性,然后产生9层的子类,每层增加一个属性。等你访问到最终的子类时,你才能得到全部10个属性。如果可能,把每个类的声明都放在不同的文件里。

编码迷局

迷惑 C

      从互联网上的各种混乱C 语言竞赛中学习,追随大师们的脚步。

追求极致

        总是追求用最迷惑的方式来做普通的任务。例如,要用数组来把整数转换为相应的字符串,可以这么做:
char *p; 
switch (n) 
{ 
case 1: 
    p = "one"; 
    if (0) 
case 2: 
    p = "two"; 
    if (0) 
case 3: 
    p = "three"; 
    printf("%s", p); 
    break; 
}

一致性的小淘气

      当你需要一个字符常量的时候,可以用多种不同格式: ‘ ‘, 32, 0×20, 040。在C或Java里10和010是不同的数(0开头的表示8进制),你也可以充分利用这个特性。

造型

      把所有数据都以 void * 形式传递,然后再造型为合适的结构。不用结构而是通过位移字节数来造型也很好玩。

嵌套 Switch

      Switch 里边还有 Switch,这种嵌套方式是人类大脑难以破解的。

利用隐式转化

      牢记编程语言中所有的隐式转化细节。充分利用它们。数组的索引要用浮点变量,循环计数器用字符,对数字执行字符串函数调用。不管怎么说,所有这些操作都是合法的,它们无非是让源代码更简洁而已。任何尝试理解它们的维护者都会对你感激不尽,因为他们必须阅读和学习整个关于隐式数据类型转化的章节,而这个章节很可能是他们来维护你的代码之前完全忽略了的。

分号!

        在所有语法允许的地方都加上分号,例如:
if(a); 
else;
{ 
int d; 
d = c; 
} 
;

使用八进制数

        把八进制数混到十进制数列表里,就像这样:
array = new int []
{ 
111, 
120, 
013,
121, 
};

嵌套

      尽可能深地嵌套。优秀的程序员能在一行代码里写10层(),在一个方法里写20层{}。

C数组

          C编译器会把 myArray[i] 转换成 *(myArray + i),它等同于 *(i + myArray) 也等同于 i[myArray]。 高手都知道怎么用好这个招。可以用下面的函数来产生索引,这样就把代码搞乱了:
int myfunc(int q, int p) { return p%q; } 
... 
myfunc(6291, 8)[Array];

遗憾的是,这一招只能在本地C类里用,Java 还不行。

放长线钓大鱼

      一行代码里堆的东西越多越好。这样可以省下临时变量的开销,去掉换行和空格还可以缩短源文件大小。记住,要去掉运算符两边的空格。优秀的程序员总是能突破某些编辑器对于255个字符行宽的限制。

异常

      在这里我要向你传授一个编程领域里鲜为人知的秘诀。异常是个讨厌的东西。良好的代码永远不会出错,所以异常实际上是不必要的。不要把时间浪费在这上面。子类异常是给那些知道自己代码会出错的低能儿用的。在整个应用里,你只用在main()里放一个try/catch,里边直接调用 System.exit()就行了。在每个方法头要贴上标准的抛出集合定义,至于会不会抛出异常你就甭管了。

使用异常的时机

      在非异常条件下才要使用异常。比如终止循环就可以用 ArrayIndexOutOfBoundsException。还可以从异常里的方法返回标准的结果。

狂热奔放地使用线程

    如题。

测试

在程序里留些bug,让后继的维护代码的程序员能做点有意思的事。精心设计的bug是无迹可寻的,而且谁也不知道它啥时候会冒出来。要做到这一点,最简单的办法的就是不要测试代码。

永不测试

      永远不要测试负责处理错误、当机或操作系统故障的任何代码。反正这些代码永远也不会执行,只会拖累你的测试。还有,你怎么可能测试处理磁盘错误、文件读取错误、操作系统崩溃这些类型的事件呢?为啥你要用特别不稳定的计算机或者用测试脚手架来模拟这样的环境?现代化的硬件永远不会崩溃,谁还愿意写一些仅仅用于测试的代码?这一点也不好玩。万一将来出了事用户抱怨,你就怪到操作系统或者硬件头上。他们永远不会知道真相的。

永远不要做性能测试

      嘿,如果软件运行不够快,只要告诉客户买个更快的机器就行了。如果你真的做了性能测试,你可能会发现一个瓶颈,这会导致修改算法,然后导致整个产品要重新设计。谁想要这种结果?而且,在客户那边发现性能问题意味着你可以免费到外地旅游。你只要备好护照和最新照片就行了。

永远不要写任何测试用例

      永远不要做代码覆盖率或路径覆盖率测试。自动化测试是给那些窝囊废用的。搞清楚哪些特性占到你的例程使用率的90%,然后把90%的测试用在这些路径上。毕竟说起来,这种方法可能只测试到了大约你代码的60%,这样你就节省了40%的测试工作。这能帮助你赶上项目后端的进度。等到有人发现所有这些漂亮的“市场特性”不能正常工作的时候,你早就跑路了。一些有名的大软件公司就是这样测试代码的,所以你也应该这样做。如果因为某种原因你还没走,那就接着看下一节。

测试是给懦夫用的

    勇敢的程序员会跳过这个步骤。太多程序员害怕他们的老板,害怕丢掉工作,害怕客户的投诉邮件,害怕遭到起诉。这种恐惧心理麻痹了行动,降低了生产率。有科学研究成果表明,取消测试阶段意味着经理有把握能提前确定交付时间,这对于规划流程显然是有利的。消除了恐惧心理,创新和实验之花就随之绽放。程序员的角色是生产代码,调试工作完全可以由技术支持和遗留代码维护组通力合作来进行。

如果我们对自己的编程能力有充分信心,那么测试就没有必要了。如果我们逻辑地看待这个问题,随便一个傻瓜都能认识到测试根本都不是为了解决技术问题,相反,它是一种感性的信心问题。针对这种缺乏信心的问题,更有效的解决办法就是完全取消测试,送我们的程序员去参加自信心培训课程。毕竟说起来,如果我们选择做测试,那么我们就要测试每个程序的变更,但其实我们只需要送程序员去一次建立自信的培训课就行了。很显然这么做的成本收益是相当可观的。

 

编程语言的选择

计算机语言正在逐步进化,变得更加傻瓜化。使用最新的语言算什么好汉?尽可能坚持使用你会用的最老的语言,先考虑用穿孔纸带,不行就用汇编,再不行用FORTRAN 或者 COBOL,再不行就用C 还有 BASIC,实在不行再用 C++。
FORTRAN

      用 FORTRAN 写所有的代码。如果老板问你为啥,你可以回答说它有很多非常有用的库,你用它可以节约时间。不过,用 FORTRAN 写出可维护代码的概率是 0,所以,要达到不可维护代码编程指南里的要求就容易多了。

用 ASM

      把所有的通用工具函数都转成汇编程序。

用 QBASIC

      所有重要的库函数都要用 QBASIC 写,然后再写个汇编的封包程序来处理 large 到 medium 的内存模型映射。

内联汇编

      在你的代码里混杂一些内联的汇编程序,这样很好玩。这年头几乎没人懂汇编程序了。只要放几行汇编代码就能让维护代码的程序员望而却步。

宏汇编调用C

    如果你有个汇编模块被C调用,那就尽可能经常从汇编模块再去调用C,即使只是出于微不足道的用途,另外要充分利用 goto, bcc 和其他炫目的汇编秘籍。

与他人共事之道

老板才是真行家

      如果你的老板认为他20年的 FORTRAN 编程经验对于现代软件开发具有很高的指导价值,你务必严格采纳他的所有建议。投桃报李,你的老板也会信任你。这会对你的职业发展有利。你还会从他那里学到很多搞乱程序代码的新方法。

颠覆技术支持

      确保代码中到处是bug的有效方法是永远不要让维护代码的程序员知道它们。这需要颠覆技术支持工作。永远不接电话。使用自动语音答复“感谢拨打技术支持热线。需要人工服务请按1,或在嘀声后留言。”,请求帮助的电子邮件必须忽略,不要给它分配服务追踪号。对任何问题的标准答复是“我估计你的账户被锁定了,有权限帮你恢复的人现在不在。”

沉默是金

      永远不要对下一个危机保持警觉。如果你预见到某个问题可能会在一个固定时间爆发,摧毁西半球的全部生命,不要公开讨论它。不要告诉朋友、同事或其他你认识的有本事的人。在任何情况下都不要发表任何可能暗示到这种新的威胁的内容。只发送一篇正常优先级的、语焉不详的备忘录给管理层,保护自己免遭秋后算账。如果可能的话,把这篇稀里糊涂的信息作为另外一个更紧急的业务问题的附件。这样就可以心安理得地休息了,你知道将来你被强制提前退休之后一段时间,他们又会求着你回来,并给你对数级增长的时薪!

每月一书俱乐部

    加入一个计算机每月一书俱乐部。选择那些看上去忙着写书不可能有时间真的去写代码的作者。去书店里找一些有很多图表但是没有代码例子的书。浏览一下这些书,从中学会一些迂腐拗口的术语,用它们就能唬住那些自以为是的维护代码的程序员。你的代码肯定会给他留下深刻印象。如果人们连你写的术语都理解不了,他们一定会认为你非常聪明,你的算法非常深奥。不要在你的算法说明里作任何朴素的类比。

自立门户

你一直想写系统级的代码。现在机会来了。忽略标准库, 编写你自己的标准,这将会是你简历中的一大亮点。

推出你自己的 BNF 范式

      总是用你自创的、独一无二的、无文档的BNF范式记录你的命令语法。永远不要提供一套带注解的例子(合法命令和非法命令之类)来解释你的语法体系。那样会显得完全缺乏学术严谨性。确保没有明显的方式来区分终结符和中间符号。永远不要用字体、颜色、大小写和其他任何视觉提示帮助读者分辨它们。在你的 BNF 范式用和命令语言本身完全一样的标点符号,这样读者就永远无法分清一段 (…), [...], {…} 或 “…” 到底是你在命令行里真正输入的,还是想提示在你的BNF 范式里哪个语法元素是必需的、可重复的、或可选的。不管怎么样,如果他们太笨,搞不清你的BNF 范式的变化,就没资格使用你的程序。

推出你自己的内存分配

    地球人儿都知道,调试动态存储是复杂和费时的。与其逐个类去确认它没有内存溢出,还不如自创一套存储分配机制呢。其实它无非是从一大片内存中 malloc 一块空间而已。用不着释放内存,让用户定期重启动系统,这样不就清除了堆么。重启之后系统需要追踪的就那么一点东西,比起解决所有的内存泄露简单得不知道到哪里去了!而且,只要用户记得定期重启系统,他们也永远不会遇到堆空间不足的问题。一旦系统被部署,你很难想象他们还能改变这个策略。

 

其他杂七杂八的招

如果你给某人一段程序,你会让他困惑一天;如果你教他们如何编程,你会让他困惑一辈子。 — Anonymous

不要重编译
让我们从一条可能是有史以来最友好的技巧开始:把代码编译成可执行文件。如果它能用,就在源代码里做一两个微小的改动 — 每个模块都照此办理。但是不要费劲巴拉地再编译一次了。 你可以留着等以后有空而且需要调试的时候再说。多年以后,等可怜的维护代码的程序员更改了代码之后发现出错了,他会有一种错觉,觉得这些肯定是他自己最近修改的。这样你就能让他毫无头绪地忙碌很长时间。

挫败调试工具
对于试图用行调试工具追踪来看懂你的代码的人,简单的一招就能让他狼狈不堪,那就是把每一行代码都写得很长。特别要把 then 语句 和 if 语句放在同一行里。他们无法设置断点。他们也无法分清在看的分支是哪个 if 里的。

公制和美制
在工程方面有两种编码方式。一种是把所有输入都转换为公制(米制)计量单位,然后在输出的时候自己换算回各种民用计量单位。另一种是从头到尾都保持各种计量单位混合在一起。总是选择第二种方式,这就是美国之道!

持续改进
要持续不懈地改进。要常常对你的代码做出“改进”,并强迫用户经常升级 — 毕竟没人愿意用一个过时的版本嘛。即便他们觉得他们对现有的程序满意了,想想看,如果他们看到你又“完善“了它,他们会多么开心啊!不要告诉任何人版本之间的差别,除非你被逼无奈 — 毕竟,为什么要告诉他们本来永远也不会注意到的一些bug呢?

“关于”
”关于“一栏应该只包含程序名、程序员姓名和一份用法律用语写的版权声明。理想情况下,它还应该链接到几 MB 的代码,产生有趣的动画效果。但是,里边永远不要包含程序用途的描述、它的版本号、或最新代码修改日期、或获取更新的网站地址、或作者的email地址等。这样,所有的用户很快就会运行在各种不同的版本上,在安装N+1版之前就试图安装N+2版。

变更
在两个版本之间,你能做的变更自然是多多益善。你不会希望用户年复一年地面对同一套老的接口或用户界面,这样会很无聊。最后,如果你能在用户不注意的情况下做出这些变更,那就更好了 — 这会让他们保持警惕,戒骄戒躁。

无需技能
写无法维护代码不需要多高的技术水平。喊破嗓子不如甩开膀子,不管三七二十一开始写代码就行了。记住,管理层还在按代码行数考核生产率,即使以后这些代码里的大部分都得删掉。

只带一把锤子
一招鲜吃遍天,会干什么就吆喝什么,轻装前进。如果你手头只有一把锤子,那么所有的问题都是钉子。

规范体系
有可能的话,忽略当前你的项目所用语言和环境中被普罗大众所接受的编程规范。比如,编写基于MFC 的应用时,就坚持使用STL 编码风格。

翻转通常的 True False 惯例
把常用的 true 和 false 的定义反过来用。这一招听起来平淡无奇,但是往往收获奇效。你可以先藏好下面的定义:

#define TRUE 0 
#define FALSE 1

把这个定义深深地藏在代码中某个没人会再去看的文件里不易被发现的地方,然后让程序做下面这样的比较

if ( var == TRUE )
if ( var != FALSE )

某些人肯定会迫不及待地跳出来“修正”这种明显的冗余,并且在其他地方照着常规去使用变量var:

if ( var )

还有一招是为 TRUE 和 FALSE赋予相同的值,虽然大部分人可能会看穿这种骗局。给它们分别赋值 1 和 2 或者 -1 和 0 是让他们瞎忙乎的方式里更精巧的,而且这样做看起来也不失对他们的尊重。你在Java 里也可以用这一招,定义一个叫 TRUE 的静态常量。在这种情况下,其他程序员更有可能怀疑你干的不是好事,因为Java里已经有了内建的标识符 true。

第三方库
在你的项目里引入功能强大的第三方库,然后不要用它们。潜规则就是这样,虽然你对这些工具仍然一无所知,却可以在你简历的“其他工具”一节中写上这些没用过的库。

不要用库
假装不知道有些库已经直接在你的开发工具中引入了。如果你用VC++编程,忽略MFC 或 STL 的存在,手工编写所有字符串和数组的实现;这样有助于保持你玩指针技术的高水平,并自动阻止任何扩展代码功能的企图。

创建一套Build顺序
把这套顺序规则做得非常晦涩,让维护者根本无法编译任何他的修改代码。秘密保留 SmartJ ,它会让 make脚本形同废物。类似地,偷偷地定义一个 javac 类,让它和编译程序同名。说到大招,那就是编写和维护一个定制的小程序,在程序里找到需要编译的文件,然后通过直接调用 sun.tools.javac.Main 编译类来进行编译。

Make 的更多玩法
用一个 makefile-generated-batch-file 批处理文件从多个目录复制源文件,文件之间的覆盖规则在文档中是没有的。这样,无需任何炫酷的源代码控制系统,就能实现代码分支,并阻止你的后继者弄清哪个版本的 DoUsefulWork() 才是他需要修改的那个。

搜集编码规范
尽可能搜集所有关于编写可维护代码的建议,例如 SquareBox 的建议 ,然后明目张胆地违反它们。

规避公司的编码规则
某些公司有严格的规定,不允许使用数字标识符,你必须使用预先命名的常量。要挫败这种规定背后的意图太容易了。比如,一位聪明的 C++ 程序员是这么写的:

#define K_ONE 1 
#define K_TWO 2 
#define K_THOUSAND 999

编译器警告
一定要保留一些编译器警告。在 make 里使用 “-” 前缀强制执行,忽视任何编译器报告的错误。这样,即使维护代码的程序员不小心在你的源代码里造成了一个语法错误,make 工具还是会重新把整个包build 一遍,甚至可能会成功!而任何程序员要是手工编译你的代码,看到屏幕上冒出一堆其实无关紧要的警告,他们肯定会觉得是自己搞坏了代码。同样,他们一定会感谢你让他们有找错的机会。学有余力的同学可以做点手脚让编译器在打开编译错误诊断工具时就没法编译你的程序。当然了,编译器也许能做一些脚本边界检查,但是真正的程序员是不用这些特性的,所以你也不该用。既然你用自己的宝贵时间就能找到这些精巧的bug,何必还多此一举让编译器来检查错误呢?

把 bug 修复和升级混在一起
永远不要发布什么“bug 修复”版本。一定要把 bug 修复和数据库结构变更、复杂的用户界面修改,还有管理界面重写等混在一起。那样的话,升级就变成一件非常困难的事情,人们会慢慢习惯 bug 的存在并开始称他们为特性。那些真心希望改变这些”特性“的人们就会有动力升级到新版本。这样从长期来说可以节省你的维护工作量,并从你的客户那里获得更多收入。

在你的产品发布每个新版本的时候都改变文件结构
没错,你的客户会要求向上兼容,那就去做吧。不过一定要确保向下是不兼容的。这样可以阻止客户从新版本回退,再配合一套合理的 bug 修复规则(见上一条),就可以确保每次新版本发布后,客户都会留在新版本。学有余力的话,还可以想办法让旧版本压根无法识别新版本产生的文件。那样的话,老版本系统不但无法读取新文件,甚至会否认这些文件是自己的应用系统产生的!温馨提示:PC 上的 Word 文字处理软件就典型地精于此道。

抵消 Bug
不用费劲去代码里找 bug 的根源。只要在更高级的例程里加入一些抵销它的代码就行了。这是一种很棒的智力测验,类似于玩3D棋,而且能让将来的代码维护者忙乎很长时间都想不明白问题到底出在哪里:是产生数据的低层例程,还是莫名其妙改了一堆东西的高层代码。这一招对天生需要多回合执行的编译器也很好用。你可以在较早的回合完全避免修复问题,让较晚的回合变得更加复杂。如果运气好,你永远都不用和编译器前端打交道。学有余力的话,在后端做点手脚,一旦前端产生的是正确的数据,就让后端报错。

使用旋转锁
不要用真正的同步原语,多种多样的旋转锁更好 — 反复休眠然后测试一个(non-volatile的) 全局变量,直到它符合你的条件为止。相比系统对象,旋转锁使用简便,”通用“性强,”灵活“多变,实为居家旅行必备。

随意安插 sync 代码
把某些系统同步原语安插到一些用不着它们的地方。本人曾经在一段不可能会有第二个线程的代码中看到一个临界区(critical section)代码。本人当时就质问写这段代码的程序员,他居然理直气壮地说这么写是为了表明这段代码是很”关键“(单词也是critical)的!

优雅降级
如果你的系统包含了一套 NT 设备驱动,就让应用程序负责给驱动分配 I/O 缓冲区,然后在任何交易过程中对内存中的驱动加锁,并在交易完成后释放或解锁。这样一旦应用非正常终止,I/O缓存又没有被解锁,NT服务器就会当机。但是在客户现场不太可能会有人知道怎么弄好设备驱动,所以他们就没有选择(只能请你去免费旅游了)。

定制脚本语言
在你的 C/S 应用里嵌入一个在运行时按字节编译的脚本命令语言。

依赖于编译器的代码
如果你发现在你的编译器或解释器里有个bug,一定要确保这个bug的存在对于你的代码正常工作是至关重要的。毕竟你又不会使用其他的编译器,其他任何人也不允许!

一个货真价实的例子
下面是一位大师编写的真实例子。让我们来瞻仰一下他在这样短短几行 C 函数里展示的高超技巧。

void* Realocate(void*buf, int os, int ns) 
{
     void*temp; 
     temp = malloc(os); 
     memcpy((void*)temp, (void*)buf, os); 
     free(buf); 
     buf = malloc(ns); 
     memset(buf, 0, ns); 
     memcpy((void*)buf, (void*)temp, ns); 
     return buf;
}
  • 重新发明了标准库里已有的简单函数。
  • Realocate 这个单词拼写错误。所以说,永远不要低估创造性拼写的威力。
  • 无缘无故地给输入缓冲区产生一个临时的副本。
  • 无缘无故地造型。 memcpy() 里有 (void*),这样即使我们的指针已经是 (void*) 了也要再造型一次。另外,这样做可以传递任何东西作为参数,加10分。
  • 永远不必费力去释放临时内存空间。这样会导致缓慢的内存泄露,一开始看不出来,要程序运行一段时间才行。
  • 把用不着的东西也从缓冲区里拷贝出来,以防万一。这样只会在Unix上产生core dump,Windows 就不会。
  • 很显然,os 和 ns 的含义分别是”old size” 和 “new size”。
  • 给 buf 分配内存之后,memset 初始化它为 0。不要使用 calloc(),因为某些人会重写 ANSI 规范,这样将来保不齐 calloc() 往 buf 里填的就不是 0 了。(虽然我们复制过去的数据量和 buf 的大小是一样的,不需要初始化,不过这也无所谓啦)

如何修复 “unused variable” 错误

如果你的编译器冒出了 “unused local variable” 警告,不要去掉那个变量。相反,要找个聪明的办法把它用起来。我最喜欢的方法是:

i = i;

 

大小很关键
差点忘了说了,函数是越大越好。跳转和 GOTO 语句越多越好。那样的话,想做任何修改都需要分析很多场景。这会让维护代码的程序员陷入千头万绪之中。如果函数真的体型庞大的话,对于维护代码的程序员就是哥斯拉怪兽了,它会在他搞清楚情况之前就残酷无情地将他踩翻在地。

一张图片顶1000句话,一个函数就是1000行
把每个方法体写的尽可能的长 — 最好是你写的任何一个方法或函数都不会少于1000行代码,而且里边是深度嵌套,这是必须的。

少个文件
一定要保证一个或多个关键文件无法找到。利用includes 里边再 includes 就能做到这一点。例如,在你的 main 模块里,你写上:

#include <stdcode.h>

Stdcode.h 是有的。但是在 stdcode.h 里,还有个引用:

#include "a:\\refcode.h"

然后,refcode.h 就没地方能找到了。

(【译者注】为啥找不到呢?仔细看看,现在还有人知道 a:\ 是什么吗?A盘!传说中的软盘…)

到处都写,无处会读
至少要把一个变量弄成这样:到处被设置,但是几乎没有哪里用到它。不幸的是,现代编译器通常会阻止你做相反的事:到处读,没处写。不过你在C 或 C++ 里还是可以这样做的。


【译注】:原文在后面还有一些内容,翻译时略有删减。删节的内容主要是:

  1. 我看不懂的部分;
  2. 我觉得不怎么好笑的部分(其实很可能是因为没看懂所以找不到笑点);
  3. 不容易引起现代程序猿共鸣的老旧内容。

本人水平有限,时间匆忙,难免有误,请读者不吝指出。谢谢!

老码农冒死揭开行业黑幕:如何编写无法维护的代码,首发于博客 - 伯乐在线

24 Nov 00:31

Linux Performance Tools 2014

Last month I gave an updated Linux Performance Tools talk for LinuxCon Europe 2014 in Düsseldorf. This included static performance tuning, as well as other updates. My performance observability tools diagram now has rdmsr(1), after my post on The MSRs of EC2, where I discovered that MSRs could be useful:

22 Nov 10:12

Why I do not want to work at Google (2011)

21 Nov 06:23

为什么你的代码如此难以理解

by Leo

“我到底在想什么?!?”

凌晨1:30分,我正盯着不到一个月前我写的一段代码。当时它看起来像是件艺术品,全部是可理解的,优雅、简单、让人叹为观止。这一切都不再了,明天是我的最后期限,数小时前我发现了一个bug。当时看起来的简单和逻辑再也说不通了。可以肯定的是,如果我写代码,我应该足以聪明到理解代码?

经过了多次这种经历以后,我开始认真思考,为什么我的代码在我编写的时候很清楚、而当我数周或数月后回头看的时候,它们却那么费解。

问题1,过度复杂的心智模型

为了理解当你间隔一段时间返回到你的代码、却发现代码难以理解的第一步,就是理解我们如何从心智上建立问题模型。你写的几乎所有代码都是尽量解决现实世界的问题。在你写代码之前,你需要理解你正试图解决的问题。这常常是编程里最难的一步。

为了解决现实世界的问题,我们首先需要形成该问题的心智模型【注1】,以此作为编程意图。接下来你需要形成实现编程意图的方案模型,我们姑且称为语义模型(semantic model)。从来不要混淆你的编程意图和此意图的方案。我们倾向于主要考虑方案方面的东东,而常常忽略意图的模型。

你接下来的步骤是形成可能最简单的语义模型。这是容易搞错的第二步。如果你不花时间去真正理解你正试图解决的问题,你将在写代码时被绊倒在模型上。另一方面,如果你真正考虑了你正尽量做的事情,你经常得到一个非常简单的模型,这足以让你掌握最初的意图。

如果你想容易地维护简单的代码,就尽可能多些地消除意外的复杂性。我们正试图解决的问题是足够复杂的。如果你不必那么做,就不要把意外的复杂性增加进来。

问题2,语义模型到代码的糟糕转化

一旦你尽全力形成了最好的语义模型,那么就到了把它转化为代码的时候了。我们称之为句法模型(syntactic model)。你正试图把你的语义模型的意义转化为计算机能够理解的句法。

如果你有非常不错的语义模型、而在转化为代码时搞砸了,那么在你需要在以后某个阶段回头修改代码时,你将比较痛苦。当你脑子里还有语义模型时,把你代码映射到语义模型是容易的。回忆起变量“x”实际上代表一条记录被创建的日期、而“y”代码记录被删除的日期,这是不难的。当你3个月后再回来看代码,你的脑子里将没有这个语义模型了,因此无法理解同样的变量名字。

把语义模型转化为句法的任务就是尽量多地留下线索,让你在今后返回时,能够重建当初的语义模型。

好了,你该怎么做呢?

类结构和命名

如果你在使用面向对象语义,请尽量让你的类结构和命名靠近语义模型。领域驱动设计(Domain Driven Design)【注2】是在这种练习上投入了相当重要性的一种运动。即使你没有相信完全的DDD方法,你也应当非常小心地考虑类结构和命名。每个类都是你留给自己和其他人的线索,它帮助你在将来返回的时候重建你的心智模型。

变量、参数和方法命名

尽量避免普通的变量和方法命名。不要把方法命名为“Process”,因为“PaySalesCommision”更有意义。不要把变量命名为“x”,因为它应当是“currentContract”。不要把参数命名为“input”,因为“outstandingInvoices“更好。

单一功能原则(Single responsibility principle,简称SRP)

SRP【注3】是面对对象设计原则的核心之一,关联着好的类和变量命名。它认为,任何类或方法都应该完成一个单一的功能,只能是一个单一的功能。如果你想为类和方法给出有意义的名字,那么它们需要有一个唯一的较好定义的目的。如果一个单一类从数据库读和写、计算营业税、通知交易客户并生成账单,那么你就可能无法给出合适的名字。我常常停留在重构类上,因为我总是努力取一个足够短的名字,以描述它做的每个功能。为了更多地讨论SRP和其它面向对象原则,可以参考我的博文《面向对象设计》。

适当的注释

如果因为某种原因,你不能让代码变得清晰,你同情将来的自己,需要不得不做些事情,那就留下注释来说明你为什么不得不那样做。注释倾向于快速地变得陈旧,因此我宁愿尽可能让代码自描述,注释用来说明为什么你不得不那样做,而不是它如何做。

问题3,没有足够的组块

心理学上的组块被定义是,把信息组块定位为单一的实体。那么这该如何应用到编程上呢?作为一名开发者,在你积累经验时,你开始发现你解决方案里反复出现的模式。极具影响的设计模式:《可重用的面向对象软件》是第一本整理和解释一些模式的书。尽管如此,组块不仅仅用在设计模式和面向对象。在函数式编程(FP)里,存在大量的著名标准函数具备这同样的目的。算法是组块的另一种形式(后续会更多)。

当你合理地使用组块(设计模式、算法和标准函数)时,它让你停下来思考,你编写的代码是如何运行的、而不是考虑它做了什么。这缩短了你的语义模型(你的代码)和句法模型(你脑子里的模型)的距离。这个距离越短,你就越容易重建你的心智模型。

如果你有兴趣了解更多FP里的函数,请移步到我的文章面向web开发者的函数式编程

问题4,费解的用法

目前,我们主要讨论了如何结构化你的类、方法和变量命名。心智模型的另一个重要部分是理解这些方法应该怎样被使用。再次强调,当你最初形成心智模型时,这是相当清晰的。当你后来返回时,就非常难以重建你的类和方法的、所有有意图的用法了。通常这是因为不同的用法散布在你的程序其它地方。有时候甚至出现在很多不同的项目中。

我就是在这种情况下发现测试用例是非常有用的。除了相应地知道一个修改是否破坏了代码的明显好处,测试为你的代码提供了一整套的用例。你不必搜遍100个文件,只需看测试就能得到引用的全景。

注意为了达到这个目的,你需要有一整套完整的测试用例。如果你的测试仅仅覆盖了一部分、而你认为测试是完整的,那么你后来将陷入困境。

问题5,不同的模型之间没有清晰的途径

你的代码从技术角度看,常常是优秀的、非常优雅,但是从程序意图到语义模型、再到代码存在非常不自然的跳跃。考虑你选择的一堆模型的透明性是重要的。从程序意图到语义模型、再到代码的过程需要尽可能平滑。你应当能够看透对应到问题的每个模型的所有方面。多数情况下,最好选择特定类结构或算法不是为了它在隔离方面的优雅,而是可以连接各种模型,为你重建的目的而留下 一条自然的途径。当你从抽象的编程意图走到具体的代码时,你做的选择应该受到 你能够表现更为抽象模型 的清晰度驱使。

问题6,发明算法

作为程序员,我们经常认为,我们在为了解决问题而发明着算法。事实很难是这样的。几乎所有情况下,已经有现成的算法可以被组合在一起解决你的问题了。像最短路径搜索法、字符串相似度算法、粒子群算法等。大部分编程是以正确的组合、选择现存算法来解决你的问题。如果你正在发明新算法,那么,要么你不知道合适的算法、要么你正忙于你的博士论文。

总结

最后总结:作为一名程序员,你的目标是建立能够解决你问题的、尽可能简单的语义模型。把语义模型尽可能靠近地转化为句法模型(代码),尽可能多地提供线索,便于你之后无论哪个人看你的代码,都能重建像你最初脑子里的、相同的语义模型。

设想一下,当你走过你代码的被照亮的森林时,你在身后留了面包屑。相信我,当你需要找到回去的路时,森林将充满了黑暗、朦胧和不详的预感。

听起来容易,实际做起来是很难的。

  • 原文地址:https://medium.com/on-coding/why-your-code-is-so-hard-to-understand-83057c115a2b
  • 注1:心智模型是用于解释个体为现实世界中之某事所运作的内在认知历程。http://zh.wikipedia.org/wiki/心智模型
  • 注2:要通过创建领域模型来加速复杂的软件开发,就需要利用大量最佳实践和标准模式在开发团队中形成统一的交流语言;不仅重构代码,而且要重构代码底层的模型;同时采取反复迭代的敏捷开发方法,深入理解领域特点,促进领域专家与程序员的良好沟通。http://baike.baidu.com/view/3705331.htm
  • 注3:马丁把功能(职责)定义为:“改变的原因”,并且总结出一个类或者模块应该有且只有一个改变的原因。一个具体的例子就是,想象有一个用于编辑和打印报表的模块。这样的一个模块存在两个改变的原因。第一,报表的内容可以改变(编辑)。第二,报表的格式可以改变(打印)。这两方面会的改变因为完全不同的起因而发生:一个是本质的修改,一个是表面的修改。单一功能原则认为这两方面的问题事实上是两个分离的功能,因此他们应该分离在不同的类或者模块里。把有不同的改变原因的事物耦合在一起的设计是糟糕的。http://zh.wikipedia.org/wiki/单一功能原则

为什么你的代码如此难以理解,首发于博客 - 伯乐在线

20 Nov 10:36

科技创业公司的效率工具箱

by chosendai

如何保持效率,不仅仅是个人的挑战,这对一个组织或公司而言也至关重要,这也与公司的信息流动和沟通交流密切相关。在科技型创业公司,效率会变得更致命,因为这些公司通常都运作得非常快,而且还常常面临着巨大的压力(例如产品发布日期,资金的运转等)。

所以,这就需要在创业公司所有成员之间要保持高度的一致性,去确保产品适应市场,后期的平稳运行,和公司的整体透明度。

在 Worktile 这一年的创业旅程中,我们遇到很多值得学习的优秀产品,我们也希望将Worktile打造成一款优秀的效率工具,再这里,我们希望将我们这一年的经验和我们认为非常值得使用的工具分享给大家。

在我们开始之前,我们首先要明确典型的科技初创公司的几点需求:基本上,一般有四种需求:

1.团队内部以及与外部的沟通需求;

2.用于跟踪和协调的工作流程和任务的需要;

3.提升协作效率的需求;

4.扁平化和足够的透明度。

5.Toolbox for Tech Startup

 

 

  • Slack for 沟通好吧,我承认,最近快要被 Slack 各种叼爆天的新闻刷屏了。但追求极致,简单干脆满足人类沟通需求的 Slack 确实太有魅力。在Slack中你可以跟你的团队成员进行高效的内部沟通,还可以对团队成员以及各成员分享的文件,文档进行搜索。你可以创建一些基于你们需要讨论各种话题的工作和任务,也可以有专门的主题内容,如聚会,开发,设计等等。除了这些,Slack 还整合了 Twitter、Zendesk、Crashlytics 和 Heroku 等服务,将它们的通知提醒、Bug 追踪等数据融入到公司内的信息流中。让整个公司的内部交流只关注于跟工作有关的信息流,极大地提高了沟通效率。就像 Slack 的 Slogan 一样:Be less busy!

 

  • 文件存储的 Dropbox Dropbox 在存储方面展现的魅力已无需多言(对 Tech Startup 来说,被墙完全不是个问题好吧)全世界数以亿计的用户正在使用Dropbox,这可以让我们在电脑、手机、平板,甚至服务器上同步、备份或共享文件资料。文件的管理与共享在团队效率方面有着最明显的表现,版本控制,文件查找再也不会成为绊脚石了。

 

  • 针对中小团队的团队协同工具 WorktileWorktile,正是我远程实习的一家科技创业公司,是面向中小团队的协同工具。提倡通过任务驱动来连接团队在执行项目的过程中的事和事,人和事,人和人。在实习过程中,我们之间的任务分配和协调工作都是在 Worktile 上完成的。在 Worktile ,你可以根据公司不同的部门创建不同的项目,如“店小二”的运营部,苦逼的开发部等。得益于 Worktile 的看板式任务列表和不同层级权限管理机制,我们的工作效率得到大大的提升,知道Worktile每两周甚至每周就升级版本的秘诀么?就是Worktile带给我们的:不知不觉就提高了工作效率。如果正打算使用Worktile,那么,上吧!你将享受到一种前所未有的协同工作方式,就算是异地办公也是毫无压力(PS:我就是异地办公)。还有一点就是,Worktile有一个非常有爱的用户社区,你可以在社区提一些建议或者分享一些关于协同工作的干货等,而一旦你在社区发帖,我们Worktile的官方人员会竭诚为你“秒回”!这个得点32个赞!快来点赞

 

  • 可爱的大象头 Evernote在我看来是一个更偏向于个人使用的工具,我们团队中很多人也是它的付费用户,主要是负责管理一些私人的日记和记录一些牛逼的灵感,在产品上,Evernote 的极致追求我们作为用户是切实的感受到了!我最初使用Evernote是在2012年,当时Evernote已经宣布对所有用户提供最基础免费空间和功能。而在全平台支持的Evernote上,我一般使用的是Web版,因为对于我个人来说,我使用Evernote的时候无非就是编辑一下日记而已,没用上很多Evernote在微信推送的高大上功能,所以我基本是懒得下PC端,直接在Web上使用就很完美。

 

  • 在线作图工具 ProcessOnProcessOn是国内目前做得比较好的在线作图协作平台。在创业的过程中,难免会有一些图表,像流程图,图表式的Roadmap等,既然我们的重心是在产品上,那么我向你推荐ProcessOn,它可以快速地制作出你想要的各种图。而且,ProcessOn还支持协作画图,你可以跟你的小伙伴们一边作图,一边沟通交流。这不是跟我们倡导的理念一样么?哇哈哈。

 

  • 大百度的思维导图 百度脑图百度脑图的功能不算很多,但足够日常使用了,我通常是在总结工作和理清代码思路的时候用它。优点蛮多:支持随处拖拽,云储存,不占用硬盘空间,一键分享给你们的小伙伴,你甚至还可以导入百度的Doc文件……而其中最赞的功能莫过于百度脑图支持多格式保存文件,你既可以到出成「.xmind」格式,也可以导出成「.mm」格式,也可以导出成「.km」格式……这样导出的思路脑图就可以在你的电脑客户端完美进行在编辑了,如果你需要的话。(大百度也还是有如此精致的产品嘛)

 

  • PDF Word Excel转换 Convertii我只是默默将这个链接分享给你。这样的工具有很多,但首先还是推荐这个,界面简洁,支持直接拖拽文件上传等,各种用户体验都非常好,你可以「PDF转Word」,还可以转Excel,转Text等。你不用安装任何Adobe Acrobat XI Pro或者类似的软件了,直接将文件拖入到这个网页的就可以见证奇迹了。Yeah~~~

 

  • 数据统计可视化工具 infogr.aminfogr.am 是一个很强大的数据可视化图表创作平台,这是我老大@小泽马君给我推荐的工具,你可以在上面创建很多种表格,包括常见的圆饼图,条形图,柱状图和一些炫酷吊炸天信息图表(一共14种)。还在为数据可视化烦恼?小白不懂D3.js?不用怕,现在你只要注册一个 infogr.am账号可以分分钟创作出各种亮瞎的数据图表了。这对于一些产品经理来说不就是一个开挂的工具么?不过……它不是免费的,你可能需要每月交上几美元。

 

  • Defonic for Relax

    说了这么多生产工具,最后分享一个让你放松的网站:Defonic ,我通常会在工作累了的时候或者有时候直接一边写东西一边放背景音乐。这是一个能产生环境音效的线上服务,公提供24种不同的环境声音,包括海洋、河流、雨滴、森林、篝火、闪电、风声、夜晚、浪涛等等,你无需下载或暗转任何软件,只要将网页打开,点击你想播放的声音按钮就行。
    Tips:Defonic 默认情况下是不显示背景的高清大图的,你需要点击右上的HD小按钮才会出现如此的身临其境的图片。

    整体来说,Defonic 提供的音效品质相当好,即使有背景音乐搭配播放,也不会有很吵杂、难以忍受的感觉。强烈推荐各种设计师,工程师到上面放松自己

     

    痛并快乐着

    对于怀揣着梦想的创业者们,大家享受着这旅程上的一切惊奇与喜悦,也承担着巨大压力与质疑。我们希望将这些实用的工具分享给大家,多少可以减轻些工作中的负担,减少重复性工作,将精力放到更加重要的或者我们更享受的事情上,拼尽全力的去追寻各自心中的秘宝!

科技创业公司的效率工具箱,首发于博客 - 伯乐在线

20 Nov 08:16

成为谷歌软件工程师,你需要准备什么?

by 青劲草

【伯乐在线导读】:本文源自 Quora 同名问答贴。Google 程序员 Gaurav Jha 的回答获得了 8000+ 顶。他从谷歌员工角度给出了 6 个重要的建议,并且推荐了很多学习资源。数学是成就卓越开发人员的必备技能,文章最后一部分是为准程序员推荐的数学课程。

谷歌员工眼中的 6 个关键点

  • 在我向你提供课程列表前,先读第一二点
  • 全职工作人员——基于你的行业经验和学术背景去选择性的看待这个回答
  • 准大学生——请直接跳到第七点

标记说明:

  • # 可选的
  • **必须的

#第一点:让我们回到这个问题本身,也即是如何准备才能让自己成为“优秀”的软件工程师?

是的!这个问题的剩余部分都是可选的。加入谷歌不是登月计划。任何优秀的软件工程师都有好机会成为谷歌工作文化的一部分。问题是你如何定义“优秀”。


**第二点:调整态度

在你给谷歌招聘人员留下深刻印象之前,让我们来看看谷歌的软件工程师这一角色是否是你真正想要的。

软件工程师并不是像普遍观念所说的那样有趣。除了用户界面和用户体验的职位,通常来说,不管你用什么文字编辑器——Eclipse、Vim或者Emacs——你的屏幕将是黑的,无聊和枯燥的。全职的软件工程师,不仅需要有从事复杂算法的能力,还需要足够的包容和耐心来一丝不苟地关注大型程序上的细节。

在谷歌,通常大多数软件工程师的角色主要是处理数学问题。你掌握了多少种语言或者你把Java、C、C++等玩得多溜是无所谓的。

重要的是这个四个目标:

  • 你创造有效算法的能力
  • 你阅读别人写的代码以及发现其中存在任何问题的细心品质
  • 你的学习和实现新技术趋势并且适应需求的好奇心
  • 最后也是最重要的:你创造了什么,如何创造的?

我必须之处要实现以上四个目标并不容易。我们大多数谷歌员工都有一段苦逼时间来达到这些目标,但是我们尝试过,所以你也应该去尝试。

每个人都有不同的学习方法。于我,我每天读一篇我在Quora链接上找到的研究论文(可能和也可能不和计算机科学有关),一篇谷歌的研究文章(内部记录)。

一旦你加入谷歌,将可以访问所有的代码库,数据库,论坛,研究论文和一些能给你学习时提供巨大帮助而你却无法在维基百科上找到的项目。但当你在为加入谷歌而准备路上时,有几样事情在学习的过程中很常见。在第五点中,你将会更多地了解到——怎样来实现这四个目标——但在这之前有些前提还是需要看看的。所以,我们进入下一点吧。也即是:

#第三点: 2014年技术发展指南——来自谷歌

作为一个成功的软件工程师,有着扎实的计算机基础是很重要的。对于大学生,通过自我把握节奏地亲身实践学习,来专业性地或者非专业性地培养他们的技术能力,跟随这份谷歌指南是一条建议路径。

  • 请自己权衡使用这份指南
  • 这份指南之外可能也有你想学或者想做的其他东西——尽管去做吧!

**第四点:对专业性学习的建议

  • 计算机科学入门课程

注:计算机科学的入门课能提供编程的一些指导。

在线资源:

Udacity – intro to CS course,
Coursera – Computer Science 101

*译者注:这些在线资源大都是英文授课,因此没有翻译课程名(下同),另外作为程序员英语必须得好啊,可以参看伯乐在线老码农写的《老码农教你学英语一文。

  • 至少用一种面向对象的编程语言写代码:C++,Java,或者Python

初学者在线资源:

  • 学习其他编程语言

注:可以将这些语言加到你的仓库里:Java Script, CSS, HTML, Ruby, PHP, C, Perl, Shell. Lisp, Scheme.

在线资源:w3school.com – HTML教程*, Learn to code

  • 测试你的代码

注:学会如何跟踪bugs,创建测试,并且破坏你的软件

在线资源: Udacity – Software Testing MethodsUdacity – Software Debugging

  • 培养逻辑思维和积累离散数学知识

在线资源:

MIT Mathematics for Computer Science,
Coursera – Introduction to Logic,
Coursera – Linear and Discrete Optimization,
Coursera – Probabilistic Graphical Models,
Coursera – Game Theory.

译者注:coursera课程大多都有中文字幕,对于学习语言门槛会降低,不过仍推荐学习原版课程。

  • 培养算法和数据结构的深刻理解能力

注:了解一些基本数据类型(栈、队列和包),排序算法(快排、合并排序、堆排序)和数据结构(二分查找、红黑树、哈希表),大O表示法等

在线资源:

MIT Introduction to Algorithms,
Coursera – Introduction to Algorithms Part 1 & Part 2,
Wikipedia - List of Algorithms,
Wikipedia - List of Data Structures,
Book: The Algorithm Design Manual

  • 培养对操作系统的深刻理解能力

在线资源:UC Berkeley Computer Science 162*

*译者注:这个链接是YouTube上的,国内有很多电驴的资源,亲测可用(如果找不到可用链接,译者可提供)

  • 学习人工智能的知识

在线资源:Stanford University - Introduction to RoboticsNatural Language ProcessingMachine Learning*

*译者注:斯坦福Andrew Ng的这门机器学习课程强烈推荐,译者也有大量该课程资源。

  • 学习如何构造编译器

在线资源:Coursera – Compilers*

*译者注:这门课程也是相当实用,最好跟着可能动手完成课程的编译器项目。

  • 学习密码学

在线资源:Coursera – CryptographyUdacity – Applied Cryptography

  • 学习并行编程

在线资源:Coursera – Heterogeneous Parallel Programming

**第五点:对非专业性学习建议

  • 参与课堂之外的项目

注:创建和维护一个网站,构建你自己的服务器,或者做一个机器人。

在线资源:Apache List of ProjectsGoogle Summer of Code,Google Developer Group

  • 参与大系统(代码库)中的小代码片段,阅读和理解已有的代码,查文档并且跟踪调试。

注:用GitHub来阅读别人的代码或者去贡献一个项目是一种很好的方式。

在线资源:GithubKiln

  • 和其他程序员一起参与项目

注:这将会帮你提高在团队工作的能力,也使你能够向他人学习。

  • 锻炼你的算法知识和编程能力

注:通过像CodeJam或者ACM ICPC这些编程竞赛来锻炼你的算法知识

在线资源: CodeJamACM ICPC*

*译者注:国内有很多OJ也可以起到这个作用,比如POJ、九度等

  • 成为一个助教

注:帮助教其他学生将会有助于增加你在这个学科的知识

  • 软件工程方面的实习经历

注:确保你在实习招聘期来临前申请了实习工作。在印度和美国,实习期在暑假,5至9月份,而申请通道通常提前几个月就打开了。

在线资源:google.com/jobs

#第六点:谷歌推荐/赞助的项目和团队

在你兴趣领域内,你可能选择订阅的课程很少。这些课程和项目是非常好的学习地方,但他们不会增加或减少你进谷歌的机会——他们不等于实习。(更多信息来自 Robert Love

  1. 谷歌课程——Making Sense of Data

这门自我把握节奏的在线课程是为任何想学习更多关于结构、可视化、操作数据的人准备的。

2. 谷歌课程——BOLD Discovery

这个为期两天的互动会议将给一二年级的大学生提供一些关于谷歌文化和这家公司职业前景的介绍。

3. 谷歌编程之夏

一个全球在线项目,提供给上完中学且年龄在18以上学生开发者津贴,让他们为各种各样的开源软件项目写代码。

4. 谷歌奖学金政策

这个项目提供学生在暑假期间为互联网工作的机会和在公共利益组织上提出的技术政策。

5. 谷歌学生退伍军人峰会

谷歌学生退伍军人峰会包括为老兵适应工作环境而准备的职业培养课程。也可以参看:Ellen Spertus 在 How can I effectively use my last two years of college to prepare for a great Software Engineering job at Google/FB or a startup? 这个问题上的观点。*

*译者注:这是Quora上Ellen Spertus对另一个问题(我是怎样高效地利用大学最后两年来为谷歌/Facebook或者初创企业的一个很好的软件工程师职位做准备的)的回答。

**第七点:对数学课程的建议

(对准大学生而言)

个人观点:任何忽视这些学科企图将使你进入完全以错误方式学习的平庸程序员的范畴。之前准备的越充分就越使得写代码越享受。这些是软件工程的几个前提,对软件工程你需要去理解算法的精髓。如果软件工程师能够回忆起学校里学的简单知识,大多数他们犯的错误本可以避免的。

在你深入学习数学或者计算机科学中,你将意识到你“大学本科时学过的数据结构”和“研究生时学过的机器学习”的重要性。因此,要想设计复杂的算法,一下是你必须精通的课程清单。对于大学研究生,如果你忽视了这些学科,我强烈建议你应该你能做到的最好的方式去复习它们。

在你学习完学校课本上的介绍性大纲之后,练习下面的课程来加深理解吧。大多数大学生(甚至在职员工)低估了这些课程然后成为了另一类平庸的程序员;

  • 线性代数

1. Linear Algebra | Mathematics | MIT OpenCourseWare (我推荐的)
2. Coding the Matrix: Linear Algebra Through Computer Science Application (同事推荐的)

学习这些会帮你理解后续的回归模型——机器学习基本的一步。任何学校、大学、研究室或者机构都不会教你这些线性代数课程。自己去学吧。

  • 微积分

1. Calculus 1 - Ohio State University
2. Pre-Calculus Courses - Universitat Autonoma de Barcelona
3. Calculus for Beginners and Artists – MIT

  • 统计&概率

注:当你上大学和读研究生时,大纲就会变成机器学习的算法了。对于大多数大学生,机器学习课程的头三个月里数学使他们苦不堪言,而当他们好不容易赶上进度了,大纲又推进到更加机器学习复杂的领域,比如深度学习,神经网络和神经网络流处理。

#准大学生:谷歌推荐/赞助的项目

这个比赛为年龄在13至17岁的准大学生介绍了各种各样使得开源软件开发成为可能的贡献。

谷歌RISE是一种对合伙人的奖励,它奖励旨在促进和支援为全世界的K12*小学生和中学生的STEM*以及计算机教育举措的项目。

*K12,从幼儿园到12年级

*STEM,Science,Technology,Engineering,Math,科学、技术、工程、数学

这个项目使得来自不同社区的中学生可以接触到STEM领域的大学和职业。

编程是种新能力-它承载着创造、创新和文明改造世界的潜能。这个举措旨在激励数以百万计的女孩体验代码的魔力。

这个为期一天的项目旨在为优秀的高年级中学生在上大学前提供有价值的商业技巧。

先驱者项目是一个全球性的网络,这里聚集着领导者、倡导者和计算机教育大使,大使们负责让全球的青年和教育工作者通过计算机科学被联系和激励。

谷歌科技博览会是对13到18岁青少年开放的全球性竞赛。学生在线提交项目并得到赢得大奖的机会。

CSSI是一个为期三周的暑期项目,针对即将进入大学且对学习计算机科学感兴趣的新生而设。

30天的DIY和制作活动。Maker Camp是一个在Google+上对所有人开放的免费虚拟暑期露营活动

在Google+ 上和老师、教育组织协作来为K12学生提供经验性的学习机会。

成为谷歌软件工程师,你需要准备什么?,首发于博客 - 伯乐在线

20 Nov 00:57

Deep Learning Tutorial [pdf]

20 Nov 00:56

Cache is the new RAM

20 Nov 00:30

Java不为人知的10个真相

你是不是一开始就用Java来编程的呢?还记得当年它还被称为"Oak",OO还是热门的话题,C++的用户觉得Java没有前景,applets还只是个小玩意,菊花也还是一种花的时候吗?

我敢打赌下面至少有一半是你不清楚的。这周我们来看一下跟Java的内部实现相关的一些神奇的事情。

1. 其实根本没有受检查异常这回事

没错!JVM压根儿就不知道有这个东西,它只存在于Java语言里。

如今大家都承认受检查异常就是个错误。正如Bruce Eckel最近在布拉格的的GeeCON会议上所说的,除了Java外没有别的语言会使用受检查异常这种东西,即使是Java 8的新的Streams API中也不再使用这一异常了(不然当你的lambda表达式中用到IO或者JDBC的话就得痛苦死了)。

如何能证实JVM确实不知道这个异常?看下下面这段代码:

public class Test {
  
    // No throws clause here
    public static void main(String[] args) {
        doThrow(new SQLException());
    }
  
    static void doThrow(Exception e) {
        Test.<RuntimeException> doThrow0(e);
    }
  
    @SuppressWarnings("unchecked")
    static <E extends Exception> 
    void doThrow0(Exception e) throws E {
        throw (E) e;
    }
}

这不仅能通过编译,而且也的确会抛出SQLException异常,并且完全不需要用到Lombok的@SneakyThrows注解。

更进一步的分析可以看下这篇文章,或者Stack Overflow上的这个问题

2. 不同的返回类型也可以进行方法重载

这个应该是编译不了的吧?

class Test {
    Object x() { return "abc"; }
    String x() { return "123"; }
}

是的。Java语言并不允许同一个类中出现两个重写等价("override-equivalent")的方法,不管它们的throws子句和返回类型是不是不同的。

不过等等。看下Java文档中的 Class.getMethod(String, Class...)是怎么说的。里面写道:

尽管Java语言不允许一个类中的多个相同签名的方法返回不同的类型,但是JVM并不禁止,所以一个类中可能会存在多个相同签名的方法。这添加了虚拟机的灵活性,可以用来实现许多语言特性。比如说,可以通过bridge方法来实现协变返回(covariant return,即虚方法可以返回子类而不一定得是基类),bridge方法和被重写的方法拥有相同的签名,但却返回不同的类型。

哇,这倒有点意思。事实上,下面这段代码就会触发这种情况:

abstract class Parent<T> {
    abstract T x();
}
 
class Child extends Parent<String> {
    @Override
    String x() { return "abc"; }
}

看一下Child类所生成的字节码:

// Method descriptor #15 ()Ljava/lang/String;
// Stack: 1, Locals: 1
java.lang.String x();
  0  ldc <String "abc"> [16]
  2  areturn
    Line numbers:
      [pc: 0, line: 7]
    Local variable table:
      [pc: 0, pc: 3] local: this index: 0 type: Child
 
// Method descriptor #18 ()Ljava/lang/Object;
// Stack: 1, Locals: 1
bridge synthetic java.lang.Object x();
  0  aload_0 [this]
  1  invokevirtual Child.x() : java.lang.String [19]
  4  areturn
    Line numbers:
      [pc: 0, line: 1
      

在字节码里T其实就是Object而已。这理解起来就容易多了。

synthetic bridge方法是由编译器生成的,因为在特定的调用点Parent.x()签名的返回类型应当是Object类型。如果使用了泛型却没有这个bridge方法的话,代码的二进制形式就无法兼容了。因此,修改JVM以支持这个特性貌似更容易一些(这顺便还实现了协变返回),看起来还挺不错 的吧?

你有深入了解过Java语言的规范和内部实现吗?这里有许多很有意思的东西。

3. 它们都是二维数组!

class Test {
    int[][] a()  { return new int[0][]; }
    int[] b() [] { return new int[0][]; }
    int c() [][] { return new int[0][]; }
}

是的,没错。也许你无法马上说出上述方法的返回类型是什么,但它们的确都是一样的!同样的还有下面这段代码:

class Test {
    int[][] a = ;
    int[] b[] = ;
    int c[][] = ;
}

你是不是觉得这有点疯狂?相像一下如果再用上JSR-308/Java 8中的类型注解吧。各种写法数不胜数。

@Target(ElementType.TYPE_USE)
@interface Crazy {}
 
class Test {
    @Crazy int[][]  a1 = ;
    int @Crazy [][] a2 = ;
    int[] @Crazy [] a3 = ;
 
    @Crazy int[] b1[]  = ;
    int @Crazy [] b2[] = ;
    int[] b3 @Crazy [] = ;
 
    @Crazy int c1[][]  = ;
    int c2 @Crazy [][] = ;
    int c3[] @Crazy [] = ;
}
类型注解。神秘之极,强大之极。

或者这么说:

亲爱的同事,提交完这段代码下月我就要休假了:

这些写法到底有什么用,这个就留给你自己慢慢探索吧。

4. 你根本就不懂条件表达式

那么,你以为条件表达式你就很了解了吗?我告诉你吧,你压根就不懂。很多人都认为下面两段代码是一样的:

Object o1 = true ? new Integer(1) : new Double(2.0);

它和下面这个是一样的吧?

Object o2;
 
if (true)
    o2 = new Integer(1);
else
    o2 = new Double(2.0);
    
    

不是的。我们来测试下。

System.out.println(o1);
System.out.println(o2);

这段代码会输出:

1.0
1

没错,条件操作符在"必要"的时候会进行数值类型的提升,这个“必要”得加上一个重重的引号。你能想到下面这段程序居然会抛出一个空指针异常吗?

Integer i = new Integer(1);
if (i.equals(1))
    i = null;
Double d = new Double(2.0);
Object o = true ? i : d; // NullPointerException!
System.out.println(o);

想了解更多请参考这里

5. 你也不懂复合赋值操作符

很奇怪吧?我们再来看下这两段代码:

i += j;
i = i + j;

显然他们都是一样的嘛。不过,其实不然。Java语言规范中是这么说的:

E1 op= E2形式的复合赋值表达式等价于E = (T)((E1) op (E2)),这里的T的类型是E1,E1仅会进行一次求值。

这太奇妙了。我想引用一下Peter Lawre在Stack Overflow上面的一个回答:

这种类型的类型转换通过*=或者/=可以很容易地说明:

byte b = 10;
b *= 5.7;
System.out.println(b); // prints 57

或者

byte b = 100;
b /= 2.5;
System.out.println(b); // prints 40

或者

char ch = '0';
ch *= 1.1;
System.out.println(ch); // prints '4'

又或者:

char ch = 'A';
ch *= 1.5;
System.out.println(ch); // prints 'a'

那么这么做到底有什么用?你可以在你的程序里试一下char类型的类型转换和乘法操作。因为你懂的,这够装逼。。

6. 随机整数

这更像是一个脑筋急转弯。先不要着急看答案。看你能不能自己搞定。当运行如下这段程序时:

for (int i = 0; i < 10; i++) {
  System.out.println((Integer) i);
}

“偶尔”它会输出这样的结果:

92
221
45
48
236
183
39
193
33
84

这特么可能么?

. . . . . . . . .

要剧透了,答案就要来了。。

. . . . . . . .

好吧,答案在这里,你得通过反射重写掉JDK的Integer里面的缓存,然后再使用自动装箱和拆箱的功能。不要真的这么做!换句话说,我们再强调一次:

亲爱的同事,提交完这段代码,我下月就休假了哦(你看不懂可别怪我):

image

7. GOTO

这是我最津津乐道的。Java其实也有GOTO!敲下代码看看

int goto = 1;

它的输出是

Test.java:44: error: <identifier> expected
    int goto = 1;
    

这是因为goto是一个预留的关键字,万一以后有用呢。。。

不过这还不是重点。有意思的是你可以通过break,continue以及标签块来实现goto的功能:

跳转到前面:

label: {
  // do stuff
  if (check) break label;
  // do more stuff
}

它的字节码是:

2  iload_1 [check]
3  ifeq 6          // Jumping forward
6  ..

跳转到后面:

label: do {
  // do stuff
  if (check) continue label;
  // do more stuff
  break label;
} while(true);

它的字节码是:

 2  iload_1 [check]
 3  ifeq 9
 6  goto 2          // Jumping backward
 9  ..
 
 

8.Java也有类型别名

在其它语言中(比如说Ceylon,定义类型别名非常简单:

 
interface People => Set<Person>;

这样所定义出来的People类型可以和Set交替使用:

People?      p1 = null;
Set<Person>? p2 = p1;
People?      p3 = p2;

Java中无法在最外层实现类型别名。不过我们可以在类或者方法的作用域内来实现这点。假设我们现在觉得Integer, Long的名字看着不爽了,希望能更简短些:I以及L。这很简单:

class Test<I extends Integer> {
    <L extends Long> void x(I i, L l) {
        System.out.println(
            i.intValue() + ", " +
            l.longValue()
        );
    }
}

上面这段程序中,在Test类的作用域内,Integer的别名是I,而在x方法内,Long的是L。我们可以这样来调用上面的方法:

new Test().x(1, 2L);

这当然不是什么正式的别名的方式。在本例中,Integer和Long都是final类型,也就是说类型I和L就是真正意义上的别名(算是吧。因为它只有一种赋值相容(assignment-compatibility)的方式)。如果你使用的是非final类型时(比如说Object),那你用的其实就是泛型而已了。

这种雕虫小技就先说到这吧。现在我们来讲点有意义的!

9. 某些类型关系是无法确定的

好吧,这个就有点费脑了,先喝杯咖啡集中下精神吧。假设有下面两种类型:

// A helper type. You could also just use List
interface Type<T> {}
 
class C implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}

那么,C类型和D类型到底是什么?

这看起来是有点像递归了,java.lang.Enum看起来也是类似的递归(尽管略有不同)。看下它的定义:

public abstract class Enum<E extends Enum<E>> { ... }

知道了上面这个规范之后,我们就明白了,枚举的实现其实不过是个语法糖罢了:

// This
enum MyEnum {}
 
// Is really just sugar for this
class MyEnum extends Enum<MyEnum> { ... }

记住这点,我们再回来看一下这两个类型。下面这段代码能通过编译吗?

class Test {
    Type<? super C> c = new C();
    Type<? super D<Byte>> d = new D<Byte>();
}

这个问题有点难,不过Ross Tate曾经回答过它。这个问题其实是无解的:

C是Type<? super C>的子类吗?

Step 0) C Step 1) Type>

还有

D是Type<? super D>的子类型吗?

Step 0) D > Step 1) Type>>> > Step 2) D >> Step 3) List>> > Step 4) D> >> Step . . . (expand forever)

试试在你的Eclispe里编译一下,这会让它崩溃的!(别担心,这个BUG我已经提交了)

要想彻底搞清楚这点。。。

Java中的某些类型是不确定的

如果你对Java的这个罕见的奇怪行为感兴趣,可以读下Ross Tate的这篇论文“如何驾驭Java类型系统中的通配符”(与Alan Leung及Sorin Lerner共同发表),或者看下我们的几点愚见“论子类型多态与泛型多态的关联关系”

10. 类型交集

Java有一个非常古怪的特性叫做类型交集(虽然乍看上去有点像并集)。你可以声明一个泛型,它其实是两个类型的交集。比如说:

class Test<T extends Serializable & Cloneable> {
}

绑定到Test类中的这个泛型参数T必须同时实现Serializable接口以及Cloneable接口。比方说,String就没有同时实现这两个接口,但Date是:

// Doesn't compile
Test<String> s = null;
 
// Compiles
Test<Date> d = null;

Java 8中也用到了这一特性,你可以将某个类型转换成两个类型的交集。这有什么用?好像是没啥用,但是如果你要把一个lambda表达式强转成这样一个类型的话,除此之外就别无它法了。假设你的方法里面有这么一个看似疯狂的约束条件:

<T extends Runnable & Serializable> void execute(T t) {}

你希望Runnable对象同时还实现了Serializable接口,这样你既可以去执行它,也可以将它发送出去。Lambda表达式和序列化?这看上去有点基情。

Lambda表达式其实是可以进行序列化的

如果Lambda表达式的目标类型和参数都是可序列化的,那么它也是可序列化的。

尽管这是可以实现的,但它并没有直接实现Serializable这个接口。为了能适配成这个类型,你必须得进行类型转换。但是如果你只转成Serializable了的话:

execute((Serializable) (() -> {}));

那么这个Lambda表达式就不是一个Runnable类型了。

那么,

就只能转成两个类型了:

execute((Runnable & Serializable) (() -> {}));

原创文章转载请注明出处:Java不为人知的10个真相

英文原文链接

19 Nov 00:30

UDP有多不可靠?

by honpey

最近我意识到了一件事:我实际上对UDP一无所知。好吧,我知道它是无连接的,没有三次握手过程,所以它对传输的质量不作任何保证。但是,在实际工程应用时,UDP的这些特征意味什么呢?

我启用了5个VPS(虚拟专用服务器,译者注),在7个小时相互发送UDP包,不过网络负载并不大(不过可以尝试下加大负载的情况)。每台服务器,每9-11秒就会随机地接收一个包并且发送5-10个包,包的大小从16到1016字节不等。

其中两个服务器位于新泽西州(NJ)的同一个数据中心,其余三台分别位于洛杉矶(LA)、阿姆斯特丹(NLD)和东京(JPN)。

可靠性分析

我想知道的第一件事是UDP到底有多不可靠。看到下表,我很好奇,我们是在讨论25%,50%,75%的传送率吗?

包的接收数目

 

接收方

 

NJ 1

NJ 2

LA

NLD

JPN

NJ 1

- 2981/2981 2888/2889 2964/2964 3053/3054

NJ 2

3016/3016 - 3100/3101 2734/2735 3054/3054

LA

2901/2941 2932/2975 - 2938/2942 2712/2712

NLD

3038/3038 2771/2772 2724/2724 - 2791/2791

JPN

2551/2552 2886/2886 2836/2838 2887/2887 -

包的接收率

 

接收方

 

NJ 1

NJ 2

LA

NLD

JPN

NJ 1

- 100 99.97 100 99.97

NJ 2

100 - 99.97 99.97 100

LA

98.64 98.55 - 99.86 100

NLD

100 99.97 100 - 100

JPN

99.96 100 100 100 -

这些数据远超过我的预期。我原以为从NLD—JPN一线会有明显超出均值的丢包,但是事实并不是这样的。反而是从LA发出、传送到NJ的数据有些异常。原因何在?

首先,我将原因锁定在包的大小。我会使包尽量小(16字节的头,0-1000字节的有效数据):

每种大小的包的丢失个数

0-115 116-215 216-315 316-515 516-715
13 11 12 13 23

没有什么异常。那么,这些包丢失的时间如何分布呢?

不幸的是,我没有保存时间戳啊(Why?!),但是我统计了每一对服务器间丢包时间分布。从LA到NJ2的丢失的所有的43个包中,其中29个包在第1-2分钟内丢失。NJ1的包也大部分在刚开始很短的时间内丢失。

 

排队

我关注的另一个点是排队。

为了探讨这个问题,我们首先要讨论下数组的逆序数。逆序数就是数组中位置顺序与大小顺序相反的一对数。假设现有一个数组10,8,3,7,4,那么你必须要调换8次才能达到正确的顺序,这8次分别是:((10,8),(10,3),(10,7),(10,4),(8,3),(8,7),(8,4),(7,4))。

逆序数

 

NJ 1

NJ 2

LA

NLD

JPN

NJ 1

- 0 2994 2581 4658

NJ 2

0 - 3147 2459 4645

LA

3980 3861 - 3237 4010

NLD

3125 1826 3133 - 4189

JPN

3920 4417 4147 4425 -

不知道你觉得怎样,我不确定这组数据是否有价值。这确实看上去很高,当然,使用UDP的一个很重要的原因是你可以丢弃掉某些包。如果你发送了10000个包,最后一个包先来,之前的9999个包之后才依次到来,那么你就不需要做9999次调换了,直接把那第1个包丢掉即可。

如果我们把比已经处理过的包的号码小的包丢弃会怎样?比如,现在有5个包来了,1,5,4,3,6,7,由于我们已经处理过了5,所以把3和4给丢弃了。那么还剩下几个“good”的包呢?

正常顺序的包的数目

 

NJ 1

NJ 2

LA

NLD

JPN

NJ 1

- 100 52.40 55.94 36.77

NJ 2

100 - 52.47 54.22 38.02

LA

41.72 42.32 - 50.48 39.34

NLD

46.32 59.34 44.79 - 39.27

JPN

980 1083 1141 1087 -

正常顺序的包的比率

 

NJ 1

NJ 2

LA

NLD

JPN

NJ 1

- 100 52.40 55.94 36.77

NJ 2

100 - 52.47 54.22 38.02

LA

41.72 42.32 - 50.48 39.34

NLD

46.32 59.34 44.79 - 39.27

JPN

38.40 37.53 40.20 37.65 -

做一个小小的调整,如果我们将5个包整合到一起,再次使用上面的丢弃算法:

正常顺序的包的数目(包整合之后):

 

NJ 1

NJ 2

LA

NLD

JPN

NJ 1

- 2981 2061 2235 1807

NJ 2

3016 - 2214 2041 1889

LA

1868 1873 - 2066 1720

NLD

2200 2273 1920 - 1712

JPN

60.38 62.51 61.13 59.99 -

正常顺序的包的比率(包整合之后):

 

NJ 1

NJ 2

LA

NLD

JPN

NJ 1

- 100 71.34 75.40 59.17

NJ 2

100 - 71.40 74.63 61.85

LA

63.52 62.96 - 70.22 63.42

NLD

72.42 82.00 70.48 - 61.34

JPN

1541 1804 1735 1732 -

 

结论:

没有长时间的、大量的数据做支撑,很难得到任何结论。然而,上面的数据表明UDP的可靠性还是相当不错的。但是距离越远,遇到的跳变就越多,这也意味着发生不可预知错误的概率就越大,但是如果一切都还OK,距离也不成问题了。

排队机制是个问题。通过整合包,我们发现性能有了很大的提升。在许多场合,排队都不会产生质的影响,除非你在疯狂发包,否则通过一个简单的时间戳和接收端的重排机制,UDP的性能依旧可观。

我会做更多的测试、更长的时间、更多的数据、更多的地点。同时,我还会把UDP和TCP的相关性能做个对比。但是无论如何,我认为,可靠性超出我预期的UDP会成为我工具箱中的一员了。

UDP有多不可靠?,首发于博客 - 伯乐在线

18 Nov 06:53

Java集合总览

by 赖 信涛

这篇文章总结了所有的Java集合(Collection)。主要介绍各个集合的特性和用途,以及在不同的集合类型之间转换的方式。

Arrays

Array是Java特有的数组。在你知道所要处理数据元素个数的情况下非常好用。java.util.Arrays 包含了许多处理数据的实用方法:

  • Arrays.asList:可以从 Array 转换成 List。可以作为其他集合类型构造器的参数。
  • Arrays.binarySearch:在一个已排序的或者其中一段中快速查找。
  • Arrays.copyOf:如果你想扩大数组容量又不想改变它的内容的时候可以使用这个方法。
  • Arrays.copyOfRange:可以复制整个数组或其中的一部分。
  • Arrays.deepEqualsArrays.deepHashCode:Arrays.equals/hashCode的高级版本,支持子数组的操作。
  • Arrays.equals:如果你想要比较两个数组是否相等,应该调用这个方法而不是数组对象中的 equals方法(数组对象中没有重写equals()方法,所以这个方法之比较引用而不比较内容)。这个方法集合了Java 5的自动装箱和无参变量的特性,来实现将一个变量快速地传给 equals() 方法——所以这个方法在比较了对象的类型之后是直接传值进去比较的。
  • Arrays.fill:用一个给定的值填充整个数组或其中的一部分。
  • Arrays.hashCode:用来根据数组的内容计算其哈希值(数组对象的hashCode()不可用)。这个方法集合了Java 5的自动装箱和无参变量的特性,来实现将一个变量快速地传给 Arrays.hashcode方法——只是传值进去,不是对象。
  • Arrays.sort:对整个数组或者数组的一部分进行排序。也可以使用此方法用给定的比较器对对象数组进行排序。
  • Arrays.toString:打印数组的内容。

如果想要复制整个数组或其中一部分到另一个数组,可以调用 System.arraycopy方法。此方法从源数组中指定的位置复制指定个数的元素到目标数组里。这无疑是一个简便的方法。(有时候用 ByteBuffer bulk复制会更快。可以参考这篇文章).

最后,所有的集合都可以用T[] Collection.toArray( T[] a ) 这个方法复制到数组中。通常会用这样的方式调用:

return coll.toArray( new T[ coll.size() ] );

这个方法会分配足够大的数组来储存所有的集合,这样 toArray 在返回值时就不必再分配空间了。

单线程集合

这一部分介绍的是不支持多线程的集合。这些集合都在java.util包里。其中一些在Java 1.o的时候就有了(现在已经弃用),其中大多数在Java 1.4中重新发布。枚举集合在Java 1.5中重新发布,并且从这个版本之后所有的集合都支持泛型。PriorityQueue也在Java 1.5中加入。非线程安全的集合架构的最后一个版本是ArrayDeque ,也在Java 1.6中重新发布了。

List

  • ArrayList:最有用的List集合实现。由一个整形数字或数组存储了集合的大小(数组中第一个没有使用的元素)。像所有的List集合一样,ArrayList可以在必要的时候扩展它的大小。ArrayList访问元素的时间开销固定。在尾部添加元素成本低(为常数复杂度),而在头部添加元素成本很高(线性复杂度)。这是由ArrayList的实现原理——所有的元素的从角标为0开始一个接着一个排列造成的。也就是说,从要插入的元素位置往后,每个元素都要向后移动一个位置。CPU缓存友好的集合是基于数组的。(其实也不是很友好,因为有时数组会包含对象,这样存储的只是指向实际对象的指针)。
  • LinkedListDeque实现:每一个节点都保存着上一个节点和下一个节点的指针。这就意味着数据的存取和更新具有线性复杂度(这也是一个最佳化的实现,每次操作都不会遍历数组一半以上,操作成本最高的元素就是数组中间的那个)。如果想写出高效的LinkedList代码可以使用 ListIterators 。如果你想用一个Queue/Deque实现的话(你只需读取第一个和最后一个元素就行了)——考虑用ArrayDeque代替。
  • Vector:一个带有线程同步方法的ArrayList版本。现在直接用ArrayList代替了。

Queues/deques

  • ArrayDequeDeque是基于有首尾指针的数组(环形缓冲区)实现的。和LinkedList不同,这个类没有实现List接口。因此,如果没有首尾元素的话就不能取出任何元素。这个类比LinkedList要好一些,因为它产生的垃圾数量较少(在扩展的时候旧的数组会被丢弃)。
  • Stack:一种后进先出的队列。不要在生产代码中使用,使用别的Deque来代替(ArrayDeque比较好)。
  • PriorityQueue:一个基于优先级的队列。使用自然顺序或者制定的比较器来排序。他的主要属性——poll/peek/remove/element会返回一个队列的最小值。不仅如此,PriorityQueue还实现了Iterable接口,队列迭代时不进行排序(或者其他顺序)。在需要排序的集合中,使用这个队列会比TreeSet等其他队列要方便。

Maps

  • HashMap:最常用的Map实现。只是将一个键和值相对应,并没有其他的功能。对于复杂的hashCode methodget/put方法有固定的复杂度。
  • EnumMap:枚举类型作为键值的Map。因为键的数量相对固定,所以在内部用一个数组储存对应值。通常来说,效率要高于HashMap
  • HashTable:旧HashMap的同步版本,新的代码中也使用了HashMap
  • IdentityHashMap:这是一个特殊的Map版本,它违背了一般Map的规则:它使用 “==” 来比较引用而不是调用Object.equals来判断相等。这个特性使得此集合在遍历图表的算法中非常实用——可以方便地在IdentityHashMap中存储处理过的节点以及相关的数据。
  • LinkedHashMap :HashMapLinkedList的结合,所有元素的插入顺序存储在LinkedList中。这就是为什么迭代LinkedHashMap的条目(entry)、键和值的时候总是遵循插入的顺序。在JDK中,这是每元素消耗内存最大的集合。
  • TreeMap:一种基于已排序且带导向信息Map的红黑树。每次插入都会按照自然顺序或者给定的比较器排序。这个Map需要实现equals方法和Comparable/ComparatorcompareTo需要前后一致。这个类实现了一个NavigableMap接口:可以带有与键数量不同的入口,可以得到键的上一个或者下一个入口,可以得到另一Map某一范围的键(大致和SQL的BETWEEN运算符相同),以及其他的一些方法。
  • WeakHashMap:这种Map通常用在数据缓存中。它将键存储在WeakReference中,就是说,如果没有强引用指向键对象的话,这些键就可以被垃圾回收线程回收。值被保存在强引用中。因此,你要确保没有引用从值指向键或者将值也保存在弱引用中m.put(key, new WeakReference(value))

Sets

  • HashSet:一个基于HashMapSet实现。其中,所有的值为“假值”(同一个Object对象具备和HashMap同样的性能。基于这个特性,这个数据结构会消耗更多不必要的内存。
  • EnumSet:值为枚举类型的Set。Java的每一个enum都映射成一个不同的int。这就允许使用BitSet——一个类似的集合结构,其中每一比特都映射成不同的enumEnumSet有两种实现,RegularEnumSet——由一个单独的long存储(能够存储64个枚举值,99.9%的情况下是够用的),JumboEnumSet——由long[]存储。
  • BitSet:一个比特Set。需要时常考虑用BitSet处理一组密集的整数Set(比如从一个预先知道的数字开始的id集合)。这个类用 long[]来存储bit
  • LinkedHashMap:与HashSet一样,这个类基于LinkedHashMap实现。这是唯一一个保持了插入顺序的Set
  • TreeSet:与HashSet类似。这个类是基于一个TreeMap实例的。这是在单线程部分唯一一个排序的Set

java.util.Collections

就像有专门的java.util.Arrays来处理数组,Java中对集合也有java.util.Collections来处理。

第一组方法主要返回集合的各种数据:

  • Collections.checkedCollection / checkedList / checkedMap / checkedSet / checkedSortedMap / checkedSortedSet:检查要添加的元素的类型并返回结果。任何尝试添加非法类型的变量都会抛出一个ClassCastException异常。这个功能可以防止在运行的时候出错。//fixme
  • Collections.emptyList / emptyMap / emptySet :返回一个固定的空集合,不能添加任何元素。
  • Collections.singleton / singletonList / singletonMap:返回一个只有一个入口的 set/list/map 集合。
  • Collections.synchronizedCollection / synchronizedList / synchronizedMap / synchronizedSet / synchronizedSortedMap / synchronizedSortedSet:获得集合的线程安全版本(多线程操作时开销低但不高效,而且不支持类似putupdate这样的复合操作)
  • Collections.unmodifiableCollection / unmodifiableList / unmodifiableMap / unmodifiableSet / unmodifiableSortedMap / unmodifiableSortedSet:返回一个不可变的集合。当一个不可变对象中包含集合的时候,可以使用此方法。

第二组方法中,其中有一些方法因为某些原因没有加入到集合中:

  • Collections.addAll:添加一些元素或者一个数组的内容到集合中。
  • Collections.binarySearch:和数组的Arrays.binarySearch功能相同。
  • Collections.disjoint:检查两个集合是不是没有相同的元素。
  • Collections.fill:用一个指定的值代替集合中的所有元素。
  • Collections.frequency:集合中有多少元素是和给定元素相同的。
  • Collections.indexOfSubList / lastIndexOfSubList:和String.indexOf(String) / lastIndexOf(String)方法类似——找出给定的List中第一个出现或者最后一个出现的子表。
  • Collections.max / min:找出基于自然顺序或者比较器排序的集合中,最大的或者最小的元素。
  • Collections.replaceAll:将集合中的某一元素替换成另一个元素。
  • Collections.reverse:颠倒排列元素在集合中的顺序。如果你要在排序之后使用这个方法的话,在列表排序时,最好使用Collections.reverseOrder比较器。
  • Collections.rotate:根据给定的距离旋转元素。
  • Collections.shuffle:随机排放List集合中的节点,可以给定你自己的生成器——例如 java.util.Random / java.util.ThreadLocalRandom or java.security.SecureRandom
  • Collections.sort:将集合按照自然顺序或者给定的顺序排序。
  • Collections.swap:交换集合中两个元素的位置(多数开发者都是自己实现这个操作的)。

并发集合

这一部分将介绍java.util.concurrent包中线程安全的集合。这些集合的主要属性是一个不可分割的必须执行的方法。因为并发的操作,例如addupdate或者checkupdate,都有一次以上的调用,必须同步。因为第一步从集合中组合操作查询到的信息在开始第二步操作时可能变为无效数据。

多数的并发集合是在Java 1.5引入的。ConcurrentSkipListMap / ConcurrentSkipListSetLinkedBlockingDeque是在Java 1.6新加入的。Java 1.7加入了最后的 ConcurrentLinkedDequeLinkedTransferQueue

Lists

  • CopyOnWriteArrayList:list的实现每一次更新都会产生一个新的隐含数组副本,所以这个操作成本很高。通常用在遍历操作比更新操作多的集合,比如listeners/observers集合。

Queues/deques

  • ArrayBlockingQueue:基于数组实现的一个有界阻塞队,大小不能重新定义。所以当你试图向一个满的队列添加元素的时候,就会受到阻塞,直到另一个方法从队列中取出元素。
  • ConcurrentLinkedDeque / ConcurrentLinkedQueue:基于链表实现的无界队列,添加元素不会堵塞。但是这就要求这个集合的消费者工作速度至少要和生产这一样快,不然内存就会耗尽。严重依赖于CAS(compare-and-set)操作。
  • DelayQueue:无界的保存Delayed元素的集合。元素只有在延时已经过期的时候才能被取出。队列的第一个元素延期最小(包含负值——延时已经过期)。当你要实现一个延期任务的队列的时候使用(不要自己手动实现——使用ScheduledThreadPoolExecutor)。
  • LinkedBlockingDeque / LinkedBlockingQueue:可选择有界或者无界基于链表的实现。在队列为空或者满的情况下使用ReentrantLock-s
  • LinkedTransferQueue:基于链表的无界队列。除了通常的队列操作,它还有一系列的transfer方法,可以让生产者直接给等待的消费者传递信息,这样就不用将元素存储到队列中了。这是一个基于CAS操作的无锁集合。
  • PriorityBlockingQueue:PriorityQueue的无界的版本。
  • SynchronousQueue:一个有界队列,其中没有任何内存容量。这就意味着任何插入操作必须等到响应的取出操作才能执行,反之亦反。如果不需要Queue接口的话,通过Exchanger类也能完成响应的功能。

Maps

  • ConcurrentHashMap:get操作全并发访问,put操作可配置并发操作的哈希表。并发的级别可以通过构造函数中concurrencyLevel参数设置(默认级别16)。该参数会在Map内部划分一些分区。在put操作的时候只有只有更新的分区是锁住的。这种Map不是代替HashMap的线程安全版本——任何 get-then-put的操作都需要在外部进行同步。
  • ConcurrentSkipListMap:基于跳跃列表(Skip List)的ConcurrentNavigableMap实现。本质上这种集合可以当做一种TreeMap的线程安全版本来使用。

Sets

  • ConcurrentSkipListSet:使用 ConcurrentSkipListMap来存储的线程安全的Set
  • CopyOnWriteArraySet:使用CopyOnWriteArrayList来存储的线程安全的Set

相关阅读

推荐阅读

如果想要了解更多关于Java集合的知识,推荐阅读以下书籍:

总结

  单线程 并发
Lists
  • ArrayList——基于泛型数组
  • LinkedList——不推荐使用
  • Vector——已废弃(deprecated)
  • CopyOnWriteArrayList——几乎不更新,常用来遍历
Queues / deques
  • ArrayDeque——基于泛型数组
  • Stack——已废弃(deprecated)
  • PriorityQueue——读取操作的内容已排序
  • ArrayBlockingQueue——带边界的阻塞式队列
  • ConcurrentLinkedDeque / ConcurrentLinkedQueue——无边界的链表队列(CAS)
  • DelayQueue——元素带有延迟的队列
  • LinkedBlockingDeque / LinkedBlockingQueue——链表队列(带锁),可设定是否带边界
  • LinkedTransferQueue——可将元素`transfer`进行w/o存储
  • PriorityBlockingQueue——并发PriorityQueue
  • SynchronousQueue——使用Queue接口进行Exchanger
Maps
  • HashMap——通用Map
  • EnumMap——键使用enum
  • Hashtable——已废弃(deprecated)
  • IdentityHashMap——键使用==进行比较
  • LinkedHashMap——保持插入顺序
  • TreeMap——键已排序
  • WeakHashMap——适用于缓存(cache)
  • ConcurrentHashMap——通用并发Map
  • ConcurrentSkipListMap——已排序的并发Map
Sets
  • HashSet——通用set
  • EnumSet——enum Set
  • BitSet——比特或密集的整数Set
  • LinkedHashSet——保持插入顺序
  • TreeSet——排序Set
  • ConcurrentSkipListSet——排序并发Set
  • CopyOnWriteArraySet——几乎不更新,通常只做遍历

相关文章

18 Nov 00:37

You only get 1,000 lines of code a week

18 Nov 00:28

精美实用的 PDF 文档:Java 三大 IDE 的快捷键

by aoi

来自 RebelLabs 制作分享的一份精美实用的 PDF 文档,涵盖 Java 三大 IDE (Eclipse、IntelliJ IDEA、NetBeans)的实用快捷键。

The post 精美实用的 PDF 文档:Java 三大 IDE 的快捷键 appeared first on 头条 - 伯乐在线.

14 Nov 00:29

Apache Commons IO入门教程

by yewenhai

Apache Commons IO是Apache基金会创建并维护的Java函数库。它提供了许多类使得开发者的常见任务变得简单,同时减少重复(boiler-plate)代码,这些代码可能遍布于每个独立的项目中,你却不得不重复的编写。这些类由经验丰富的开发者维护,对各种问题的边界条件考虑周到,并持续修复相关bug。

在下面的例子中,我们会向你演示一些不同功能的方法,这些功能都是在org.apache.commons.io包下。Apache Commons IO 是一个巨大工程,我们不会深入去剖析它的底层原理,但是会演示一些比较常用的例子,不管你是不是新手,相信这些例子都会对你有所帮助。

1. Apache Commons IO 示例

我们分别会用几段代码来演示下面的功能,每部分功能都代表Apache Commons IO所覆盖的一个单独领域,具体如下:

  • 工具类
  • 输入
  • 输出
  • 过滤器
  • 比较器
  • 文件监控器

为了更方便读者进行理解,我们会把创建的每个类的输出进行单独展示。并且会把示例中功能演示所用到的到文件放到工程目录(ExampleFolder目录)中。

注意:为了能使用org.apache.commons.io中的功能,你首先需要下载jar包(请点击这里),并且将jar包添加到Eclipse工程的编译路径下,右键点工程文件夹 ->  Build Path -> Add external archives。

ApacheCommonsExampleMain.java
public class ApacheCommonsExampleMain {

    public static void main(String[] args) {
        UtilityExample.runExample();

        FileMonitorExample.runExample();

        FiltersExample.runExample();

        InputExample.runExample();

        OutputExample.runExample();

        ComparatorExample.runExample();
    }
}

这个main方法会运行所有的示例,你可以将其他行的代码注释来执行你想要的示例。

1.1 Utility Classes

org.apache.commons.io包中有很多工具类,里面多数类都是完成文件操作以及字符串比较的功能,下面列举了一下常用的工具类: FilenameUtils 这个工具类是用来处理文件名(译者注:包含文件路径)的,他可以轻松解决不同操作系统文件名称规范不同的问题(比如windows和Unix)(在Unix系统以及Linux系统中文件分隔符是“/”,不支持”\“,windows中支持”\“以及”/“)。
 FileUtils提供文件操作(移动文件,读取文件,检查文件是否存在等等)的方法。  IOCase提供字符串操作以及比较的方法。
 FileSystemUtils:提供查看指定目录剩余空间的方法。
UtilityExample.java
import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileSystemUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.LineIterator;
import org.apache.commons.io.IOCase;

public final class UtilityExample {

    // We are using the file exampleTxt.txt in the folder ExampleFolder,
    // and we need to provide the full path to the Utility classes.
    private static final String EXAMPLE_TXT_PATH =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFolderexampleTxt.txt";

    private static final String PARENT_DIR =
            "C:UsersLilykosworkspaceApacheCommonsExample";

    public static void runExample() throws IOException {
        System.out.println("Utility Classes example...");

        // FilenameUtils

        System.out.println("Full path of exampleTxt: " +
                FilenameUtils.getFullPath(EXAMPLE_TXT_PATH));

        System.out.println("Full name of exampleTxt: " +
                FilenameUtils.getName(EXAMPLE_TXT_PATH));

        System.out.println("Extension of exampleTxt: " +
                FilenameUtils.getExtension(EXAMPLE_TXT_PATH));

        System.out.println("Base name of exampleTxt: " +
                FilenameUtils.getBaseName(EXAMPLE_TXT_PATH));

        // FileUtils

        // We can create a new File object using FileUtils.getFile(String)
        // and then use this object to get information from the file.
        File exampleFile = FileUtils.getFile(EXAMPLE_TXT_PATH);
        LineIterator iter = FileUtils.lineIterator(exampleFile);

        System.out.println("Contents of exampleTxt...");
        while (iter.hasNext()) {
            System.out.println("t" + iter.next());
        }
        iter.close();

        // We can check if a file exists somewhere inside a certain directory.
        File parent = FileUtils.getFile(PARENT_DIR);
        System.out.println("Parent directory contains exampleTxt file: " +
                FileUtils.directoryContains(parent, exampleFile));

        // IOCase

        String str1 = "This is a new String.";
        String str2 = "This is another new String, yes!";

        System.out.println("Ends with string (case sensitive): " +
                IOCase.SENSITIVE.checkEndsWith(str1, "string."));
        System.out.println("Ends with string (case insensitive): " +
                IOCase.INSENSITIVE.checkEndsWith(str1, "string."));

        System.out.println("String equality: " +
                IOCase.SENSITIVE.checkEquals(str1, str2));

        // FileSystemUtils
        System.out.println("Free disk space (in KB): " + FileSystemUtils.freeSpaceKb("C:"));
        System.out.println("Free disk space (in MB): " + FileSystemUtils.freeSpaceKb("C:") / 1024);
    }
}
输出
Utility Classes example...
Full path of exampleTxt: C:UsersLilykosworkspaceApacheCommonsExampleExampleFolder
Full name of exampleTxt: exampleTxt.txt
Extension of exampleTxt: txt
Base name of exampleTxt: exampleTxt
Contents of exampleTxt...
	This is an example text file.
	We will use it for experimenting with Apache Commons IO.
Parent directory contains exampleTxt file: true
Ends with string (case sensitive): false
Ends with string (case insensitive): true
String equality: false
Free disk space (in KB): 32149292
Free disk space (in MB): 31395

1.2 文件监控器

org.apache.commons.io.monitor包下的类包含的方法可以获取文件的指定信息,不过更重要的是,它可以创建处理器(handler)来跟踪指定文件或目录的变化并且可以在文件或目录发生变化的时候进行一些操作。让我们来看看下面的代码:

import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileDeleteStrategy;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.monitor.FileAlterationListenerAdaptor;
import org.apache.commons.io.monitor.FileAlterationMonitor;
import org.apache.commons.io.monitor.FileAlterationObserver;
import org.apache.commons.io.monitor.FileEntry;

public final class FileMonitorExample {

    private static final String EXAMPLE_PATH =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFolderexampleFileEntry.txt";

    private static final String PARENT_DIR =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFolder";

    private static final String NEW_DIR =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFoldernewDir";

    private static final String NEW_FILE =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFoldernewFile.txt";

    public static void runExample() {
        System.out.println("File Monitor example...");

        // FileEntry

        // We can monitor changes and get information about files
        // using the methods of this class.
        FileEntry entry = new FileEntry(FileUtils.getFile(EXAMPLE_PATH));

        System.out.println("File monitored: " + entry.getFile());
        System.out.println("File name: " + entry.getName());
        System.out.println("Is the file a directory?: " + entry.isDirectory());

        // File Monitoring

        // Create a new observer for the folder and add a listener
        // that will handle the events in a specific directory and take action.
        File parentDir = FileUtils.getFile(PARENT_DIR);

        FileAlterationObserver observer = new FileAlterationObserver(parentDir);
        observer.addListener(new FileAlterationListenerAdaptor() {

                @Override
                public void onFileCreate(File file) {
                    System.out.println("File created: " + file.getName());
                }

                @Override
                public void onFileDelete(File file) {
                    System.out.println("File deleted: " + file.getName());
                }

                @Override
                public void onDirectoryCreate(File dir) {
                    System.out.println("Directory created: " + dir.getName());
                }

                @Override
                public void onDirectoryDelete(File dir) {
                    System.out.println("Directory deleted: " + dir.getName());
                }
        });

        // Add a monior that will check for events every x ms,
        // and attach all the different observers that we want.
        FileAlterationMonitor monitor = new FileAlterationMonitor(500, observer);
        try {
            monitor.start();

            // After we attached the monitor, we can create some files and directories
            // and see what happens!
            File newDir = new File(NEW_DIR);
            File newFile = new File(NEW_FILE);

            newDir.mkdirs();
            newFile.createNewFile();

            Thread.sleep(1000);

            FileDeleteStrategy.NORMAL.delete(newDir);
            FileDeleteStrategy.NORMAL.delete(newFile);

            Thread.sleep(1000);

            monitor.stop();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
输出
File Monitor example...
File monitored: C:UsersLilykosworkspaceApacheCommonsExampleExampleFolderexampleFileEntry.txt
File name: exampleFileEntry.txt
Is the file a directory?: false
Directory created: newDir
File created: newFile.txt
Directory deleted: newDir
File deleted: newFile.tx
让我们来看看这里发生了什么,我们使用org.apache.commons.io.monitor包下的类创建了一个处理器来监听一些特定的事件(在上面的例子中就是我们对文件或目录所做的所有操作事件),为了获得这些信息,我们需要做以下几步操作:
1、创建一个File对象,这个对象指向我们需要监听变化的目录。
2、创建一个FileAlterationObserver对象,这个对象会观察这些变化。
3、通过调用addListener()方法,为observer对象添加一个 FileAlterationListenerAdaptor对象。你可以通过很多种方式来创建一个适配器,在我们的例子中我们使用内部类的方式进行创建并且只实现其中的一部分方法(只需要实现我们例子中需要用的方法即可)。

4、创建一个FileAlterationMonitor 对象,将已经创建好的observer对象添加其中并且传入时间间隔参数(单位是毫秒)。

5、调用start()方法即可开启监视器,如果你想停止监视器,调用stop()方法即可。

1.3 过滤器

过滤器可以以组合的方式使用并且它的用途非常多样。它可以轻松的区分不同的文件并且找到满足我们条件的文件。我们可以组合不同的过滤器来执行文件的逻辑比较并且精确的获取我们所需要文件,而无需使用冗余的字符串比较来寻找我们的文件。

FiltersExample.java

import java.io.File;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.io.filefilter.AndFileFilter;
import org.apache.commons.io.filefilter.NameFileFilter;
import org.apache.commons.io.filefilter.NotFileFilter;
import org.apache.commons.io.filefilter.OrFileFilter;
import org.apache.commons.io.filefilter.PrefixFileFilter;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.filefilter.WildcardFileFilter;

public final class FiltersExample {

    private static final String PARENT_DIR =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFolder";

    public static void runExample() {
        System.out.println("File Filter example...");

        // NameFileFilter
        // Right now, in the parent directory we have 3 files:
        //      directory example
        //      file exampleEntry.txt
        //      file exampleTxt.txt

        // Get all the files in the specified directory
        // that are named "example".
        File dir = FileUtils.getFile(PARENT_DIR);
        String[] acceptedNames = {"example", "exampleTxt.txt"};
        for (String file: dir.list(new NameFileFilter(acceptedNames, IOCase.INSENSITIVE))) {
            System.out.println("File found, named: " + file);
        }

        //WildcardFileFilter
        // We can use wildcards in order to get less specific results
        //      ? used for 1 missing char
        //      * used for multiple missing chars
        for (String file: dir.list(new WildcardFileFilter("*ample*"))) {
            System.out.println("Wildcard file found, named: " + file);
        }

        // PrefixFileFilter 
        // We can also use the equivalent of startsWith
        // for filtering files.
        for (String file: dir.list(new PrefixFileFilter("example"))) {
            System.out.println("Prefix file found, named: " + file);
        }

        // SuffixFileFilter
        // We can also use the equivalent of endsWith
        // for filtering files.
        for (String file: dir.list(new SuffixFileFilter(".txt"))) {
            System.out.println("Suffix file found, named: " + file);
        }

        // OrFileFilter 
        // We can use some filters of filters.
        // in this case, we use a filter to apply a logical 
        // or between our filters.
        for (String file: dir.list(new OrFileFilter(
                new WildcardFileFilter("*ample*"), new SuffixFileFilter(".txt")))) {
            System.out.println("Or file found, named: " + file);
        }

        // And this can become very detailed.
        // Eg, get all the files that have "ample" in their name
        // but they are not text files (so they have no ".txt" extension.
        for (String file: dir.list(new AndFileFilter( // we will match 2 filters...
                new WildcardFileFilter("*ample*"), // ...the 1st is a wildcard...
                new NotFileFilter(new SuffixFileFilter(".txt"))))) { // ...and the 2nd is NOT .txt.
            System.out.println("And/Not file found, named: " + file);
        }
    }
}
输出
File Filter example...
File found, named: example
File found, named: exampleTxt.txt
Wildcard file found, named: example
Wildcard file found, named: exampleFileEntry.txt
Wildcard file found, named: exampleTxt.txt
Prefix file found, named: example
Prefix file found, named: exampleFileEntry.txt
Prefix file found, named: exampleTxt.txt
Suffix file found, named: exampleFileEntry.txt
Suffix file found, named: exampleTxt.txt
Or file found, named: example
Or file found, named: exampleFileEntry.txt
Or file found, named: exampleTxt.txt
And/Not file found, named: example

1.4 比较器

使用org.apache.commons.io.comparator 包下的类可以让你轻松的对文件或目录进行比较或者排序。你只需提供一个文件列表,选择不同的类就可以实现不同方式的文件比较。

ComparatorExample.java

import java.io.File;
import java.util.Date;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOCase;
import org.apache.commons.io.comparator.LastModifiedFileComparator;
import org.apache.commons.io.comparator.NameFileComparator;
import org.apache.commons.io.comparator.SizeFileComparator;

public final class ComparatorExample {

    private static final String PARENT_DIR =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFolder";

    private static final String FILE_1 =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFolderexample";

    private static final String FILE_2 =
            "C:UsersLilykosworkspaceApacheCommonsExampleExampleFolderexampleTxt.txt";

    public static void runExample() {
        System.out.println("Comparator example...");

        //NameFileComparator

        // Let's get a directory as a File object
        // and sort all its files.
        File parentDir = FileUtils.getFile(PARENT_DIR);
        NameFileComparator comparator = new NameFileComparator(IOCase.SENSITIVE);
        File[] sortedFiles = comparator.sort(parentDir.listFiles());

        System.out.println("Sorted by name files in parent directory: ");
        for (File file: sortedFiles) {
            System.out.println("t"+ file.getAbsolutePath());
        }

        // SizeFileComparator

        // We can compare files based on their size.
        // The boolean in the constructor is about the directories.
        //      true: directory's contents count to the size.
        //      false: directory is considered zero size.
        SizeFileComparator sizeComparator = new SizeFileComparator(true);
        File[] sizeFiles = sizeComparator.sort(parentDir.listFiles());

        System.out.println("Sorted by size files in parent directory: ");
        for (File file: sizeFiles) {
            System.out.println("t"+ file.getName() + " with size (kb): " + file.length());
        }

        // LastModifiedFileComparator

        // We can use this class to find which file was more recently modified.
        LastModifiedFileComparator lastModified = new LastModifiedFileComparator();
        File[] lastModifiedFiles = lastModified.sort(parentDir.listFiles());

        System.out.println("Sorted by last modified files in parent directory: ");
        for (File file: lastModifiedFiles) {
            Date modified = new Date(file.lastModified());
            System.out.println("t"+ file.getName() + " last modified on: " + modified);
        }

        // Or, we can also compare 2 specific files and find which one was last modified.
        //      returns > 0 if the first file was last modified.
        //      returns  0)
            System.out.println("File " + file1.getName() + " was modified last because...");
        else
            System.out.println("File " + file2.getName() + "was modified last because...");

        System.out.println("t"+ file1.getName() + " last modified on: " +
                new Date(file1.lastModified()));
        System.out.println("t"+ file2.getName() + " last modified on: " +
                new Date(file2.lastModified()));
    }
}
输出
Comparator example...
Sorted by name files in parent directory: 
	C:UsersLilykosworkspaceApacheCommonsExampleExampleFoldercomparator1.txt
	C:UsersLilykosworkspaceApacheCommonsExampleExampleFoldercomperator2.txt
	C:UsersLilykosworkspaceApacheCommonsExampleExampleFolderexample
	C:UsersLilykosworkspaceApacheCommonsExampleExampleFolderexampleFileEntry.txt
	C:UsersLilykosworkspaceApacheCommonsExampleExampleFolderexampleTxt.txt
Sorted by size files in parent directory: 
	example with size (kb): 0
	exampleTxt.txt with size (kb): 87
	exampleFileEntry.txt with size (kb): 503
	comperator2.txt with size (kb): 1458
	comparator1.txt with size (kb): 4436
Sorted by last modified files in parent directory: 
	exampleTxt.txt last modified on: Sun Oct 26 14:02:22 EET 2014
	example last modified on: Sun Oct 26 23:42:55 EET 2014
	comparator1.txt last modified on: Tue Oct 28 14:48:28 EET 2014
	comperator2.txt last modified on: Tue Oct 28 14:48:52 EET 2014
	exampleFileEntry.txt last modified on: Tue Oct 28 14:53:50 EET 2014
File example was modified last because...
	example last modified on: Sun Oct 26 23:42:55 EET 2014
	exampleTxt.txt last modified on: Sun Oct 26 14:02:22 EET 2014

让我们来看看这里用到了哪些类:

NameFileComparator:通过文件名来比较文件。

SizeFileComparator:通过文件大小来比较文件。

LastModifiedFileComparator:通过文件的最新修改时间来比较文件。

在这里你需要注意,比较可以在定的文件夹中(文件夹下的文件已经被sort()方法排序过了),也可以在两个指定的文件之间(通过使用compare()方法)。

1.5 输入

在org.apache.commons.io.input包下有许多InputStrem类的实现,我们来测试一个最实用的类,TeeInputStream,将InputStream以及OutputStream作为参数传入其中,自动实现将输入流的数据读取到输出流中。而且,通过传入第三个参数,一个boolean类型参数,可以在数据读取完毕之后自动关闭输入流和输出流。

InputExample.java

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.io.input.XmlStreamReader;

public final class InputExample {

    private static final String XML_PATH =
            "C:UsersLilykosworkspaceApacheCommonsExampleInputOutputExampleFolderweb.xml";

    private static final String INPUT = "This should go to the output.";

    public static void runExample() {
        System.out.println("Input example...");
        XmlStreamReader xmlReader = null;
        TeeInputStream tee = null;

        try {

            // XmlStreamReader

            // We can read an xml file and get its encoding.
            File xml = FileUtils.getFile(XML_PATH);

            xmlReader = new XmlStreamReader(xml);
            System.out.println("XML encoding: " + xmlReader.getEncoding());

            // TeeInputStream

            // This very useful class copies an input stream to an output stream
            // and closes both using only one close() method (by defining the 3rd
            // constructor parameter as true).
            ByteArrayInputStream in = new ByteArrayInputStream(INPUT.getBytes("US-ASCII"));
            ByteArrayOutputStream out = new ByteArrayOutputStream();

            tee = new TeeInputStream(in, out, true);
            tee.read(new byte[INPUT.length()]);

            System.out.println("Output stream: " + out.toString());         
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try { xmlReader.close(); }
            catch (IOException e) { e.printStackTrace(); }

            try { tee.close(); }
            catch (IOException e) { e.printStackTrace(); }
        }
    }
}
输出
Input example...
XML encoding: UTF-8
Output stream: This should go to the output.

1.6 输出

与org.apache.commons.io.input包中的类相似, org.apache.commons.io.output包中同样有OutputStream类的实现,他们可以在多种情况下使用,一个非常有意思的类就是 TeeOutputStream,它可以将输出流进行分流,换句话说我们可以用一个输入流将数据分别读入到两个不同的输出流。

OutputExample.java

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import org.apache.commons.io.input.TeeInputStream;
import org.apache.commons.io.output.TeeOutputStream;

public final class OutputExample {

    private static final String INPUT = "This should go to the output.";

    public static void runExample() {
        System.out.println("Output example...");
        TeeInputStream teeIn = null;
        TeeOutputStream teeOut = null;

        try {

            // TeeOutputStream

            ByteArrayInputStream in = new ByteArrayInputStream(INPUT.getBytes("US-ASCII"));
            ByteArrayOutputStream out1 = new ByteArrayOutputStream();
            ByteArrayOutputStream out2 = new ByteArrayOutputStream();

            teeOut = new TeeOutputStream(out1, out2);
            teeIn = new TeeInputStream(in, teeOut, true);
            teeIn.read(new byte[INPUT.length()]);

            System.out.println("Output stream 1: " + out1.toString());
            System.out.println("Output stream 2: " + out2.toString());

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // No need to close teeOut. When teeIn closes, it will also close its
            // Output stream (which is teeOut), which will in turn close the 2
            // branches (out1, out2).
            try { teeIn.close(); }
            catch (IOException e) { e.printStackTrace(); }
        }
    }
}
输出
Output example...
Output stream 1: This should go to the output.
Output stream 2: This should go to the output.

2. 下载完整的示例

这是一个Apache Commons IO的入门指导,为开发者介绍了一些可以为你提供轻松解决方案的类。在这个庞大的函数库里面还有包含很多其他的功能,相信这些例子可以在你未来的项目开发中成为你非常有用工具!

你可以在这里下载源代码。

相关文章

12 Nov 10:46

打补丁无需重启补丁有望合并到内核

by WinterIsComing
甲骨文的Ksplice,SUSE的kGraft和Red Hat的kpatch,是三种不用重启就能为Linux Kernel打补丁的机制,这项功能对于基础设施运营商具有重要价值。现在,内核Live Patching机制有望在未来合并到内核主支。Red Hat的高级软件工程师Seth Jennings&nbsp; 在邮件列表上描述了正在开发的内核Live Patching方案,该方案使用了 基于ftrace的机制和内核接口,代表了kpatch和kGraft最常用的功能集,能接受kGraft和 Kpatch构建的内核补丁。新的方案只为内核增加了一千多行代码。






12 Nov 06:37

Intro to Distributed Hash Tables

12 Nov 00:48

ssh连接远程主机执行脚本的环境变量问题

by feihu

近日在使用ssh命令ssh user@remote ~/myscript.sh登陆到远程机器remote上执行脚本时,遇到一个奇怪的问题:

~/myscript.sh: line n: app: command not found

app是一个新安装的程序,安装路径明明已通过/etc/profile配置文件加到环境变量中,但这里为何会找不到?如果直接登陆机器remote并执行~/myscript.sh时,app程序可以找到并顺利执行。但为什么使用了ssh远程执行同样的脚本就出错了呢?两种方式执行脚本到底有何不同?如果你也心存疑问,请跟随我一起来展开分析。

目录

说明 ,本文所使用的机器是:SUSE Linux Enterprise。

问题定位

这看起来像是环境变量引起的问题,为了证实这一猜想,我在这条命令之前加了一句:which app,来查看app的安装路径。在remote本机上执行脚本时,它会打印出app正确的安装路径。但再次用ssh来执行时,却遇到下面的错误:

which: no app in (/usr/bin:/bin:/usr/sbin:/sbin)

这很奇怪,怎么括号中的环境变量没有了app程序的安装路径?不是已通过/etc/profile设置到PATH中了?再次在脚本中加入echo $PATH并以ssh执行,这才发现,环境变量仍是系统初始化时的结果:

/usr/bin:/bin:/usr/sbin:/sbin

这证明/etc/profile根本没有被调用。为什么?是ssh命令的问题么?

随后我又尝试了将上面的ssh分解成下面两步:

user@local > ssh user@remote    # 先远程登陆到remote上
user@remote> ~/myscript.sh      # 然后在返回的shell中执行脚本

结果竟然成功了。那么ssh以这两种方式执行的命令有何不同?带着这个问题去查询了man ssh:

If command is specified, it is executed on the remote host instead of a login shell.

这说明在指定命令的情况下,命令会在远程主机上执行,返回结果后退出。而未指定时,ssh会直接返回一个登陆的shell。但到这里还是无法理解,直接在远程主机上执行和在返回的登陆shell中执行有什么区别?即使在远程主机上执行不也是通过shell来执行的么?难道是这两种方式使用的shell有什么不同?

暂时还没有头绪,但隐隐感到应该与shell有关。因为我通常使用的是bash,所以又去查询了man bash,才得到了答案。

bash的四种模式

在man page的INVOCATION 一节讲述了bash的四种模式,bash会依据这四种模式而选择加载不同的配置文件,而且加载的顺序也有所不同。本文ssh问题的答案就存在于这几种模式当中,所以在我们揭开谜底之前先来分析这些模式。

interactive + login shell

第一种模式是交互式的登陆shell,这里面有两个概念需要解释:interactive和login:

login故名思义,即登陆,login shell是指用户以非图形化界面或者以ssh登陆到机器上时获得的第一个 shell,简单些说就是需要输入用户名和密码的shell。因此通常不管以何种方式登陆机器后用户获得的第一个shell就是login shell。

interactive意为交互式,这也很好理解,interactive shell会有一个输入提示符,并且它的标准输入、输出和错误输出都会显示在控制台上。所以一般来说只要是需要用户交互的,即一个命令一个命令的输入的shell都是interactive shell。而如果无需用户交互,它便是non-interactive shell。通常来说如bash script.sh此类执行脚本的命令就会启动一个non-interactive shell,它不需要与用户进行交互,执行完后它便会退出创建的shell。

那么此模式最简单的两个例子为:

  • 用户直接登陆到机器获得的第一个shell
  • 用户使用ssh user@remote获得的shell

加载配置文件

这种模式下,shell首先加载/etc/profile,然后再尝试依次去加载下列三个配置文件之一,一旦找到其中一个便不再接着寻找

  • ~/.bash_profile
  • ~/.bash_login
  • ~/.profile

下面给出这个加载过程的伪代码:

execute /etc/profile
IF ~/.bash_profile exists THEN
    execute ~/.bash_profile
ELSE
    IF ~/.bash_login exist THEN
        execute ~/.bash_login
    ELSE
        IF ~/.profile exist THEN
            execute ~/.profile
        END IF
    END IF
END IF

为了验证这个过程,我们来做一些测试。首先设计每个配置文件的内容如下:

1 user@remote > cat /etc/profile
2 echo @ /etc/profile
3 user@remote > cat ~/.bash_profile
4 echo @ ~/.bash_profile
5 user@remote > cat ~/.bash_login
6 echo @ ~/.bash_login
7 user@remote > cat ~/.profile
8 echo @ ~/.profile

然后打开一个login shell,注意,为方便起见,这里使用bash -l命令,它会打开一个login shell,在man bash中可以看到此参数的解释:

-l Make bash act as if it had been invoked as a login shell

进入这个新的login shell,便会得到以下输出:

@ /etc/profile
@ /home/user/.bash_profile

果然与文档一致,bash首先会加载全局的配置文件/etc/profile,然后去查找~/.bash_profile,因为其已经存在,所以剩下的两个文件不再会被查找。

接下来移除~/.bash_profile,启动login shell得到结果如下:

@ /etc/profile
@ /home/user/.bash_login

因为没有了~/.bash_profile的屏蔽,所以~/.bash_login被加载,但最后一个~/.profile仍被忽略。

再次移除~/.bash_login,启动login shell的输出结果为:

@ /etc/profile
@ /home/user/.profile

~/.profile终于熬出头,得见天日。通过以上三个实验,配置文件的加载过程得到了验证,除去/etc/profile首先被加载外,其余三个文件的加载顺序为:~/.bash_profile>~/.bash_login>~/.profile,只要找到一个便终止查找。

前面说过,使用ssh也会得到一个login shell,所以如果在另外一台机器上运行ssh user@remote时,也会得到上面一样的结论。

配置文件的意义

那么,为什么bash要弄得这么复杂?每个配置文件存在的意义是什么?

/etc/profile很好理解,它是一个全局的配置文件。后面三个位于用户主目录中的配置文件都针对用户个人,也许你会问为什么要有这么多,只用一个~/.profile不好么?究竟每个文件有什么意义呢?这是个好问题。

Cameron Newham和Bill Rosenblatt在他们的著作《Learning the bash Shell, 2nd Edition》的59页解释了原因:

bash allows two synonyms for .bash_profile: .bash_login, derived from the C shell’s file named .login, and .profile, derived from the Bourne shell and Korn shell files named .profile. Only one of these three is read when you log in. If .bash_profile doesn’t exist in your home directory, then bash will look for .bash_login. If that doesn’t exist it will look for .profile.

One advantage of bash’s ability to look for either synonym is that you can retain your .profile if you have been using the Bourne shell. If you need to add bash-specific commands, you can put them in .bash_profile followed by the command source .profile. When you log in, all the bash-specific commands will be executed and bash will source .profile, executing the remaining commands. If you decide to switch to using the Bourne shell you don’t have to modify your existing files. A similar approach was intended for .bash_login and the C shell .login, but due to differences in the basic syntax of the shells, this is not a good idea.

原来一切都是为了兼容,这么设计是为了更好的应付在不同shell之间切换的场景。因为bash完全兼容Bourne shell,所以.bash_profile和.profile可以很好的处理bash和Bourne shell之间的切换。但是由于C shell和bash之间的基本语法存在着差异,作者认为引入.bash_login并不是个好主意。所以由此我们可以得出这样的最佳实践:

  • 应该尽量杜绝使用.bash_login,如果已经创建,那么需要创建.bash_profile来屏蔽它被调用
  • .bash_profile适合放置bash的专属命令,可以在其最后读取.profile,如此一来,便可以很好的在Bourne shell和bash之间切换了

non-interactive + login shell

第二种模式的shell为non-interactive login shell,即非交互式的登陆shell,这种是不太常见的情况。一种创建此shell的方法为:bash -l script.sh,前面提到过-l参数是将shell作为一个login shell启动,而执行脚本又使它为non-interactive shell。

对于这种类型的shell,配置文件的加载与第一种完全一样,在此不再赘述。

interactive + non-login shell

第三种模式为交互式的非登陆shell,这种模式最常见的情况为在一个已有shell中运行bash,此时会打开一个交互式的shell,而因为不再需要登陆,因此不是login shell。

加载配置文件

对于此种情况,启动shell时会去查找并加载/etc/bash.bashrc和~/.bashrc文件。

为了进行验证,与第一种模式一样,设计各配置文件内容如下:

1 user@remote > cat /etc/bash.bashrc
2 echo @ /etc/bash.bashrc
3 user@remote > cat ~/.bashrc
4 echo @ ~/.bashrc

然后我们启动一个交互式的非登陆shell,直接运行bash即可,可以得到以下结果:

@ /etc/bash.bashrc
@ /home/user/.bashrc

由此非常容易的验证了结论。

bashrc VS profile

从刚引入的两个配置文件的存放路径可以很容易的判断,第一个文件是全局性的,第二个文件属于当前用户。在前面的模式当中,已经出现了几种配置文件,多数是以profile命名的,那么为什么这里又增加两个文件呢?这样不会增加复杂度么?我们来看看此处的文件和前面模式中的文件的区别。

首先看第一种模式中的profile类型文件,它是某个用户唯一的用来设置全局环境变量的地方, 因为用户可以有多个shell比如bash, sh, zsh等, 但像环境变量这种其实只需要在统一的一个地方初始化就可以, 而这个地方就是profile,所以启动一个login shell会加载此文件,后面由此shell中启动的新shell进程如bash,sh,zsh等都可以由login shell中继承环境变量等配置。

接下来看bashrc,其后缀rc的意思为Run Commands,由名字可以推断出,此处存放bash需要运行的命令,但注意,这些命令一般只用于交互式的shell,通常在这里会设置交互所需要的所有信息,比如bash的补全、alias、颜色、提示符等等。

所以可以看出,引入多种配置文件完全是为了更好的管理配置,每个文件各司其职,只做好自己的事情。

non-interactive + non-login shell

最后一种模式为非交互非登陆的shell,创建这种shell典型有两种方式:

  • bash script.sh
  • ssh user@remote command

这两种都是创建一个shell,执行完脚本之后便退出,不再需要与用户交互。

加载配置文件

对于这种模式而言,它会去寻找环境变量BASH_ENV,将变量的值作为文件名进行查找,如果找到便加载它。

同样,我们对其进行验证。首先,测试该环境变量未定义时配置文件的加载情况,这里需要一个测试脚本:

1 user@remote > cat ~/script.sh
2 echo Hello World

然后运行bash script.sh,将得到以下结果:

Hello World

从输出结果可以得知,这个新启动的bash进程并没有加载前面提到的任何配置文件。接下来设置环境变量BASH_ENV:

1 user@remote > export BASH_ENV=~/.bashrc

再次执行bash script.sh,结果为:

@ /home/user/.bashrc
Hello World

果然,~/.bashrc被加载,而它是由环境变量BASH_ENV设定的。

更为直观的示图

至此,四种模式下配置文件如何加载已经讲完,因为涉及的配置文件有些多,我们再以两个图来更为直观的进行描述:

第一张图来自这篇文章,bash的每种模式会读取其所在列的内容,首先执行A,然后是B,C。而B1,B2和B3表示只会执行第一个存在的文件:

+----------------+--------+-----------+---------------+
|                | login  |interactive|non-interactive|
|                |        |non-login  |non-login      |
+----------------+--------+-----------+---------------+
|/etc/profile    |   A    |           |               |
+----------------+--------+-----------+---------------+
|/etc/bash.bashrc|        |    A      |               |
+----------------+--------+-----------+---------------+
|~/.bashrc       |        |    B      |               |
+----------------+--------+-----------+---------------+
|~/.bash_profile |   B1   |           |               |
+----------------+--------+-----------+---------------+
|~/.bash_login   |   B2   |           |               |
+----------------+--------+-----------+---------------+
|~/.profile      |   B3   |           |               |
+----------------+--------+-----------+---------------+
|BASH_ENV        |        |           |       A       |
+----------------+--------+-----------+---------------+

上图只给出了三种模式,原因是第一种login实际上已经包含了两种,因为这两种模式下对配置文件的加载是一致的。

另外一篇文章给出了一个更直观的图:

Bash加载文件顺序

上图的情况稍稍复杂一些,因为它使用了几个关于配置文件的参数:--login,--rcfile,--noprofile,--norc,这些参数的引入会使配置文件的加载稍稍发生改变,不过总体来说,不影响我们前面的讨论,相信这张图不会给你带来更多的疑惑。

典型模式总结

为了更好的理清这几种模式,下面我们对一些典型的启动方式各属于什么模式进行一个总结:

  • 登陆机器后的第一个shell:login + interactive
  • 新启动一个shell进程,如运行bash:non-login + interactive
  • 执行脚本,如bash script.sh:non-login + non-interactive
  • 运行头部有如#!/usr/bin/env bash的可执行文件,如./executable:non-login + non-interactive
  • 通过ssh登陆到远程主机:login + interactive
  • 远程执行脚本,如ssh user@remote script.sh:non-login + non-interactive
  • 远程执行脚本,同时请求控制台,如ssh user@remote -t 'echo $PWD':non-login + interactive
  • 在图形化界面中打开terminal:
  • Linux上: non-login + interactive
  • Mac OS X上: login + interactive

相信你在理解了login和interactive的含义之后,应该会很容易对上面的启动方式进行归类。

再次尝试

在介绍完bash的这些模式之后,我们再回头来看文章开头的问题。ssh user@remote ~/myscript.sh属于哪一种模式?相信此时你可以非常轻松的回答出来:non-login + non-interactive。对于这种模式,bash会选择加载$BASH_ENV的值所对应的文件,所以为了让它加载/etc/profile,可以设定:

1 user@local > export BASH_ENV=/etc/profile

然后执行上面的命令,但是很遗憾,发现错误依旧存在。这是怎么回事?别着急,这并不是我们前面的介绍出错了。仔细查看之后才发现脚本myscript.sh的第一行为#!/usr/bin/env sh,注意看,它和前面提到的#!/usr/bin/env bash不一样,可能就是这里出了问题。我们先尝试把它改成#!/usr/bin/env bash,再次执行,错误果然消失了,这与我们前面的分析结果一致。

第一行的这个语句有什么用?设置成sh和bash有什么区别?带着这些疑问,再来查看man bash:

If the program is a file beginning with #!, the remainder of the first line specifies an interpreter for the program.

它表示这个文件的解释器,即用什么程序来打开此文件,就好比Windows上双击一个文件时会以什么程序打开一样。因为这里不是bash,而是sh,那么我们前面讨论的都不复有效了,真糟糕。我们来看看这个sh的路径:

1 user@remote > ll `which sh`
2 lrwxrwxrwx 1 root root 9 Apr 25  2014 /usr/bin/sh -> /bin/bash

原来sh只是bash的一个软链接,既然如此,BASH_ENV应该是有效的啊,为何此处无效?还是回到man bash,同样在INVOCATION 一节的下部看到了这样的说明:

If bash is invoked with the name sh, it tries to mimic the startup behavior of historical versions of sh as closely as possible, while conforming to the POSIX standard as well. When invoked as an interactive login shell, or a non-interactive shell with the –login option, it first attempts to read and execute commands from /etc/profile and ~/.profile, in that order. The –noprofile option may be used to inhibit this behavior. When invoked as an interactive shell with the name sh, bash looks for the variable ENV, expands its value if it is defined, and uses the expanded value as the name of a file to read and execute. Since a shell invoked as sh does not attempt to read and execute commands from any other startup files, the –rcfile option has no effect. A non-interactive shell invoked with the name sh does not attempt to read any other startup files. When invoked as sh, bash enters posix mode after the startup files are read.

简而言之,当bash以是sh命启动时,即我们此处的情况,bash会尽可能的模仿sh,所以配置文件的加载变成了下面这样:

  • interactive + login: 读取/etc/profile和~/.profile
  • non-interactive + login: 同上
  • interactive + non-login: 读取ENV环境变量对应的文件
  • non-interactive + non-login: 不读取任何文件

这样便可以解释为什么出错了,因为这里属于non-interactive + non-login,所以bash不会读取任何文件,故而即使设置了BASH_ENV也不会起作用。所以为了解决问题,只需要把sh换成bash,再设置环境变量BASH_ENV即可。

另外,其实我们还可以设置参数到第一行的解释器中,如#!/bin/bash --login,如此一来,bash便会强制为login shell,所以/etc/profile也会被加载。相比上面那种方法,这种更为简单。

配置文件建议

回顾一下前面提到的所有配置文件,总共有以下几种:

  • /etc/profile
  • ~/.bash_profile
  • ~/.bash_login
  • ~/.profile
  • /etc/bash.bashrc
  • ~/.bashrc
  • $BASH_ENV
  • $ENV

不知你是否会有疑问,这么多的配置文件,究竟每个文件里面应该包含哪些配置,比如PATH应该在哪?提示符应该在哪配置?启动的程序应该在哪?等等。所以在文章的最后,我搜罗了一些最佳实践供各位参考。(这里只讨论属于用户个人的配置文件)

  • ~/.bash_profile:应该尽可能的简单,通常会在最后加载.profile和.bashrc(注意顺序)
  • ~/.bash_login:在前面讨论过,别用它
  • ~/.profile:此文件用于login shell,所有你想在整个用户会话期间都有效的内容都应该放置于此,比如启动进程,环境变量等
  • ~/.bashrc:只放置与bash有关的命令,所有与交互有关的命令都应该出现在此,比如bash的补全、alias、颜色、提示符等等。特别注意:别在这里输出任何内容 (我们前面只是为了演示,别学我哈)

写在结尾

至此,我们详细的讨论完了bash的几种工作模式,并且给出了配置文件内容的建议。通过这些模式的介绍,本文开始遇到的问题也很容易的得到了解决。以前虽然一直使用bash,但真的不清楚里面包含了如此多的内容。同时感受到Linux的文档的确做得非常细致,在完全不需要其它安装包的情况下,你就可以得到一个非常完善的开发环境,这也曾是Eric S. Raymond在其著作《UNIX编程艺术》中提到的:UNIX天生是一个非常完善的开发机器。本文几乎所有的内容你都可以通过阅读man page得到。最后,希望在这样一个被妖魔化的特殊日子里,这篇文章能够为你带去一丝帮助。

(全文完)

feihu

2014.11.11 于 Shenzhen

12 Nov 00:37

TCP is harder than it looks

12 Nov 00:34

为何我喜欢数据库

by wanqu

原链接

Square 的工程师 Jeeyoung Kim 写的,挺好的科普文章。有一段时间我一度认为,学校里学术圈里接触的感知的 database 跟现实工业界中用的 database 根本就是两个平行世界里的东西 。。。工业界中讲的 database 没那么多浪漫主义情怀,需要考虑很多运维方面的事情。

11 Nov 02:58

Show HN: GitCop – Automated Commit Message Validation for GitHub Pull Requests

10 Nov 00:52

这些聪明人为什么不来创业?聊聊投行和咨询公司的学霸们……

by tips+u1374080746@36kr.com(狐狸君raphael)

编者按:这篇文章是两个月前就约下的稿件。那时候氪星人虽然已经迎来我们的蔡崇信——周尤,但是我们有时候想招一些金融、财务背景的名校应届生还是挺困难的(如果你碰巧是这样的人,愿意来报道互联网金融,做上市公司财报分析等等欢迎投简历到zyh#36kr.com)。

曾有一位就职于四大的同学毫不客气的告诉Zuo:“招不到的,你们太Low了”。大倒苦水之后,在某顶级管理咨询公司就职,在氪空间某团队兼职的狐狸君说最近在思考这个问题还有一些建设性的意见。于是就有了这篇文章。

Professional service firms(下文简称PSF)指的是投行、一线咨询公司以及顶级律师事务所和会计事务所等一系列提供专业服务的乙方公司。这些公司大概是世界上智力密度最高的地方。

而在这里工作的人呢,虽然不同行业和公司互相之间也有鄙视的食物链,但是他们身上通常有以下几个共同的特点:

  • 学霸:学东西很快,一般是名校背景+光鲜履历,一路走来没遇到过重大挫折,最多也就是和别的学霸交手互有胜负。从小就是顺风顺水,受大家瞩目,是长辈口中“别人家的孩子”;
  • 理性思维:对事实、逻辑、数字非常敏感;
  • 拥有某些领域的特长:如公司财务、战略咨询、会计等等;
  • 财务状况健康:收入通常还不错,而且日常开销基本都被公司Cover,收入=资产。
  • 会吹水(Bullshit):特别是咨询和投行的人。

这些学霸们不喜欢创业公司,当然不是没有,也有,比如阿里巴巴的蔡崇信。但是在大多数情况下,创业公司在成长到一定阶段之后非常需要这样的人。马云说过:

蔡崇信这样的人在公司内部是培养不出来的,只能从外部来找。而且找到越早越少走弯路。

当年阿里巴巴第一次拿软银的投资,就是蔡崇信建议马云拒绝了孙正义4000万美元的Offer,只拿了2000万美元,让出了更少的股权。虽然创业公司在成长到一定阶段之后非常需要这样的人,但是想招到的难度很大。且不说蔡崇信,有的名校精英应届生只想着MBB(麦肯锡、贝恩和波士顿咨询),连BAT都不入法眼,何况创业公司呢?

在解开这个结之前,我先讲几个故事吧!

故事一

上周末和某知名管理咨询公司的一个大美女下午茶。她说起自己对时尚领域的热忱,也抛了一个上周刚刚敲定下来的创业idea,关于女性时尚行业的,跃跃欲试。我说创业要趁早啊,今年融资环境那么爽、钱那么好拿,快点起草个商业计划书去骗个天使啊。她笑道:“我接下来找些报告看看,分析分析,找找合伙人,想想能怎么开始做。”

不知道她现在分析得怎么样了。但是这周我跑来北京出差,在国贸那边看到她的一个潜在对手开了个门店。之前跟她谈话的时候我说,这个市场机会很大的其中一个原因就是这家潜在对手还没进入中国,于是我随手拍了个照片发给她。

故事二

前几天和同事客户在吃饭的时候就在聊着,今年融资环境这么火咱怎么还不出去创业呢。某客户(以前也从事管理咨询工作)娓娓道来:某个程度上来说,选择到咨询公司工作的人都比较喜欢规避风险,做事情会瞻前顾后,抛一堆模型做一堆研究,最后通常得出的结论就是——这个东西风险太大,不值得做。

席间也聊到一个他之前鼓动朋友去做的一个项目——做信息中介把中国女生介绍到韩国的整容医院、抽成。这东西佣金比例高达40%,市场分散(现在都是整过的小模特小明星介绍小姐妹过去),客单价高(几万到几十万),现金回流快,初期投资少。不过用户规模增长有限,不是风投特别喜欢的概念。后来他的那个朋友看不上这个idea,觉得不够高大上,也就不做了。再后来,“完美诊所”、“美丽加”、“有氧”等如雨后春笋。他们要涉足跨境这一块也只是早晚的事情。

故事三

两三个月前,我和我司的一个领导吃饭,聊到我心中有个创业梦的时候,她就拼命鼓动我快点出去。 她有过很多次机会,做外面企业的高管,但是最后纠结来纠结去,还是觉得待在公司蛮好的——专车接送、五星级酒店、吃饭有预算、没有KPI。一旦离开了公司,你没有酒店积分,没有航空里程,不再是酒店白金会员,吃饭掏的是自己的钱。她语重心长地告诉我

在这里你呆的越久,越发现离开的成本太高。

当下忽然想起一个同事分享过的一个故事:她在机场遇到以前做投行时一起共事的同事,发现大家同一班飞机。上了飞机后,她的前同事在商务舱坐下了,她还要继续往后面走。“这才是世界上最遥远的距离”,她笑着对我说。

故事四

上个月,我兼职的项目Gradchef.com(毕老师)告一段落,因为我们没有把一个兴趣项目转化成一个全职的创业项目。对这个产品有兴趣的朋友可以看看我们CEO Ming的总结。我们的团队,除了我之外,还有两个在做投资银行,一个在做PE。只有一个人(CEO)全职。

说到这里,这些在PSF学霸们的特点似乎很鲜明了,一个是不喜欢风险,另外一个就是不够接地气。前者导致他们不愿全职创业、做事情更愿意计算风险,但很少愿意试错,没有MVP概念、放不下高薪高福利的诱惑。后者导致他们即使克服了规避风险的强迫症,心里仍然只想着做高大上的项目,不愿意卷起袖子来干。而且他们没有兴趣真正深入研究市场,创业也总是以失败告终。着各种失败反过来又成为那些还在犹豫的前同事眼中的教训,如此一来,恶性循环。

但是事情仍然在起着变化,以华尔街为例。根据新浪财经今年的一篇文章,最近几年来华尔街精英们正在掀起第二次西迁旧金山的热潮。以硅谷为核心的高科技迅猛发展,打破了以往美国金融中心固守纽约城的布局,让更多的华尔街精英开始考虑投向更有发展空间的高科技产业,来发挥他们的特长。根据沃顿商学院的最新报告:

2014年有13.5%的毕业的MBA选择到高科技行业就业,而2008年同比只有5.6%;2013年18%的哈佛商学院的毕业生进入高科技领域,是2008年的3倍。在美国顶级的商学院,MBA毕业后受到的雇佣起薪在金融业和IT业基本不相上下,后者有时还会更高一些。

当然空谈趋势是没有用的,以我这些年和创业公司接触的经历来看,有些办法可以帮助那些愿意到创业公司试一试的PSF聪明人们:

  • 男女搭配,干活不累:一般夫妻两个人,一个继续从事PSF工作,另外一个去创业,以分散家庭面临的现金流风险。就算创业的那一方一直不赚钱,继续留在PSF的那一方一个月几万块收入也够两个人花啦;
  • 给老板留一封情真意切的离职信:和老板聊聊,假装创业只是你的备胎,“组织”才是真爱,创业失败了也可以回来求老板收留;
  • 多感受一下外面的世界 :做项目的时候尽量选二三线城市的项目或Implementation(战略落地)的项目做。工作上若没法自主选择,则可以在休假的时候多出去逛逛,开阔一下眼界,了解一下到底是什么样的市场和顾客在支撑你的项目。
  • 没有调查就没有发言权:偶尔也做做神秘顾客,就算不买也可以Window shopping或者打个cold call问问嘛,这些人年轻的时候在大学,一定也做过类似的学生项目吧。
  • 结识一线创业者:在开始项目前多和业内人士聊聊,说不定就被他们忽悠入伙了不用从头开始了呢

因此PSF出来的小伙伴们也渐渐有了一些成功的创业案例,他们大多数都使用了我上面说的一条或者几条经验:

  • 上海北京白领们颇喜欢的Pantry's Best派悦坊是麦肯锡出身的Alumni做的;
  • YC的目前唯一华人团队Strikingly是摩根·斯坦利出来的朋友做的;
  • 昨天和一个经纬的朋友晚饭,听说了个89年的男生的故事。他是那一届最早实现财务自由的人。当时放弃某VC的投资经理工作,转到了经纬投的一个公司从底层做起,一个月拿5、6千的工资,之后花了两年时间做到创业公司的VP——他加入的公司叫陌陌。
  • 香港中环有家煎饼果子店叫做老金煎饼(香港版的黄太吉啊),是一个从Soc Gen辞职的trader做的。

不过即使是这样,PSF的聪明人们来到创业公司还是要准备好很多的文化冲击,从西装革履到短袖T恤之间,需要的可不只是脱几层衣服,而是扒几层皮。有兴趣也可以看看一篇过来人写的文章:《How quitting my corporate job for my startup dream f*cked my life up》by Ali Mese。

如果你认真看完了,那么请问,“你准备好了吗?”

本文作者是狐狸君raphael,即将出版图书《风口上的猪:一张图看懂互联网金融》作者,现就职于某管理咨询公司,欢迎关注微信号fantastic_fox。

除非注明,本站文章均为原创或编译,转载请注明: 文章来自 36氪

36氪官方iOS应用正式上线,支持『一键下载36氪报道的移动App』和『离线阅读』 立即下载!

05 Nov 06:47

[视频]《小黄人》大电影首曝预告:各种欢乐

继《马达加斯加》中的企鹅之后,《神偷奶爸》系列中的“小黄人”也凭借飙涨的人气出演独立电影。环球影业最新公布了《小黄人》(Minions)大电影的首支预告片,其中讲述了这群不明生物在遇到“坏人”格鲁之前的生活,包括帮助埃及人修建金字塔、协助拿破仑打仗等等,但小黄人们总是状况百出,令人捧腹。






03 Nov 03:00

编写最简单的内核:HelloWorld

by Leo

内核是操作系统最核心的内容,主要提供硬件抽象层、磁盘及文件系统控制、多任务等功能,由于其涉及非常广泛的计算机知识,很少被人们所熟悉,因而披上了一层神秘的面纱。

本文将从零开始实现一个最简单的内核,其可以通过x86系统的GRUB引导启动,并向屏幕输出“Hello World!“字符串。该内核代码非常简短,并且在本人的Debian 7系统中可以正常运行。

x86机器启动过程

在具体实现这个内核之前,我们先看看机器具体是怎么启动并且把控制权交给内核的。

x86的CPU固定地在物理地址为[0xFFFFFFF0]的地方开始运行,这是32位地址空间的最后16个字节。这里只包含了一个跳转指令,跳转到BIOS把它自己拷贝到的内存区域的地址。

然后,BIOS开始执行。它首先根据配置的设备启动顺序依次寻找可启动的设备(根据一个特定的魔数可以决定一个设备是否启动)。一旦找到一个可启动的设备,它就把该设备第一个扇区的内容复制到RAM中物理地址从[0x7C00]开始的地方,然后跳转到该地址并且开始执行那里加载的代码。这段代码称为启动引导装载程序(bootloader)。Bootloader然后在物理地址为[0x100000]的地方加载内核,地址[0x100000]就是x86机器上内核的起始地址。

需要的工具

  • 一台x86电脑
  • Linux
  • NASM汇编器
  • gcc
  • ld(GNU链接器)
  • grub

汇编入口点

我们希望用C来写所有的代码,但免不了要写一点汇编代码。我们会写一个x86汇编语言的小文件来作为内核的起始点,这段汇编所做的事情就是调用一个我们用C写的外部函数,然后停止程序运行。

怎么确定这段汇编代码会作为内核的起始点呢?
我们会使用一个链接脚本来链接所有的目标文件来产生一个最终的内核可执行映像。在这个链接脚本中,我们会显式指明二进制文件要加载在地址为[0x100000]的地方,这就是内核所在的地方。于是,bootloader会负责触发这个内核的入口点。

以下是汇编代码:

;;kernel.asm,内核汇编代码
bits 32         ;nasm伪指令
section .text   ;代码段

global start    ;全局变量
extern kmain    ;kmain定义在C文件中

start:
    cli         ;禁止中断
    call kmain  ;调用kmain函数
    hlt         ;终止CPU运行

第一条指令中的bit 32不是x86汇编指令,而是NASM汇编器的伪指令,表明将会产生一段运行在32位处理器上代码。这句代码不是必须的,但显示加上会是一个好的编程实践。

第二行开始就是代码段,即放置代码的地方。
global也是NASM的伪指令,表示把源代码中的一个符号设置成全局符号。于是链接器知道start符号在哪里,其实这就是我们的入口点。
kmain是将会在kernel.c中实现的一个函数,extern表明这个函数会在其他地方定义。

于是,我们有了start函数,它会调用kmain函数,然后通过hlt指令停止CPU。由于中断会从hlt指令中唤醒CPU,所以我们事先使用cli(意为clear interrupts)指令禁止中断。

C语言内核

我们在kernel.asm中调用kmain()函数,所以C代码会从kmain()开始执行。

//kernel.c文件
void kmain(void)
{
	char *str = "Hello World!";
	char *vidptr = (char*)0xb8000; //显存开始地址
	unsigned int i = 0;
	unsigned int j = 0;
	//清空屏幕,共25行,每行80个字符,每个字符2字节
	while(j < 80 * 25 * 2) {
		//空白字符
		vidptr[j] = ' ';
		//属性字节:黑色背景,灰色前景
		vidptr[j+1] = 0x07; 		
		j = j + 2;
	}
	j = 0;
	while(str[j] != '') {
		vidptr[i] = str[j];
		vidptr[i+1] = 0x07;
		++j;
		i = i + 2;
	}
	return;
}

这里内核所做的事情就是:清空屏幕,打印字符串“Hello World!”。

首先是指针vidptr指向地址[0xb8000],这是保护模式下显存的开始地址。屏幕的文本内存只是地址空间的一连串内存区域,它从[0xb8000]开始映射屏幕的输入输出,支持25行,每行80个ASCII字符,每个字符用16位(2字节)表示,而不是我们熟悉的8位(1字节)。2字节中第1个字节是该字符的ASCII表示,第2个字节是属性字节,描述字符的包括颜色在内的属性。如果要让背景为黑色而字体为绿色,可以在第1个字节保存字符的ASCII值,在第2个字节保存值[0x02]:0代表黑色背景,2代表绿色前景。

其它颜色属性定义如下:

0 1 2 3 4 5 6 7
Black Blue Green Cyan Red Magenta Brown Light Grey
8 9 10 11 12 13 14 15
Dark Grey Light Blue Light Green Light Cyan Light Red Light Magenta Light Brown White

我们的内核使用了黑色背景以及灰色字体,所以属性字节为[0x07]。

在第一个while循环中,程序在所有的25行80列中写入空字符和[0x07]属性,从而清空了屏幕。
在第二个while循环中,字符串”Hello World!“被写到了显存的开始区域,每个字符仍是拥有[0x07]属性。这就在屏幕上打印了该字符串。

链接部分

使用NASM把kernel.asm编译成目标文件,再使用GCC把kernel.c编译成另一个目标文件,然后需要把这两个目标文件链接成一个可以启动的内核映像。
我们使用链接脚本来达到这个目的,链接脚本可以作为参数传递进链接器ld中以控制链接的过程。

//link.ld文件
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
{
    . = 0x100000;
    .text : { *(.text) }
    .data : { *(.data) }
    .bss  : { *(.bss)  }
}

OUTPUT_FORMAT设置输出的可执行文件为32位的ELF文件,ELF是x86架构上类Unix系统的标准二进制文件格式。

ENTRY接受一个参数,指定其为最终可执行文件的入口点。

SECTION是这里最关键的部分,它指定不同的段怎么合并以及放在什么地方,从而定义最终可执行文件的布局。
大括号内就是SECTION的语句,句点(.)为位置计数器,一般被初始化为SECTIONS块开始的地方[0x0],但可以任意修改。因为内核代码需要在地址[0x100000]处开始,所以设置位置计数器为[0x100000]。
第二行中的星号是通配符,可以匹配任何文件名,*(.text)即表示匹配所有输入文件的代码段。于是,链接器合并所有目标文件的代码段到可执行文件的代码段中,具体地址由位置计数器决定,这里即为[0x100000]。链接器产生代码段后,位置计数器会变成:0×100000 + 输出代码段的大小。
同样地,数据段和bss段会被合并,并放置在位置计数器指定的地方。

Grub和多重引导

现在已经准备好了构建内核的所有文件了,但要用GRUB进行引导还需要最后一个步骤。

多重引导规范(Multiboot specification)是一个使用bootloader加载不同X86内核的标准,GRUB只会加载满足这个规范的内核。根据这个规范,内核必须在它的前8KB字节中包含头信息(Multiboot header)。这个头信息包含4字节对齐的3个域,分别为:

  • 魔数域:包含魔数[0x1BADB002]。
  • 标志域:这里不关心这个域,置为0。
  • 校验和域:检验和域和前面两个域相加之后的结果必须为0。

于是kernel.asm应该修改为,代码中的dd表示定义一个4字节的双字:

;;kernel.asm,内核汇编代码

bits 32         ;nasm伪指令
section .text   ;代码段
        ;多重引导规范
        align 4
        dd 0x1BADB002            ;魔数
        dd 0x00                  ;标志
        dd - (0x1BADB002 + 0x00) ;校验和

global start    ;全局变量
extern kmain    ;kmain定义在C文件中

start:
    cli         ;禁止中断
    call kmain  ;调用kmain函数
    hlt         ;终止CPU运行

构建内核

现在可以从kernel.asm和kernel.c生成目标文件,然后使用连接脚本进行链接。

使用汇编器nasm产生ELF-32格式的目标文件kasm.o:

nasm -f elf32 kernel.asm -o kasm.o

使用编译器gcc产生目标文件kc.o,”-c”参数保证只编译,不进行链接:

gcc -m32 -c kernel.c -o kc.o

使用链接器ld根据链接控制脚本产生可执行映像文件kernel:

ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o

配置GRUB并运行内核

GRUB需要内核以kernel-<version>形式命名,于是把内核kernel重命名为kernel-701,并利用超级管理员权限放到/boot目录下。

对于bootloader为GRUB的发行版,修改配置文件/boot/grub/grub.cfg,添加以下条目:

title myKernel
	root (hd0,0)
	kernel /boot/kernel-701 ro

对于bootloader为GRUB2的发行版,添加的配置应该为:

menuentry 'kernel 701' {
	set root='hd0,msdos1'
	multiboot /boot/kernel-701 ro
}

重启电脑,选择GRUB列表中新增加的kernel-701内核选项,这时可以看到屏幕上显示”Hello World!“。
这就是你的内核!

编写最简单的内核:HelloWorld,首发于博客 - 伯乐在线

28 Oct 06:54

Java GC系列(2):Java垃圾回收是如何工作的?

by cool1014

目录

  1. 垃圾回收介绍
  2. 垃圾回收是如何工作的?
  3. 垃圾回收的类别
  4. 垃圾回收监视和分析

本教程是为了理解基本的Java垃圾回收以及它是如何工作的。这是垃圾回收教程系列的第二部分。希望你已经读过了第一部分:《Java 垃圾回收介绍》

Java 垃圾回收是一项自动化的过程,用来管理程序所使用的运行时内存。通过这一自动化过程,JVM 解除了程序员在程序中分配和释放内存资源的开销。

启动Java垃圾回收

作为一个自动的过程,程序员不需要在代码中显示地启动垃圾回收过程。System.gc()Runtime.gc()用来请求JVM启动垃圾回收。

虽然这个请求机制提供给程序员一个启动 GC 过程的机会,但是启动由 JVM负责。JVM可以拒绝这个请求,所以并不保证这些调用都将执行垃圾回收。启动时机的选择由JVM决定,并且取决于堆内存中Eden区是否可用。JVM将这个选择留给了Java规范的实现,不同实现具体使用的算法不尽相同。

毋庸置疑,我们知道垃圾回收过程是不能被强制执行的。我刚刚发现了一个调用System.gc()有意义的场景。通过这篇文章了解一下适合调用System.gc() 这种极端情况。

Java垃圾回收过程

垃圾回收是一种回收无用内存空间并使其对未来实例可用的过程。

Eden 区:当一个实例被创建了,首先会被存储在堆内存年轻代的 Eden 区中。

注意:如果你不能理解这些词汇,我建议你阅读这篇 垃圾回收介绍 ,这篇教程详细地介绍了内存模型、JVM 架构以及这些术语。

Survivor 区(S0 和 S1):作为年轻代 GC(Minor GC)周期的一部分,存活的对象(仍然被引用的)从 Eden 区被移动到 Survivor 区的 S0 中。类似的,垃圾回收器会扫描 S0 然后将存活的实例移动到 S1 中。

(译注:此处不应该是Eden和S0中存活的都移到S1么,为什么会先移到S0再从S0移到S1?)

死亡的实例(不再被引用)被标记为垃圾回收。根据垃圾回收器(有四种常用的垃圾回收器,将在下一教程中介绍它们)选择的不同,要么被标记的实例都会不停地从内存中移除,要么回收过程会在一个单独的进程中完成。

老年代: 老年代(Old or tenured generation)是堆内存中的第二块逻辑区。当垃圾回收器执行 Minor GC 周期时,在 S1 Survivor 区中的存活实例将会被晋升到老年代,而未被引用的对象被标记为回收。

老年代 GC(Major GC):相对于 Java 垃圾回收过程,老年代是实例生命周期的最后阶段。Major GC 扫描老年代的垃圾回收过程。如果实例不再被引用,那么它们会被标记为回收,否则它们会继续留在老年代中。

内存碎片:一旦实例从堆内存中被删除,其位置就会变空并且可用于未来实例的分配。这些空出的空间将会使整个内存区域碎片化。为了实例的快速分配,需要进行碎片整理。基于垃圾回收器的不同选择,回收的内存区域要么被不停地被整理,要么在一个单独的GC进程中完成。

垃圾回收中实例的终结

在释放一个实例和回收内存空间之前,Java 垃圾回收器会调用实例各自的 finalize() 方法,从而该实例有机会释放所持有的资源。虽然可以保证 finalize() 会在回收内存空间之前被调用,但是没有指定的顺序和时间。多个实例间的顺序是无法被预知,甚至可能会并行发生。程序不应该预先调整实例之间的顺序并使用 finalize() 方法回收资源。

  • 任何在 finalize过程中未被捕获的异常会自动被忽略,然后该实例的 finalize 过程被取消。
  • JVM 规范中并没有讨论关于弱引用的垃圾回收机制,也没有很明确的要求。具体的实现都由实现方决定。
  • 垃圾回收是由一个守护线程完成的。

对象什么时候符合垃圾回收的条件?

  • 所有实例都没有活动线程访问。
  • 没有被其他任何实例访问的循环引用实例。

Java 中有不同的引用类型。判断实例是否符合垃圾收集的条件都依赖于它的引用类型。

引用类型 垃圾收集
强引用(Strong Reference) 不符合垃圾收集
软引用(Soft Reference) 垃圾收集可能会执行,但会作为最后的选择
弱引用(Weak Reference) 符合垃圾收集
虚引用(Phantom Reference) 符合垃圾收集

在编译过程中作为一种优化技术,Java 编译器能选择给实例赋 null 值,从而标记实例为可回收。

class Animal {
    public static void main(String[] args) {
        Animal lion = new Animal();
        System.out.println("Main is completed.");
    }

    protected void finalize() {
        System.out.println("Rest in Peace!");
    }
}

在上面的类中,lion 对象在实例化行后从未被使用过。因此 Java 编译器作为一种优化措施可以直接在实例化行后赋值lion = null。因此,即使在 SOP 输出之前, finalize 函数也能够打印出 'Rest in Peace!'。我们不能证明这确定会发生,因为它依赖JVM的实现方式和运行时使用的内存。然而,我们还能学习到一点:如果编译器看到该实例在未来再也不会被引用,能够选择并提早释放实例空间。

  • 关于对象什么时候符合垃圾回收有一个更好的例子。实例的所有属性能被存储在寄存器中,随后寄存器将被访问并读取内容。无一例外,这些值将被写回到实例中。虽然这些值在将来能被使用,这个实例仍然能被标记为符合垃圾回收。这是一个很经典的例子,不是吗?
  • 当被赋值为null时,这是很简单的一个符合垃圾回收的示例。当然,复杂的情况可以像上面的几点。这是由 JVM 实现者所做的选择。目的是留下尽可能小的内存占用,加快响应速度,提高吞吐量。为了实现这一目标, JVM 的实现者可以选择一个更好的方案或算法在垃圾回收过程中回收内存空间。
  • 当 finalize() 方法被调用时,JVM 会释放该线程上的所有同步锁。

GC Scope 示例程序

Class GCScope {
	GCScope t;
	static int i = 1;

	public static void main(String args[]) {
		GCScope t1 = new GCScope();
		GCScope t2 = new GCScope();
		GCScope t3 = new GCScope();

		// No Object Is Eligible for GC

		t1.t = t2; // No Object Is Eligible for GC
		t2.t = t3; // No Object Is Eligible for GC
		t3.t = t1; // No Object Is Eligible for GC

		t1 = null;
		// No Object Is Eligible for GC (t3.t still has a reference to t1)

		t2 = null;
		// No Object Is Eligible for GC (t3.t.t still has a reference to t2)

		t3 = null;
		// All the 3 Object Is Eligible for GC (None of them have a reference.
		// only the variable t of the objects are referring each other in a
		// rounded fashion forming the Island of objects with out any external
		// reference)
	}

	protected void finalize() {
		System.out.println("Garbage collected from object" + i);
		i++;
	}

class GCScope {
	GCScope t;
	static int i = 1;

	public static void main(String args[]) {
		GCScope t1 = new GCScope();
		GCScope t2 = new GCScope();
		GCScope t3 = new GCScope();

		// 没有对象符合GC
		t1.t = t2; // 没有对象符合GC
		t2.t = t3; // 没有对象符合GC
		t3.t = t1; // 没有对象符合GC

		t1 = null;
		// 没有对象符合GC (t3.t 仍然有一个到 t1 的引用)

		t2 = null;
		// 没有对象符合GC (t3.t.t 仍然有一个到 t2 的引用)

		t3 = null;
		// 所有三个对象都符合GC (它们中没有一个拥有引用。
		// 只有各对象的变量 t 还指向了彼此,
		// 形成了一个由对象组成的环形的岛,而没有任何外部的引用。)
	}

	protected void finalize() {
		System.out.println("Garbage collected from object" + i);
		i++;
	}

GC OutOfMemoryError 的示例程序

GC并不保证内存溢出问题的安全性,粗心写下的代码会导致 OutOfMemoryError

import java.util.LinkedList;
import java.util.List;

public class GC {
	public static void main(String[] main) {
		List l = new LinkedList();
		// Enter infinite loop which will add a String to the list: l on each
		// iteration.
		do {
			l.add(new String("Hello, World"));
		} while (true);
	}
}

输出:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.LinkedList.linkLast(LinkedList.java:142)
	at java.util.LinkedList.add(LinkedList.java:338)
	at com.javapapers.java.GCScope.main(GCScope.java:12)

接下来是垃圾收集系列教程的第三部分,我们将会看到常用的 不同 的Java垃圾收集器

相关文章

27 Oct 06:16

Soft Machines:把CPU内核虚拟化

by tips+boxiyang@36kr.com(boxi)


据WSJ报道,一家神秘的芯片初创企业Soft Machines刚刚揭开了其神秘的面纱,它的目标很有野心:要实现内核的虚拟化。

这家初创企业的名字叫做Soft Machines,总部位于硅谷,由英特尔前雇员Lingareddy和Mohammad Abdallah联合创立于2007年。目前Soft Machines共有250名员工,在印度和俄罗斯设有分支机构。此前这家芯片公司一直处于隐身模式,本周四,这家公司首次现身,在芯片业研究机构Linley Group举办的活动上公布了自己的计划。

我们知道,芯片的工作频率(时钟频率)1990年代及2000年代早期一直在稳步提升,但是主频太快会导致芯片出现功耗过大和过热的问题,因此英特尔等芯片制造商开始走多核化的路线,即限制单个微处理器的主频,通过集成多个处理器内核来提高处理性能。这属于一种分布式分而治之并发处理的思路,云计算、云存储、分布式网络等等都是用这种思路来解决规模问题。

问题是在应用端,能充分利用多核处理优势的寥寥,所以给用户带来的速度提升感知越来越不明显。Soft Machines决心要改变这种状况。其基本思路也是一样—分而治之,把计算任务拆分为可并发运行的更小部分。但是Soft Machines的做法有所不同。

以往,芯片要程序员设计产品来发送独立的指令流(即所谓的线程),然后由处理器芯片内的各个内核进行处理,也就是说,任务的分解需要应用开发者来设计实现。这无疑提高了充分发挥多核处理器性能的门槛。

而现在,Soft Machines开发了一种特殊的电路模块,这种模块可以自动将线程分解,然后传递给所谓的虚拟内核,再由这些处理引擎对任务进行分配(虚拟硬件线程)。

Soft Machines把这种新型的CPU架构称为是VISC,以区别于以往的CISC和RISC架构。VISC可以基于不同的应用需求动态分配资源,对单/多线程的应用在性能与功耗之间做出平衡。这种方式比传统的内核调度更加灵活,效率更高,而且省却了开发者的干预。根据Soft Machines对其芯片工作样本的测试,其计算性能是普通多核处理器的2到4倍。Soft Machines据称拥有微芯片方面的30项专利。


这意味着对芯片的设计可以有多种选择:即可让它保持正常时钟频率下获得显著的性能提高,也可以让芯片维持在较早前的性能水平,但是却因此大幅降低功耗,从而提高了电池的续航时间。

不过,业界对此可能会提出质疑。因为早在1990年代时即有人进行过芯片虚拟化的努力。当时一家名为Transmeta的初创企业也想把软件从芯片硬件中抽象出来,但在秘密攻关数年后仍宣告失败。

但是Soft Machines除了技术上不一样以外,它的商业策略也有所不同。它不打算自己生产芯片,而是要把自己的发明卖给芯片公司,这样一来可以显著降低变现成本。Soft Machines把初始客户定位在Android芯片生产商上,尽管这个领域ARM占据了绝对的主导地位,但是Soft Machines在这个蓬勃发展的生态体系中应该仍能觅得不少商机。此外,Soft Machines称自己的技术也可以运行为英特尔、IBM等其他公司的芯片编写的软件。这也许就是Soft Machines之所以得名的原因,而这种开放性也可以给它带来更加广阔的市场空间。

迄今为止Soft Machines似乎赢得了不少人的青睐。融资额说明了一切。上个月底,CBinsights曾做出了一个隐身模式初创企业融资排行榜,Soft Machines以9600万美元高居榜首。而根据演示的片子,现在其总融资额已达1.25亿美元,由此可推断最近一个月它又获得了将近3000万美元的融资。投资者当中包括了Albert Yu(虞有澄)和Richard Wirt这两位英特尔的前高管,Gordon Campbell,以及三星的风投机构、AMD还有阿联酋的投资机构Mubadala。

Soft Machines在Linley Processor Conference上一个PPT介绍了自己的这种VISC新型CPU架构,感兴趣的可以参阅一下。

除非注明,本站文章均为原创或编译,转载请注明: 文章来自 36氪

36氪官方iOS应用正式上线,支持『一键下载36氪报道的移动App』和『离线阅读』 立即下载!

23 Oct 07:09

[图]iPad Air 2拆解:确认2GB内存 发现NFC模组存在

本月16日在苹果总部召开的新品发布会上iPad Air 2正式亮相,根据官方页面该机最大的改变在于添加了Touch ID指纹解锁功能、新增土豪金版本,机身厚度也压缩到6.1mm,并装备最新更加强悍的A8X处理器,但是依然对于处理器时钟频率和内存大小依然不确定,不过随着正式发货,国外知名拆解网站iFixit对其进行了专业拆解,让我们来看看吧。






22 Aug 09:05

【每周技术分享】【BAT】近年面试题

by Ray

 百度2014校园招聘-研发工程师笔试题(济南站)

一,简答题(30分)

1,当前计算机系统一般会采用层次结构存储数据,请介绍下典型计算机存储系统一般分为哪几个层次,为什么采用分层存储数据能有效提高程序的执行效率?(10分)

所谓存储系统的层次结构,就是把各种不同存储容量、存取速度和价格的存储器按层次结构组成多层存储器,并通过管理软件和辅助硬件有机组合成统一的整体,使所存放的程序和数据按层次分布在各种存储器中。目前,在计算机系统中通常采用三级层次结构来构成存储系统,主要由高速缓冲存储器Cache、主存储器和辅助存储器组成。
存储系统多级层次结构中,由上向下分三级,其容量逐渐增大,速度逐级降低,成本则逐次减少。整个结构又可以看成两个层次:它们分别是主存一辅存层次和cache一主存层次。这个层次系统中的每一种存储器都不再是孤立的存储器,而是一个有机的整体。它们在辅助硬件和计算机操作系统的管理下,可把主存一辅存层次作为一个存储整体,形成的可寻址存储空间比主存储器空间大得多。由于辅存容量大,价格低,使得存储系统的整体平均价格降低。由于Cache的存取速度可以和CPU的工作速度相媲美,故cache一主存层次可以缩小主存和cPu之间的速度差距,从整体上提高存储器系统的存取速度。尽管Cache成本高,但由于容量较小,故不会使存储系统的整体价格增加很多。
综上所述,一个较大的存储系统是由各种不同类型的存储设备构成,是一个具有多级层次结构的存储系统。该系统既有与CPU相近的速度,又有极大的容量,而成本又是较低的。其中高速缓存解决了存储系统的速度问题,辅助存储器则解决了存储系统的容量问题。采用多级层次结构的存储器系统可以有效的解决存储器的速度、容量和价格之间的矛盾。

2,Unix/Linux系统中僵尸进程是如何产生的?有什么危害?如何避免?(10分)

一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。
在Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸,如果他的父进程没安装SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了,那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是为什么系统中有时会有很多的僵尸进程。

避免zombie的方法:
1)在SVR4中,如果调用signal或sigset将SIGCHLD的配置设置为忽略,则不会产生僵死子进程。另外,使用SVR4版的sigaction,则可设置SA_NOCLDWAIT标志以避免子进程 僵死。
Linux中也可使用这个,在一个程序的开始调用这个函数 signal(SIGCHLD,SIG_IGN);
2)调用fork两次。
3)用waitpid等待子进程返回.

3,简述Unix/Linux系统中使用socket库编写服务器端程序的流程,请分别用对应的socket通信函数表示(10分)

TCP socket通信
服务器端流程如下:
1.创建serverSocket
2.初始化 serverAddr(服务器地址)
3.将socket和serverAddr 绑定 bind
4.开始监听 listen
5.进入while循环,不断的accept接入的客户端socket,进行读写操作write和read
6.关闭serverSocket
客户端流程:
1.创建clientSocket
2.初始化 serverAddr
3.链接到服务器 connect
4.利用write和read 进行读写操作
5.关闭clientSocket

这个列表是一个Berkeley套接字API库提供的函数或者方法的概要:
socket() 创建一个新的确定类型的套接字,类型用一个整型数值标识,并为它分配系统资源。
bind() 一般用于服务器端,将一个套接字与一个套接字地址结构相关联,比如,一个指定的本地端口和IP地址。
listen() 用于服务器端,使一个绑定的TCP套接字进入监听状态。
connect() 用于客户端,为一个套接字分配一个自由的本地端口号。 如果是TCP套接字的话,它会试图获得一个新的TCP连接。
accept() 用于服务器端。 它接受一个从远端客户端发出的创建一个新的TCP连接的接入请求,创建一个新的套接字,与该连接相应的套接字地址相关联。
send()和recv(),或者write()和read(),或者recvfrom()和sendto(), 用于往/从远程套接字发送和接受数据。
close() 用于系统释放分配给一个套接字的资源。 如果是TCP,连接会被中断。
gethostbyname()和gethostbyaddr() 用于解析主机名和地址。
select() 用于修整有如下情况的套接字列表: 准备读,准备写或者是有错误。
poll() 用于检查套接字的状态。 套接字可以被测试,看是否可以写入、读取或是有错误。
getsockopt() 用于查询指定的套接字一个特定的套接字选项的当前值。
setsockopt() 用于为指定的套接字设定一个特定的套接字选项。

二,算法与程序设计题

1,使用C/C++编写函数,实现字符串反转,要求不使用任何系统函数,且时间复杂度最小,函数原型:char* reverse_str(char* str)。(15分)

获取首尾指针,然后将首尾指针指向的元素交换,将首指针指向下一个,将尾指针指向前一个,交换指针指向的元素,然后重复执行,直到首尾指针相遇。

2,给定一个如下格式的字符串(1,(2,3),(4,(5,6),7))括号内的元素可以是数字,也可以是另一个括号,请实现一个算法消除嵌套的括号,比如把上面的表达式变成:(1,2,3,4,5,6,7),如果表达式有误请报错。(15分)

使用栈和队列实现

 

 2013年阿里巴巴暑期实习招聘笔试题目及部分答案

答题说明:

1.答题时间90分钟,请注意把握时间;

2.试题分为四个部分:单项选择题(10题,20分)、不定向选择题(4题,20分)、填空问答(5题,40分)、综合体(1题,20分);

3.其他一些乱七八糟的考试说明。


一、单项选择题

1.下列说法不正确的是:

A.SATA硬盘的速度速度大约为500Mbps/s

B.读取18XDVD光盘数据的速度为1Gbps

C.前兆以太网的数据读取速度为1Gpbs

D.读取DDR3内存数据的速度为100Gbps

2.()不能用于Linux中的进程通信

A.共享内存

B.命名管道

C.信号量

D.临界区

3.设在内存中有P1,P2,P3三道程序,并按照P1,P2,P3的优先级次序运行,其中内部计算和IO操作时间由下表给出(CPU计算和IO资源都只能同时由一个程序占用):

P1:计算60ms—》IO 80ms—》计算20ms

P2:计算120ms—》IO 40ms—》计算40ms

P3:计算40ms—》IO 80ms—》计算40ms

完成三道程序比单道运行节省的时间是()

A.80ms

B.120ms

C.160ms

D.200ms

4.两个等价线程并发的执行下列程序,a为全局变量,初始为0,假设printf、++、–操作都是原子性的,则输出不肯哪个是()

void foo() {
    if(a <= 0) {
        a++;
    }
    else {
        a--;
    }
    printf("%d", a);
}

A.01

B.10

C.12

D.22

5.给定fun函数如下,那么fun(10)的输出结果是()

int fun(int x) {
    return (x==1) ? 1 : (x + fun(x-1));
}

A.0

B.10

C.55

D.3628800

6.在c++程序中,如果一个整型变量频繁使用,最好将他定义为()

A.auto

B.extern

C.static

D.register

7.长为n的字符串中匹配长度为m的子串的复杂度为()

A.O(N)

B.O(M+N)

C.O(N+LOGM)

D.O(M+LOGN)

8.判断一包含n个整数a[]中是否存在i、j、k满足a[i] + a[j] = a[k]的时间复杂度为()

A.O(n) B.O(n^2) C.O(nlog(n)) D.O(n^2log(n))

9.三次射击能中一次的概率是0.95,请问一次射击能中的概率是多少?
A.0.63

B.0.5

C.**

D.0.85

10.下列序排算法中最坏复杂度不是n(n-1)/2的是_

A.快速序排 B.冒泡序排 C.直接插入序排 D.堆序排

二、不定向选择题

1.以下哪些进程状态转换是正确的()

A.就绪到运行 B.运行到就绪 C.运行到阻塞 D.阻塞到运行 E.阻塞到就绪

2.一个栈的入栈数列为:1、2、3、4、5、6;下列哪个是可能的出栈顺序。(选项不记得)

3.下列哪些代码可以使得a和b交换数值。(选项不记得)

4.A和B晚上无聊就开始数星星。每次只能数K个(20<=k<=30)A和B轮流数。最后谁把星星数完谁就获胜,那么当星星数量为多少时候A必胜?(选项不记得)

三、填空问答题

1.给你一个整型数组A[N],完成一个小程序代码(20行之内),使得A[N]逆向,即原数组为1,2,3,4,逆向之后为4,3,2,1

void revense(int * a,int n) {

 

}

2.自选调度方面的问题,题目很长,就是给你三个线程,分别采用先来先分配的策略和最短执行之间的调度策略,然后计算每个线程从提交到执行完成的时间。题目实在太长,还有几个表格。考察的是操作系统里面作业调度算法先进先出和最短作业优先。

3.有个苦逼的上班族,他每天忘记定闹钟的概率为0.2,上班堵车的概率为0.5,如果他既没定闹钟上班又堵车那他迟到的概率为1.0,如果他定了闹钟但是上班堵车那他迟到的概率为0.9,如果他没定闹钟但是上班不堵车他迟到的概率为0.8,如果他既定了闹钟上班又不堵车那他迟到的概率为0.0,那么求出他在60天里上班迟到的期望。

4.战报交流:战场上不同的位置有N个战士(n>4),每个战士知道当前的一些战况,现在需要这n个战士通过通话交流,互相传达自己知道的战况信息,每次通话,可以让通话的双方知道对方的所有情报,设计算法,使用最少的通话次数,是的战场上的n个士兵知道所有的战况信息,不需要写程序代码,得出最少的通话次数。

5.有N个人,其中一个明星和n-1个群众,群众都认识明星,明星不认识任何群众,群众和群众之间的认识关系不知道,现在如果你是机器人R2T2,你每次问一个人是否认识另外一个人的代价为O(1),试设计一种算法找出明星,并给出时间复杂度(没有复杂度不得分)。

解答:这个问题等价于找未知序列数中的最小数,我们将reg这个函数等价为以下过程:,如果i认识j,记作i大于等于j,同样j不一定大于等于i,满足要求,i不认识j记作i<j,对明星k,他不认识所有人,则k是其中最小的数,且满足其余的人都认识他,也就是其余的人都大于等于k.这样问题就被转换了。就拿N=5来说,首先有数组S[5]={A,B,C,D,E}这5个变量,里边存放着随机数,求是否存在唯一最小数,如果存在位置在S中的哪里。(楼主这里是这个意思,按我的理解题中这个最小数一定是存在且唯一的)

int finds(S,N)
{
    int flag=0;//用于判定是否有明星,即当前最小数另外出现几次
    int temp=0;//存放最小数在S中的位置
    for(i=1;i<N;i++)
   
      if(!reg(S[i],S[temp])//如果temp标号的数小于i标号的数
     
         temp=i;
         flag=0;//更换怀疑对象(最小数)时,标记清零
      
      elseif(reg(S[temp],S[i])//如果temp里存放的确实是唯一最小数是不会跑进这里来的
      {
           flag++;
`     }
    
    if(flag>0) return -1;//表示没有明星,例如所有的数都相等
    return temp;//返回明星在S中的位置
}

四、综合题

有一个淘宝商户,在某城市有n个仓库,每个仓库的储货量不同,现在要通过货物运输,将每次仓库的储货量变成一致的,n个仓库之间的运输线路围城一个圈,即1->2->3->4->…->n->1->…,货物只能通过连接的仓库运输,设计最小的运送成本(运货量*路程)达到淘宝商户的要求,并写出代码。

解答:这个题目类似的题目有:

题目:http://www.lydsy.com/JudgeOnline/problem.php?id=1045
有n个小朋友坐成一圈,每人有ai个糖果。每人只能给左右两人传递糖果。每人每次传
递一个糖果代价为1,求使所有人获得均等糖果的最小代价。
分析:
假设a1分给an的糖果数为k,则可以得到以下的信息:
a1 a2  a3         an-1              an
当前数目:a1-k a2         a3         an-1              an+k
所需代价:|a1-k-ave| |a1+a2-k-2*ave| |a1+a2+a3-k-3*ave||a1+..+a(n-1)-k-(n-1)*ave| |k|
以sum[i]表示从a1加到ai减掉i*ave的和值,这以上可以化简为
总代价 = |s1-k|+|s2-k|+…+|s(n-1)-k|+|k|
不难看出:当k为s1…s(n-1)中的中位数的时候,所需的代价最小

代码转载于网络:

#include <cstring>
#include <iostream>
#include <algorithm>
                                                                                                                                                                                                                                             
using namespace std;
const int X = 1000005;
typedef long long ll;
ll sum[X],a[X];
ll n;
ll Abs(ll x){
    return max(x,-x);
}
int main(){
    //freopen("sum.in","r",stdin);
    while(cin>>n){
        ll x;
        ll tot = 0;
        for(int i=1;i<=n;i++){
            scanf("%lld",&a[i]);
            tot += a[i];
        }
        ll ave = tot/n;
        for(int i=1;i<n;i++)
            sum[i] = a[i]+sum[i-1]-ave;
        sort(sum+1,sum+n);
        ll mid = sum[n/2];
        ll ans = Abs(mid);
        for(int i=1;i<n;i++)
            ans += Abs(sum[i]-mid);
        cout<<ans<<endl;
    }
    return 0;
}
 腾讯面试题

1、请定义一个宏,比较两个数a、b的大小,不能使用大于、小于、if语句
2、如何输出源文件的标题和目前执行行的行数
3、两个数相乘,小数点后位数没有限制,请写一个高精度算法
4、写一个病毒
5、有A、B、C、D四个人,要在夜里过一座桥。他们通过这座桥分别需要耗时1、2、5、10分钟,只有一支手电,并且同时最多只能两个人一起过桥。请问,如何安排,能够在17分钟内这四个人都过桥?

2008年腾讯招聘
选择题 (60)
c/c++ os linux 方面的基础知识 c的Sizeof函数有好几个 !
程序填空 (40)
1.(20) 4空 x5
不使用额外空间,将 A,B两链表的元素交叉归并
2.(20) 4空 x5
MFC 将树序列化 转存在数组或 链表中 !

1, 计算 a^b << 2 (运算符优先级问题 )

2 根据先序中序求后序

3 a[3][4]哪个不能表示 a[1][1]: *(&a[0][0]) *(*(a+1)+1) *(&a[1]+1) *(&a[0][0]+4)

4 for(int i…)
for(int j…)
printf(i,j);
printf(j)
会出现什么问题

5 for(i=0;i<10;++i,sum+=i);的运行结果

6 10个数顺序插入查找二叉树,元素62的比较次数

7 10个数放入模10hash链表,最大长度是多少

8 fun((exp1,exp2),(exp3,exp4,exp5))有几个实参

9 希尔 冒泡 快速 插入 哪个平均速度最快

10 二分查找是 顺序存储 链存储 按value有序中的哪些

11 顺序查找的平均时间

12 *p=NULL *p=new char[100] sizeof(p)各为多少

13 频繁的插入删除操作使用什么结构比较合适,链表还是数组

14 enum的声明方式

15 1-20的两个数把和告诉A,积告诉B,A说不知道是多少,

B也说不知道,这时A说我知道了,B接着说我也知道了,问这两个数是多少

大题:

1 把字符串转换为小写,不成功返回NULL,成功返回新串

char* toLower(char* sSrcStr)
{
char* sDest= NULL;
if( __1___)
{
int j;
sLen = strlen(sSrcStr);
sDest = new [_______2_____];
if(*sDest == NULL)
return NULL;
sDest[sLen] = ‘/0′;
while(_____3____)
sDest[sLen] = toLowerChar(sSrcStr[sLen]);
}
return sDest;
}

2 把字符串转换为整数 例如: “-123″ -> -123

main()
{
…..
if( *string == ‘-’ )
n = ____1______;
else
n = num(string);
…..
}

int num(char* string)
{
for(;!(*string==0);string++)
{
int k;
k = __2_____;
j = –sLen;
while( __3__)
k = k * 10;
num = num + k;
}
return num;
}

附加题:1 linux下调试core的命令,察看堆栈状态命令
2 写出socks套接字 服务端 客户端 通讯程序
3 填空补全程序,按照我的理解是添入:win32调入dll的函数名
查找函数入口的函数名 找到函数的调用形式
把formView加到singledoc的声明 将singledoc加到app的声明4 有关系 s(sno,sname) c(cno,cname) sc(sno,cno,grade)
1 问上课程 “db”的学生 no
2 成绩最高的学生号
3 每科大于90分的人数
主要是c/c++、数据结构、操作系统等方面的基础知识。好像有sizeof、树等选择题。填空题是补充完整程序。附加题有写算法的、编程的、数据库sql语句查询的。还有一张开放性问题。
请定义一个宏,比较两个数a、b的大小,不能使用大于、小于、if语句
#define Max(a,b) ( a/b)?a:b
如何输出源文件的标题和目前执行行的行数
int line = __LINE__;
char *file = __FILE__;
cout<<”file name is “<<(file)<<”,line is “<<line<<endl;
两个数相乘,小数点后位数没有限制,请写一个高精度算法
写一个病毒
while (1)
       {
              int *p = new int[10000000];

       }

不使用额外空间,将 A,B两链表的元素交叉归并

将树序列化 转存在数组或 链表中
struct st{
int i;
short s;
char c;
};
sizeof(struct st);
8
    char * p1;
void * p2;
int p3;
char p4[10];
sizeof(p1…p4) =?

4,4,4, 10

二分查找
快速排序
双向链表的删除结点
有12个小球,外形相同,其中一个小球的质量与其他11个不同
给一个天平,问如何用3次把这个小球找出来
并且求出这个小球是比其他的轻还是重
来源:
http://user.qzone.qq.com/89332874#!app=2&via=QZ.HashRefresh&pos=1380766525
http://blog.csdn.net/wincol/article/details/4811066

(全文完)如果您喜欢此文请点赞,分享,评论。



您可能感兴趣的文章