首页 文章 精选 留言 我的

精选列表

搜索[面试],共4915篇文章
优秀的个人博客,低调大师

2018年一线互联网公司Java高级面试题总结

1、hashcode相等两个类一定相等吗?equals呢?相反呢? 2、介绍一下集合框架? 3、hashmap hastable 底层实现什么区别?hashtable和concurrenthashtable呢? 4、hashmap和treemap什么区别?低层数据结构是什么? 5、线程池用过吗都有什么参数?底层如何实现的? 6、sychnized和Lock什么区别?sychnize 什么情况情况是对象锁? 什么时候是全局锁为什么? 7、ThreadLocal 是什么底层如何实现?写一个例子呗? 8、volitile的工作原理? 9、cas知道吗如何实现的? 10、请用至少四种写法写一个单例模式? 推荐一个交流学习群:614478470 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 点击:加入 JVM 1、请介绍一下JVM内存模型??用过什么垃圾回收器都说说呗 2、线上发送频繁full gc如何处理? CPU 使用率过高怎么办? 如何定位问题?如何解决说一下解决思路和处理方法 3、知道字节码吗?字节码都有哪些?Integer x =5,int y =5,比较x =y 都经过哪些步骤? 4、讲讲类加载机制呗都有哪些类加载器,这些类加载器都加载哪些文件? 手写一下类加载Demo 5、知道osgi吗? 他是如何实现的??? 6、请问你做过哪些JVM优化?使用什么方法达到什么效果??? 7、classforName("java.lang.String")和String classgetClassLoader() LoadClass("java.lang.String") 什么区别啊? Spring 1、spring都有哪些机制啊AOP底层如何实现的啊IOC呢?? 2、cgLib知道吗?他和jdk动态代理什么区别?手写一个jdk动态代理呗? 数据库 1、使用mysq1索引都有哪些原则? ?索引什么数据结构? 3+tree 和B tree 什么区别? 2、mysq1有哪些存储引擎啊?都有啥区别? 要详细! 3、设计高并发系统数据库层面该怎么设计??数据库锁有哪些类型?如何实现呀? 4、数据库事务有哪些? 分库分表 1、如何设计可以动态扩容缩容的分库分表方案? 2、用过哪些分库分表中间件,有啥优点和缺点?讲一下你了解的分库分表中间件的底层实现原理? 3、我现在有一个未分库分表的系统,以后系统需分库分表,如何设计,让未分库分表的系统动态切换到分库分表的系统上???TCC? 那若出现网络原因,网络连不通怎么办啊??? 4、分布式事务知道吗? 你们怎么解决的? 5、为什么要分库分表啊??? 6、分布式寻址方式都有哪些算法知道一致性hash吗?手写一下java实现代码??你若userId取摸分片,那我要查一段连续时间里的数据怎么办??? 7、如何解决分库分表主键问题有什么实现方案?? 分布式缓存 1、redis和memcheched 什么区别为什么单线程的redis比多线程的memched效率要高啊? 2、redis有什么数据类型都在哪些场景下使用啊? 3、reids的主从复制是怎么实现的redis的集群模式是如何实现的呢redis的key是如何寻址的啊? 4、使用redis如何设计分布式锁?使用zk可以吗?如何实现啊这两种哪个效率更高啊?? 5、知道redis的持久化吗都有什么缺点优点啊? ?具体底层实现呢? 6、redis过期策略都有哪些LRU 写一下java版本的代码吧?? 分布式服务框架 1、说一下dubbo的实现过程注册中心挂了可以继续通信吗?? 2、zk原理知道吗zk都可以干什么Paxos算法知道吗?说一下原理和实现?? 3、dubbo支持哪些序列化协议?hessian 说一下hessian的数据结构PB知道吗为啥PB效率是最高的啊?? 4、知道netty吗'netty可以干嘛呀NIO,BIO,AIO 都是什么啊有什么区别啊? 5、dubbo复制均衡策略和高可用策略都有哪些啊动态代理策略呢? 6、为什么要进行系统拆分啊拆分不用dubbo可以吗'dubbo和thrift什么区别啊? 分布式消息队列 1、为什么使用消息队列啊消息队列有什么优点和缺点啊? 2、如何保证消息队列的高可用啊如何保证消息不被重复消费啊 3、kafka ,activemq,rabbitmq ,rocketmq都有什么优点,缺点啊??? 4、如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路 分布式搜索引擎 1、es的工作过程实现是如何的?如何实现分布式的啊 2、es在数据量很大的情况下( 数十亿级别)如何提高查询效率啊? 3、es的查询是一个怎么的工作过程?底层的lucence介绍一下呗倒排索引知道吗?es和mongdb什么区别啊都在什么场景下使用啊? 高并发高可用架构设计 1、如何设计一个高并发高可用系统 2、如何限流?工程中怎么做的,说一下具体实现 3、缓存如何使用的缓存使用不当会造成什么后果? 4、如何熔断啊?熔断框架都有哪些?具体实现原理知道吗? 5、如何降级如何进行系统拆分,如何数据库拆分???? 通信协议 1、说一下TCP 'IP四层? 2、http的工作流程?? ?http1.0 http1.1http2.0 具体哪些区别啊? 3、TCP三次握手,四层分手的工作流程画一下流程图为什么不是四次五次或者二次啊? 4、画一下https的工作流程?具体如何实现啊?如何防止被抓包啊?? 算法 1、比较简单,我一个文件,有45亿个阿拉伯数字,如何进行去重啊如何找出最大的那个数啊? 数据结构 1、二叉树和红黑树等。 源码中所用到的经典设计思想及常用设计模式 推荐一个交流学习群:614478470 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多 点击:加入

优秀的个人博客,低调大师

听说你Binder机制学的不错,来面试下这几个问题(一)

