Android面试准备(中高级)

Android

Activity生命周期

这里写图片描述

onStart()与onResume()有什么区别?

onStart()是activity界面被显示出来的时候执行的,但不能与它交互;  onResume()是当该activity与用户能进行交互时被执行,用户能够得到activity的焦点,可以与用户交互。html

Activity启动流程

startActivity最终都会调用startActivityForResult,经过ActivityManagerProxy调用system_server进程中ActivityManagerService的startActvity方法,若是须要启动的Activity所在进程未启动,则调用Zygote孵化应用进程,进程建立后会调用应用的ActivityThread的main方法,main方法调用attach方法将应用进程绑定到ActivityManagerService(保存应用的ApplicationThread的代理对象)并开启loop循环接收消息。ActivityManagerService经过ApplicationThread的代理发送Message通知启动Activity,ActivityThread内部Handler处理handleLaunchActivity,依次调用performLaunchActivity,handleResumeActivity(即activity的onCreate,onStart,onResume)。
深刻理解Activity启动流程java

Android类加载器

Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把全部Class文件进行合并,优化,而后生成一个最终的class.dex,目的是把不一样class文件重复的东西只需保留一份,若是咱们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。 Android中经常使用的有两种类加载器,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。区别在于调用父类构造器时,DexClassLoader多传了一个optimizedDirectory参数,这个目录必须是内部存储路径,用来缓存系统建立的Dex文件。而PathClassLoader该参数为null,只能加载内部存储目录的Dex文件。因此咱们能够用DexClassLoader去加载外部的apk。android

Android消息机制

  1. 应用启动是从ActivityThread的main开始的,先是执行了Looper.prepare(),该方法先是new了一个Looper对象,在私有的构造方法中又建立了MessageQueue做为此Looper对象的成员变量,Looper对象经过ThreadLocal绑定MainThread中;
  2. 当咱们建立Handler子类对象时,在构造方法中经过ThreadLocal获取绑定的Looper对象,并获取此Looper对象的成员变量MessageQueue做为该Handler对象的成员变量;
  3. 在子线程中调用上一步建立的Handler子类对象的sendMesage(msg)方法时,在该方法中将msg的target属性设置为本身自己,同时调用成员变量MessageQueue对象的enqueueMessag()方法将msg放入MessageQueue中;
  4. 主线程建立好以后,会执行Looper.loop()方法,该方法中获取与线程绑定的Looper对象,继而获取该Looper对象的成员变量MessageQueue对象,并开启一个会阻塞(不占用资源)的死循环,只要MessageQueue中有msg,就会获取该msg,并执行msg.target.dispatchMessage(msg)方法(msg.target即上一步引用的handler对象),此方法中调用了咱们第二步建立handler子类对象时覆写的handleMessage()方法,以后将该msg对象存入回收池;

Looper.loop()为何不会阻塞主线程

Android是基于事件驱动的,即全部Activity的生命周期都是经过Handler事件驱动的。loop方法中会调用MessageQueue的next方法获取下一个message,当没有消息时,基于Linux pipe/epoll机制会阻塞在loop的queue.next()中的nativePollOnce()方法里,并不会消耗CPU。git

IdleHandler (闲时机制)

IdleHandler是一个回调接口,能够经过MessageQueue的addIdleHandler添加实现类。当MessageQueue中的任务暂时处理完了(没有新任务或者下一个任务延时在以后),这个时候会回调这个接口,返回false,那么就会移除它,返回true就会在下次message处理完了的时候继续回调。github

同步屏障机制(sync barrier)

同步屏障能够经过MessageQueue.postSyncBarrier函数来设置。该方法发送了一个没有target的Message到Queue中,在next方法中获取消息时,若是发现没有target的Message,则在必定的时间内跳过同步消息,优先执行异步消息。再换句话说,同步屏障为Handler消息机制增长了一种简单的优先级机制,异步消息的优先级要高于同步消息。在建立Handler时有一个async参数,传true表示此handler发送的时异步消息。ViewRootImpl.scheduleTraversals方法就使用了同步屏障,保证UI绘制优先执行。web

View的绘制原理

View的绘制从ActivityThread类中Handler的处理RESUME_ACTIVITY事件开始,在执行performResumeActivity以后,建立Window以及DecorView并调用WindowManager的addView方法添加到屏幕上,addView又调用ViewRootImpl的setView方法,最终执行performTraversals方法,依次执行performMeasure,performLayout,performDraw。也就是view绘制的三大过程。
measure过程测量view的视图大小,最终须要调用setMeasuredDimension方法设置测量的结果,若是是ViewGroup须要调用measureChildren或者measureChild方法进而计算本身的大小。
layout过程是摆放view的过程,View不须要实现,一般由ViewGroup实现,在实现onLayout时能够经过getMeasuredWidth等方法获取measure过程测量的结果进行摆放。 draw过程先是绘制背景,其次调用onDraw()方法绘制view的内容,再而后调用dispatchDraw()调用子view的draw方法,最后绘制滚动条。ViewGroup默认不会执行onDraw方法,若是复写了onDraw(Canvas)方法,须要调用 setWillNotDraw(false);清楚不须要绘制的标记。
Android视图绘制流程彻底解析,带你一步步深刻了解View(二)算法

