Apache Thrift系列详解(二) - 网络服务模型

前言

Thrift提供的网络服务模型单线程多线程事件驱动,从另一个角度划分为:阻塞服务模型非阻塞服务模型

  • 阻塞服务模型:TSimpleServerTThreadPoolServer

  • 非阻塞服务模型:TNonblockingServerTHsHaServerTThreadedSelectorServer

TServer类的层次关系:

正文

TServer

TServer定义了静态内部类ArgsArgs继承自抽象类AbstractServerArgsAbstractServerArgs采用了建造者模式,向TServer提供各种工厂:

工厂属性 工厂类型 作用
ProcessorFactory TProcessorFactory 处理层工厂类,用于具体的TProcessor对象的创建
InputTransportFactory TTransportFactory 传输层输入工厂类,用于具体的TTransport对象的创建
OutputTransportFactory TTransportFactory 传输层输出工厂类,用于具体的TTransport对象的创建
InputProtocolFactory TProtocolFactory 协议层输入工厂类,用于具体的TProtocol对象的创建
OutputProtocolFactory TProtocolFactory 协议层输出工厂类,用于具体的TProtocol对象的创建

下面是TServer的部分核心代码:

public abstract class TServer {
    public static class Args extends org.apache.thrift.server.TServer.AbstractServerArgs<org.apache.thrift.server.TServer.Args> {
        public Args(TServerTransport transport) {
            super(transport);
        }
    }

    public static abstract class AbstractServerArgs<T extends org.apache.thrift.server.TServer.AbstractServerArgs<T>> {
        final TServerTransport serverTransport;
        TProcessorFactory processorFactory;
        TTransportFactory inputTransportFactory = new TTransportFactory();
        TTransportFactory outputTransportFactory = new TTransportFactory();
        TProtocolFactory inputProtocolFactory = new TBinaryProtocol.Factory();
        TProtocolFactory outputProtocolFactory = new TBinaryProtocol.Factory();

        public AbstractServerArgs(TServerTransport transport) {
            serverTransport = transport;
        }
    }

    protected TProcessorFactory processorFactory_;
    protected TServerTransport serverTransport_;
    protected TTransportFactory inputTransportFactory_;
    protected TTransportFactory outputTransportFactory_;
    protected TProtocolFactory inputProtocolFactory_;
    protected TProtocolFactory outputProtocolFactory_;
    private boolean isServing;

    protected TServer(org.apache.thrift.server.TServer.AbstractServerArgs args) {
        processorFactory_ = args.processorFactory;
        serverTransport_ = args.serverTransport;
        inputTransportFactory_ = args.inputTransportFactory;
        outputTransportFactory_ = args.outputTransportFactory;
        inputProtocolFactory_ = args.inputProtocolFactory;
        outputProtocolFactory_ = args.outputProtocolFactory;
    }

    public abstract void serve();
    public void stop() {}

    public boolean isServing() {
        return isServing;
    }

    protected void setServing(boolean serving) {
        isServing = serving;
    }
}

TServer的三个方法:serve()stop()isServing()serve()用于启动服务,stop()用于关闭服务,isServing()用于检测服务的起停状态。

TServer不同实现类的启动方式不一样,因此serve()定义为抽象方法。不是所有的服务都需要优雅的退出, 因此stop()方法没有被定义为抽象。


TSimpleServer

TSimpleServer工作模式采用最简单的阻塞IO,实现方法简洁明了,便于理解,但是一次只能接收和处理一个socket连接,效率比较低。它主要用于演示Thrift的工作过程,在实际开发过程中很少用到它。

(一) 工作流程

image

(二) 使用入门

服务端:

    ServerSocket serverSocket = new ServerSocket(ServerConfig.SERVER_PORT);
    TServerSocket serverTransport = new TServerSocket(serverSocket);
    HelloWorldService.Processor processor =
            new HelloWorldService.Processor<HelloWorldService.Iface>(new HelloWorldServiceImpl());
    TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();

    TSimpleServer.Args tArgs = new TSimpleServer.Args(serverTransport);
    tArgs.processor(processor);
    tArgs.protocolFactory(protocolFactory);
    // 简单的单线程服务模型 一般用于测试
    TServer tServer = new TSimpleServer(tArgs);
    System.out.println("Running Simple Server");
    tServer.serve();