Binder承担了绝大部分Android进程通信的职责,可以看做是Android的血管系统,负责不同服务模块进程间的通信。在对Binder的理解上,可大可小,日常APP开发并不怎么涉及Binder通信知识,最多就是Service及AIDL的使用会涉及部分Binder知识。Binder往小了说可总结成一句话:一种IPC进程间通信方式,负责进程A的数据,发送到进程B。往大了说,其实涉及的知识还是很多的,如Android 对于原Binder驱动的扩展、Zygote进程孵化中对于Binder通信的支持、Java层Binder封装,Native层对于Binder通信的封装、Binder讣告机制等等。很多分析Binder框架的文都是从ServiceManager、Binder驱动、addService、getService来分析等来分析,其实这些主要是针对系统提供的服务,但是bindService启动的服务走的却还是有很大不同的。本篇文章主要简述一些Binder难以理解的点,但不会太细的跟踪分析,只抛砖,自己去发掘玉。 Binder的定向制导,如何找到目标Binder,唤起进程或者线程Binder中的红黑树,为什么会有两棵binder_ref红黑树Binder一次拷贝原理(直接拷贝到目标线程的内核空间,内核空间与用户空间对应)Binder传输数据的大小限制(内核4M 上层限制1m-8k),传输Bitmap过大,就会崩溃的原因,Activity之间传输BitMap系统服务与bindService等启动的服务的区别Binder线程、Binder主线程、Client请求线程的概念与区别Client是同步而Server是异步到底说的什么Android APP进程天生支持Binder通信的原理是什么Android APP有多少Binder线程,是固定的么Binder线程的睡眠与唤醒(请求线程睡在哪个等待队列上,唤醒目标端哪个队列上的线程)Binder协议中BC与BR的区别Binder在传输数据的时候是如何层层封装的--不同层次使用的数据结构(命令的封装)Binder驱动传递数据的释放(释放时机)一个简单的Binder通信C/S模型ServiceManager addService的限制(并非服务都能使用ServiceManager的addService)bindService启动Service与Binder服务实体的流程Java层Binder实体与与BinderProxy是如何实例化及使用的,与Native层的关系是怎样的Parcel readStrongBinder与writeStrongBinder的原理Binder如何精确制导,找到目标Binder实体,并唤醒进程或者线程 Binder实体服务其实有两种,一是通过addService注册到ServiceManager中的服务,比如ActivityManagerService、PackageManagerService、PowerManagerService等,一般都是系统服务;还有一种是通过bindService拉起的一些服务,一般是开发者自己实现的服务。这里先看通过addService添加的被ServiceManager所管理的服务。有很多分析ServiceManager的文章,本文不分析ServiceManager,只是简单提一下,ServiceManager是比较特殊的服务,所有应用都能直接使用,因为ServiceManager对于Client端来说Handle句柄是固定的,都是0,所以ServiceManager服务并不需要查询,可以直接使用。 理解Binder定向制导的关键是理解Binder的四棵红黑树,先看一下binder_proc结构体,在它内部有四棵红黑树,threads,nodes,refs_by_desc,refs_by_node,nodes就是Binder实体在内核中对应的数据结构,binder_node里记录进程相关的binder_proc,还有Binder实体自身的地址等信息,nodes红黑树位于binder_proc,可以知道Binder实体其实是进程内可见,而不是线程内。 struct binder_proc { struct hlist_node proc_node; struct rb_root threads; struct rb_root nodes; struct rb_root refs_by_desc; struct rb_root refs_by_node; 。。。 struct list_head todo; wait_queue_head_t wait; 。。。 }; 现在假设存在一堆Client与Service,Client如何才能访问Service呢?首先Service会通过addService将binder实体注册到ServiceManager中去,Client如果想要使用Servcie,就需要通过getService向ServiceManager请求该服务。在Service通过addService向ServiceManager注册的时候,ServiceManager会将服务相关的信息存储到自己进程的Service列表中去,同时在ServiceManager进程的binder_ref红黑树中为Service添加binder_ref节点,这样ServiceManager就能获取Service的Binder实体信息。而当Client通过getService向ServiceManager请求该Service服务的时候,ServiceManager会在注册的Service列表中查找该服务,如果找到就将该服务返回给Client,在这个过程中,ServiceManager会在Client进程的binder_ref红黑树中添加binder_ref节点,可见本进程中的binder_ref红黑树节点都不是本进程自己创建的,要么是Service进程将binder_ref插入到ServiceManager中去,要么是ServiceManager进程将binder_ref插入到Client中去。之后,Client就能通过Handle句柄获取binder_ref,进而访问Service服务。 getService之后,便可以获取binder_ref引用,进而获取到binder_proc与binder_node信息,之后Client便可有目的的将binder_transaction事务插入到binder_proc的待处理列表,并且,如果进程正在睡眠,就唤起进程,其实这里到底是唤起进程还是线程也有讲究,对于Client向Service发送请求的状况,一般都是唤醒binder_proc上睡眠的线程: struct binder_ref { int debug_id; struct rb_node rb_node_desc; struct rb_node rb_node_node; struct hlist_node node_entry; struct binder_proc *proc; struct binder_node *node; uint32_t desc; int strong; int weak; struct binder_ref_death *death; }; binder_proc为何会有两棵binder_ref红黑树 binder_proc中存在两棵binder_ref红黑树,其实两棵红黑树中的节点是复用的,只是查询方式不同,一个通过handle句柄,一个通过node节点查找。个人理解:refs_by_node红黑树主要是为了binder驱动往用户空间写数据所使用的,而refs_by_desc是用户空间向Binder驱动写数据使用的,只是方向问题。比如在服务addService的时候,binder驱动会在在ServiceManager进程的binder_proc中查找binder_ref结构体,如果没有就会新建binder_ref结构体,再比如在Client端getService的时候,binder驱动会在Client进程中通过 binder_get_ref_for_node为Client创建binder_ref结构体,并分配句柄,同时插入到refs_by_desc红黑树中,可见refs_by_node红黑树,主要是给binder驱动往用户空间写数据使用的。相对的refs_by_desc主要是为了用户空间往binder驱动写数据使用的,当用户空间已经获得Binder驱动为其创建的binder_ref引用句柄后,就可以通过binder_get_ref从refs_by_desc找到响应binder_ref,进而找到目标binder_node。可见有两棵红黑树主要是区分使用对象及数据流动方向,看下面的代码就能理解: // 根据32位的uint32_t desc来查找,可以看到,binder_get_ref不会新建binder_ref节点 static struct binder_ref *binder_get_ref(struct binder_proc *proc, uint32_t desc) { struct rb_node *n = proc->refs_by_desc.rb_node; struct binder_ref *ref; while (n) { ref = rb_entry(n, struct binder_ref, rb_node_desc); if (desc < ref->desc) n = n->rb_left; else if (desc > ref->desc) n = n->rb_right; else return ref; } return NULL; } 可以看到binder_get_ref并具备binder_ref的创建功能,相对应的看一下binder_get_ref_for_node,binder_get_ref_for_node红黑树主要通过binder_node进行查找,如果找不到,就新建binder_ref,同时插入到两棵红黑树中去 static struct binder_ref *binder_get_ref_for_node(struct binder_proc *proc, struct binder_node *node) { struct rb_node *n; struct rb_node **p = &amp;proc->refs_by_node.rb_node; struct rb_node *parent = NULL; struct binder_ref *ref, *new_ref; while (*p) { parent = *p; ref = rb_entry(parent, struct binder_ref, rb_node_node); if (node < ref->node) p = &amp;(*p)->rb_left; else if (node > ref->node) p = &amp;(*p)->rb_right; else return ref; } // binder_ref 可以在两棵树里面,但是,两棵树的查询方式不同,并且通过desc查询,不具备新建功能 new_ref = kzalloc(sizeof(*ref), GFP_KERNEL); if (new_ref == NULL) return NULL; binder_stats_created(BINDER_STAT_REF); new_ref->debug_id = ++binder_last_id; new_ref->proc = proc; new_ref->node = node; rb_link_node(&amp;new_ref->rb_node_node, parent, p); // 插入到proc->refs_by_node红黑树中去 rb_insert_color(&amp;new_ref->rb_node_node, &amp;proc->refs_by_node); // 是不是ServiceManager的 new_ref->desc = (node == binder_context_mgr_node) ? 0 : 1; // 分配Handle句柄,为了插入到refs_by_desc for (n = rb_first(&amp;proc->refs_by_desc); n != NULL; n = rb_next(n)) { ref = rb_entry(n, struct binder_ref, rb_node_desc); if (ref->desc > new_ref->desc) break; new_ref->desc = ref->desc + 1; } // 找到目标位置 p = &amp;proc->refs_by_desc.rb_node; while (*p) { parent = *p; ref = rb_entry(parent, struct binder_ref, rb_node_desc); if (new_ref->desc < ref->desc) p = &amp;(*p)->rb_left; else if (new_ref->desc > ref->desc) p = &amp;(*p)->rb_right; else BUG(); } rb_link_node(&amp;new_ref->rb_node_desc, parent, p); // 插入到refs_by_desc红黑树中区 rb_insert_color(&amp;new_ref->rb_node_desc, &amp;proc->refs_by_desc); if (node) { hlist_add_head(&amp;new_ref->node_entry, &amp;node->refs); binder_debug(BINDER_DEBUG_INTERNAL_REFS, "binder: %d new ref %d desc %d for " "node %d\n", proc->pid, new_ref->debug_id, new_ref->desc, node->debug_id); } else { binder_debug(BINDER_DEBUG_INTERNAL_REFS, "binder: %d new ref %d desc %d for " "dead node\n", proc->pid, new_ref->debug_id, new_ref->desc); } return new_ref; } 该函数调用在binder_transaction函数中,其实就是在binder驱动访问target_proc的时候,这也也很容易理解,Handle句柄对于跨进程没有任何意义,进程A中的Handle,放到进程B中是无效的。 Binder一次拷贝原理 Android选择Binder作为主要进程通信的方式同其性能高也有关系,Binder只需要一次拷贝就能将A进程用户空间的数据为B进程所用。这里主要涉及两个点: Binder的map函数,会将内核空间直接与用户空间对应,用户空间可以直接访问内核空间的数据A进程的数据会被直接拷贝到B进程的内核空间(一次拷贝) #define BINDER_VM_SIZE ((1*1024*1024) - (4096 *2)) ProcessState::ProcessState() : mDriverFD(open_driver()) , mVMStart(MAP_FAILED) , mManagesContexts(false) , mBinderContextCheckFunc(NULL) , mBinderContextUserData(NULL) , mThreadPoolStarted(false) , mThreadPoolSeq(1){ if (mDriverFD >= 0) { .... // mmap the binder, providing a chunk of virtual address space to receive transactions. mVMStart = mmap(0, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0); ... } } mmap函数属于系统调用,mmap会从当前进程中获取用户态可用的虚拟地址空间(vm_area_struct *vma),并在mmap_region中真正获取vma,然后调用file->f_op->mmap(file, vma),进入驱动处理,之后就会在内存中分配一块连续的虚拟地址空间,并预先分配好页表、已使用的与未使用的标识、初始地址、与用户空间的偏移等等,通过这一步之后,就能把Binder在内核空间的数据直接通过指针地址映射到用户空间,供进程在用户空间使用,这是一次拷贝的基础,一次拷贝在内核中的标识如下: struct binder_proc { struct hlist_node proc_node; // 四棵比较重要的树 struct rb_root threads; struct rb_root nodes; struct rb_root refs_by_desc; struct rb_root refs_by_node; int pid; struct vm_area_struct *vma; //虚拟地址空间,用户控件传过来 struct mm_struct *vma_vm_mm; struct task_struct *tsk; struct files_struct *files; struct hlist_node deferred_work_node; int deferred_work; void *buffer; //初始地址 ptrdiff_t user_buffer_offset; //这里是偏移 struct list_head buffers;//这个列表连接所有的内存块,以地址的大小为顺序,各内存块首尾相连 struct rb_root free_buffers;//连接所有的已建立映射的虚拟内存块,以内存的大小为index组织在以该节点为根的红黑树下 struct rb_root allocated_buffers;//连接所有已经分配的虚拟内存块,以内存块的开始地址为index组织在以该节点为根的红黑树下 } 上面只是在APP启动的时候开启的地址映射,但并未涉及到数据的拷贝,下面看数据的拷贝操作。当数据从用户空间拷贝到内核空间的时候,是直从当前进程的用户空间接拷贝到目标进程的内核空间,这个过程是在请求端线程中处理的,操作对象是目标进程的内核空间。看如下代码: static void binder_transaction(struct binder_proc *proc, struct binder_thread *thread, struct binder_transaction_data *tr, int reply){ ... 在通过进行binder事物的传递时,如果一个binder事物(用struct binder_transaction结构体表示)需要使用到内存, 就会调用binder_alloc_buf函数分配此次binder事物需要的内存空间。 需要注意的是:这里是从目标进程的binder内存空间分配所需的内存 //从target进程的binder内存空间分配所需的内存大小,这也是一次拷贝,完成通信的关键,直接拷贝到目标进程的内核空间 //由于用户空间跟内核空间仅仅存在一个偏移地址,所以也算拷贝到用户空间 t->buffer = binder_alloc_buf(target_proc, tr->data_size, tr->offsets_size, !reply &amp;&amp; (t->flags &amp; TF_ONE_WAY)); t->buffer->allow_user_free = 0; t->buffer->debug_id = t->debug_id; //该binder_buffer对应的事务 t->buffer->transaction = t; //该事物对应的目标binder实体 ,因为目标进程中可能不仅仅有一个Binder实体 t->buffer->target_node = target_node; trace_binder_transaction_alloc_buf(t->buffer); if (target_node) binder_inc_node(target_node, 1, 0, NULL); // 计算出存放flat_binder_object结构体偏移数组的起始地址,4字节对齐。 offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *))); // struct flat_binder_object是binder在进程之间传输的表示方式 // // 这里就是完成binder通讯单边时候在用户进程同内核buffer之间的一次拷贝动作 // // 这里的数据拷贝,其实是拷贝到目标进程中去,因为t本身就是在目标进程的内核空间中分配的, if (copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)) { binder_user_error("binder: %d:%d got transaction with invalid " "data ptr\n", proc->pid, thread->pid); return_error = BR_FAILED_REPLY; goto err_copy_data_failed; } 可以看到binder_alloc_buf(target_proc, tr->data_size,tr->offsets_size, !reply && (t->flags & TF_ONE_WAY))函数在申请内存的时候,是从target_proc进程空间中去申请的,这样在做数据拷贝的时候copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)),就会直接拷贝target_proc的内核空间,而由于Binder内核空间的数据能直接映射到用户空间,这里就不在需要拷贝到用户空间。这就是一次拷贝的原理。内核空间的数据映射到用户空间其实就是添加一个偏移地址,并且将数据的首地址、数据的大小都复制到一个用户空间的Parcel结构体,具体可以参考Parcel.cpp的Parcel::ipcSetDataReference函数。 Binder传输数据的大小限制 虽然APP开发时候,Binder对程序员几乎不可见,但是作为Android的数据运输系统,Binder的影响是全面性的,所以有时候如果不了解Binder的一些限制,在出现问题的时候往往是没有任何头绪,比如在Activity之间传输BitMap的时候,如果Bitmap过大,就会引起问题,比如崩溃等,这其实就跟Binder传输数据大小的限制有关系,在上面的一次拷贝中分析过,mmap函数会为Binder数据传递映射一块连续的虚拟地址,这块虚拟内存空间其实是有大小限制的,不同的进程可能还不一样。 1024) - (4096 *2) :这个限制定义在ProcessState类中,如果传输说句超过这个大小,系统就会报错,因为Binder本身就是为了进程间频繁而灵活的通信所设计的,并不是为了拷贝大数据而使用的: define BINDER_VM_SIZE ((110241024) - (4096 *2)) 而在内核中,其实也有个限制,是4M,不过由于APP中已经限制了不到1M,这里的限制似乎也没多大用途: static int binder_mmap(struct file *filp, struct vm_area_struct *vma) { int ret; struct vm_struct *area; struct binder_proc *proc = filp->private_data; const char *failure_string; struct binder_buffer *buffer; //限制不能超过4M if ((vma->vm_end - vma->vm_start) > SZ_4M) vma->vm_end = vma->vm_start + SZ_4M; 。。。 } 有个特殊的进程ServiceManager进程,它为自己申请的Binder内核空间是128K,这个同ServiceManager的用途是分不开的,ServcieManager主要面向系统Service,只是简单的提供一些addServcie,getService的功能,不涉及多大的数据传输,因此不需要申请多大的内存: int main(int argc, char **argv) { struct binder_state *bs; void *svcmgr = BINDER_SERVICE_MANAGER; // 仅仅申请了128k bs = binder_open(128*1024); if (binder_become_context_manager(bs)) { ALOGE("cannot become context manager (%s)\n", strerror(errno)); return -1; } svcmgr_handle = svcmgr; binder_loop(bs, svcmgr_handler); return 0; } 系统服务与bindService等启动的服务的区别 服务可分为系统服务与普通服务,系统服务一般是在系统启动的时候,由SystemServer进程创建并注册到ServiceManager中的。而普通服务一般是通过ActivityManagerService启动的服务,或者说通过四大组件中的Service组件启动的服务。这两种服务在实现跟使用上是有不同的,主要从以下几个方面: 服务的启动方式服务的注册与管理服务的请求使用方式首先看一下服务的启动上,系统服务一般都是SystemServer进程负责启动,比如AMS,WMS,PKMS,电源管理等,这些服务本身其实实现了Binder接口,作为Binder实体注册到ServiceManager中,被ServiceManager管理,而SystemServer进程里面会启动一些Binder线程,主要用于监听Client的请求,并分发给响应的服务实体类,可以看出,这些系统服务是位于SystemServer进程中(有例外,比如Media服务)。在来看一下bindService类型的服务,这类服务一般是通过Activity的startService或者其他context的startService启动的,这里的Service组件只是个封装,主要的是里面Binder服务实体类,这个启动过程不是ServcieManager管理的,而是通过ActivityManagerService进行管理的,同Activity管理类似。 再来看一下服务的注册与管理:系统服务一般都是通过ServiceManager的addService进行注册的,这些服务一般都是需要拥有特定的权限才能注册到ServiceManager,而bindService启动的服务可以算是注册到ActivityManagerService,只不过ActivityManagerService管理服务的方式同ServiceManager不一样,而是采用了Activity的管理模型,详细的可以自行分析 最后看一下使用方式,使用系统服务一般都是通过ServiceManager的getService得到服务的句柄,这个过程其实就是去ServiceManager中查询注册系统服务。而bindService启动的服务,主要是去ActivityManagerService中去查找相应的Service组件,最终会将Service内部Binder的句柄传给Client。 Binder线程、Binder主线程、Client请求线程的概念与区别 Binder线程是执行Binder服务的载体,只对于服务端才有意义,对请求端来说,是不需要考虑Binder线程的,但Android系统的处理机制其实大部分是互为C/S的。比如APP与AMS进行交互的时候,都互为对方的C与S,这里先不讨论这个问题,先看Binder线程的概念。 Binder线程就是执行Binder实体业务的线程,一个普通线程如何才能成为Binder线程呢?很简单,只要开启一个监听Binder字符设备的Loop线程即可,在Android中有很多种方法,不过归根到底都是监听Binder,换成代码就是通过ioctl来进行监听。 拿ServerManager进程来说,其主线就是Binder线程,其做法是通过binder_loop实现不死线程: void binder_loop(struct binder_state *bs, binder_handler func) { ... for (;;) { <!--关键点1--> res = ioctl(bs->fd, BINDER_WRITE_READ, &amp;bwr); <!--关键点2--> res = binder_parse(bs, 0, readbuf, bwr.read_consumed, func); 。。 } } 上面的关键代码1就是阻塞监听客户端请求,2 就是处理请求,并且这是一个死循环,不退出。再来看SystemServer进程中的线程,在Android4.3(6.0以后打代码就不一样了)中SystemSever主线程便是Binder线程,同时一个Binder主线程,Binder线程与Binder主线程的区别是:线程是否可以终止Loop,不过目前启动的Binder线程都是无法退出的,其实可以全部看做是Binder主线程,其实现原理是,在SystemServer主线程执行到最后的时候,Loop监听Binder设备,变身死循环线程,关键代码如下: extern "C" status_t system_init() { ... ALOGI("System server: entering thread pool.\n"); ProcessState::self()->startThreadPool(); IPCThreadState::self()->joinThreadPool(); ALOGI("System server: exiting thread pool.\n"); return NO_ERROR; } ProcessState::self()->startThreadPool()是新建一个Binder主线程,而PCThreadState::self()->joinThreadPool()是将当前线程变成Binder主线程。其实startThreadPool最终也会调用joinThreadPool,看下其关键函数: void IPCThreadState::joinThreadPool(bool isMain) { ... status_t result; do { int32_t cmd; ...关键点1 result = talkWithDriver(); if (result >= NO_ERROR) { ...关键点2 result = executeCommand(cmd); } // 非主线程的可以退出 if(result == TIMED_OUT &amp;&amp; !isMain) { break; } // 死循环,不完结,调用了这个,就好比是开启了Binder监听循环, } while (result != -ECONNREFUSED &amp;&amp; result != -EBADF); } status_t IPCThreadState::talkWithDriver(bool doReceive) { do { ...关键点3 if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &amp;bwr) >= 0) } 先看关键点1 talkWithDriver,其实质还是去掉用ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)去不断的监听Binder字符设备,获取到Client传输的数据后,再通过executeCommand去执行相应的请求,joinThreadPool是普通线程化身Binder线程最常见的方式。不信,就再看一个MediaService,看一下main_mediaserver的main函数: int main(int argc, char** argv) { 。。。 sp<ProcessState> proc(ProcessState::self()); sp<IServiceManager> sm = defaultServiceManager(); ALOGI("ServiceManager: %p", sm.get()); AudioFlinger::instantiate(); MediaPlayerService::instantiate(); CameraService::instantiate(); AudioPolicyService::instantiate(); registerExtensions(); ProcessState::self()->startThreadPool(); IPCThreadState::self()->joinThreadPool(); } 其实还是通过joinThreadPool变身Binder线程,至于是不是主线程,看一下下面的函数: void IPCThreadState::joinThreadPool(bool isMain) void ProcessState::spawnPooledThread(bool isMain) { if (mThreadPoolStarted) { String8 name = makeBinderThreadName(); ALOGV("Spawning new pooled thread, name=%s\n", name.string()); sp<Thread> t = new PoolThread(isMain); t->run(name.string()); } } 其实关键就是就是传递给joinThreadPool函数的isMain是否是true,不过是否是Binder主线程并没有什么用,因为源码中并没有为这两者的不同处理留入口,感兴趣可以去查看一下binder中的TIMED_OUT。 最后来看一下普通Client的binder请求线程,比如我们APP的主线程,在startActivity请求AMS的时候,APP的主线程成其实就是Binder请求线程,在进行Binder通信的过程中,Client的Binder请求线程会一直阻塞,知道Service处理完毕返回处理结果。 Binder请求的同步与异步 很多人都会说,Binder是对Client端同步,而对Service端异步,其实并不完全正确,在单次Binder数据传递的过程中,其实都是同步的。只不过,Client在请求Server端服务的过程中,是需要返回结果的,即使是你看不到返回数据,其实还是会有个成功与失败的处理结果返回给Client,这就是所说的Client端是同步的。至于说服务端是异步的,可以这么理解:在服务端在被唤醒后,就去处理请求,处理结束后,服务端就将结果返回给正在等待的Client线程,将结果写入到Client的内核空间后,服务端就会直接返回了,不会再等待Client端的确认,这就是所说的服务端是异步的,可以从源码来看一下: Client端同步阻塞请求 status_t IPCThreadState::transact(int32_t handle, uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) { if (reply) { err = waitForResponse(reply); } ... Client在请求服务的时候 Parcel* reply基本都是非空的(还没见过空用在什么位置),非空就会执行waitForResponse(reply),如果看过几篇Binder分析文章的人应该都会知道,在A端向B写完数据之后,A会返回给自己一个BR_TRANSACTION_COMPLETE命令,告知自己数据已经成功写入到B的Binder内核空间中去了,如果是需要回复,在处理完BR_TRANSACTION_COMPLETE命令后会继续阻塞等待结果的返回: status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult){ ... while (1) { if ((err=talkWithDriver()) < NO_ERROR) break; cmd = mIn.readInt32(); switch (cmd) { <!--关键点1 --> case BR_TRANSACTION_COMPLETE: if (!reply &amp;&amp; !acquireResult) goto finish; break; <!--关键点2 --> case BR_REPLY: { binder_transaction_data tr; // free buffer,先设置数据,直接 if (reply) { if ((tr.flags &amp; TF_STATUS_CODE) == 0) { // 牵扯到数据利用,与内存释放 reply->ipcSetDataReference(...) } goto finish; } finish: ... return err; } 关键点1就是处理BR_TRANSACTION_COMPLETE,如果需要等待reply,还要通过talkWithDriver等待结果返回,最后执行关键点2,处理返回数据。对于服务端来说,区别就在于关键点1 ,来看一下服务端Binder线程的代码,拿常用的joinThreadPool来看,在talkWithDriver后,会执行executeCommand函数, void IPCThreadState::joinThreadPool(bool isMain) { ... status_t result; do { int32_t cmd; ...关键点1 result = talkWithDriver(); if (result >= NO_ERROR) { ...关键点2 result = executeCommand(cmd); } // 非主线程的可以退出 if(result == TIMED_OUT &amp;&amp; !isMain) { break; } // 死循环,不完结,调用了这个,就好比是开启了Binder监听循环, } while (result != -ECONNREFUSED &amp;&amp; result != -EBADF); } executeCommand会进一步调用sendReply函数,看一下这里的特点waitForResponse(NULL, NULL),这里传递的都是null,在上面的关键点1的地方我们知道,这里不需要等待Client返回,因此会直接 goto finish,这就是所说的Client同步,而服务端异步的逻辑。 // BC_REPLY status_t IPCThreadState::sendReply(const Parcel&amp; reply, uint32_t flags) { // flag 0 status_t err; status_t statusBuffer; err = writeTransactionData(BC_REPLY, flags, -1, 0, reply, &amp;statusBuffer); if (err < NO_ERROR) return err; return waitForResponse(NULL, NULL); } case BR_TRANSACTION_COMPLETE: if (!reply && !acquireResult) goto finish; break; 请求同步最好的例子就是在Android6.0之前,国产ROM权限的申请都是同步的,在申请权限的时候,APP申请权限的线程会阻塞,就算是UI线程也会阻塞,ROM为了防止ANR,都会为权限申请设置一个倒计时,不操作,就给个默认操作,有兴趣可以自己分析。 Android APP进程天生支持Binder通信的原理是什么 Android APP进程都是由Zygote进程孵化出来的。常见场景:点击桌面icon启动APP,或者startActivity启动一个新进程里面的Activity,最终都会由AMS去调用Process.start()方法去向Zygote进程发送请求,让Zygote去fork一个新进程,Zygote收到请求后会调用Zygote.forkAndSpecialize()来fork出新进程,之后会通过RuntimeInit.nativeZygoteInit来初始化Andriod APP运行需要的一些环境,而binder线程就是在这个时候新建启动的,看下面的源码(Android 4.3): 这里不分析Zygote,只是给出其大概运行机制,Zygote在启动后,就会通过runSelectLoop不断的监听socket,等待请求来fork进程,如下: private static void runSelectLoop() throws MethodAndArgsCaller { ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>(); ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>(); FileDescriptor[] fdArray = new FileDescriptor[4]; ... int loopCount = GC_LOOP_COUNT; while (true) { int index; ... boolean done; done = peers.get(index).runOnce(); ... }}} 每次fork请求到来都会调用ZygoteConnection的runOnce()来处理请求, boolean runOnce() throws ZygoteInit.MethodAndArgsCaller { String args[]; Arguments parsedArgs = null; FileDescriptor[] descriptors; 。。。 try { ...关键点1 pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids, parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo, parsedArgs.niceName); } try { if (pid == 0) { // in child ...关键点2 handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr); 。。。 } runOnce()有两个关键点,关键点1 Zygote.forkAndSpecialize就是通过fork系统调用来新建进程,关键点2 handleChildProc就是对新建的APP进程进行一些初始化工作,为Android Java进程创建一些必须的场景。Zygote.forkAndSpecialize没什么可看的,就是Linux中的fork进程,这里主要看一下handleChildProc private void handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr) throws ZygoteInit.MethodAndArgsCaller { //从Process.start启动的parsedArgs.runtimeInit一般都是true if (parsedArgs.runtimeInit) { if (parsedArgs.invokeWith != null) { WrapperInit.execApplication(parsedArgs.invokeWith, parsedArgs.niceName, parsedArgs.targetSdkVersion, pipeFd, parsedArgs.remainingArgs); } else { // Android应用启动都走该分支 RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs); } } 接着看 RuntimeInit.zygoteInit函数 public static final void zygoteInit(int targetSdkVersion, String[] argv) throws ZygoteInit.MethodAndArgsCaller { redirectLogStreams(); commonInit(); <!--关键点1--> nativeZygoteInit(); <!--关键点2--> applicationInit(targetSdkVersion, argv); } 先看关键点1,nativeZygoteInit属于Native方法,该方法位于AndroidRuntime.cpp中,其实就是调用调用到app_main.cpp中的onZygoteInit static void com_android_internal_os_RuntimeInit_nativeZygoteInit(JNIEnv* env, jobject clazz) { gCurRuntime->onZygoteInit(); } 关键就是onZygoteInit virtual void onZygoteInit() { sp proc = ProcessState::self(); //启动新binder线程loop proc->startThreadPool(); } 首先,ProcessState::self()函数会调用open()打开/dev/binder设备,这个时候Client就能通过Binder进行远程通信;其次,proc->startThreadPool()负责新建一个binder线程,监听Binder设备,这样进程就具备了作为Binder服务端的资格。每个APP的进程都会通过onZygoteInit打开Binder,既能作为Client,也能作为Server,这就是Android进程天然支持Binder通信的原因。 Android APP有多少Binder线程,是固定的么? 通过上一个问题我们知道了Android APP线程为什么天然支持Binder通信,并且可以作为Binder的Service端,同时也对Binder线程有了一个了解,那么在一个Android APP的进程里面究竟有多少个Binder线程呢?是固定的吗。在分析上一个问题的时候,我们知道Android APP进程在Zygote fork之初就为它新建了一个Binder主线程,使得APP端也可以作为Binder的服务端,这个时候Binder线程的数量就只有一个,假设我们的APP自身实现了很多的Binder服务,一个线程够用的吗?这里不妨想想一下SystemServer进程,SystemServer拥有很多系统服务,一个线程应该是不够用的,如果看过SystemServer代码可能会发现,对于Android4.3的源码,其实一开始为该服务开启了两个Binder线程。还有个分析Binder常用的服务,media服务,也是在一开始的时候开启了两个线程。 先看下SystemServer的开始加载的线程:通过 ProcessState::self()->startThreadPool()新加了一个Binder线程,然后通过IPCThreadState::self()->joinThreadPool();将当前线程变成Binder线程,注意这里是针对Android4.3的源码,android6.0的这里略有不同。 extern "C" status_t system_init() { ... ALOGI("System server: entering thread pool.\n"); ProcessState::self()->startThreadPool(); IPCThreadState::self()->joinThreadPool(); ALOGI("System server: exiting thread pool.\n"); return NO_ERROR; } 再看下Media服务,同SystemServer类似,也是开启了两个Binder线程: int main(int argc, char** argv) { ... ProcessState::self()->startThreadPool(); IPCThreadState::self()->joinThreadPool(); } 可以看出Android APP上层应用的进程一般是开启一个Binder线程,而对于SystemServer或者media服务等使用频率高,服务复杂的进程,一般都是开启两个或者更多。来看第二个问题,Binder线程的数目是固定的吗?答案是否定的,驱动会根据目标进程中是否存在足够多的Binder线程来告诉进程是不是要新建Binder线程,详细逻辑,首先看一下新建Binder线程的入口: status_t IPCThreadState::executeCommand(int32_t cmd) { BBinder* obj; RefBase::weakref_type* refs; status_t result = NO_ERROR; switch (cmd) { ... // 可以根据内核返回数据创建新的binder线程 case BR_SPAWN_LOOPER: mProcess->spawnPooledThread(false); break; } executeCommand一定是从Bindr驱动返回的BR命令,这里是BR_SPAWN_LOOPER,什么时候,Binder驱动会向进程发送BR_SPAWN_LOOPER呢?全局搜索之后,发现只有一个地方binder_thread_read,如果直观的想一下,什么时候需要新建Binder线程呢?很简单,不够用的时候,注意上面使用的是spawnPooledThread(false),也就是说这里启动的都是普通Binder线程。为了了解启动时机,先看一些binder_proc内部判定参数的意义: struct binder_proc { ... int max_threads; // 进程所能启动的最大非主Binder线程数目 int requested_threads; // 请求启动的非主线程数 int requested_threads_started;//已经启动的非主线程数 int ready_threads; // 当前可用的Binder线程数 ... }; 再来看binder_thread_read函数中是么时候会去请求新建Binder线程,以Android APP进程为例子,通过前面的分析知道APP进程天然支持Binder通信,因为它有一个Binder主线程,启动之后就会阻塞等待Client请求,这里会更新proc->ready_threads,第一次阻塞等待的时候proc->ready_threads=1,之后睡眠。 binder_thread_read(){ ... retry: //当前线程todo队列为空且transaction栈为空,则代表该线程是空闲的 ,看看是不是自己被复用了 wait_for_proc_work = thread->transaction_stack == NULL &amp;&amp; list_empty(&amp;thread->todo); ...//可用线程个数+1 if (wait_for_proc_work) proc->ready_threads++; binder_unlock(__func__); if (wait_for_proc_work) { ... //当进程todo队列没有数据,则进入休眠等待状态 ret = wait_event_freezable_exclusive(proc->wait, binder_has_proc_work(proc, thread)); } else { if (non_block) { ... } else //当线程todo队列没有数据,则进入休眠等待状态 ret = wait_event_freezable(thread->wait, binder_has_thread_work(thread)); } binder_lock(__func__); //被唤醒可用线程个数-1 if (wait_for_proc_work) proc->ready_threads--; thread->looper &amp;= ~BINDER_LOOPER_STATE_WAITING; ... while (1) { uint32_t cmd; struct binder_transaction_data tr; struct binder_work *w; struct binder_transaction *t = NULL; //先考虑从线程todo队列获取事务数据 if (!list_empty(&amp;thread->todo)) { w = list_first_entry(&amp;thread->todo, struct binder_work, entry); //线程todo队列没有数据, 则从进程todo对获取事务数据 } else if (!list_empty(&amp;proc->todo) &amp;&amp; wait_for_proc_work) { w = list_first_entry(&amp;proc->todo, struct binder_work, entry); } else { } .. if (t->buffer->target_node) { cmd = BR_TRANSACTION; //设置命令为BR_TRANSACTION } else { cmd = BR_REPLY; //设置命令为BR_REPLY } .. done: *consumed = ptr - buffer; //创建线程的条件 if (proc->requested_threads + proc->ready_threads == 0 &amp;&amp; proc->requested_threads_started < proc->max_threads &amp;&amp; (thread->looper &amp; (BINDER_LOOPER_STATE_REGISTERED | BINDER_LOOPER_STATE_ENTERED))) { //需要新建的数目线程数+1 proc->requested_threads++; // 生成BR_SPAWN_LOOPER命令,用于创建新的线程 put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer); } return 0; } 被Client唤醒后proc->ready_threads会-1,之后变成0,这样在执行到done的时候,就会发现proc->requested_threads + proc->ready_threads == 0,这是新建Binder线程的一个必须条件,再看下其他几个条件 if (proc->requested_threads + proc->ready_threads == 0 &amp;&amp; proc->requested_threads_started < proc->max_threads &amp;&amp; (thread->looper &amp; (BINDER_LOOPER_STATE_REGISTERED | BINDER_LOOPER_STATE_ENTERED))) proc->requested_threads + proc->ready_threads == 0 :如果目前还没申请新建Binder线程,并且proc->ready_threads空闲Binder线程也是0,就需要新建一个Binder线程,其实就是为了保证有至少有一个空闲的线程。 proc->requested_threads_started < proc->max_threads:目前启动的普通Binder线程数requested_threads_started还没达到上限(默认APP进程是15) thread->looper & (BINDER_LOOPER_STATE_REGISTERED | BINDER_LOOPER_STATE_ENTERED) 当先线程是Binder线程,这个是一定满足的,不知道为什么列出来 proc->max_threads是多少呢?不同的进程其实设置的是不一样的,看普通的APP进程,在ProcessState::self()新建ProcessState单利对象的时候会调用ioctl(fd, BINDER_SET_MAX_THREADS, &maxThreads);设置上限,可以看到默认设置的上限是15。 static int open_driver() { int fd = open("/dev/binder", O_RDWR); ... size_t maxThreads = 15; result = ioctl(fd, BINDER_SET_MAX_THREADS, &amp;maxThreads); ... } 如果满足新建的条件,就会将proc->requested_threads加1,并在驱动执行完毕后,利用put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer);通知服务端在用户空间发起新建Binder线程的操作,新建的是普通Binder线程,最终再进入binder_thread_write的BC_REGISTER_LOOPER: int binder_thread_write(struct binder_proc *proc, struct binder_thread *thread, void __user *buffer, int size, signed long *consumed) { ... case BC_REGISTER_LOOPER: ... // requested_threads -- proc->requested_threads--; proc->requested_threads_started++; } } 这里会将proc->requested_threads复原,其实就是-1,并且启动的Binder线程数+1。 个人理解,之所以采用动态新建Binder线程的意义有两点,第一:如果没有Client请求服务,就保持线程数不变,减少资源浪费,需要的时候再分配新线程。第二:有请求的情况下,保证至少有一个空闲线程是给Client端,以提高Server端响应速度。 不过这里有一点要注意,对于同一个线程的请求,如果是阻塞的,那么没什么问题,肯定是等待上一个请求结束才能处理下一个,但是对于oneway方式的binder请求呢,这里就会存在这么一个场景,对于oneway的请求,如果上一个还没处理完,同一个线程的新的oneway请求会被塞到同一个目标线程等待执行,而不会触发创建新的Binder线程,因为这并不会妨碍另一端的处理,因为它压根无需等待,但是这可能会造成服务端单个线程任务繁重,而其他线程保持空闲,不过在一定程度上实现了同一种任务的顺序执行,可能也有一定的好处吧。 原文发布时间为:2018-07-18本文作者:看书的小蜗牛本文来自云栖社区合作伙伴“安卓巴士Android开发者门户”,了解相关信息可以关注“安卓巴士Android开发者门户”。

