Advertisement

Android面试准备(中高级)

阅读量:

Android

Activity生命周期

这里写图片描述

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

onStart()会在指定条件下触发该activity界面展示时使用;此功能不可让用户直接操作该界面。
onResume()会在此活动状态允许与其他应用通信时被调用;当活动状态允许与其他应用通信时开启,并使当前焦点转移给该activity。

Activity启动流程

backsit命令最终都会触发startActivityForResult的方法执行。借助于一个中间层(如SystemServer进程),通过其提供的API或接口来实现与系统服务交互。如果目标活动所在的进程尚未启动,则利用Zygote框架或工具来生成新的运行时环境并启动新进程。在新启动的应用进程中开始一个线程循环来处理消息传递,并将一个消息传递给该活动管理器服务以指示其开始。当处理...时(具体来说是当处理...时),该活动会依次执行performLaunch..., handleResume...等操作...

Android类加载器

Android平台下的虚拟机运行Dex字节码是一种对class文件进行优化的方式,在传统的Java源码生成.class文件时会生成.class.dex文件以提高运行效率并减少内存占用。然而在Android系统中如果不进行分dex处理则会导致最终生成的应用包仅包含一个 dex 文件这一现象可能会带来性能上的损失甚至影响用户体验因此分dex处理对于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触发。该机制会在队列为空的情况下阻塞系统调用 nativePollOnce(), 从而避免了资源浪费。然而,在实际运行过程中发现该机制并不会消耗CPU资源

IdleHandler (闲时机制)

IdleHandler是一个支持中断机制的回调接口,在MessageQueue中通过调用MessageQueue.addIdleHandler方法来实现对其他类的绑定。每当MessageQueue中的作业暂时完成(未接收到新的作业请求或下一个作业延迟将在之后),该接口会被触发并返回一个布尔值。若返回false,则表示该作业已不再被跟踪;若返回true,则表示将继续跟踪该作业直到下一个作业完成为止。

同步屏障机制(sync barrier)

该同步屏障可借助MessageQueue.postSyncBarrier函数进行配置。该方法向Queue中发送了一个无目标标识的Message,在next方法执行过程中获取消息时若未发现无目标标识的Message,则会在一段时间内跳过同步消息优先处理异步消息。换句话说,同步屏障为Handler消息机制增添了简单的优先级管理机制,在此机制下异步消息的优先级高于同步消息。当创建Handler对象时需指定一个async参数,默认情况下为false;当调用此handler时传true则表示此handler发送的是异步消息。ViewRootImpl.scheduleTraversals方法实际采用了这一同步屏障策略以确保UI绘制操作能够得到优先处理。

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位整数值,在二进制表示中前两位决定了其模式(SpecMode),剩余的30位则决定了具体大小(SpecSize)。
具体来说:

  • UNSPECIFIED表明父容器未对View施加任何限制条件,默认状态通常用于内部系统使用;
  • EXACTLY表示父容器已精确确定所需的View大小 SpecSize 正好等于 match_parent 或指定的具体数值;
  • AT_MOST表示父容器设置了最大可用容量 SpecSize 视图大小不能超过此值 最大容量状态取决于视图的具体实现情况 相当于 wrap_content状态

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

首先,在measure()过程完成后立即可用的是getMeasureWidth()方法;相比之下,在layout()过程完成后才可获得的是getWidth()方法。此外,在其数值上则由setMeasuredDimension()方法进行赋值;相反地,则是由视图右边界坐标与左边界坐标的差值来确定。

事件分发机制

图解 Android 事件分发机制

requestLayout,invalidate,postInvalidate区别与联系

相同点:三个方法都具备刷新界面的功能。不同点:invalidate和postInvalidate仅会触发onDraw()方法;而请求布局则会依次触发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均属于内 kernel组件,在核心体系中发挥着重要作用。然而,在Linux系统中Binder并不是直接作为核心组件存在的。这种情况下该如何实现进程间的通信呢?这得益于Linux系统的动态可加载核心模块(Loadable Kernel Module, LKM)机制;模块则具备独立的功能特性,并能够被独立编译生成。然而其能够被单独编译生成但无法独立执行运行。在运行时被整合到整个核心体系中持续执行以完成相应的功能任务。因此,在Android系统中可以通过动态加载并启用特定核心模块来扩展其功能范围