什么是MeasureSpec

MeasureSpec表明一个32位int值,高两位表明SpecMode(测量模式),低30位表明SpecSize(具体大小)。 SpecMode有三类:json

  • UNSPECIFIED 表示父容器不对View有任何限制,通常用于系统内部,表示一种测量状态;
  • EXACTLY 父容器已经检测出view所需的精确大小,这时候view的最终大小SpecSize所指定的值,至关于match_parent或指定具体数值。
  • AT_MOST 父容器指定一个可用大小即SpecSize,view的大小不能大于这个值,具体多大要看view的具体实现,至关于wrap_content。

getWidth()方法和getMeasureWidth()区别呢?

首先getMeasureWidth()方法在measure()过程结束后就能够获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是经过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是经过视图右边的坐标减去左边的坐标计算出来的。数组

事件分发机制

图解 Android 事件分发机制缓存

requestLayout,invalidate,postInvalidate区别与联系

相同点:三个方法都有刷新界面的效果。 不一样点:invalidate和postInvalidate只会调用onDraw()方法;requestLayout则会从新调用onMeasure、onLayout、onDraw。

调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器经过计算得出自身须要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制须要重绘的视图)。
调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每个含有标记位的view及其子View都会进行测量onMeasure、布局onLayout、绘制onDraw。
Android View 深度分析requestLayout、invalidate与postInvalidate

Binder机制,共享内存实现原理

为何使用Binder?

v2-30dce36be4e6617596b5fab96ef904c6_hd.jpg

概念 进程隔离 进程空间划分:用户空间(User Space)/内核空间(Kernel Space) 系统调用:用户态与内核态

原理 跨进程通讯是须要内核空间作支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,所以经过内核支持来实现进程间通讯天然是没问题的。可是 Binder 并非 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具备独立功能的程序,它能够被单独编译,可是不能独立运行。它在运行时被连接到内核做为内核的一部分运行。这样,Android 系统就能够经过动态添加一个内核模块运行在内核空间,用户进程之间经过这个内核模块做为桥梁来实现通讯。

在 Android 系统中,这个运行在内核空间,负责各个用户进程经过 Binder 实现通讯的内核模块就叫 Binder 驱动(Binder Dirver)。

那么在 Android 系统中用户进程之间是如何经过这个内核模块(Binder 驱动)来实现通讯的呢?难道是和前面说的传统 IPC 机制同样,先将数据从发送方进程拷贝到内核缓存区,而后再将数据从内核缓存区拷贝到接收方进程,经过两次拷贝来实现吗?显然不是,不然也不会有开篇所说的 Binder 在性能方面的优点了。

这就不得不通道 Linux 下的另外一个概念:内存映射

Binder IPC 机制中涉及到的内存映射经过 mmap() 来实现,mmap() 是操做系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系创建后,用户对这块内存区域的修改能够直接反应到内核空间;反以内核空间对这段区域的修改也能直接反应到用户空间。
一次完整的 Binder IPC 通讯过程一般是这样:

  1. 首先 Binder 驱动在内核空间建立一个数据接收缓存区;
  2. 接着在内核空间开辟一块内核缓存区,创建内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
  3. 发送方进程经过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,因为内核缓存区和接收进程的用户空间存在内存映射,所以也就至关于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通讯。

Binder通信模型 Binder是基于C/S架构的,其中定义了4个角色:Client、Server、Binder驱动和ServiceManager。

  • Binder驱动:相似网络通讯中的路由器,负责将Client的请求转发到具体的Server中执行,并将Server返回的数据传回给Client。
  • ServiceManager:相似网络通讯中的DNS服务器,负责将Client请求的Binder描述符转化为具体的Server地址,以便Binder驱动可以转发给具体的Server。Server如需提供Binder服务,须要向ServiceManager注册。 具体的通信过程
  1. Server向ServiceManager注册。Server经过Binder驱动向ServiceManager注册,声明能够对外提供服务。ServiceManager中会保留一份映射表。
  2. Client向ServiceManager请求Server的Binder引用。Client想要请求Server的数据时,须要先经过Binder驱动向ServiceManager请求Server的Binder引用(代理对象)。
  3. 向具体的Server发送请求。Client拿到这个Binder代理对象后,就能够经过Binder驱动和Server进行通讯了。
  4. Server返回结果。Server响应请求后,须要再次经过Binder驱动将结果返回给Client。