优秀的个人博客,低调大师

java面试-计算机网络传输层知识点全覆盖

点击打开网络通信的七层协议 传输层概述 作用:传输层为它上面的应用层提供通信服务。 在OSI七层参考模型中,传输层是面向通信的最高层,也是用户功能的最底层。 传输层两大重要的功能:复用 和 分用。 复用:在发送端,多个应用进程公用一个传输层; 分用:在接收端,传输层会根据端口号将数据分派给不同的应用进程。 和网络层的区别: 网络层为不同主机提供通信服务,而传输层为不同主机的不同应用提供通信服务。 网络层只对报文头部进行差错检测,而传输层对整个报文进行差错检测。 UDP(用户数据报协议)详解 UDP的特点 UDP只在IP数据报服务的基础上增加了少量的功能:复用与分用、对整个报文的差错检测。 UDP是无连接的通信前不需要建立连接,通信结束也无需释放连接。 UDP是不可靠的它是尽力而为交付,不能确保每一个数据报都送达。 UDP是面向报文的所谓『面向报文』就是指:UDP数据传输的单位是报文,且不会对数据作任何 拆分 和 拼接 操作。在发送端,应用程序给传输层的UDP什么样的数据,UDP不会对数据进行切分,只增加一个UDP头并交给网络层。在接收端,UDP收到网络层的数据报后,去除IP数据报头部后遍交给应用层,不会作任何拼接操作。 UDP没有拥塞控制UDP始终以恒定的速率发送数据,并不会根据网络拥塞情况对发送速率作调整。这种方式有利有弊。弊端:网络拥塞时有些报文可能会丢失,因此UDP不可靠。优点:有些使用场景允许报文丢失,如:直播、语音通话,但对实时性要求很高,此时UDP还是很有用武之地的。 UDP支持一对一、一对多、多对多、多对一通信而TCP只支持一对一通信。 UDP首部开销小,只有8字节。而TCP头部至少由20字节,相比于TCP要高效很多。 PS:问:UDP不可靠具体体现在哪些方面?数据报丢失?数据报顺序? UDP报文头 源端口 目的端口 长度:整个数据报的长度 检验和:整个数据报的检验和。 TCP(传输控制协议)详解 TCP特点 TCP是面向连接的通信前需要建立连接,通信结束需要释放连接。 TCP提供可靠交付服务所谓『可靠』指的是:TCP发送的数据无重复、无丢失、无错误、与发送端顺序一致。 TCP是面向字节流的所谓『面向字节流』指的是:TCP以字节为单位。虽然传输的过程中数据被划分成一个个数据报,但这只是为了方便传输,接收端最终接受到的数据将与发送端的数据一模一样。 TCP提供全双工通信所谓『全双工通信』指的是:TCP的两端既可以作为发送端,也可以作为接收端。 一条TCP连接的两端只能有两个端点TCP只能提供点到点的通信,而UDP可以任意方式的通信。 TCP连接 与 套接字 什么是『TCP连接』?TCP连接是一种抽象的概念,表示一条可以通信的链路。每条TCP连接有且仅有两个端点,表示通信的双方。且双发在任意时刻都可以作为发送者和接收者。 什么是『套接字』?一条TCP连接的两端就是两个套接字。套接字=IP地址:端口号。因此,TCP连接=(套接字1,套接字2)=(IP1:端口号1,IP2:端口号2) TCP头部 TCP头部长度有20字节的固定部分,选项部分长度不定,但最多40字节,因此TCP头部在20-60字节之间。 源端口 和 目的端口传输层和网络层一大重要区别就是传输层指定了数据报发往的应用进程,因此需要端口号标识。 序号当前TCP数据报数据部分的第一个字节的序号。我们知道,TCP是面向字节的,它会对发送的每一个字节进行编号,而且不同数据报之间是连续编号的。由于本字段4字节,可以给[0,2^32-1]个字节进行编号(大约4G),而且序号循环使用,当发送完2^32-1个字节后,序号又从0开始。一般来说,当2^32-1个字节被发送的时候,前面的字节早就发送成功了,因此序号可以循环使用。 确认号表示当前主机作为接收端时,期望接收的下一个字节的编号是多少。也表示,当前主机已经正确接收的最后一个字节序号+1。 数据偏移(报文长度)它表明了数据报头部的长度。 保留字段 标识符TCP有7种标识符,用于表示TCP报文的性质。它们只能为0或1。 URG=1当URG字段被置1,表示本数据报的数据部分包含紧急信息,此时紧急指针有效。紧急数据一定位于当前数据包数据部分的最前面,紧急指针标明了紧急数据的尾部。如control+c:这个命令要求操作系统立即停止当前进程。此时,这条命令就会存放在数据包数据部分的开头,并由紧急指针标识命令的位置,并URG字段被置1。 ACK=1ACK被置1后确认号字段才有效。此外,TCP规定,在连接建立后传送的所有报文段都必须把ACK置1。 PSH=1当接收方收到PSH=1的报文后,会立即将数据交付给应用程序,而不会等到缓冲区满后再提交。一些交互式应用需要这样的功能,降低命令的响应时间。 RST=1当该值为1时,表示当前TCP连接出现严重问题,必须要释放重连。 SYN=1SYN在建立连接时使用。当SYN=1,ACK=0时,表示当前报文段是一个连接请求报文。当SYN=1,ACK=1时,表示当前报文段是一个同意建立连接的应答报文。 FIN=1FIN=1表示此报文段是一个释放连接的请求报文。 接收窗口大小该字段用于实现TCP的流量控制。它表示当前接收方的接收窗口的剩余容量,发送方收到该值后会将发送窗口调整成该值的大小。发送窗口的大小又决定了发送速率,所以接收方通过设置该值就可以控制发送放的发送速率。发送方每收到一个数据报都要调整当前的发送窗口。 检验和用于接收端检验整个数据包在传输过程中是否出错。 紧急指针用于标识紧急数据的尾部。 选项字段上述字段都是每个TCP头部必须要有的,而选项字段是可选的,且长度可变,最长40字节。最常用的选项字段为MMS:最大报文长度。 TCP三次握手 PS:TCP协议中,主动发起请求的一端称为『客户端』,被动连接的一端称为『服务端』。不管是客户端还是服务端,TCP连接建立完后都能发送和接收数据。 起初,服务器和客户端都为CLOSED状态。在通信开始前,双方都得创建各自的传输控制块(TCB)。服务器创建完TCB后遍进入LISTEN状态,此时准备接收客户端发来的连接请求。 第一次握手客户端向服务端发送连接请求报文段。该报文段的头部中SYN=1,ACK=0,seq=x。请求发送后,客户端便进入SYN-SENT状态。 PS1:SYN=1,ACK=0表示该报文段为连接请求报文。 PS2:x为本次TCP通信的字节流的初始序号。TCP规定:SYN=1的报文段不能有数据部分,但要消耗掉一个序号。 第二次握手服务端收到连接请求报文段后,如果同意连接,则会发送一个应答:SYN=1,ACK=1,seq=y,ack=x+1。该应答发送完成后便进入SYN-RCVD状态。 PS1:SYN=1,ACK=1表示该报文段为连接同意的应答报文。 PS2:seq=y表示服务端作为发送者时,发送字节流的初始序号。 PS3:ack=x+1表示服务端希望下一个数据报发送序号从x+1开始的字节。 第三次握手当客户端收到连接同意的应答后,还要向服务端发送一个确认报文段,表示:服务端发来的连接同意应答已经成功收到。该报文段的头部为:ACK=1,seq=x+1,ack=y+1。客户端发完这个报文段后便进入ESTABLISHED状态,服务端收到这个应答后也进入ESTABLISHED状态,此时连接的建立完成! 为什么连接建立需要三次握手,而不是两次握手?防止失效的连接请求报文段被服务端接收,从而产生错误。 PS:失效的连接请求:若客户端向服务端发送的连接请求丢失,客户端等待应答超时后就会再次发送连接请求,此时,上一个连接请求就是『失效的』。 若建立连接只需两次握手,客户端并没有太大的变化,仍然需要获得服务端的应答后才进入ESTABLISHED状态,而服务端在收到连接请求后就进入ESTABLISHED状态。此时如果网络拥塞,客户端发送的连接请求迟迟到不了服务端,客户端便超时重发请求,如果服务端正确接收并确认应答,双方便开始通信,通信结束后释放连接。此时,如果那个失效的连接请求抵达了服务端,由于只有两次握手,服务端收到请求就会进入ESTABLISHED状态,等待发送数据或主动发送数据。但此时的客户端早已进入CLOSED状态,服务端将会一直等待下去,这样浪费服务端连接资源。 TCP四次挥手 TCP连接的释放一共需要四步,因此称为『四次挥手』。我们知道,TCP连接是双向的,因此在四次挥手中,前两次挥手用于断开一个方向的连接,后两次挥手用于断开另一方向的连接。 第一次挥手若A认为数据发送完成,则它需要向B发送连接释放请求。该请求只有报文头,头中携带的主要参数为:FIN=1,seq=u。此时,A将进入FIN-WAIT-1状态。 PS1:FIN=1表示该报文段是一个连接释放请求。 PS2:seq=u,u-1是A向B发送的最后一个字节的序号。 第二次挥手B收到连接释放请求后,会通知相应的应用程序,告诉它A向B这个方向的连接已经释放。此时B进入CLOSE-WAIT状态,并向A发送连接释放的应答,其报文头包含:ACK=1,seq=v,ack=u+1。 PS1:ACK=1:除TCP连接请求报文段以外,TCP通信过程中所有数据报的ACK都为1,表示应答。 PS2:seq=v,v-1是B向A发送的最后一个字节的序号。 PS3:ack=u+1表示希望收到从第u+1个字节开始的报文段,并且已经成功接收了前u个字节。 A收到该应答,进入FIN-WAIT-2状态,等待B发送连接释放请求。 第二次挥手完成后,A到B方向的连接已经释放,B不会再接收数据,A也不会再发送数据。但B到A方向的连接仍然存在,B可以继续向A发送数据。 第三次挥手当B向A发完所有数据后,向A发送连接释放请求,请求头:FIN=1,ACK=1,seq=w,ack=u+1。B便进入LAST-ACK状态。 第四次挥手A收到释放请求后,向B发送确认应答,此时A进入TIME-WAIT状态。该状态会持续2MSL时间,若该时间段内没有B的重发请求的话,就进入CLOSED状态,撤销TCB。当B收到确认应答后,也便进入CLOSED状态,撤销TCB。 为什么A要先进入TIME-WAIT状态,等待2MSL时间后才进入CLOSED状态?为了保证B能收到A的确认应答。若A发完确认应答后直接进入CLOSED状态,那么如果该应答丢失,B等待超时后就会重新发送连接释放请求,但此时A已经关闭了,不会作出任何响应,因此B永远无法正常关闭。 TCP可靠传输的实现 TCP的可靠性表现在:它向应用层提供的数据是 无差错的、有序的、无丢失的,简单的说就是:TCP最终递交给应用层的数据和发送者发送的数据是一模一样的。TCP采用了流量控制、拥塞控制、连续ARQ等技术来保证它的可靠性。 PS:网络层传输的数据单元为『数据报』,传输层的数据单元为『报文段』,但为了方便起见,可以统称为『分组』。 停止等待协议(ARQ协议) TCP保证其可靠性采用的是更为复杂的滑动窗口协议,但停止等待协议是它的简化版,为了方便理解,这里先介绍停止等待协议。 AQR协议 ARQ(Automatic Repeat reQuest)自动重传请求。顾名思义,当请求失败时它会自动重传,直到请求被正确接收为止。这种机制保证了每个分组都能被正确接收。停止等待协议是一种ARQ协议。 停止等待协议的原理 无差错的情况A向B每发送一个分组,都要停止发送,等待B的确认应答;A只有收到了B的确认应答后才能发送下一个分组。 分组丢失和出现差错的情况发送者拥有超时计时器。每发送一个分组便会启动超时计时器,等待B的应答。若超时仍未收到应答,则A会重发刚才的分组。分组出现差错:若B收到分组,但通过检查和字段发现分组在运输途中出现差错,它会直接丢弃该分组,并且不会有任何其他动作。A超时后便会重新发送该分组,直到B正确接收为止。分组丢失:若分组在途中丢失,B并没有收到分组,因此也不会有任何响应。当A超时后也会重传分组,直到正确接收该分组的应答为止。综上所述:当分组丢失 或 出现差错 的情况下,A都会超时重传分组。 应答丢失 和 应答迟到 的情况TCP会给每个字节都打上序号,用于判断该分组是否已经接收。应答丢失:若B正确收到分组,并已经返回应答,但应答在返回途中丢失了。此时A也收不到应答,从而超时重传。紧接着B又收到了该分组。接收者根据序号来判断当前收到的分组是否已经接收,若已接收则直接丢弃,并补上一个确认应答。应答迟到:若由于网络拥塞,A迟迟收不到B发送的应答,因此会超时重传。B收到该分组后,发现已经接收,便丢弃该分组,并向A补上确认应答。A收到应答后便继续发送下一个分组。但经过了很长时间后,那个失效的应答最终抵达了A,此时A可根据序号判断该分组已经接收,此时只需简单丢弃即可。 停止等待协议的注意点 每发送完一个分组,该分组必须被保留,直到收到确认应答为止。 必须给每个分组进行编号。以便按序接收,并判断该分组是否已被接收。 必须设置超时计时器。每发送一个分组就要启动计时器,超时就要重发分组。 计时器的超时时间要大于应答的平均返回时间,否则会出现很多不必要的重传,降低传输效率。但超时时间也不能太长。 滑动窗口协议(连续ARQ协议) 连续ARQ协议在ARQ协议发送者每次只能发送一个分组,在应答到来前必须等待。而连续ARQ协议的发送者拥有一个发送窗口,发送者可以在没有得到应答的情况下连续发送窗口中的分组。这样降低了等待时间,提高了传输效率。 累计确认在连续ARQ协议中,接收者也有个接收窗口,接收者并不需要每收到一个分组就返回一个应答,可以连续收到分组之后统一返回一个应答。这样能节省流量。TCP头部的ack字段就是用来累计确认,它表示已经确认的字节序号+1,也表示期望发送者发送的下一个分组的起始字节号。 发送窗口发送窗口的大小由接收窗口的剩余大小决定。接收者会把当前接收窗口的剩余大小写入应答TCP报文段的头部,发送者收到应答后根据该值和当前网络拥塞情况设置发送窗口的大小。发送窗口的大小是不断变化的。发送窗口由三个指针构成: p1p1指向发送窗口的后沿,它后面的字节表示已经发送且已收到应答。 p2p2指向尚未发送的第一个字节。p1-p2间的字节表示已经发送,但还没收到确认应答。这部分的字节仍需保留,因为可能还要超时重发。p2-p3间的字节表示可以发送,但还没有发送的字节。 p3p3指向发送窗口的前沿,它前面的字节尚未发送,且不允许发送。 发送者每收到一个应答,后沿就可以向前移动指定的字节。此时若窗口大小仍然没变,前沿也可以向前移动指定字节。当p2和前沿重合时,发送者必须等待确认应答。 接收窗口接收者收到的字节会存入接收窗口,接收者会对已经正确接收的有序字节进行累计确认,发送完确认应答后,接收窗口就可以向前移动指定字节。如果某些字节并未按序收到,接收者只会确认最后一个有序的字节,从而乱序的字节就会被重新发送。 连续ARQ的注意点 同一时刻发送窗口的大小并不一定和接收窗口一样大。虽然发送窗口的大小是根据接收窗口的大小来设定的,但应答在网络中传输是有时间的,有可能t1时间接收窗口大小为m,但当确认应答抵达发送者时,接收窗口的大小已经发生了变化。此外发送窗口的大小还随网络拥塞情况影响。当网络出现拥塞时,发送窗口将被调小。 TCP标准并未规定未按序到达的字节的处理方式。但TCP一般都会缓存这些字节,等缺少的字节到达后再交给应用层处理。这比直接丢弃乱序的字节要节约带宽。 TCP标准规定接收方必须要有累计确认功能。接收方可以对多个TCP报文段同时确认,但不能拖太长时间,一般是0.5S以内。此外,TCP允许接收者在有数据要发送的时候捎带上确认应答。但这种情况一般较少,因为一般很少有两个方向都要发送数据的情况。 流量控制 什么是流量控制?如果发送者发送过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。 流量控制的目的?流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面。 如何实现流量控制?由滑动窗口协议(连续ARQ协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。 流量控制引发的死锁当发送者收到了一个窗口为0的应答,发送者便停止发送,等待接收者的下一个应答。但是如果这个窗口不为0的应答在传输过程丢失,发送者一直等待下去,而接收者以为发送者已经收到该应答,等待接收新数据,这样双方就相互等待,从而产生死锁。 持续计时器为了避免流量控制引发的死锁,TCP使用了持续计时器。每当发送者收到一个零窗口的应答后就启动该计时器。时间一到便主动发送报文询问接收者的窗口大小。若接收者仍然返回零窗口,则重置该计时器继续等待;若窗口不为0,则表示应答报文丢失了,此时重置发送窗口后开始发送,这样就避免了死锁的产生。 拥塞控制 拥塞控制 和 流量控制 的区别?1. 拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;2. 流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收。PS:拥塞控制是针对于网络而言的,它是防止往网络中写入太多分组,从而导致网络拥塞的情况;而流量控制是针对接收者的,它是通过控制发送者的发送速度保证接收者能够来得及接收。 拥塞控制的目的?1. 缓解网络压力2. 保证分组按时到达 慢开始算法 和 拥塞避免算法 发送方维护一个发送窗口,发送窗口的大小取决于网络的拥塞情况和接收窗口的大小,发送窗口是动态变化的。 发送方还维护一个慢开始门限 发送窗口 < 慢开始门限:使用慢开始算法 发送窗口 > 慢开始门限:使用拥塞避免算法 发送窗口 = 慢开始门限:使用慢开始算法或拥塞避免算法 算法的具体过程: 通信开始时,发送方的发送窗口设为1,并发送第一个分组M1; 接收方收到M1后,返回确认应答,此时发送方发送窗口扩大两倍,并发送M2、M3;(即,发送方每次收到确认应答后,都将发送窗口设为当前值的两倍) 若发送窗口>慢开始门限,则使用拥塞避免算法,每次收到确认应答后都将发送窗口+1; 若发送方出现了超时重传,则表明网络出现拥塞,此时:a)慢开始门限设为当前发送窗口的一半;b)发送窗口设为1;c)启用拥塞避免算法;PS:发送超时重传时,发送窗口有可能已经超过了慢开始门限,也有可能还没超过;此时不管何种情况,都一律启用拥塞避免算法,并执行上述三步操作! 慢开始算法的作用:慢开始算法将发送窗口从小扩大,而且按指数级扩大,从而避免一开始就往网络中注入过多的分组从而导致拥塞;它将窗口慢慢扩大的过程其实也在探测网络拥塞情况的过程,当发现出现拥塞时,及时降低发送速度,从而减缓网络拥塞。 拥塞避免算法的作用:拥塞避免算法使发送窗口以线性方式增长,而非指数级增长,从而使网络更加不容易发生拥塞。 AIMD算法(加法增大乘法减小算法)慢开始算法 和 拥塞避免算法 还有个名称叫做『加法增大乘法减小算法』。 加法增加:指的是拥塞避免算法,使得发送窗口以线性的方式增长; 乘法减小:指的是不管当前正使用慢开始算法还是拥塞避免算法,只要发生拥塞时,慢开始门限将会变成当前窗口的一半。 快重传算法 和 快恢复算法 上述慢开始算法和拥塞避免算法能保证网络出现拥塞时进行相应的处理,而快重传和快恢复是一种拥塞预防的方式,此时网络可能尚未出现拥塞,但已经有拥塞的征兆,因此得作出一些预防措施。 快重传原理:因为TCP具有累计确认的能力,因此接收者收到一个分组的时候不会立即发出应答,可能需要等待收到多个分组之后再同一发出累计确认。但快重传算法就要求,接收者如果接收到一个乱序的分组的话,就必须立即发出前一个正确分组的确认应答,这样能让发送者尽早地知道有一个分组可能丢失。 快恢复原理:当发送者收到同一个分组的三个确认应答后,就基本可以判断这个分组已经丢失了;这时候无需等待超时,直接执行『乘法减小加法增大』: 将慢开始门限减半; 将发送窗口减半(不设为1); 使用拥塞避免算法;