Android 系统中的一个内核模块,在内核空间中负责各用户进程间通过 Binder 方式进行通信,并称为 Binder 驱动(Binder Dirver)。

显然并非如此

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

Binder IPC 机制中内存映射通过 mmap() 函数实现,而 mmap() 则是操作系统中一种实现内存管理的重要技术手段。它的工作原理是将一段由用户程序申请的物理内存区域,与系统内的逻辑地址进行一一对应,从而实现了内外存之间的高效通信。一旦用户的内存区域发生变更,该变化会即时反映到系统核心部分;同样地,核心部分的操作也会即时同步到用户的地址空间。

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是一个独立运行的过程?当Android系统启动时会创建一个名为servicemanager的过程该过程会通过指定命令BINDERSET_CONTEXT_MGR与Binder驱动进行注册连接以正式加入到ServiceManager的服务体系中**[1]。这一操作由Binder驱动自动完成并生成相应的Binder实体对象其对应的引用标识在所有客户端中均被设置为0值以确保各客户端能够唯一识别并通信连接到同一个ServiceManager实例上[2]。具体而言Server端应用通过其绑定到的服务器地址上的0号引用与ServiceManager进行通信连接而客户端则可以通过该0号引用获取到对应的服务器实例并进而访问其绑定的服务体内容服务信息等资源[3]**。(注:以上描述基于对Android Binder机制的基本理解如有误请读者自行核实相关资料)

序列化的方式

Serializable是Java中提供的一个空接口,在Java中用于标识对象是否可进行序列化操作,并基于ObjectOutputStream和ObjectInputStream机制实现对目标对象的数据打包与解包过程。建议对需要进行序列化的对象指定一个唯一的serialVersionUID值,并提醒系统会对文件中的该字段进行一致性检查;若发现不一致,则表明该类发生修改进而导致反序列化失败;因此,在可能变更的对象最好预先设定该字段的具体数值以避免潜在的问题。

Fragment的懒加载实现

当Fragment的可见状态发生变化时,会触发setUserVisibleHint()方法。为了实现Fragment的懒加载功能,可以通过重写此方法来实现Fragment的懒加载机制。需要注意的是,在onViewCreated事件发生之前调用该方法可能会导致问题,请确认该方法在onViewCreated事件之前被调用,并确保界面已经初始化完成后再进行数据加载操作。以防止因界面未初始化导致的空指针异常发生。

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

缓存区别:

  1. 在屏幕内外分别设置了两层缓存机制。
  2. 相比ListView增加了两层缓存功能,并提供了针对离屏ItemView的多级缓存管理能力。具体而言:
  • 当存在匹配时可直接引用已绑定的状态。
  • 支持开发者根据自身需求定制完整的缓存策略。
  • 实现了一个统一的 RecyclerView 缓存池机制。
  1. 列表视图主要负责存储视图对象及其相关信息。
  2. Android平台下的List Views采用简单的层次结构,并实现了一个相对简单的层次化架构。
  3. 相比之下Android平台下的List Views实现了一个相对复杂的层次化架构,
  4. 该方案通过将数据以树形结构存储并结合队列特性实现了高效的查询效率

RecylerView支持局部刷新接口,在这种情况下无需频繁调用不必要的bindView操作。

Android两种虚拟机区别与联系

