安卓活动生命周期

onStart和onResume有什么区别?

OnStart在活动界面显示时执行,但不能与之交互。

OnResume在活动可以与用户交互时执行,用户可以获得活动的焦点,与用户交互。

活动开始过程

最终,startActivity将调用startActivityForResult,并通过ActivityManagerProxy调用system_server进程中ActivityManagerService的startActivity方法。如果活动需要开始的过程没有开始,那么调用受精卵孵化申请过程。进程创建后,调用应用程序ActivityThread的main方法,main方法调用attach方法将应用程序进程绑定到ActivityManagerService并打开循环接收消息。活动管理器服务通过应用程序线程的代理发送消息通知来启动活动,活动读取中的处理程序依次处理句柄启动活动、调用执行启动活动和处理恢复活动。

深入了解活动启动流程

安卓类加载器

Android平台上的虚拟机运行Dex字节码,是优化Class文件的产物。传统的类文件是一个Java源文件,它会生成一个. Class文件,而Android会合并并优化所有的类文件,然后生成一个最终的class.dex目的是只保留不同类文件的一个副本。如果我们的Android应用程序不做dex处理,那么最后一个应用程序的apk将只有一个dex文件。

Android中常用的加载器有两种,DexClassLoader和PathClassLoader,继承自BaseDexClassLoader。不同之处在于,在调用父类构造函数时,DexClassLoader会传递一个optimizedDirectory参数,该参数必须是缓存系统创建的Dex文件的内部存储路径。PathClassLoader为null,只能加载内部存储目录的Dex文件。因此,我们可以使用DexClassLoader来加载外部apk。

安卓消息机制

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

Android是事件驱动的,即所有Activity的生命周期都是由Handler事件驱动的。循环方法将调用消息队列的下一个方法来获取下一条消息。当没有消息时,Linux-Linux管道/epoll机制会在循环的queue.next的nativePollOnce方法中被阻塞,不会消耗CPU。

空闲处理器

IdleHandler是一个回调接口,可以通过MessageQueue的addIdleHandler添加实现类。当messageQueue中的任务被临时处理时,这个接口会在此时被回调,如果返回false,则被移除,如果返回true,则回调会在下一次处理Message时继续。

同步屏障机制

同步屏障可以通过MessageQueue.postSyncBarrier函数来设置。该方法向队列发送不带目标的消息。如果在下一个方法中找到了没有目标的消息,那么同步消息将被跳过一段时间,异步消息将首先被执行。换句话说,同步屏障给Handler消息机制增加了一个简单的优先级机制,异步消息的优先级高于同步消息。创建处理程序时有一个异步参数,传递true意味着该处理程序发送的异步消息。viewrootmpl。scheduletraversals方法使用同步屏障来确保首先执行用户界面绘制。

视图的绘制原则

视图的呈现从处理活动读取类中的恢复活动事件的处理程序开始。在执行执行恢复测量活动后,创建窗口和取消视图,并调用窗口管理器的添加视图方法将它们添加到屏幕上。AddView调用ViewRootImpl的setView方法,最后执行performTraversals方法,然后依次执行performMeasure、performLayout、performDraw。也就是视图绘制的三大流程。

测量过程测量视图的视图大小,最后调用setMeasuredDimension方法设置测量结果。如果是ViewGroup,则需要调用measureChildren或measureChild方法来计算自己的大小。

布局过程就是放置视图的过程,通常由ViewGroup实现,不需要实现。当实施onLayout时,测量过程的测量结果可以通过getMeasuredWidth和其他方法获得和放置。

绘制过程首先绘制背景,然后调用onDraw方法绘制视图的内容,再调用dispatchDraw调用子视图的绘制方法,最后绘制滚动条。默认情况下,视图组不会执行onDraw方法。如果拷贝了onDraw方法,则需要调用SetWillNodraw。无需绘制的清晰标记。

安卓视图绘制过程经过全面分析,带你一步步了解视图

什么是测量规格

测量规格表示一个32位的整数值,高两位表示规格模式,低30位表示规格大小。

SpecMode有三种类型:

UNSPECIFIED 表示父容器不对View有任何限制,一般用于系统内部,表示一种测量状态;EXACTLY 父容器已经检测出view所需的精确大小,这时候view的最终大小SpecSize所指定的值,相当于match_parent或指定具体数值。AT_MOST 父容器指定一个可用大小即SpecSize,view的大小不能大于这个值,具体多大要看view的具体实现,相当于wrap_content。getWidth方法和getMeasureWidth区别呢?