ServiceManager是一个单独的进程,那么Server与ServiceManager通信是靠什么呢? 当Android系统启动后,会建立一个名称为servicemanager的进程,这个进程经过一个约定的命令BINDERSETCONTEXT_MGR向Binder驱动注册,申请成为为ServiceManager,Binder驱动会自动为ServiceManager建立一个Binder实体。而且这个Binder实体的引用在全部的Client中都为0,也就说各个Client经过这个0号引用就能够和ServiceManager进行通讯。Server经过0号引用向ServiceManager进行注册,Client经过0号引用就能够获取到要通讯的Server的Binder引用。 写给 Android 应用工程师的 Binder 原理剖析
一篇文章了解相见恨晚的 Android Binder 进程间通信机制

序列化的方式

Serializable是Java提供的一个序列化接口,是一个空接口,用于标示对象是否能够支持序列化,经过ObjectOutputStrean及ObjectInputStream实现序列化和反序列化的过程。注意能够为须要序列化的对象设置一个serialVersionUID,在反序列化的时候系统会检测文件中的serialVersionUID是否与当前类的值一致,若是不一致则说明类发生了修改,反序列化失败。所以对于可能会修改的类最好指定serialVersionUID的值。
Parcelable是Android特有的一个实现序列化的接口,在Parcel内部包装了可序列化的数据,能够在Binder中自由传输。序列化的功能由writeToParcel方法来完成,最终经过Parcel的一系列write方法完成。反序列化功能由CREAOR来完成,其内部标明了如何建立序列化对象和数组,并经过Parcel的一系列read方法来完成反序列化的过程。

Fragment的懒加载实现

Fragment可见状态改变时会被调用setUserVisibleHint()方法,能够经过复写该方法实现Fragment的懒加载,但须要注意该方法可能在onVIewCreated以前调用,须要确保界面已经初始化完成的状况下再去加载数据,避免空指针。
Fragment的懒加载

RecyclerView与ListView(缓存原理,区别联系,优缺点)

缓存区别:

  1. 层级不一样: ListView有两级缓存,在屏幕与非屏幕内。
    RecyclerView比ListView多两级缓存,支持多个离屏ItemView缓存(匹配pos获取目标位置的缓存,若是匹配则无需再次bindView),支持开发者自定义缓存处理逻辑,支持全部RecyclerView共用同一个RecyclerViewPool(缓存池)。
  2. 缓存不一样: ListView缓存View。
    RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为: View + ViewHolder(避免每次createView时调用findViewById) + flag(标识状态);

优势 RecylerView提供了局部刷新的接口,经过局部刷新,就能避免调用许多无用的bindView。 RecyclerView的扩展性更强大(LayoutManager、ItemDecoration等)。

Android两种虚拟机区别与联系

Android中的Dalvik虚拟机相较于Java虚拟机针对手机的特色作了不少优化。
Dalvik基于寄存器,而JVM基于栈。在基于寄存器的虚拟机里,能够更为有效的减小冗余指令的分发和减小内存的读写访问。
Dalvik通过优化,容许在有限的内存中同时运行多个虚拟机的实例,而且每个 Dalvik应用做为一个独立的Linux进程执行。
java虚拟机运行的是java字节码。(java类会被编译成一个或多个字节码.class文件,打包到.jar文件中,java虚拟机从相应的.class文件和.jar文件中获取相应的字节码) Dalvik运行的是自定义的.dex字节码格式。(java类被编译成.class文件后,会经过一个dx工具将全部的.class文件转换成一个.dex文件,而后dalvik虚拟机会从其中读取指令和数据)
Android开发之浅谈java虚拟机和Dalvik虚拟机的区别

adb经常使用命令行

查看当前链接的设备:adb devices 安装应用:adb install -r <apk_path> -r表示覆盖安装 卸载apk:adb uninstall

ADB 用法大全

apk打包流程

  1. aapt工具打包资源文件,生成R.java文件
  2. aidl工具处理AIDL文件,生成对应的.java文件
  3. javac工具编译Java文件,生成对应的.class文件
  4. 把.class文件转化成Davik VM支持的.dex文件
  5. apkbuilder工具打包生成未签名的.apk文件
  6. jarsigner对未签名.apk文件进行签名
  7. zipalign工具对签名后的.apk文件进行对齐处理

Android应用程序(APK)的编译打包过程

apk安装流程

  1. 复制APK到/data/app目录下,解压并扫描安装包。
  2. 资源管理器解析APK里的资源文件。
  3. 解析AndroidManifest文件,并在/data/data/目录下建立对应的应用数据目录。
  4. 而后对dex文件进行优化,并保存在dalvik-cache目录下。
  5. 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。
  6. 安装完成后,发送广播。

apk瘦身