Android Dalvil虚拟机构成了针对手机特性而进行系统性优化的对象,在其设计架构上采用了寄存器机制与传统栈结构有着本质区别。经过优化设计,Dalvil能够在有限内存环境中支持多实例运行,每个 Dalvil应用独立运行于一个Linux进程空间中并能显著降低冗余指令分配频率以及内存读写操作频率。经过优化设计,Dalv能够在有限内存环境中支持多实例运行具体而言它允许在同一时间运行多个 Dalv应用实例每个实例都能高效利用可用资源同时保证系统的稳定性和响应速度这一特点使其成为移动设备开发中更具优势的选择之一经过进一步优化,Dalv还能够实现跨设备功能从而提升了其在 Android 平台上的适用性和竞争力

adb常用命令行

查询现在连接的设备列表:adb devices

ADB 用法大全

apk打包流程

  1. aapt工具整合资源文件并生成R.java文件
  2. aidl工具解析AIDL文档并编译对应的Java代码
  3. javac运行Java源代码并输出对应类字节码
  4. 将.class字节码转换为Davik虚拟机兼容的.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 文件进行优化处理,并将其存储于 dalkicache 目录中。
  5. 将从 AndroidManifest 中解析出的四大组件信息注册至 packageNameService。
  6. 应用安装完成后向系统发送广播通知。

apk瘦身

APK主要由以下几部分组成:
+ META-INF/ :它包括了签名文件CERT.SF、CERT.RSA以及MANIFEST.MF文件。
+ assets/ :它存储各种资源文件,并非所有资源都会被编译成二进制形式。
+ lib/ :其中包含了外部引用的一些第三方库信息。
+ resources.arsc :它涵盖了res/values目录中所有资源内容(如strings、styles等),还包括其他未包含在此目录中的资源路径信息(如layout布局文件、图片等)。
+ res/ :它存储那些没有被移动到resources.arsc中的资源文件内容。
+ classes.dex :这些经过dx编译生成的Java源码文件能够被Android虚拟机正确解析执行。
+ AndroidManifest.xml :该文件是一个清单列表形式

在内存占用方面较为显著的是res资源、lib以及class.dex文件,在项目开发初期就需要对这些关键组件进行优化处理

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原理

Oneweb通过newCall方法实现了对Request对象的封装功能,在此基础上生成一个Call对象。每当该Call对象被创建后,默认会触发executed方法或者enqueue方法,并会立即调用Dispatcher类的相关方法来启动相应的处理流程,在当前线程环境下执行相应的请求操作。经过RealInterceptorChain拦截器链系统后即可获得最终处理结果。该拦截器链系统由以下五个核心拦截器依次组成:自定义开发者的独立拦截器;用于处理单次失败后重试请求的retryAndFollowUpInterceptor;用于附加必要的HTTP头信息以支持双向通信的BridgeInterceptor;用于缓存GET方式请求中的重复查询数据点的CacheInterceptor;以及负责连接相关资源并为后续发送做准备工作的ConnectInterceptor;最后由CallServerInterceptor负责将整个流程中的所有事务整合起来,并通过HttpCodec完成最终的实际发送操作

okhttp源码解析

Retrofit的实现与原理

基于动态代理机制生成声明式服务接口的对象实例,在进行时将会触发InvocationHandler中的invoke方法。在其中步骤如下:首先通过解析功能将服务方法转换为ServiceMethod类实例;该类能够进一步将设置好的参数转化为Request对象;随后通过 serviceMethod 和 args参数获取到一个okHttpCall对象,在这个过程中实际将会调用okhttp库的具体网络请求方法,并且会利用serviceMethod中的responseConverter来进行响应体的转码处理;最后系统会将okHttpCall结果进一步封装成与声明一致的服务返回类型(默认情况下采用ExecutorCallbackCall模式,并将原本的服务回调转发至UI线程处理)。

Retrofit2详细使用说明:深入剖析其在源码中的工作原理
全面解读Retrofit2的功能特点:探讨其与okhttp体系的独特关联

ARouter原理

可能是最全面的ARouter源代码解析

RxLifecycle原理