首先,可以在measure过程之后获得getMeasureWidth方法,而在layout过程之后获得getWidth方法。另外,getMeasureWidth方法中的值是通过setMeasuredDimension方法设置的,getWidth方法中的值是通过视图的右坐标减去左坐标计算出来的。

事件分发机制requestLayout、invalid和postInvalidate的区别和联系

相似性:三种方法都有刷新界面的效果。

区别:invalidate和postInvalidate只调用onDraw方法;请求布局再次调用OnMeasureOnlayout和onDraw。

调用invalidate方法后,将向视图添加一个标记位,并不断请求父容器进行刷新。父容器会计算出需要重画的区域,直到转移到ViewRootImpl,最后触发performTraversals方法开始View树的重画过程。

调用requestLayout方法将标记当前视图及其父容器,并逐层提交它,直到viewrootmpl处理该事件。viewrootmpl将调用三个主要进程。从测量开始,对于每个视图及其带有标记位的子视图,在测量上进行测量,在布局上进行布局并绘制草图。

安卓视图深入分析请求布局、无效日期和无效日期后

绑定机制,共享内存实现原理

为什么要用Binder?

概念

过程隔离

进程间分区空:用户空间)/内核空间空

系统调用:用户模式和内核模式

原则

跨进程通信需要内核之间的支持空。传统的IPC机制,比如管道和Socket,都是内核的一部分,通过内核支持实现进程间通信自然没问题。但是Binder不是Linux系统内核的一部分,怎么办?这是由于Linux的可加载内核模块的机制。模块是具有独立功能的程序,可以独立编译,但不能独立运行。它在运行时链接到内核,并作为内核的一部分运行。这样,Android系统可以动态添加一个内核模块在内核之间运行空,用户进程可以通过这个内核模块作为桥梁进行通信。

在Android系统中,这个内核模块运行在内核空之间,负责通过Binder进行各种用户进程之间的通信,称为Binder dirver。

那么在Android系统中,用户进程是如何通过这个内核模块进行相互通信的呢?是不是和上面说的传统的IPC机制一样,先把数据从发送方进程复制到内核缓冲区,再把数据从内核缓冲区复制到接收方进程?显然不是,否则也就没有开头提到的Binder的性能优势了。

这不得不引入Linux下的另一个概念:内存映射。

Binder IPC机制中涉及的内存映射是通过操作系统中的内存映射方法mmap实现的。内存映射只是在user 空和kernel 空之间映射一个内存区域。映射关系建立后,用户对该内存区域的修改可以直接体现在内核空;相反,内核空之间这个区域的修改可以直接反映给用户空。

一个完整的Binder IPC通信过程通常是这样的:

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

活页夹传播模型

Binder基于C/S架构,定义了四个角色:客户端、服务器、Binder驱动和ServiceManager。

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

服务管理器是一个独立的过程,那么服务器和服务管理器之间的通信是什么?

Android系统启动后,会创建一个名为servicemanager的进程。这个过程将通过一个商定的命令BINDERSETCONTEXT_MGR向绑定驱动程序注册,并申请成为服务管理器。绑定驱动程序将自动为服务管理器创建绑定实体。并且这个Binder实体在所有Clients中的引用都是0,也就是说每个客户端都可以通过这个0的引用与ServiceManager进行通信。服务器通过参考号0向服务管理器注册,客户端可以通过参考号0获得要通信的服务器的绑定器参考

Android应用工程师编写的Binder原理分析

一篇文章了解Android Binder的进程间通信机制

序列化的方式

Serializable是Java提供的序列化接口,是空接口。用于表示对象是否可以支持序列化,通过ObjectOutputStrean和ObjectInputStream实现序列化和反序列化。请注意,可以为要序列化的对象设置序列化版本号。反序列化时,系统会检查文件中的serialVersionUID是否与当前类的值一致。如果不一致,则该类已被修改,反序列化失败。因此,最好为可能被修改的类指定serialVersionUID的值。

Parcelable是一个Android特有的接口,实现序列化。可序列化数据打包在包裹中,可以在活页夹中自由传输。序列化是通过writeToParcel方法完成的,最后是通过宗地的一系列写方法完成的。反序列化功能由CREAOR完成,指示如何创建序列化对象和数组,并通过宗地的一系列读取方法完成反序列化过程。

片段延迟加载的实现