客户端:

    TTransport transport = new TSocket(ServerConfig.SERVER_IP, ServerConfig.SERVER_PORT, ServerConfig.TIMEOUT);
    TProtocol protocol = new TBinaryProtocol(transport);
    HelloWorldService.Client client = new HelloWorldService.Client(protocol);
    transport.open();

    String result = client.say("Leo");
    System.out.println("Result =: " + result);
    transport.close();

(三) 源码分析

查看上述流程的源代码,即TSimpleServer.java中的serve()方法如下:

image

serve()方法的操作:

  1. 设置TServerSocketlisten()方法启动连接监听
  2. 阻塞的方式接受客户端地连接请求,每进入一个连接即为其创建一个通道TTransport对象。
  3. 为客户端创建处理器对象输入传输通道对象输出传输通道对象输入协议对象输出协议对象
  4. 通过TServerEventHandler对象处理具体的业务请求。

ThreadPoolServer

TThreadPoolServer模式采用阻塞socket方式工作,主线程负责阻塞式监听是否有新socket到来,具体的业务处理交由一个线程池来处理。

(一) 工作流程

image

(二) 使用入门

服务端:

    ServerSocket serverSocket = new ServerSocket(ServerConfig.SERVER_PORT);
    TServerSocket serverTransport = new TServerSocket(serverSocket);
    HelloWorldService.Processor<HelloWorldService.Iface> processor =
            new HelloWorldService.Processor<>(new HelloWorldServiceImpl());

    TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();
    TThreadPoolServer.Args ttpsArgs = new TThreadPoolServer.Args(serverTransport);
    ttpsArgs.processor(processor);
    ttpsArgs.protocolFactory(protocolFactory);

    // 线程池服务模型 使用标准的阻塞式IO 预先创建一组线程处理请求
    TServer ttpsServer = new TThreadPoolServer(ttpsArgs);
    System.out.println("Running ThreadPool Server");
    ttpsServer.serve();

客户端:

    TTransport transport = new TSocket(ServerConfig.SERVER_IP, ServerConfig.SERVER_PORT, ServerConfig.TIMEOUT);
    TProtocol protocol = new TBinaryProtocol(transport);
    HelloWorldService.Client client = new HelloWorldService.Client(protocol);

    transport.open();
    String result = client.say("ThreadPoolClient");
    System.out.println("Result =: " + result);
    transport.close();

(三) 源码分析

ThreadPoolServer解决了TSimpleServer不支持并发多连接的问题,引入了线程池。实现的模型是One Thread Per Connection。查看上述流程的源代码,先查看线程池的代码片段:

image

TThreadPoolServer.java中的serve()方法如下:

image

serve()方法的操作:

  1. 设置TServerSocketlisten()方法启动连接监听
  2. 阻塞的方式接受客户端连接请求,每进入一个连接,将通道对象封装成一个WorkerProcess对象(WorkerProcess实现了Runnabel接口),并提交到线程池
  3. WorkerProcessrun()方法负责业务处理,为客户端创建了处理器对象输入传输通道对象输出传输通道对象输入协议对象输出协议对象
  4. 通过TServerEventHandler对象处理具体的业务请求。

WorkerProcessrun()方法:

image

(四) 优缺点

TThreadPoolServer模式的优点

拆分了监听线程(Accept Thread)和处理客户端连接工作线程(Worker Thread),数据读取业务处理都交给线程池处理。因此在并发量较大时新连接也能够被及时接受。

线程池模式比较适合服务器端能预知最多有多少个客户端并发的情况,这时每个请求都能被业务线程池及时处理,性能也非常高。

TThreadPoolServer模式的缺点

线程池模式的处理能力受限于线程池的工作能力,当并发请求数大于线程池中的线程数时,新请求也只能排队等待


TNonblockingServer

TNonblockingServer模式也是单线程工作,但是采用NIO的模式,借助Channel/Selector机制, 采用IO事件模型来处理。

所有的socket都被注册到selector中,在一个线程中通过seletor循环监控所有的socket

每次selector循环结束时,处理所有的处于就绪状态socket,对于有数据到来的socket进行数据读取操作,对于有数据发送的socket则进行数据发送操作,对于监听socket则产生一个新业务socket并将其注册selector上。