优秀的个人博客,低调大师

Java常见面试题及答案 21-30(JVM)(集合类)HashMap

21.HashMap的工作原理是什么? HashMap内部是通过一个数组实现的,只是这个数组比较特殊,数组里存储的元素是一个Entry实体(jdk 8为Node),这个Entry实体主要包含key、value以及一个指向自身的next指针。HashMap是基于hashing实现的,当我们进行put操作时,根据传递的key值得到它的hashcode,然后再用这个hashcode与数组的长度进行模运算,得到一个int值,就是Entry要存储在数组的位置(下标);当通过get方法获取指定key的值时,会根据这个key算出它的hash值(数组下标),根据这个hash值获取数组下标对应的Entry,然后判断Entry里的key,hash值或者通过equals()比较是否与要查找的相同,如果相同,返回value,否则的话,遍历该链表(有可能就只有一个Entry,此时直接返回null),直到找到为止,否则返回null。 HashMap之所以在每个数组元素存储的是一个链表,是为了解决hash冲突问题,当两个对象的hash值相等时,那么一个位置肯定是放不下两个值的,于是hashmap采用链表来解决这种冲突,hash值相等的两个元素会形成一个链表。 22.HashMap与HashTable的区别是什么? 1.HashTable基于Dictionary类,而HashMap是基于AbstractMap。Dictionary是任何可将键映射到相应值的类的抽象父类,而AbstractMap是基于Map接口的实现,它以最大限度地减少实现此接口所需的工作。(在java 8中我查看源码发现Hashtable并没有继承Dictionary,而且里面也没有同步方法,是不是java 8中Hashtable不在同步的了?有没有人解释一下?) 2. HashMap的key和value都允许为null,而Hashtable的key和value都不允许为null。HashMap遇到key为null的时候,调用putForNullKey方法进行处理,而对value没有处理;Hashtable遇到null,直接返回NullPointerException。 3. Hashtable是同步的,而HashMap是非同步的,但是我们也可以通过Collections.synchronizedMap(hashMap),使其实现同步。 23.CorrentHashMap的工作原理? jdk 1.6版: ConcurrenHashMap可以说是HashMap的升级版,ConcurrentHashMap是线程安全的,但是与Hashtablea相比,实现线程安全的方式不同。Hashtable是通过对hash表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待其释放锁。ConcurrentHashMap是采用分离锁的方式,它并没有对整个hash表进行锁定,而是局部锁定,也就是说当一个线程占有这个局部锁时,不影响其他线程对hash表其他地方的访问。 具体实现: ConcurrentHashMap内部有一个Segment数组, 该Segment对象可以充当锁。Segment对象内部有一个HashEntry数组,于是每个Segment可以守护若干个桶(HashEntry),每个桶又有可能是一个HashEntry连接起来的链表,存储发生碰撞的元素。 每个ConcurrentHashMap在默认并发级下会创建包含16个Segment对象的数组,每个数组有若干个桶,当我们进行put方法时,通过hash方法对key进行计算,得到hash值,找到对应的segment,然后对该segment进行加锁,然后调用segment的put方法进行存储操作,此时其他线程就不能访问当前的segment,但可以访问其他的segment对象,不会发生阻塞等待。jdk 1.8版 在jdk 8中,ConcurrentHashMap不再使用Segment分离锁,而是采用一种乐观锁CAS算法来实现同步问题,但其底层还是“数组+链表->红黑树”的实现。 24.遍历一个List有哪些不同的方式? List<String> strList = new ArrayList<>(); //for-each for(String str:strList) { System.out.print(str); } //use iterator 尽量使用这种 更安全(fail-fast) Iterator<String> it = strList.iterator(); while(it.hasNext) { System.out.printf(it.next()); } 25.fail-fast与fail-safe有什么区别? Iterator的fail-fast属性与当前的集合共同起作用,因此它不会受到集合中任何改动的影响。Java.util包中的所有集合类都被设计为fail->fast的,而java.util.concurrent中的集合类都为fail-safe的。当检测到正在遍历的集合的结构被改变时,Fail-fast迭代器抛出ConcurrentModificationException,而fail-safe迭代器从不抛出ConcurrentModificationException。 26.Array和ArrayList有何区别?什么时候更适合用Array? Array可以容纳基本类型和对象,而ArrayList只能容纳对象。 Array是指定大小的,而ArrayList大小是固定的 27.哪些集合类提供对元素的随机访问? ArrayList、HashMap、TreeMap和HashTable类提供对元素的随机访问。 28.HashSet的底层实现是什么? 通过看源码知道HashSet的实现是依赖于HashMap的,HashSet的值都是存储在HashMap中的。在HashSet的构造法中会初始化一个HashMap对象,HashSet不允许值重复,因此,HashSet的值是作为HashMap的key存储在HashMap中的,当存储的值已经存在时返回false。 29.LinkedHashMap的实现原理? LinkedHashMap也是基于HashMap实现的,不同的是它定义了一个Entry header,这个header不是放在Table里,它是额外独立出来的。LinkedHashMap通过继承hashMap中的Entry,并添加两个属性Entry before,after,和header结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。LinkedHashMap定义了排序模式accessOrder,该属性为boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。 30.LinkedList和ArrayList的区别是什么? ArrayList是基于数组实现,LinkedList是基于链表实现 ArrayList在查找时速度快,LinkedList在插入与删除时更具优势 原文地址http://www.bieryun.com/560.html