当Fragment的可见状态改变时会调用setUserVisibleHint方法,复制这个方法可以实现Fragment的惰性加载。但是需要注意的是,这个方法可能是在onVIewCreated之前调用的,需要确保在加载数据之前已经初始化了接口,避免出现空指针。

片段的延迟加载

RecyclerView和ListView

缓存差异:

层级不同: ListView有两级缓存,在屏幕与非屏幕内。 RecyclerView比ListView多两级缓存,支持多个离屏ItemView缓存(匹配pos获取目标位置的缓存,如果匹配则无需再次bindView),支持开发者自定义缓存处理逻辑,支持所有RecyclerView共用同一个RecyclerViewPool。缓存不同: ListView缓存View。 RecyclerView缓存RecyclerView.ViewHolder,抽象可理解为: View + ViewHolder + flag;

优势

RecylerView提供了一个本地刷新的接口,通过这个接口可以避免很多无用的bindView。

回收视图更具可扩展性。).

Android中两个虚拟机的区别与联系

相比Java虚拟机,Android中的Dalvik虚拟机针对手机的特性做了很多优化。

Dalvik基于寄存器,JVM基于栈。在基于寄存器的虚拟机中,可以更有效地减少冗余指令的分布和内存的读写访问。

Dalvik经过优化,允许多个虚拟机实例在有限的内存中同时运行,每个Dalvik应用程序作为独立的Linux进程执行。

java虚拟机正在运行Java字节码。

达尔维克经营着一个习俗。dex字节码格式。

Android开发中java虚拟机和Dalvik虚拟机的区别

Adb通用命令行

检查当前连接的设备:adb设备

安装应用:adb install -r -r代表覆盖安装

卸载apk:adb卸载

亚行使用百科全书

Apk包装工艺

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

安卓应用编译打包流程

Apk安装流程

复制APK到/data/app目录下,解压并扫描安装包。资源管理器解析APK里的资源文件。解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。然后对dex文件进行优化,并保存在dalvik-cache目录下。将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。安装完成后,发送广播。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 resources、lib和class.dex占用大量内存,可以从以下几个方面入手:

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

缓存响应头:

Cache-control:指示缓存的最大生存期;

日期:服务器告诉客户端资源的发送时间;

过期:指示过期时间;

上次修改时间:服务器告诉客户端上次修改资源的时间;

还有一个图中没有显示的字段,就是E-Tag:服务器中当前资源的唯一标识,可以用来判断资源的内容是否被修改。

除了上面的响应头字段,您还需要知道两个相关的请求头:如果-修改-自和如果-不匹配。这两个字段与上次修改和电子标签一起使用。一般过程如下:

服务器收到请求后,会在200 OK中发回资源的Last-Modified和ETag头,客户端将资源保存在缓存中并记录这两个属性。当客户端需要发送同样的请求时,根据Date+Cache-control判断缓存是否过期。如果过期,请求将带有如果修改自和如果不匹配。这两个头的值是响应中的最后修改和ETag头。服务器通过这两个头判断本地资源没有变化,客户端不需要重新下载,返回304的响应。

模块化

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

OkHttpClient可以通过newCall将一个请求构建成一个Call,Call表示要执行的请求。调用已执行或入队的调用会调用Dispatcher对应的方法开始执行当前线程或步骤中的请求,并通过RealInterceptorChain得到最终结果,RealInterceptorChain是一个拦截器链,依次包括以下拦截器:

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

Okhttp源代码分析

改造的实施和原则

改装使用动态代理来创建声明服务接口的实现对象。当我们调用服务的方法时,就会执行InvocationHandler的调用方法。在这个方法中:首先通过方法转换成ServiceMethod,这是对声明的方法的分析,可以进一步将设置的参数变成请求;然后通过serviceMethod,args获取okHttpCall对象,实际调用okhttp的网络请求方法在这个类中,serviceMethod中的responseConverter会用来转换ResponseBody最后,okHttpCall被进一步封装为一个声明的返回对象。

Retrofit2使用详细的解释,并从源代码中分析原理

追溯2充分分析了探索和okhttp的关系

环绕原理

大概是对ARouter源代码最详细的分析

rx生命周期原理

在活动中,可观察对象被定义为在不同的生命周期中启动不同的事件。

上游数据由合成操作符定义。当它收到主题的特定事件时,它取消订阅。