APK主要由如下几部分组成:

  • META-INF/ :包含了签名文件CERT.SF、CERT.RSA,以及 manifest 文件MANIFEST.MF。
  • assets/ : 存放资源文件,这些资源不会被编译成二进制。
  • lib/ :包含了一些引用的第三方库。
  • resources.arsc :包含res/values/中全部资源,例如strings,styles,以及其余未被包含在resources.arsc中的资源路径信息,例如layout 文件、图片等。
  • res/ :包含res中没有被存放到resources.arsc的资源。
  • classes.dex :通过dx编译能被android虚拟机理解的Java源码文件。
  • AndroidManifest.xml :清单文件

其中占据较大内存的是res资源、lib、class.dex,所以咱们能够从下面的几个方面下手:

  1. 代码方面能够经过代码混淆,这个通常都会去作。平时也能够删除一些没有使用类。
  2. 去除无用资源。使用lint工具来检测没有使用到的资源,或者在gradle中配置shrinkResources来删除包括库中全部的无用的资源,须要配合proguard压缩代码使用。这里须要注意项目中是否存在使用getIdentifier方式获取资源,这种方式相似反射lint及shrinkResources没法检测状况。若是存在这种方式,则须要配置一个keep.xml来记录使用反射获取的资源。压缩代码和资源
  3. 去除无用国际化支持。对于一些第三库来讲(如support),由于国际化的问题,它们可能会支持了几十种语言,但咱们的应用可能只须要支持几种语言,能够经过配置resConfigs提出不要的语言支持。
  4. 不一样尺寸的图片支持。一般状况下只须要一套xxhpi的图片就能够支持大部分分辨率的要求了,所以,咱们只须要保留一套图片。
  5. 图片压缩。 png压缩或者使用webP图片,完美支持须要Android版本4.2.1+
  6. 使用矢量图形。简单的图标可使用矢量图片。

HTTP缓存机制

图片来自上述连接

缓存的响应头:

20171103144205821.png

Cache-control:标明缓存的最大存活时常; Date:服务器告诉客户端,该资源的发送时间; Expires:表示过时时间(该字段是1.0的东西,当cache-control和该字段同时存在的条件下,cache-control的优先级更高); Last-Modified:服务器告诉客户端,资源的最后修改时间; 还有一个字段,这个图没给出,就是E-Tag:当前资源在服务器的惟一标识,可用于判断资源的内容是否被修改了。 除以上响应头字段之外,还需了解两个相关的Request请求头:If-Modified-since、If-none-Match。这两个字段是和Last-Modified、E-Tag配合使用的。大体流程以下: 服务器收到请求时,会在200 OK中回送该资源的Last-Modified和ETag头(服务器支持缓存的状况下才会有这两个头哦),客户端将该资源保存在cache中,并记录这两个属性。当客户端须要发送相同的请求时,根据Date + Cache-control来判断是否缓存过时,若是过时了,会在请求中携带If-Modified-Since和If-None-Match两个头。两个头的值分别是响应中Last-Modified和ETag头的值。服务器经过这两个头判断本地资源未发生变化,客户端不须要从新下载,返回304响应。

组件化

  • 在gradle.properties声明一个变量用于控制是不是调试模式,并在dependencies中根据是不是调试模式依赖必要组件。
  • 经过resourcePrefix规范module中资源的命名前缀。
  • 组件间经过ARouter完成界面跳转和功能调用。

MVP

三方库

okhttp原理

OkHttpClient经过newCall能够将一个Request构建成一个Call,Call表示准备被执行的请求。Call调用executed或enqueue会调用Dispatcher对应的方法在当前线程或者一步开始执行请求,通过RealInterceptorChain得到最终结果,RealInterceptorChain是一个拦截器链,其中依次包含如下拦截器:

  • 自定义的拦截器
  • retryAndFollowUpInterceptor 请求失败重试
  • BridgeInterceptor 为请求添加请求头,为响应添加响应头
  • CacheInterceptor 缓存get请求
  • ConnectInterceptor 链接相关的拦截器,分配一个Connection和HttpCodec为最终的请求作准备
  • CallServerInterceptor 该拦截器就是利用HttpCodec完成最终请求的发送

okhttp源码解析

Retrofit的实现与原理

Retrofit采用动态代理,建立声明service接口的实现对象。当咱们调用service的方法时候会执行InvocationHandler的invoke方法。在这方法中:首先,经过method把它转换成ServiceMethod,该类是对声明方法的解析,能够进一步将设定参数变成Request ;而后,经过serviceMethod, args获取到okHttpCall 对象,实际调用okhttp的网络请求方法就在该类中,而且会使用serviceMethod中的responseConverter对ResponseBody转化;最后,再把okHttpCall进一步封装成声明的返回对象(默认是ExecutorCallbackCall,将本来call的回调转发至UI线程)。

Retrofit2使用详解及从源码中解析原理
Retrofit2 彻底解析 探索与okhttp之间的关系

