年后想跳槽?那你必须得这100道面试题
码个蛋(codeegg) 第 816 次推文
码妞看世界
风中飘动的芦花
1. Linux自带多种通讯方式,为什么Android用了Binder
进程: 进程是操作系统的概念. 每当我们执行一个程序时,对于操作系统来讲就创建了一个进程. 在这个过程中,伴随着资源的分配和释放. 可以认为进程是一个程序的一次执行过程.
进程间通信: 进程用户空间是相互独立的,一般而言是不能相互访问的. 但很多情况下进程间需要互相通信,来完成系统的某项功能. 进程通过与内核及其它进程之间的互相通信来协调它们的行为.
Linux现有进程间通信: 1)管道:在创建时分配一个page大小的内存,缓存区大小比较有限. 2)消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信. 3)共享内存:无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决. 4)套接字:作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信. 5)信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源.因此,主要作为进程间以及同一进程内不同线程之间的同步手段. 6)信号:不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等.
对比: 1)从性能的角度(数据拷贝次数) 0次:共享内存 1次:Binder 2次:管道 消息队列 套接字 从性能角度看,Binder性能仅次于共享内存.
2)从稳定性的角度 Binder基于C/S架构,C和S相对独立,稳定性较好. 共享内存实现方式复杂,没有客户与服务端之别,需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题. 从稳定性角度看,Binder优越于共享内存.
3)从安全的角度 传统Linux的IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份. 而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要. 对于普通用户,绝不希望从App商店下载偷窥隐私数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保.
Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志. 前面提到C/S架构,Android系统中对外只暴露Client端,Client端将任务发送给Server端. Server端会根据权限控制策略判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行.
传统IPC只能由用户在数据包里填入UID/PID. 另外,可靠的身份标记只有由IPC机制本身在内核中添加. 其次传统IPC访问接入点是开放的,无法建立私有通道. 从安全角度,Binder的安全性更高.
4)从语言层面的角度 Linux基于C(面向过程),Android基于Java(面向对象). 而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法. 而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中. 可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样. Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中. 从语言层面,Binder更适合基于面向对象语言的Android系统.
5)从公司战略的角度 Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力. Android之父Andy Rubin对于GPL显然是不能接受的.
为此,Google巧妙地将GPL协议控制在内核空间. 将用户空间的协议采用Apache-2.0协议. 同时在GPL协议与Apache-2.0之间的Lib库中采用BSD授权方法,有效隔断了GPL的传染性.
综合上述5点,可知Binder是Android系统上层进程间通信的不二选择.
2. Java中用到的线程调度算法是什么
计算机通常只有一个CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线程轮流获得CPU的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待CPU,JAVA虚拟机的一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配CPU的使用权.
有两种调度模型:分时调度模型和抢占式调度模型。
分时调度模型是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片这个也比较好理解。
java虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃CPU。
一般线程调度模式分为两种——抢占式调度和协同式调度.
抢占式调度指的是每条线程执行的时间、线程的切换都由系统控制,线程的切换不由线程本身决定,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
协同式调度指某一线程执行完后主动通知系统切换到另一线程上执行,线程的执行时间由线程本身控制,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。
Java使用的是哪种线程调度模式?此问题涉及到JVM的实现,JVM规范中规定每个线程都有优先级,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。JVM的规范没有严格地给调度策略定义,一般Java使用的线程调度是抢占式调度,在JVM中体现为让可运行池中优先级高的线程拥有CPU使用权,如果可运行池中线程优先级一样则随机选择线程,但要注意的是实际上一个绝对时间点只有一个线程在运行(这里是相对于一个CPU来说),直到此线程进入非可运行状态或另一个具有更高优先级的线程进入可运行线程池,才会使之让出CPU的使用权
3. 谈谈对Java反射的理解
反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。
通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。
反射的核心是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。
Java 反射主要提供以下功能:
在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
在运行时调用任意一个对象的方法
重点:是运行时而不是编译时
**反射最重要的用途就是开发各种通用框架。**很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 Bean),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射,运行时动态加载需要加载的对象。
由于反射会额外消耗一定的系统资源,因此如果不需要动态地创建一个对象,那么就不需要用反射。
另外,反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
4.如何停止一个正在运行的线程
使用共享变量的方式 在这种方式中,之所以引入共享变量,是因为该变量可以被多个执行相同任务的线程用来作为是否中断的信号,通知中断线程的执行。
使用interrupt方法终止线程 如果一个线程由于等待某些事件的发生而被阻塞,又该怎样停止该线程呢?这种情况经常会发生,比如当一个线程由于需要等候键盘输入而被阻塞,或者调用Thread.join方法,或者Thread.sleep方法,在网络中调用ServerSocket.accept方法,或者调用了DatagramSocket.receive方法时,都有可能导致线程阻塞,使线程处于处于不可运行状态时,即使主程序中将该线程的共享变量设置为true,但该线程此时根本无法检查循环标志,当然也就无法立即中断。这里我们给出的建议是,不要使用stop方法,而是使用Thread提供的interrupt方法,因为该方法虽然不会中断一个正在运行的线程,但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态,退出堵塞代码
5. 并发集合与普通集合的区别
在java中有普通集合、同步(线程安全)的集合、并发集合。普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了 synchronized 同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。 参考阅读: ConcurrentHashMap 是线程安全的HashMap的实现,默认构造同样有initialCapacity 和loadFactor属性,不过还多了一个 concurrencyLevel属性,三属性默认值分别为16、0.75 及 16。其内部使用锁分段技术,维持这锁Segment的数组,在Segment数组中又存放着Entity数组,内部hash算法将数据较均匀分布在不同锁中。
put操作: 并没有在此方法上加上synchronized,首先对key.hashcode进行hash 操作,得到key的hash 值。hash 操作的算法和 map 也不同,根据此 hash 值计算并获取其对应的数组中的 Segment 对象(继承自ReentrantLock),接着调用此Segment对象的put方法来完成当前操作。 ConcurrentHashMap 基于concurrencyLevel划分出了多个Segment 来对key-value进行存储,从而避免每次put操作都得锁住整个数组。在默认的情况下,最佳情况下可允许16 个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。
get(key): 首先对key.hashCode进行hash 操作,基于其值找到对应的Segment 对象,调用其get方法完成当前操作。而 Segment 的 get 操作首先通过 hash 值和对象数组大小减 1 的值进行按位与操作来获取数组上对应位置的HashEntry。在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的HashEntry 产生不一致性,那么ConcurrentHashMap 是如何保证的? 对象数组大小的改变只有在put操作时有可能发生,由于HashEntry对象数组对应的变量是volatile类型的,因此可以保证如HashEntry 对象数组大小发生改变,读操作可看到最新的对象数组大小。 在获取到了 HashEntry 对象后,怎么能保证它及其 next 属性构成的链表上的对象不会改变呢?这点ConcurrentHashMap 采用了一个简单的方式,即HashEntry 对象中的hash、key、next 属性都是final 的,这也就意味着没办法插入一个HashEntry对象到基于next属性构成的链表中间或末尾。这样就可以保证当获取到HashEntry对象后,其基于next 属性构建的链表是不会发生变化的。 ConcurrentHashMap 默认情况下采用将数据分为 16 个段进行存储,并且 16 个段分别持有各自不同的锁Segment,锁仅用于put 和 remove 等改变集合对象的操作,基于volatile 及 HashEntry 链表的不变性实现了读取的不加锁。这些方式使得ConcurrentHashMap 能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的Map而言,而它采用的这些方法也可谓是对于Java内存模型、并发机制深刻掌握的体现。
今日问题:
大家有年后跳槽的心思吗?
专属升级社区:《这件事情,我终于想明白了》
推荐阅读:beoplay a9