Subject的具体事件不是ActivityEvent,而是一个简单的布尔值,已经由combineLast运算符进行了内部转换。

RxJavaJava类的加载机制

程序启动时,不会一次加载程序使用的所有类文件,而是根据程序的需要,通过Java的ClassLoader动态加载一个类文件到内存中,这样类文件加载到内存后才能被其他类引用。因此,ClassLoader用于将类文件动态加载到内存中。

一个类从加载到虚拟机内存到卸载的整个生命周期包括七个阶段:加载、验证、准备、解析、初始化、使用和卸载。其中,准备、验证和分析统称为链接。

加载:查找和导入Class文件;链接:把类的二进制数据合并到JRE中; 验证:检查载入Class文件数据的正确性; 准备:给类的静态变量分配存储空间; 解析:将符号引用转成直接引用;初始化:对类的静态变量,静态代码块执行初始化操作什么时候发生类初始化 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候,读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例左后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄锁对应的类没有进行过初始化时。双亲委派模型

Java中有三种加载器:

Bootstrap ClassLoader:将存储在lib目录或-XbootsCLaSS参数指定的、由虚拟机标识的类库加载到虚拟机内存中。Java程序不能直接引用启动类加载器。

Extension ClassLoader:加载libext目录或java.ext.dirs系统变量指定路径下的所有类库。开发人员可以直接使用扩展类加载器。

Application ClassLoader:负责加载用户ClassPath上指定的类库,开发者可以直接使用。

每个类加载器实例都有一个对父类加载器的引用。虚拟机本身内置的引导类加载器没有父类加载器,但可以用作其他类加载器实例的父类加载器。

当一个类加载器实例需要加载一个类时,它会在亲自搜索这个类之前,尝试将这个任务委托给它的父类加载器。这个过程是从上到下检查的,它首先由顶层的引导类加载器加载。如果没有加载,任务将被转移到扩展类加载器进行加载。如果没有找到,就转移到AppClassLoader进行加载,如果没有找到,就转移到委托的发起方,在指定的URL如文件系统或网络中加载类。如果尚未找到,将引发一个CLassNotFoundException异常。否则,从这个类生成一个类定义,加载到内存中,最后在内存中返回这个类的类实例对象。

为什么使用父母委托模式

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

避免重复加载,父类已经加载了,则子CLassLoader没有必要再次加载。考虑安全因素,假设自定义一个String类,除非改变JDK中CLassLoader的搜索类的默认算法,否则用户自定义的CLassLoader如法加载一个自己写的String类,因为String类在启动时就被引导类加载器Bootstrap CLassLoader加载了。HashMap原理,Hash冲突

在JDK1.6和JDK1.7中,HashMap是用数组+链表实现的,也就是用链表处理冲突,相同哈希值的链表存储在一个链表中。但是,当链表中有多个元素,即有多个元素具有相同的哈希值时,按键值依次搜索的效率较低。在JDK1.8中,HashMap是通过位数组+链表+红黑树实现的。当链表长度超过阈值时,链表转换成红黑树,大大减少了搜索时间。

当链表数组的容量超过初始容量*加载因子时,哈希会将链表数组放大2倍,并将原始链表数组移动到新数组中。为什么我需要使用负载系数?为什么需要扩张?因为如果填充比大,说明用的空多。如果不一直进行扩展,链表会变得越来越长,所以搜索效率很低。扩展后,原始链表数组的每个链表将被分成奇数和偶数子链表,分别挂在新链表数组的散列位置,从而减少了每个链表的长度,提高了搜索效率。

哈希表是非线程安全的,而哈希表和并发哈希表是线程安全的。

HashMap的键和值允许为空,HashTable和ConcurrentHashMap不允许。

由于线程安全和哈希效率,HashMap比HashTable和ConcurrentHashMap效率更高。

哈希表使用同步关键字,实际上将对象锁定为一个整体。当Hashtable的大小增加到一定值时,性能会急剧下降,因为在迭代过程中需要长时间锁定。

ConcurrentHashMap引入了分段,可以理解为将一个大地图拆分成n个小哈希表。在put方法中,它将根据哈希)决定存储哪个段。如果你看看段的put操作,我们会发现内部使用的同步机制是基于锁操作的,锁操作可以锁定Map的一部分,只影响要放入同一段的元素的put操作。在保证同步的情况下,不锁定整个映射,相比HashTable提高了多线程环境下的性能,所以HashTable被淘汰了。

Java中HashMap底层实现原理的源代码分析