注意:TNonblockingServer要求底层的传输通道必须使用TFramedTransport。

(一) 工作流程

image

(二) 使用入门

服务端:

    TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(new HelloWorldServiceImpl());
    TNonblockingServerSocket tnbSocketTransport = new TNonblockingServerSocket(ServerConfig.SERVER_PORT);

    TNonblockingServer.Args tnbArgs = new TNonblockingServer.Args(tnbSocketTransport);
    tnbArgs.processor(tprocessor);
    tnbArgs.transportFactory(new TFramedTransport.Factory());
    tnbArgs.protocolFactory(new TCompactProtocol.Factory());

    // 使用非阻塞式IO服务端和客户端需要指定TFramedTransport数据传输的方式
    TServer server = new TNonblockingServer(tnbArgs);
    System.out.println("Running Non-blocking Server");
    server.serve();

客户端:

    TTransport transport = new TFramedTransport(new TSocket(ServerConfig.SERVER_IP, ServerConfig.SERVER_PORT, ServerConfig.TIMEOUT));
    // 协议要和服务端一致
    TProtocol protocol = new TCompactProtocol(transport);
    HelloWorldService.Client client = new HelloWorldService.Client(protocol);
    transport.open();

    String result = client.say("NonBlockingClient");
    System.out.println("Result =: " + result);
    transport.close();

(三) 源码分析

TNonblockingServer继承于AbstractNonblockingServer,这里我们更关心基于NIOselector部分的关键代码。

image

(四) 优缺点

TNonblockingServer模式优点

相比于TSimpleServer效率提升主要体现在IO多路复用上TNonblockingServer采用非阻塞IO,对accept/read/writeIO事件进行监控处理,同时监控多个socket的状态变化。

TNonblockingServer模式缺点

TNonblockingServer模式在业务处理上还是采用单线程顺序来完成。在业务处理比较复杂耗时的时候,例如某些接口函数需要读取数据库执行时间较长,会导致整个服务阻塞住,此时该模式效率也不高,因为多个调用请求任务依然是顺序一个接一个执行。

THsHaServer

鉴于TNonblockingServer的缺点,THsHaServer继承于TNonblockingServer,引入了线程池提高了任务处理的并发能力THsHaServer半同步半异步(Half-Sync/Half-Async)的处理模式,Half-Aysnc用于IO事件处理(Accept/Read/Write),Half-Sync用于业务handlerrpc同步处理上。

注意:THsHaServer和TNonblockingServer一样,要求底层的传输通道必须使用TFramedTransport。

(一) 工作流程

image

(二) 使用入门

服务端:

    TNonblockingServerSocket tnbSocketTransport = new TNonblockingServerSocket(ServerConfig.SERVER_PORT);
    TProcessor tprocessor = new HelloWorldService.Processor<HelloWorldService.Iface>(new HelloWorldServiceImpl());
    // 半同步半异步
    THsHaServer.Args thhsArgs = new THsHaServer.Args(tnbSocketTransport);
    thhsArgs.processor(tprocessor);
    thhsArgs.transportFactory(new TFramedTransport.Factory());
    thhsArgs.protocolFactory(new TBinaryProtocol.Factory());

    TServer server = new THsHaServer(thhsArgs);
    System.out.println("Running HsHa Server");
    server.serve();

客户端:

    TTransport transport = new TFramedTransport(new TSocket(ServerConfig.SERVER_IP, ServerConfig.SERVER_PORT, ServerConfig.TIMEOUT));
    // 协议要和服务端一致
    TProtocol protocol = new TBinaryProtocol(transport);
    HelloWorldService.Client client = new HelloWorldService.Client(protocol);
    transport.open();

    String result = client.say("HsHaClient");
    System.out.println("Result =: " + result);
    transport.close();

(三) 源码分析

THsHaServer继承于TNonblockingServer,新增了线程池并发处理工作任务的功能,查看线程池的相关代码:

image

任务线程池的创建过程:

image

下文的TThreadedSelectorServer囊括了THsHaServer的大部分特性,源码分析可参考TThreadedSelectorServer。

(四) 优缺点

THsHaServer的优点

THsHaServerTNonblockingServer模式相比,THsHaServer在完成数据读取之后,将业务处理过程交由一个线程池来完成,主线程直接返回进行下一次循环操作,效率大大提升。