ARouter原理

多是最详细的ARouter源码分析

RxLifecycle原理

在Activity中,定义一个Observable(Subject),在不一样的生命周期发射不一样的事件; 经过compose操做符(内部实际上仍是依赖takeUntil操做符),定义了上游数据,当其接收到Subject的特定事件时,取消订阅; Subject的特定事件并不是是ActivityEvent,而是简单的boolean,它已经内部经过combineLast操做符进行了对应的转化。

RxJava

Java

类的加载机制

程序在启动的时候,并不会一次性加载程序所要用的全部class文件,而是根据程序的须要,经过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存以后,才能被其它class所引用。因此ClassLoader就是用来动态加载class文件到内存当中用的。
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为链接(Linking)。

  • 加载:查找和导入Class文件;
  • 连接:把类的二进制数据合并到JRE中;  (a) 验证:检查载入Class文件数据的正确性;  (b) 准备:给类的静态变量分配存储空间;  (c) 解析:将符号引用转成直接引用;
  • 初始化:对类的静态变量,静态代码块执行初始化操做

何时发生类初始化

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,若是类没有进行过初始化,则须要先触发其初始化。生成这4条指令的最多见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,若是类没有进行过初始化,则须要先触发其初始化。
  3. 当初始化一个类的时候,若是发现其父类尚未进行过初始化,则须要先触发其父类的初始化。
  4. 当虚拟机启动时,用户须要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
  5. 当使用JDK 1.7的动态语言支持时,若是一个java.lang.invoke.MethodHandle实例左后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且这个方法句柄锁对应的类没有进行过初始化时。

双亲委派模型

Java中存在3种类加载器: (1) Bootstrap ClassLoader : 将存放于<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,而且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即便放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器没法被Java程序直接引用 。 (2) Extension ClassLoader : 将<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的全部类库加载。开发者能够直接使用扩展类加载器。 (3) Application ClassLoader : 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。 每一个ClassLoader实例都有一个父类加载器的引用(不是继承关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)自己没有父类加载器,可是能够用作其余ClassLoader实例的父类加载器。 当一个ClassLoader 实例须要加载某个类时,它会试图在亲自搜索这个类以前先把这个任务委托给它的父类加载器,这个过程是由上而下依次检查的,首先由顶层的类加载器Bootstrap ClassLoader进行加载,若是没有加载到,则把任务转交给Extension ClassLoader加载,若是也没有找到,则转交给AppClassLoader进行加载,仍是没有的话,则交给委托的发起者,由它到指定的文件系统或者网络等URL中进行加载类。尚未找到的话,则会抛出CLassNotFoundException异常。不然将这个类生成一个类的定义,并将它加载到内存中,最后返回这个类在内存中的Class实例对象。

为何使用双亲委托模型

JVM在判断两个class是否相同时,不只要判断两个类名是否相同,还要判断是不是同一个类加载器加载的。

  1. 避免重复加载,父类已经加载了,则子CLassLoader没有必要再次加载。
  2. 考虑安全因素,假设自定义一个String类,除非改变JDK中CLassLoader的搜索类的默认算法,不然用户自定义的CLassLoader如法加载一个本身写的String类,由于String类在启动时就被引导类加载器Bootstrap CLassLoader加载了。

HashMap原理,Hash冲突

在JDK1.6,JDK1.7中,HashMap采用数组+链表实现,即便用链表处理冲突,同一hash值的链表都存储在一个链表里。可是当位于一个链表中的元素较多,即hash值相等的元素较多时,经过key值依次查找的效率较低。而JDK1.8中,HashMap采用位数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减小了查找时间。
当链表数组的容量超过初始容量*加载因子(默认0.75)时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中。为何须要使用加载因子?为何须要扩容呢?由于若是填充比很大,说明利用的空间不少,若是一直不进行扩容的话,链表就会愈来愈长,这样查找的效率很低,扩容以后,将原来链表数组的每个链表分红奇偶两个子链表分别挂在新链表数组的散列位置,这样就减小了每一个链表的长度,增长查找效率。
HashMap是非线程安全的,HashTable、ConcurrentHashMap是线程安全的。 HashMap的键和值都容许有null存在,而HashTable、ConcurrentHashMap则都不行。 由于线程安全、哈希效率的问题,HashMap效率比HashTable、ConcurrentHashMap的都要高。 HashTable里使用的是synchronized关键字,这实际上是对对象加锁,锁住的都是对象总体,当Hashtable的大小增长到必定的时候,性能会急剧降低,由于迭代时须要被锁定很长的时间。 ConcurrentHashMap引入了分割(Segment),能够理解为把一个大的Map拆分红N个小的HashTable,在put方法中,会根据hash(paramK.hashCode())来决定具体存放进哪一个Segment,若是查看Segment的put操做,咱们会发现内部使用的同步机制是基于lock操做的,这样就能够对Map的一部分(Segment)进行上锁,这样影响的只是将要放入同一个Segment的元素的put操做,保证同步的时候,锁住的不是整个Map(HashTable就是这么作的),相对于HashTable提升了多线程环境下的性能,所以HashTable已经被淘汰了。