什么是快速失效机制

快速失败是一种针对Java集合的错误检测机制。当遍历集合并修改集合时,或者当多个线程对集合进行结构更改时,可能会出现快速失效机制。记住是有可能的,不是一定的。实际上,它会引发concurrentmodificationexception异常。

集合的迭代器在调用next和remove方法时,会调用checkForComodification方法,主要检测modCount == expectedModCount?如果它们不相等,则会引发ConcurrentmodificationException异常,从而导致快速失败机制。modCount是一个值,它在每次集合数量改变时都会改变。

Java-快速失效机制的改进

java泛型

Java泛型的详细说明

Java多线程中调用wait和sleep方法有什么区别?

在Java程序中,等待和睡眠都会引起某种形式的暂停,可以满足不同的需求。wait方法用于线程间通信。如果等待条件为真,其他线程被唤醒,则释放锁,而sleep方法只释放CPU资源或暂时停止当前线程执行,不释放锁。

挥发物的作用和原理

Java代码编译后会变成Java字节码,字节码由类加载器加载到JVM中,JVM执行字节码,最终转换成汇编指令在CPU上执行。

Volatile是轻量级同步的,保证了多处理器开发中共享变量的“可见性”。可见性意味着当一个线程修改共享变量时,另一个线程可以读取修改后的值。

由于内存访问速度远小于CPU处理速度,为了提高处理速度,处理器不直接与内存通信,而是先将系统内存的数据读入内部缓存,然后进行操作,但操作结束后,不知道什么时候会写入内存。公共共享变量修改后,不确定什么时候写入主存。当其他线程读取时,内存可能还是原来的旧值,可见性无法保证。如果写了一个声明为volatile的变量,JVM会向处理器发送一个Lock前缀指令,表示当前处理器缓存行的数据会写回系统内存。

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

不安全。Volatile只能保证可见性,不能保证原子性。I++实际上分为多个步骤:1)获取I的值;2)执行I+1;3)将结果赋给i. Volatile只能保证这三个步骤不会被重新排序。在多线程的情况下,两个线程可能同时获取I,执行i+1,然后分配一个结果2。其实应该做两个+1操作。

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

您可以在java.util.concurrent.atomic包下使用原子类,比如AtomicInteger。

它的实现原理是利用CAS自旋操作更新值。CAS是compare和swap的缩写,从中文翻译过来就是compare和swap。CAS有三个操作数,内存值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对象;对于同步方法块,锁是括号中配置的对象;

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

Java对象结构

为了减少获取和释放锁带来的性能消耗,Java SE1.6引入了偏锁和轻量锁。1.6中,锁有四种状态,从低到高依次是解锁、偏锁、轻量锁、重量级锁。这些状态会随着竞争逐渐升级。锁可以升级,但不能降级。

偏置锁

部分锁定获取过程:

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

好处:1)减少资源消耗;2)提高相应的速度;3)提高线程的可管理性。

线程池的实现原理:

当提交一个新任务到线程池时,判断核心线程池里的线程是否都在执行。如果不是,则创建一个新的线程执行任务。如果核心线程池的线程都在执行任务,则进入下个流程。判断工作队列是否已满。如果未满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。判断线程池是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果满了,则交给饱和策略来处理这个任务。假如有n个网络线程,你需要当n个网络线程完成之后,再去做数据处理,你会怎么解决?

这个测试其实就是多线程同步的问题。在这种情况下,可以使用thread。join;连接方法阻止返回,直到线程线程终止。在更复杂的情况下,您也可以使用CountDownLatch,它接收一个int参数作为计数器,每次调用倒计时方法时,计数器都会递减1。进行数据处理的线程调用await方法进行阻塞,直到计数器为0。

Java中的interrupted和isInterruptedd方法有什么区别?

当中断静态方法用于检查中断状态时,中断状态将被清除。而非静态方法isInterrupted用于查询其他线程的中断状态,而不改变中断状态标识符。简单地说,任何抛出中断异常的方法都会清除中断状态。在任何情况下,一个线程的中断状态都可能被其他调用中断的线程所改变。

惰性单例的同步问题

虽然同步惰性加载是线程安全的,但它会导致性能开销。因此,会生成双重检查锁。但是,双重检查锁定有隐藏的问题。其实instance = new Instance可以分为三步:1)分配对象的内存空;2)初始化对象;3)设置实例指向新分配的内存地址;由于指令重排序,2和3的顺序不确定。在多线程的情况下,第一个线程执行1和3。此时第二个线程判断实例不为null,但实际上操作2还没有执行,所以第二个线程会得到一个未初始化的对象,直接使用会产生空指针。