优秀的个人博客,低调大师

面试官问我:线程锁导致的kafka客户端超时,如何解决?

本文分享自华为云社区《线程锁导致的kafka客户端超时问题》,作者: 张俭 。 问题背景 有一个环境的kafka client发送数据有部分超时,拓扑图也非常简单 定位历程 我们先对客户端的环境及JVM情况进行了排查,从JVM所在的虚拟机到kafka server的网络正常,垃圾回收(GC)时间也在预期范围内,没有出现异常。 紧接着,我们把目光转向了kafka 服务器,进行了一些基础的检查,同时也查看了kafka处理请求的超时日志,其中我们关心的metadata和produce请求都没有超时。 问题就此陷入了僵局,虽然也搜到了一些kafka server会对连上来的client反解导致超时的问题(https://github.com/apache/kafka/pull/10059),但通过一些简单的分析,我们确定这并非是问题所在。 同时,我们在环境上也发现一些异常情况,当时觉得不是核心问题/解释不通,没有深入去看 问题JVM线程数较高,已经超过10000,这个线程数量虽然确实较高,但并不会对1个4U的容器产生什么实质性的影响。 负责指标上报的线程CPU较高,大约占用了1/4 ~ 1/2 的CPU核,这个对于4U的容器来看问题也不大 当排查陷入僵局,我们开始考虑其他可能的调查手段。我们尝试抓包来找线索,这里的抓包是SASL鉴权+SSL加密的,非常难读,只能靠长度和响应时间勉强来推断报文的内容。 在这个过程中,我们发现了一个非常重要的线索,客户端竟然发起了超时断链,并且超时的那条消息,实际服务端是有响应回复的。 随后我们将kafka client的trace级别日志打开,这里不禁感叹kafka client日志打的相对较少,发现的确有log.debug(“Disconnecting from node {} due to request timeout.”, nodeId);的日志打印。 与网络相关的流程: try { // 这里发出了请求 client.send(request, time.milliseconds()); while (client.active()) { List<ClientResponse> responses = client.poll(Long.MAX_VALUE, time.milliseconds()); for (ClientResponse response : responses) { if (response.requestHeader().correlationId() == request.correlationId()) { if (response.wasDisconnected()) { throw new IOException("Connection to " + response.destination() + " was disconnected before the response was read"); } if (response.versionMismatch() != null) { throw response.versionMismatch(); } return response; } } } throw new IOException("Client was shutdown before response was read"); } catch (DisconnectException e) { if (client.active()) throw e; else throw new IOException("Client was shutdown before response was read"); } 这个poll方法,不是简单的poll方法,而在poll方法中会进行超时判断,查看poll方法中调用的handleTimedOutRequests方法 @Override public List<ClientResponse> poll(long timeout, long now) { ensureActive(); if (!abortedSends.isEmpty()) { // If there are aborted sends because of unsupported version exceptions or disconnects, // handle them immediately without waiting for Selector#poll. List<ClientResponse> responses = new ArrayList<>(); handleAbortedSends(responses); completeResponses(responses); return responses; } long metadataTimeout = metadataUpdater.maybeUpdate(now); try { this.selector.poll(Utils.min(timeout, metadataTimeout, defaultRequestTimeoutMs)); } catch (IOException e) { log.error("Unexpected error during I/O", e); } // process completed actions long updatedNow = this.time.milliseconds(); List<ClientResponse> responses = new ArrayList<>(); handleCompletedSends(responses, updatedNow); handleCompletedReceives(responses, updatedNow); handleDisconnections(responses, updatedNow); handleConnections(); handleInitiateApiVersionRequests(updatedNow); // 关键的超时判断 handleTimedOutRequests(responses, updatedNow); completeResponses(responses); return responses; } 由此我们推断,问题可能在于客户端hang住了一段时间,从而导致超时断链。我们通过工具Arthas深入跟踪了Kafka的相关代码,甚至发现一些简单的操作(如A.field)也需要数秒的时间。这进一步确认了我们的猜想:问题可能出在JVM。JVM可能在某个时刻出现问题,导致系统hang住,但这并非由GC引起。 为了解决这个问题,我们又检查了监控线程CPU较高的问题。我们发现线程的执行热点是从"sun.management.ThreadImpl"中的"getThreadInfo"方法。 "metrics-1@746" prio=5 tid=0xf nid=NA runnable java.lang.Thread.State: RUNNABLE at sun.management.ThreadImpl.getThreadInfo(Native Method) at sun.management.ThreadImpl.getThreadInfo(ThreadImpl.java:185) at sun.management.ThreadImpl.getThreadInfo(ThreadImpl.java:149) 进一步发现,在某些版本的JDK8中,读取线程信息是需要加锁的。 至此,问题的根源已经清晰明了:过高的线程数以及线程监控时JVM全局锁的存在导致了这个问题。您可以使用如下的demo来复现这个问题 import java.lang.management.ManagementFactory; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ThreadLockSimple { public static void main(String[] args) { for (int i = 0; i < 15_000; i++) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(200_000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }).start(); } ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); executorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("take " + " " + System.currentTimeMillis()); } }, 1, 1, TimeUnit.SECONDS); ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); ScheduledExecutorService metricsService = Executors.newSingleThreadScheduledExecutor(); metricsService.scheduleAtFixedRate(new Runnable() { @Override public void run() { long start = System.currentTimeMillis(); ThreadInfo[] threadInfoList = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds()); System.out.println("threads count " + threadInfoList.length + " cost :" + (System.currentTimeMillis() - start)); } }, 1, 1, TimeUnit.SECONDS); } } 为了解决这个问题,我们有以下几个可能的方案: 将不合理的线程数往下降,可能存在线程泄露的场景 升级jdk到jdk11或者jdk17(推荐) 将Thread相关的监控临时关闭 这个问题的解决方案应根据实际情况进行选择,希望对你有所帮助。 点击关注,第一时间了解华为云新鲜技术~

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。