Java中HashMap底层实现原理(JDK1.8)源码分析

什么是Fail-Fast机制

Fail-Fast是Java集合的一种错误检测机制。当遍历集合的同时修改集合或者多个线程对集合进行结构上的改变的操做时,有可能会产生fail-fast机制,记住是有可能,而不是必定。其实就是抛出ConcurrentModificationException 异常。
集合的迭代器在调用next()、remove()方法时都会调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。modCount是在每次改变集合数量时会改变的值。

Java提升篇(三四)-----fail-fast机制

Java泛型

Java泛型详解

Java多线程中调用wait() 和 sleep()方法有什么不一样?

Java程序中wait 和 sleep都会形成某种形式的暂停,它们能够知足不一样的须要。wait()方法用于线程间通讯,若是等待条件为真且其它线程被唤醒时它会释放锁,而 sleep()方法仅仅释放CPU资源或者让当前线程中止执行一段时间,但不会释放锁。

volatile的做用和原理

Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终须要转化为汇编指令在CPU上执行。 volatile是轻量级的synchronized(volatile不会引发线程上下文的切换和调度),它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。
因为内存访问速度远不及CPU处理速度,为了提升处理速度,处理器不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存后在进行操做,但操做完不知道什么时候会写到内存。普通共享变量被修改以后,何时被写入主存是不肯定的,当其余线程去读取时,此时内存中可能仍是原来的旧值,所以没法保证可见性。若是对声明了volatile的变量进行写操做,JVM就会想处理器发送一条Lock前缀的指令,表示将当前处理器缓存行的数据写回到系统内存。

一个int变量,用volatile修饰,多线程去操做++,线程安全吗?

不安全。volatile只能保证可见性,并不能保证原子性。i++实际上会被分红多步完成:1)获取i的值;2)执行i+1;3)将结果赋值给i。volatile只能保证这3步不被重排序,多线程状况下,可能两个线程同时获取i,执行i+1,而后都赋值结果2,实际上应该进行两次+1操做。

那如何才能保证i++线程安全?

可使用java.util.concurrent.atomic包下的原子类,如AtomicInteger。
其实现原理是采用CAS自旋操做更新值。CAS即compare and swap的缩写,中文翻译成比较并交换。CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。自旋就是不断尝试CAS操做直到成功为止。

CAS实现原子操做会出现什么问题?

  • ABA问题。由于CAS须要在操做之的时候,检查值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成,有变成A,那么使用CAS进行检查时会发现它的值没有发生变化,但实际上发生了变化。ABA问题能够经过添加版本号来解决。Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
  • 循环时间长开销大。pause指令优化。
  • 只能保证一个共享变量的原子操做。能够合并成一个对象进行CAS操做。

synchronized

Java中每一个对象均可以做为锁:

  • 对于普通同步方法,锁是当前实例对象;
  • 对于静态同步方法,锁是当前类的Class对象;
  • 对于同步方法块,锁是括号中配置的对象;

当一个线程试图访问同步代码块时,它首先必须获得锁,退出或抛出异常时必须释放锁。synchronized用的锁是存在Java对象头里的MarkWord,一般是32bit或者64bit,其中最后2bit表示锁标志位

java对象结构

Java SE1.6为了减小得到锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,在1.6中锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几种状态会随着竞争状况逐渐升级。锁能够升级但不能降级。

偏向锁

偏向锁获取过程:

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
  2. 若是为可偏向状态,则测试线程ID是否指向当前线程,若是是,进入步骤5,不然进入步骤3。
  3. 若是线程ID并未指向当前线程,则经过CAS操做竞争锁。若是竞争成功,则将Mark Word中线程ID设置为当前线程ID,而后执行5;若是竞争失败,执行4。
  4. 若是CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会致使stop the word)
  5. 执行同步代码。

轻量级锁

  1. 在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
  2. 拷贝对象头中的Mark Word复制到锁记录中;
  3. 拷贝成功后,虚拟机将使用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。若是更新成功,则执行步骤4,不然执行步骤5。
  4. 若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态。
  5. 若是这个更新操做失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了避免让线程阻塞,而采用循环去获取锁的过程。
    自旋 若是持有锁的线程能在很短期内释放锁资源,那么那些等待竞争锁的线程就不须要作内核态和用户态之间的切换进入阻塞挂起状态,它们只须要等一等(自旋),等持有锁的线程释放锁后便可当即获取锁,这样就避免用户线程和内核的切换的消耗。
    可是线程自旋是须要消耗cup的,说白了就是让cup在作无用功,若是一直获取不到锁,那线程也不能一直占用cup自旋作无用功,因此须要设定一个自旋等待的最大时间。
    若是持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会致使其它争用锁的线程在最大等待时间内仍是获取不到锁,这时争用线程会中止自旋进入阻塞状态。