解决方案是用波动性修改实例。JDK 1.5加强了volatility的语义后,用volatility修改实例防止了2和3的重新排序,从而避免了上述情况。

另一种方法是使用静态内部类:

publicclassSingleton{

privateStaticClassInstanceholder {

publicstaticSingleton实例= Newsingleton;

}

publicationstaticletinggetinstance{

returnInstanceHolder.instance

}

}

原理是用类初始化的时候加初始化锁,保证类对象的唯一性。

什么是线程本地

ThreadLocal是一个线程变量,它为使用该变量的每个线程提供了一个独立的变量副本,因此每个线程都可以独立地更改自己的副本,而不会影响对应于其他线程的副本。从线程的角度来看,目标变量就跟线程的local变量一样,也是类名中“Local”的意思。ThreadLocal是用ThreadLocal对象作为键实现的。任意对象是值的存储结构。这个结构被附加到一个线程上,这意味着一个线程可以根据ThreadLocal对象查询绑定到该线程的值。

什么是数据竞争

数据竞争的定义:在一个线程中写入一个变量,在另一个线程中读取同一个变量,写入和读取不按同步排序。

Java内存模型

JM屏蔽了各种硬件和操作系统之间的内存访问差异,使Java程序在各种平台下都能达到一致的内存访问效果。

线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,线程存储在其中以读取/写入共享变量的副本。本地内存是一个抽象概念,涵盖了缓存、写缓冲、寄存器等硬件和编译器优化。

在执行程序时,为了提高性能,编译器和处理器经常对指令进行重新排序。多线程中的重新排序会影响程序的执行结果。

JSR-133内存模型使用“发生之前”的概念来解释操作之间的内存可见性。发生-在限制重新排序以符合规则之前。

先发制人的主要规则如下:

程序顺序规则:一个线程中的每个操作,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对象清除掉。垃圾收集算法 标记-清楚算法(Mark-Sweep) 在标记阶段,确定所有要回收的对象,并做标记。清除阶段紧随标记阶段,将标记阶段确定不可用的对象清除。标记—清除算法是基础的收集算法,有两个不足:1)标记和清除阶段的效率不高;2)清除后回产生大量的不连续空间,这样当程序需要分配大内存对象时,可能无法找到足够的连续空间。复制算法(Copying) 复制算法是把内存分成大小相等的两块,每次使用其中一块,当垃圾回收的时候,把存活的对象复制到另一块上,然后把这块内存整个清理掉。复制算法实现简单,运行效率高,但是由于每次只能使用其中的一半,造成内存的利用率不高。现在的JVM 用复制方法收集新生代,由于新生代中大部分对象(98%)都是朝生夕死的,所以会分成1块大内存Eden和两块小内存Survivor,每次使用1块大内存和1块小内存,当回收时将2块内存中存活的对象赋值到另一块小内存中,然后清理剩下的。标记—整理算法(Mark-Compact) 标记—整理算法和复制算法一样,但是标记—整理算法不是把存活对象复制到另一块内存,而是把存活对象往内存的一端移动,然后直接回收边界以外的内存。标记—整理算法提高了内存的利用率,并且它适合在收集对象存活时间较长的老年代。分代收集(Generational Collection) 分代收集是根据对象的存活时间把内存分为新生代和老年代,根据各代对象的存活特点,每个代采用不同的垃圾回收算法。新生代采用复制算法,老年代采用标记—整理算法。内存分配策略 对象优先在Eden分配。大对象直接进入老年代。 大对象是指需要大量连续内存空间的Java对象,最典型的就是那种很长的字符串以及数组。长期存活的对象进入老年代。存活过一次新生代的GC,Age+1,当达到一定程度(默认15)进入老年代。动态对象年龄判定。如果在Survivor空间中相同Age所有对象大小的总和大于Survivor空间一半。那么Age大于等于该Age的对象就可以直接进入老年代。空间分配担保。 在发生新生代GC之前,会检查老年代的剩余空间是否大于新生代所有对象的总和。如果大于则是安全的,如果不大于有风险。

所有人都在看

1.《android面试题 Android中高级面试题准备整理分享》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《android面试题 Android中高级面试题准备整理分享》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/junshi/1758258.html