在Activity中进行设置,在不同生命周期阶段触发相应的事件;借助compose操作符(其实内部仍依赖于takeUntil操作符)设定 upstream 数据,在接收到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 : 将存放于\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用 。
(2) Extension ClassLoader : 将\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。
(3) Application ClassLoader : 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。
每个ClassLoader实例都有一个父类加载器的引用(不是继承关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但是可以用做其他ClassLoader实例的父类加载器。
当一个ClassLoader 实例需要加载某个类时,它会试图在亲自搜索这个类之前先把这个任务委托给它的父类加载器,这个过程是由上而下依次检查的,首先由顶层的类加载器Bootstrap ClassLoader进行加载,如果没有加载到,则把任务转交给Extension ClassLoader加载,如果也没有找到,则转交给AppClassLoader进行加载,还是没有的话,则交给委托的发起者,由它到指定的文件系统或者网络等URL中进行加载类。还没有找到的话,则会抛出CLassNotFoundException异常。否则将这个类生成一个类的定义,并将它加载到内存中,最后返回这个类在内存中的Class实例对象。

为什么使用双亲委托模型

JVM在判定两个Class对象是否相等时,不仅需要比较两者的名称是否一致,还需确认它们是由同一个ClassLoader进行加载的。如果父Class已经由ClassLoader初始化过,则无需为子ClassLoader重复执行初始化操作。从安全性角度考虑,自定义一个String类型的用户Class,除非修改Java虚拟机(JVM)中ClassLoader的工作原理,否则基于ClassLoader工作原理的不同实现可能会影响其正确性。.NET Framework中的系统类型(System Class)通常会在程序启动时被预先初始化完成。

HashMap原理,Hash冲突

在JDK1.6和JDK1.7版本中,默认使用的HashMap数据结构采用了数组加链表的设计模式,在这种设计中冲突问题通过链表来处理。然而在单个链表中存储大量数据(即相同哈希值的数据点较多)时会导致按键值顺序查找效率降低的问题。而到了JDK1.8版本中,默认使用的HashMap则采用了位数组加链表加红黑树的设计模式,在单个链表长度达到8以上时会将其转换为红黑树结构以减少查找时间上的消耗。此外在内存空间耗尽的情况下系统会自动触发再散列操作将当前的内存空间翻倍并重新组织原有数据到新的内存空间中以避免内存溢出问题的发生

为了保证程序运行效率系统会在内存需求超出当前容量限制时自动触发再散列操作将当前占用的空间翻倍并重新组织原有数据到新的内存空间中以避免内存溢出问题的发生

深入解析HashMap:基于JDK 1.8源码剖析

什么是Fail-Fast机制

FailFast是一种用于Java集合中错误检测的机制。当对集合进行修改或多个线程同时对结构进行更改时,“有可能”触发这种机制。“注意”这里“有可能”指的是可能而不是必然。“其实就是这会导致ConcurrentModificationException异常。”
迭代器在执行next()或remove()操作前会调用checkForComodification()方法。“其主要作用就是比较这两个计数器是否相等”。如果这两个计数器不一致,“就会导致该异常”。每当数据结构被修改时都会更新这个计数器。“因此这也是一种实现 concurrent modification detection 的方式。”

Java优化升级(三四)——立即丢弃机制

Java泛型

Java泛型详解

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

Java程序中wait和sleep都会导致某种形式的暂停,并能满足不同的需求.wait()方法用于线程间通信,在等待条件满足且其他线程被唤醒时会释放锁;而 sleep()方法则仅仅释放CPU资源或使当前线程暂时停止执行一段时间而不释放锁

volatile的作用和原理

Java代码经过编译处理会生成Java字节码,并将这些字节码被类加载器加载至JVM中;随后JVM会解码并执行这些字节码指令;最终这些操作会被进一步转换为CPU能直接理解并执行的汇编指令序列。volatile标识符是一种轻量级同步机制(它不会导致线程间的上下文切换或调度问题),特别适用于多处理器环境下的共享变量管理;这种机制通过确保可见性特性来实现信息的一致性传播——即当一个线程修改共享变量时其他线程能够及时获取到该修改信息。然而由于内存访问速度远低于CPU处理速度这一技术瓶颈的存在,在实际应用中普通共享变量的状态更新往往会出现延迟现象:具体而言,在其他线程读取共享变量值时系统可能会仍残留着之前未更新的数据;因此仅凭普通共享变量无法实现可靠的一致性保障;而当对声明为volatile类型的变量执行写操作时系统会触发特定机制以确保数据的一致性:例如通过向处理器发送一条带有锁权限的指令指示其将当前缓存行的数据重写回系统内存区域。

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

存在安全隐患。虽然volatile机制能够确保变量的可见性但无法确保操作的原子性。在程序执行过程中运算符会分解为多个步骤依次完成:首先在内存中读取当前变量的具体数值然后对该数值进行加一运算最后将新的计算结果写回内存中的该变量位置。这种分解处理有助于保障内存访问的安全性和一致性但在多线程环境下可能会导致以下问题:当两个线程同时尝试读取同一个变量时例如试图同步同一个计数器它们都会按照顺序化的流程执行加一操作从而避免数据竞争和丢失数据的风险

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

使用java.util.concurrent.atomic包中的原子操作类(例如AtomicInteger)。其实现机制是基于CAS自旋操作更新数据值。其中,“CAS”即‘比较与交换’的缩写,在该机制中包含三个参数:内存当前值V、预期目标值A以及目标新值B。只有在预期目标值A与当前内存值V一致时才会修改内存中的数据为新值B;而自旋机制则通过反复尝试进行CAS操作直至成功为止。

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

  • ABA 问题是由于在执行 CAS 操作时需要检测值是否发生变化而产生的。如果检测到值没有发生变化,则会尝试进行更新;然而,在某些情况下(例如原本是 A 的值却发生了变化),使用 CAS 进行检查时会误判为未发生变化而导致无法正确解决问题。通过引入版本号机制来解决这一问题。Java 1.5及以后版本中,默认情况下 JDK 的 Atomic 包提供了 AtomicStampedReference 类以解决这一问题。
  • 优化 pause 指令可以在循环中减少执行时间。
  • 仅能对单一共享变量执行原子操作;但通过将多个相关对象整合为一个复合对象后,在一次操作中完成多个 CAS 检查。

synchronized

在Java编程中,在执行普通同步关键字的方法时,默认使用的锁即为当前实例自身;而当采用静态同步关键字进行方法调用时,则所使用的锁由相应的类元属性Class来提供;若在编写代码时使用了带有参数的同步关键字,则系统会自动将这些参数打包形成一个特殊的对象作为相应的锁;

在尝试进入同步代码块时, 线程必须先获取到锁才能继续执行. 在退出或发生异常的情况下, 必须及时释放所持有的锁以避免资源泄漏. synchronized关键字所使用的互斥 lock机制通常存储于Java对象头中的 Mark Word字段中, 该字段通常为32位或64位架构设计, 其中 Mark字段的后两位二进制位用于标识互斥 lock的状态.

java对象结构

Java SE1.6通过减少因获取和释放锁而产生的性能开销这一目标,在版本1.6中引入了偏心锁和轻量锁机制,并新增了四种不同的锁状态:无锁状态、偏心锁状态、轻量lock状态以及重量级lock状态。这些状态会根据竞争情况逐步升级;而一旦被提升到更高一级的状态后,则无法降级。

偏向锁

偏 unlocked获取过程:
1. 检查Mark Word中偏向锁标志位的状态是否为1,并确认其lock state是否为01状态下的可偏向配置状态。
2. 若当前线程ID与自身的lock state一致,则直接进入步骤5;否则进入步骤3进行处理。
3. 当前线程ID未指向自身时,在满足条件的情况下优先采用CAS机制竞争当前lock state;若成功,则将Mark Word中的current thread ID字段设置为自身值并执行步骤5;若CAS操作失败,则进入步骤4继续处理。
4. 若在此过程中无法通过CAS机制获取偏向锁,则表明存在竞争关系:当系统到达全局安全点(safepoint)时会将被阻塞的带有锁定权限的主进程挂起,并将其lock state升级至轻量级锁状态;随后该进程需等待安全点重新开放后才能继续执行后续同步操作(该操作会导致主进程停止)。
5. 执行同步代码并完成相关操作后退出偏 unlocked保护机制

轻量级锁

  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结构来处理;其构造函数接受一个整数参数作为计数器,在每次调用countDown()方法时将计数器减一。处理数据相关的线程需通过await关键字等待直至对应的计数值归零。

Java中interrupted 和 isInterruptedd方法的区别?

Java中的多线程操作主要依赖于内部标识来实现操作管理。当调用Thread.interrupt()时会将当前线程的中断标志设置为true以进行操作暂停。如果使用的是静态方法如静态方法调用Thread.interrupted()来检测当前是否处于被中止的状态,则此时会立即清除当前线程的中断标志以释放资源。相反地如果使用非静态方法如isInterrupted()则用于检查其他可能运行中的线程是否存在被中止的情况并且该操作不会影响到当前所在的 thread对象的状态标记值。简单来说所有 throw InterruptedException异常的方法都会导致所在的 thread对象将其自身的 interrupted标志置为false从而完成操作并返回控制流。无论怎样一个 thread对象在其生命周期内有可能会被其他 thread同时进行操作而触发其被中止的状态变化

懒汉式单例的同步问题

虽然同步的懒加载在多线程环境中是线程安全的,并且能有效减少不必要的操作开销,在某些场景下确实是一种合理的优化策略。然而,在实际应用中可能会遇到一些潜在的问题。例如,在多数情况下,在单线程环境下执行这些步骤时不会出现问题:首先创建实例对象并完成初始化工作;随后将该对象返回给调用者使用。但在多线程的情况下,在第一个线程执行分配内存并设置指向刚分配的内存地址(步骤1和3),第二个线程会发现instance不为null而直接使用它(尽管操作2尚未完成)。这就可能导致一个未被完全初始化的对象被直接引用或操作的情况发生。
解决方案是通过将变量声明为volatile类型来阻止指令重排序的发生,在JDK 1.5中引入了volatile关键字后编译器就不再允许这种行为的发生。
另一种方式则是使用静态内部类:

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

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

什么是ThreadLocal

ThreadLocal本质上是一种线程本地变量。它通过提供独立于其他线程使用的本地副本来保护数据一致性。每个使用该机制的程序都能通过这种方式实现对共享资源的安全访问控制。从一个线程的角度来看,目标变量就象是该线程所拥有的一个局部变量。因此,在Java语言中,默认情况下只有当同一个对象引用被持有时才允许对共享实例进行修改;通过使用ThreadLocal机制可以在不影响其他引用的情况下实现对共享实例的安全访问。这个存储结构被附加到该线程上以供其查询和操作使用

什么是数据竞争

数据竞争的概念指的是:在一个线程中对一个变量进行修改,在另一个线程中对该变量进行访问,并且这些操作未采用同步机制进行排序。

Java内存模型(Java Memory Model JMM)

JM通过屏蔽硬件与操作系统间的内存访问差异,在确保Java程序能在不同平台上达到一致的内存访问效果的同时实现了跨平台兼容性。这种机制能够避免因不同平台或系统导致的内存访问不一致问题而影响程序运行效果。
共享变量被存储在主内存中作为全局资源使用,在这种机制下每个独立运行的线程都拥有一个私有的本地内存空间用于存储其特定的操作副本。
本地内存作为一个抽象概念涵盖了缓存、写缓存区、寄存器以及其他的硬件与编译器优化措施。
在程序运行过程中为了提高性能编译器与处理器经常会对指令进行重新排列这可能会影响多线程程序的整体执行效果。
JSR-133内存模型采用happens-before概念来定义操作间的相对可见性这一机制确保了重排不会破坏数据依赖关系从而保证了系统的正确性。
主要遵循happens-before规则有:

  1. 程序顺序规则:每个线程内的操作都会在其后续的操作之前发生作用
  2. 监视器锁规则:对一个锁进行解锁后才有可能影响随后对该锁再次加锁的行为
  3. volatile变量规则:对volatile域中的赋值会在后续读取时受到相应的约束
  4. 传递性法则:如果事件A发生在事件B之前而事件B又发生在事件C之前那么事件A也会发生在事件C之前

Java内存区域

  • 程序计数器:它是当前线程执行字节码指令的行号指示器,在切换线程时用于恢复状态,并且只属于当前线程;
    • Java虚拟机栈(栈):这是一个专属于每个线程的独特结构,在执行任何方法时都会生成一个栈帧来存储局部变量表、操作数栈以及动态链接信息等关键数据;每当一个方法被调用时都会生成一个栈帧,并在该方法完成执行后自动退出该栈帧;
    • 本地方法栈:它与虚拟机栈功能相似,在支持Native方法调用方面发挥着重要作用;
    • Java堆(Heap):它是所有线程共有的内存区域,在存储对象实例以及充当垃圾收集器主要管理空间的同时也存放编译后的代码等数据;
    • 方法区(Method Area):与Java堆共享内存空间的重要区域,在这里存储了已由虚拟机加载并编译好的类信息、常量以及静态常量等数据;
    • 运行时常量池(Run-time Constant Pool):它位于Method Area中负责存储各种字面量和符号引用的关键位置

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

  • 引用计数算法。实现简单,判定效率高,但不能解决循环引用问题,同时计数器的增加和减少带来额外开销,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. 复制算法(Copying)
    复制算法将内存划分为大小相等的两部分,并且每次只占用其中一部分内存空间。在垃圾回收过程中,在这种情况下会将存活对象复制到另一部分内存中,并释放被占用的那一部分空间以供下次使用。尽管复制算法实现简单且运行效率较高(这是因为其运行速度很快),但由于其每次只能利用内存的一半容量(即每一次都要切换到另一块空闲区域),导致内存的整体利用率较低。现代JVM采用这种复制方法来收集新生代内存区域(New Generation)。由于新生代中约98%的对象都具有朝生夕死的特点(即一旦不再被引用就会被回收),因此将其划分为一块较大的Eden区域和两块较小的Survivor区域(通常比例约为8:1:1)。具体操作时会交替使用一块大Eden区域和一块小Survivor区域作为当前存活对象所处的空间,在垃圾回收时将这两块区域内仍然存活的对象全部移动到另一块小Survivor区域内并清空剩余空间。

  2. 标记—整理算法(Mark-Compact)
    与复制算法类似的是标记—整理算法,在具体实现上两者存在显著差异:与复制算法不同的是,在处理存活对象时标记-整理算法并非将其复制到另一块独立的内存空间中进行处理;相反地,则是将存活的对象按照一定的规则移动至内存的一端,并直接回收超出边界范围的内存区域;此外需要指出的是该方法较之于其他方法具有更高的内存利用率,并且特别适用于那些收集对象存活时间相对较长的老年代。

  3. 分代收集(Generational Collection)
    基于对象存活时间将内存划分为新生期与成熟期,并依据各阶段对象的存活特征选择相应的垃圾回收策略。新生期运用复制法进行处理,在成熟期则采取标记-整理方式进行清理。

内存分配策略

  • Eden区域采用优先级策略分配对像。
    • 大对像被直接划入老年代区。
      • 大对像特指那些占据大量连续内存资源的Java对像。
      • 典型例子包括长度较长的字符串以及数组数据结构。
    • 存活满一个新生代周期的对象才会进入老年代区。
    • 动态判断逻辑用于评估当前Age阶段的所有对像的总占用内存是否超过Survivor空间的一半。
      • 如果上述条件满足,则Age阶段及以上的对像具备足够的生存可能性。
      • 因此会被直接划入老年度区进行内存管理。
    • 在发生新生代GC前必须确保Survivor剩余空间足以满足新生代所有目标对像的内存需求。

全部评论 (0)

还没有任何评论哟~