线程池

好处:1)下降资源消耗;2)提升相应速度;3)提升线程的可管理性。 线程池的实现原理:

  • 当提交一个新任务到线程池时,判断核心线程池里的线程是否都在执行。若是不是,则建立一个新的线程执行任务。若是核心线程池的线程都在执行任务,则进入下个流程。
  • 判断工做队列是否已满。若是未满,则将新提交的任务存储在这个工做队列里。若是工做队列满了,则进入下个流程。
  • 判断线程池是否都处于工做状态。若是没有,则建立一个新的工做线程来执行任务。若是满了,则交给饱和策略来处理这个任务。

假若有n个网络线程,你须要当n个网络线程完成以后,再去作数据处理,你会怎么解决?

这题考的实际上是多线程同步的问题。这种状况能够可使用thread.join();join方法会阻塞直到thread线程终止才返回。更复杂一点的状况也可使用CountDownLatch,CountDownLatch的构造接收一个int参数做为计数器,每次调用countDown方法计数器减一。作数据处理的线程调用await方法阻塞直到计数器为0时。

Java中interrupted 和 isInterruptedd方法的区别?

interrupted() 和 isInterrupted()的主要区别是前者会将中断状态清除然后者不会。Java多线程的中断机制是用内部标识来实现的,调用Thread.interrupt()来中断一个线程就会设置中断标识为true。当中断线程调用静态方法Thread.interrupted()来 检查中断状态时,中断状态会被清零。而非静态方法isInterrupted()用来查询其它线程的中断状态且不会改变中断状态标识。简单的说就是任何抛 出InterruptedException异常的方法都会将中断状态清零。不管如何,一个线程的中断状态有有可能被其它线程调用中断来改变。

懒汉式单例的同步问题

同步的懒加载虽然是线程安全的,可是致使性能开销。所以产生了双重检查锁定。但双重检查锁定存在隐藏的问题。instance = new Instance()实际上会分为三步操做:1)分配对象的内存空间;2)初始化对象;3)设置instance指向刚分配的内存地址;因为指令重排序,2和3的顺序并不肯定。在多线程的状况下,第一个线程执行了1,3,此时第二个线程判断instance不为null,但实际上操做2尚未执行,第二个线程就会得到一个还未初始化的对象,直接使用就会形成空指针。
解决方案是用volatile修饰instance,在JDK 1.5增强了volatile的语意以后,用volatile修饰instance就阻止了2和3的重排序,进而避免上述状况的发生。
另外一种方式则是使用静态内部类:

public class Singleton {
    private static class InstanceHolder {
        public static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return InstanceHolder.instance;
    }
}
复制代码

其原理是利用类初始化时会加上初始化锁确保类对象的惟一性。

什么是ThreadLocal

ThreadLocal即线程变量,它为每一个使用该变量的线程提供独立的变量副本,因此每个线程均可以独立地改变本身的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。ThreadLocal的实现是以ThreadLocal对象为键。任意对象为值得存储结构。这个结构被附带在线程上,也就是说一个线程能够根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

什么是数据竞争

数据竞争的定义:在一个线程写一个变量,在另外一个线程读同一个变量,并且写和读没有经过同步来排序。

Java内存模型(Java Memory Model JMM)

JM屏蔽各类硬件和操做系统的内存访问差别,以实现让Java程序在各类平台下都能达到一致的内存访问效果。
线程之间的共享变量存储在主内存中,每一个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是一个抽象概念,它涵盖了缓存、写缓存区、寄存器以及其余的硬件和编译器优化。 在执行程序时,为了提升性能,编译器和处理器经常会对指令作重排序。在多线程中重排序会对程序的执行结果有影响。
JSR-133内存模型采用happens-before的概念来阐述操做之间的内存可见性。happens-before会限制重排序以知足规则。 主要的happens-before规则有以下:

  • 程序顺序规则:一个线程中的每一个操做,happens-before于该线程中的任意后续操做。
  • 监视器锁规则:对一个锁的解锁,happens-before与锁随后对这个锁的加锁。
  • volatile变量规则:对一个volatile域的写,happens-before与任意后续对这个volatile域的读。
  • 传递性:若是A happens-before B,且B happens-before C,那么A happens-before C。