THsHaServer的缺点

主线程仍然需要完成所有socket监听接收数据读取数据写入操作。当并发请求数较大时,且发送数据量较多时,监听socket新连接请求不能被及时接受。


TThreadedSelectorServer

TThreadedSelectorServer是对THsHaServer的一种扩充,它将selector中的读写IO事件(read/write)从主线程中分离出来。同时引入worker工作线程池,它也是种Half-Sync/Half-Async的服务模型。

TThreadedSelectorServer模式是目前Thrift提供的最高级的线程服务模型,它内部有如果几个部分构成:

  1. 一个AcceptThread线程对象,专门用于处理监听socket上的新连接。
  2. 若干个SelectorThread对象专门用于处理业务socket网络I/O读写操作,所有网络数据的读写均是有这些线程来完成。
  3. 一个负载均衡器SelectorThreadLoadBalancer对象,主要用于AcceptThread线程接收到一个新socket连接请求时,决定将这个新连接请求分配给哪个SelectorThread线程
  4. 一个ExecutorService类型的工作线程池,在SelectorThread线程中,监听到有业务socket中有调用请求过来,则将请求数据读取之后,交给ExecutorService线程池中的线程完成此次调用的具体执行。主要用于处理每个rpc请求的handler回调处理(这部分是同步的)。

(一) 工作流程

image

(二) 使用入门

服务端:

    TNonblockingServerSocket serverSocket = new TNonblockingServerSocket(ServerConfig.SERVER_PORT);
    TProcessor processor = new HelloWorldService.Processor<HelloWorldService.Iface>(new HelloWorldServiceImpl());
    // 多线程半同步半异步
    TThreadedSelectorServer.Args ttssArgs = new TThreadedSelectorServer.Args(serverSocket);
    ttssArgs.processor(processor);
    ttssArgs.protocolFactory(new TBinaryProtocol.Factory());
    // 使用非阻塞式IO时 服务端和客户端都需要指定数据传输方式为TFramedTransport
    ttssArgs.transportFactory(new TFramedTransport.Factory());

    // 多线程半同步半异步的服务模型
    TThreadedSelectorServer server = new TThreadedSelectorServer(ttssArgs);
    System.out.println("Running ThreadedSelector Server");
    server.serve();

客户端:

for (int i = 0; i < 10; i++) {
    new Thread("Thread " + i) {
        @Override
        public void run() {
            // 设置传输通道 对于非阻塞服务 需要使用TFramedTransport(用于将数据分块发送)
            for (int j = 0; j < 10; j++) {
                TTransport transport = null;
                try {
                    transport = new TFramedTransport(new TSocket(ServerConfig.SERVER_IP, ServerConfig.SERVER_PORT, ServerConfig.TIMEOUT));
                    TProtocol protocol = new TBinaryProtocol(transport);
                    HelloWorldService.Client client = new HelloWorldService.Client(protocol);
                    transport.open();
                    String result = client.say("ThreadedSelector Client");
                    System.out.println("Result =: " + result);
                    transport.close();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    // 关闭传输通道
                    transport.close();
                }
            }
        }
    }.start();
}

(三) 核心代码

以上工作流程的三个组件AcceptThreadSelectorThreadExecutorService在源码中的定义如下:

TThreadedSelectorServer模式中有一个专门的线程AcceptThread用于处理新连接请求,因此能够及时响应大量并发连接请求;另外它将网络I/O操作分散到多个SelectorThread线程中来完成,因此能够快速对网络I/O进行读写操作,能够很好地应对网络I/O较多的情况。

image

TThreadedSelectorServer默认参数定义如下:

image
  • 负责网络IO读写的selector默认线程数(selectorThreads):2
  • 负责业务处理的默认工作线程数(workerThreads):5
  • 工作线程池单个线程的任务队列大小(acceptQueueSizePerThread):4

创建、初始化并启动AcceptThreadSelectorThreads,同时启动selector线程的负载均衡器(selectorThreads)。

image

AcceptThread源码

AcceptThread继承于Thread,可以看出包含三个重要的属性:非阻塞式传输通道(TNonblockingServerTransport)、NIO选择器(acceptSelector)和选择器线程负载均衡器(threadChooser)。

image