Java内存区域

  • 程序计数器:当前线程锁执行的字节码的行号指示器,用于线程切换恢复,是线程私有的;
  • Java虚拟机栈(栈):虚拟机栈也是线程私有的。每一个方法在执行的同时都会建立一个栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
  • 本地方法栈:与虚拟机栈相似,服务于Native方法。
  • Java堆:堆是被全部线程共享的一块内存,用于存放对象实例。是垃圾收集器管理的主要区域,也被称做GC堆。
  • 方法区:与Java堆同样,是线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态常量、即时编译器编译后的代码等数据。
  • 运行时常量池:是方法区的一部分,用于存放编译器生成的各类字面量和符号引用。

判断对象是否须要回收的方法

  • 引用计数算法。实现简单,断定效率高,但不能解决循环引用问题,同时计数器的增长和减小带来额外开销,JDK1.1之后废弃了。
  • 可达性分析算法/根搜索算法 。根搜索算法是经过一些“GC Roots”对象做为起点,从这些节点开始往下搜索,搜索经过的路径成为引用链(Reference Chain),当一个对象没有被GC Roots 的引用链链接的时候,说明这个对象是不可用的。 Java中可做为“GC Root”的对象包括:虚拟机栈(本地变量表)中引用的对象;方法区中类静态属性和常量引用的对象。本地方法栈中引用的对象。

引用类型

  • 强引用:默认的引用方式,不会被垃圾回收,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
  • 软引用(SoftReference):若是一个对象只被软引用指向,只有内存空间不足够时,垃圾回收器才会回收它;
  • 弱引用(WeakReference):若是一个对象只被弱引用指向,当JVM进行垃圾回收时,不管内存是否充足,都会回收该对象。
  • 虚引用(PhantomReference):虚引用和前面的软引用、弱引用不一样,它并不影响对象的生命周期。若是一个对象与虚引用关联,则跟没有引用与之关联同样,在任什么时候候均可能被垃圾回收器回收。虚引用一般和ReferenceQueue配合使用。
    ReferenceQueue 做为一个Java对象,Reference对象除了具备保存引用的特殊性以外,也具备Java对象的通常性。因此,当对象被回收以后,虽然这个Reference对象的get()方法返回null,但这个SoftReference对象已经再也不具备存在的价值,须要一个适当的清除机制,避免大量Reference对象带来的内存泄漏。 在java.lang.ref包里还提供了ReferenceQueue。咱们建立Reference对象时使用两个参数的构造传入ReferenceQueue,当Reference所引用的对象被垃圾收集器回收的同时,Reference对象被列入ReferenceQueue。也就是说,ReferenceQueue中保存的对象是Reference对象,并且是已经失去了它所软引用的对象的Reference对象。另外从ReferenceQueue这个名字也能够看出,它是一个队列,当咱们调用它的poll()方法的时候,若是这个队列中不是空队列,那么将返回队列前面的那个Reference对象。因而咱们能够在适当的时候把这些失去所软引用的对象的SoftReference对象清除掉。

垃圾收集算法

  1. 标记-清楚算法(Mark-Sweep) 在标记阶段,肯定全部要回收的对象,并作标记。清除阶段紧随标记阶段,将标记阶段肯定不可用的对象清除。标记—清除算法是基础的收集算法,有两个不足:1)标记和清除阶段的效率不高;2)清除后回产生大量的不连续空间,这样当程序须要分配大内存对象时,可能没法找到足够的连续空间。
  2. 复制算法(Copying) 复制算法是把内存分红大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另外一块上,而后把这块内存整个清理掉。复制算法实现简单,运行效率高,可是因为每次只能使用其中的一半,形成内存的利用率不高。如今的JVM 用复制方法收集新生代,因为新生代中大部分对象(98%)都是朝生夕死的,因此会分红1块大内存Eden和两块小内存Survivor(大概是8:1:1),每次使用1块大内存和1块小内存,当回收时将2块内存中存活的对象赋值到另外一块小内存中,而后清理剩下的。
  3. 标记—整理算法(Mark-Compact) 标记—整理算法和复制算法同样,可是标记—整理算法不是把存活对象复制到另外一块内存,而是把存活对象往内存的一端移动,而后直接回收边界之外的内存。标记—整理算法提升了内存的利用率,而且它适合在收集对象存活时间较长的老年代。
  4. 分代收集(Generational Collection) 分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各代对象的存活特色,每一个代采用不一样的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。

内存分配策略

  • 对象优先在Eden分配。
  • 大对象直接进入老年代。 大对象是指须要大量连续内存空间的Java对象,最典型的就是那种很长的字符串以及数组。
  • 长期存活的对象进入老年代。存活过一次新生代的GC,Age+1,当达到必定程度(默认15)进入老年代。
  • 动态对象年龄断定。若是在Survivor空间中相同Age全部对象大小的总和大于Survivor空间一半。那么Age大于等于该Age的对象就能够直接进入老年代。
  • 空间分配担保。 在发生新生代GC以前,会检查老年代的剩余空间是否大于新生代全部对象的总和。若是大于则是安全的,若是不大于有风险。
相关文章
相关标签/搜索