查看AcceptThreadrun()方法,可以看出accept线程一旦启动,就会不停地调用select()方法:

image

查看select()方法,acceptSelector选择器等待IO事件的到来,拿到SelectionKey即检查是不是accept事件。如果是,通过handleAccept()方法接收一个新来的连接;否则,如果是IO读写事件AcceptThread不作任何处理,交由SelectorThread完成。

image

handleAccept()方法中,先通过doAccept()去拿连接通道,然后Selector线程负载均衡器选择一个Selector线程,完成接下来的IO读写事件

[图片上传失败...(image-314d7a-1536936397051)]

接下来继续查看doAddAccept()方法的实现,毫无悬念,它进一步调用了SelectorThreadaddAcceptedConnection()方法,把非阻塞传输通道对象传递给选择器线程做进一步的IO读写操作

image

SelectorThreadLoadBalancer源码

SelectorThreadLoadBalancer如何创建?

image

SelectorThreadLoadBalancer是一个基于轮询算法Selector线程选择器,通过线程迭代器为新进来的连接顺序分配SelectorThread

image

SelectorThread源码

SelectorThreadAcceptThread一样,是TThreadedSelectorServer的一个成员内部类,每个SelectorThread线程对象内部都有一个阻塞式的队列,用于存放该线程被接收连接通道

[站外图片上传中...(image-ce3409-1536936397051)]

阻塞队列的大小可由构造函数指定:

image

上面看到,在AcceptThreaddoAddAccept()方法中调用了SelectorThreadaddAcceptedConnection()方法。

这个方法做了两件事:

  1. 将被此SelectorThread线程接收的连接通道放入阻塞队列中。
  2. 通过wakeup()方法唤醒SelectorThread中的NIO选择器selector
image

既然SelectorThread也是继承于Thread,查看其run()方法的实现:

image

SelectorThread方法的select()监听IO事件,仅仅用于处理数据读取数据写入。如果连接有数据可读,读取并以frame的方式缓存;如果需要向连接中写入数据,缓存并发送客户端的数据。且在数据读写处理完成后,需要向NIOselector清空注销自身的SelectionKey

image
  • 数据写操作完成以后,整个rpc调用过程也就结束了,handleWrite()方法如下:
image
  • 数据读操作完成以后,Thrift会利用已读数据执行目标方法handleRead()方法如下:
image

handleRead方法在执行read()方法,将数据读取完成后,会调用requestInvoke()方法调用目标方法完成具体业务处理。requestInvoke()方法将请求数据封装为一个Runnable对象,提交到工作任务线程池(ExecutorService)进行处理。

image

select()方法完成后,线程继续运行processAcceptedConnections()方法处理下一个连接IO事件。

image

这里比较核心的几个操作:

  1. 尝试从SelectorThread阻塞队列acceptedQueue中获取一个连接的传输通道。如果获取成功,调用registerAccepted()方法;否则,进入下一次循环。
  2. registerAccepted()方法将传输通道底层的连接注册到NIO选择器selector上面,获取到一个SelectionKey
  3. 创建一个FrameBuffer对象,并绑定到获取的SelectionKey上面,用于数据传输时的中间读写缓存

总结

本文对Thrift的各种线程服务模型进行了介绍,包括2种阻塞式服务模型TSimpleServerTThreadPoolServer,3种非阻塞式服务模型TNonblockingServerTHsHaServerTThreadedSelectorServer。对各种服务模型的具体用法工作流程原理和源码实现进行了一定程度的分析。

鉴于篇幅较长,请各位看官请慢慢批阅!


欢迎关注技术公众号: 零壹技术栈

零壹技术栈

本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,294评论 8 265
  • Java继承关系初始化顺序 父类的静态变量-->父类的静态代码块-->子类的静态变量-->子类的静态代码快-->父...
    第六象限阅读 2,142评论 0 9
  • 参加了四次线下拆书,对于拆书已经有了一些认知,听水菱说她与拆书帮结缘也是因为这本书!我想既然想成为拆书家,怎么着也...
    骅姑娘阅读 366评论 0 0
  • 文/孤鸟差鱼 心里的谷堆 长满了仓鼠 没人来问 还剩多少 吃跑的人 总在肆无忌惮的挥霍 不懂得没有的难过 是有如针...
    孤鸟差鱼阅读 205评论 3 3