目录
Collection 和 Collections 的使用区别
StringBuffer 和 StringBuilder 的区别
volatile 和 synchronized 的详解及两者区别
在下载过程中如何显示进度条?若下载操作耗时且数据大小不确定,该如何处理
给定银行卡前缀(如前 4 位或前 5 位),如何查找银行卡号所属银行?该查找操作的时间复杂度是多少(java 代码完整实现)
Android Activity 的生命周期
Activity 作为 Android 中与用户交互的核心组件,其生命周期由系统全程管理,理解各阶段的状态变化对开发稳定的应用至关重要。
当 Activity 首次创建时,会依次执行 onCreate
、onStart
和 onResume
方法。onCreate
是生命周期的起点,在此可完成布局加载、数据初始化等关键操作,如调用 setContentView
设置界面布局;onStart
方法执行时,Activity 已对用户可见但尚未获取焦点;onResume
则表示 Activity 已处于前台并能响应用户操作,此时是开启动画或注册传感器监听的合适时机。
当用户切换至其他应用或打开新 Activity 时,当前 Activity 会进入暂停状态,依次触发 onPause
方法。该方法需完成轻量级的状态保存,如暂停动画、释放非关键资源,但要避免耗时操作,否则会影响新 Activity 的显示速度。若新 Activity 完全覆盖当前 Activity,还会执行 onStop
方法,此时 Activity 不再可见,可进行更耗时的资源释放,如取消网络请求,但需注意若用户快速返回,onStop
可能尚未执行完毕。
当用户返回该 Activity 时,会按 onRestart
、onStart
、onResume
的顺序恢复运行。onRestart
仅在 Activity 从停止状态重新启动时调用,可用于重新加载数据。而当 Activity 被系统销毁时,onDestroy
方法会被调用,此处必须释放所有资源,如解绑服务、关闭数据库连接等,以避免内存泄漏。
此外,当设备配置发生变化(如旋转屏幕),Activity 会经历销毁重建的过程,依次执行 onPause
、onStop
、onDestroy
,然后重新创建并执行 onCreate
、onStart
、onResume
。为避免数据丢失,可通过 onSaveInstanceState
保存临时状态,系统会在重建时将数据传入 onCreate
或 onRestoreInstanceState
。理解这些生命周期方法的调用时机,能帮助开发者更好地管理组件状态和资源释放。
Fragment 的生命周期
Fragment 作为可嵌入 Activity 的组件,其生命周期既依赖于宿主 Activity,又有自身独特的阶段变化,与 Activity 生命周期的配合需细致处理。
在 Fragment Activity 关联时,首先会调用 onAttach
方法,此时 Fragment 已与 Activity 建立联系,可通过 getActivity()
获取宿主引用,但视图尚未创建。接着执行 onCreate
,用于初始化 Fragment 的核心数据,如接收参数 Bundle,其作用类似 Activity 的 onCreate
,但不涉及视图构建。
视图创建阶段是 Fragment 特有的流程,onCreateView
方法需返回 Fragment 的根视图,在此可通过 LayoutInflater
inflater.inflate(R.layout.fragment_layout, container, false) 加载布局,其中 container 参数为父容器,false 表示不立即添加到容器。视图创建完成后,
onViewCreated` 方法被调用,可在此对视图进行初始化,如设置点击事件,此时视图已可用但宿主 Activity 可能尚未完全启动。
当宿主 Activity 进入 onStart
和 onResume
时,Fragment 相应地执行 onStart
和 onResume
,此时 Fragment 对用户可见并可交互。而当 Activity 暂停或停止时,Fragment 依次触发 onPause
、onStop
,与 Activity 的对应方法类似,用于释放临时资源。
Fragment 与 Activity 生命周期的关键差异在于视图的销毁阶段。当 Fragment 被移除但仍与 Activity 关联时,会先调用 onDestroyView
,销毁视图但保留 Fragment 实例,适用于需要保留数据但释放视图资源的场景,如 ViewPager 中的 Fragment 切换。当 Fragment 彻底与 Activity 分离时,执行 onDestroy
销毁实例,最后通过 onDetach
解除与 Activity 的关联。
理解 Fragment 生命周期与 Activity 的嵌套关系尤为重要。例如,当 Activity 旋转屏幕时,Fragment 会经历 onDestroyView
而非 onDestroy
,因此需在 onSaveInstanceState
中保存视图相关数据,而在 onCreateView
中恢复。这种精细的生命周期管理,使得 Fragment 能更灵活地在 Activity 中复用和切换,同时避免资源泄漏。
Activity 四大启动模式
Activity 的启动模式决定了系统如何管理其在任务栈中的实例创建与复用,合理使用不同模式可优化应用性能并实现特定交互逻辑。
standard 模式是系统默认的启动方式,每次启动 Activity 都会在当前任务栈中创建新实例,无论该实例是否已存在。例如,在 Activity A 中多次启动 Activity A,任务栈中会依次压入多个 A 实例,返回时按后进先出顺序退出。这种模式适用于大多数普通页面,如详情页,每次打开都需要独立的实例状态。
singleTop 模式则会检查任务栈顶是否已存在目标 Activity 实例。若存在,则直接复用该实例并调用其 onNewIntent
方法传递参数,不再创建新实例;若不存在,则新建实例。例如,消息通知页采用该模式,当用户多次点击通知时,若通知页已在栈顶,则直接刷新内容而非创建多个页面。此模式能避免在栈顶重复创建相同 Activity,减少内存占用。
singleTask 模式具有更强的实例管理能力,它会在整个系统范围内查找是否存在包含该 Activity 的任务栈。若存在,则清空该任务栈中位于此 Activity 之上的所有实例,并将其置于栈顶;若不存在,则新建任务栈并创建实例。典型应用是应用的主界面,当用户从其他应用返回主界面时,系统会清除主界面之上的所有 Activity,确保主界面以干净的状态呈现,实现 “一键回主” 的效果。
singleInstance 模式最为特殊,它会创建一个独立的任务栈,该 Activity 在此栈中是唯一的实例,且其他 Activity 无法进入此栈。这种模式适用于需要全局唯一且独立运行的场景,如电话接听界面,无论从何处启动,都始终在单独的任务栈中运行,确保其不会被其他应用的 Activity 干扰,保证用户体验的一致性。
启动模式可通过在 AndroidManifest.xml 中为 Activity 配置 android:launchMode
属性设置,也可通过 Intent 的 FLAG_ACTIVITY_NEW_TASK
等标志位动态修改。不同模式的选择需结合业务场景,例如需要单例展示的页面用 singleTask,频繁跳转的普通页面用 standard,以平衡性能与用户体验。
Android 中的四大组件
Android 系统的核心架构基于四大组件,它们各司其职又相互协作,构成了应用的基本框架。
Activity 是与用户交互的可视化界面载体,负责展示 UI 和处理用户操作。其生命周期由系统管理,从创建到销毁经历 onCreate、onStart、onResume 等多个阶段,开发者需在不同阶段完成相应的资源管理。Activity 之间通过 Intent 进行跳转,可携带数据实现页面间通信,如在登录 Activity 中通过 Intent 传递用户信息到主界面 Activity。作为用户操作的主要入口,Activity 的设计直接影响应用的用户体验,需遵循轻量化原则,避免在其中处理耗时任务。
Service 是在后台执行长时间运行任务的组件,不提供用户界面,但可在后台处理数据下载、文件操作等任务。它有两种启动方式:“启动式” 通过 startService 启动,服务独立运行,即使启动它的组件销毁也会继续执行,需调用 stopService 停止;“绑定式” 通过 bindService 与客户端绑定,服务生命周期与绑定组件一致,可通过 ServiceConnection 实现客户端与服务的通信,如音乐播放器中通过 Service 管理播放逻辑,Activity 作为客户端绑定服务以控制播放。Service 运行在主线程中,因此耗时操作需在内部开启子线程,避免阻塞 UI。
BroadcastReceiver 用于接收系统或应用发出的广播消息,实现组件间的全局通信。广播分为 “普通广播” 和 “有序广播”,普通广播异步发送,所有接收器可同时接收;有序广播同步发送,接收器按优先级顺序接收。接收器可通过静态注册(在 Manifest 中声明)或动态注册(通过 registerReceiver 方法)监听特定广播,如监听网络状态变化、电池电量低等系统事件,或自定义广播实现应用内组件间的消息传递。广播接收器的 onReceive 方法运行在主线程,处理时间不宜超过 10 秒,否则会引发 ANR(应用无响应),复杂操作需启动 Service 处理。
ContentProvider 用于实现应用间的数据共享,封装了数据访问接口,其他应用可通过 ContentResolver 访问其提供的数据。它支持对数据库、文件、网络数据等的访问,如系统的 ContactsProvider 提供联系人数据访问。开发者自定义 ContentProvider 需继承该类并实现 query、insert、update、delete 等方法,同时通过 URI(如 content://com.example.provider/notes)唯一标识数据集合。ContentProvider 结合 ContentObserver 可实现数据变化的监听,确保多个应用间的数据一致性,是 Android 应用间数据交互的标准方式。
这四大组件通过 Intent、Binder 等机制相互协作,Activity 启动 Service、发送广播,BroadcastReceiver 响应事件后启动 Activity 或 Service,ContentProvider 为所有组件提供数据支持,共同构成了 Android 应用的完整生态。
Android 的线程通信方式
在 Android 开发中,线程通信是解决 UI 主线程与子线程数据交互的关键,多种通信方式适用于不同场景,需根据需求选择合适的方案。
Handler 机制是最基础的线程通信方式,基于 Looper、MessageQueue 和 Handler 三者协作。Looper 负责循环读取 MessageQueue 中的消息,MessageQueue 存储消息队列,Handler 用于发送消息和处理消息。在主线程中,系统会自动创建 Looper 和 MessageQueue,开发者只需创建 Handler 并重写 handleMessage 方法处理消息。若在子线程中使用,需手动调用 Looper.prepare () 和 Looper.loop () 启动循环。例如,子线程中通过 handler.sendMessage (Message.obtain ()) 发送消息,主线程的 handleMessage 接收到消息后更新 UI,这种方式灵活高效,适用于需要频繁通信的场景,但需注意 Handler 可能导致的内存泄漏,可通过弱引用方式持有 Activity 引用。
AsyncTask 是对 Handler 的封装,将异步任务分为后台执行和 UI 更新两个阶段,简化了线程通信流程。它定义了 doInBackground(在后台线程执行耗时任务)、onPreExecute(执行前在主线程准备)、onPostExecute(完成后在主线程更新 UI)等方法。使用时只需继承 AsyncTask 并实现对应方法,如网络请求可在 doInBackground 中执行,结果通过 onPostExecute 返回给 UI。但 AsyncTask 在 Android 3.0 后默认串行执行,若需并行需调用 executeOnExecutor (AsyncTask.THREAD_POOL_EXECUTOR),且其生命周期与 Activity 不一致,Activity 销毁时若 AsyncTask 未完成,可能导致空指针异常,因此更适合短耗时任务。
HandlerThread 是自带 Looper 的线程类,继承自 Thread,创建时会自动启动 Looper。通过 getLooper () 获取其 Looper 后,可创建 Handler 与该线程通信,适用于需要长期运行的后台线程,如日志记录线程。例如,创建 HandlerThread 后,在其 Looper 上创建 Handler,后续通过该 Handler 发送消息,消息会在 HandlerThread 的循环中处理,避免了手动管理 Looper 的繁琐,同时保证了线程的独立性。
IntentService 是 Service 的子类,内部通过 HandlerThread 处理任务,确保耗时操作在后台执行。它会在 onHandleIntent 方法中处理传入的 Intent,处理完成后自动停止 Service,无需手动调用 stopSelf ()。例如,文件下载服务可继承 IntentService,每次下载请求通过 startService 发送 Intent,服务在后台线程中完成下载后自动销毁,简化了服务的管理,是处理异步任务的便捷方式。
MessageQueue 和 Looper 是 Handler 机制的底层基础,直接操作它们可实现更灵活的线程通信。例如,在子线程中创建 Looper,通过 Looper.myLooper () 获取后,创建 Handler 与主线程通信,这种方式适用于需要自定义消息循环策略的场景,但使用时需注意线程安全和资源释放。
此外,第三方库如 RxJava 通过响应式编程模型,利用 Observable 和 Observer 在不同线程间切换,实现异步任务和 UI 更新的解耦,适用于复杂的异步流程;EventBus 基于发布 - 订阅模式,通过注解方式注册事件接收器,简化了跨组件的线程通信,尤其适合组件间的广播式通信。
选择线程通信方式时,需考虑任务复杂度、生命周期管理和代码可读性。简单 UI 更新可使用 AsyncTask,频繁通信或复杂流程用 Handler,后台服务用 IntentService,而 RxJava 和 EventBus 则在大型项目中能有效提升代码的可维护性。
Handler 机制详解,是否会导致内存泄漏及泄漏原理
Handler 机制是 Android 消息处理的核心框架,其工作流程基于 “发布 - 订阅” 模式,通过 Message、MessageQueue 和 Looper 三者的协同实现线程间通信。当创建 Handler 时,它会自动关联当前线程的 Looper,Looper 负责从 MessageQueue 中循环取出消息并分发给 Handler 处理。具体来说,MessageQueue 是一个基于单链表实现的消息队列,用于存储 Handler 发送的 Message 对象;Looper 通过 loop () 方法不断从 MessageQueue 中获取消息,若队列为空则进入阻塞状态,直到有新消息插入时被唤醒。
Handler 可能导致内存泄漏,其核心原因在于非静态内部类对外部类的隐式引用。例如,在 Activity 中直接创建 Handler 实例时,Handler 作为内部类会持有 Activity 的强引用。当 Activity 销毁时,若 Handler 仍有未处理的消息或正在处理消息,这些消息会持有 Handler 的引用,进而导致 Activity 无法被垃圾回收,形成内存泄漏。具体泄漏流程如下:Activity -> Handler -> Message -> MessageQueue -> Looper -> Thread,由于 Thread 持有 Looper 的引用,最终导致 Activity 的生命周期被延长至 Thread 结束。
避免内存泄漏的常见做法是使用静态内部类搭配弱引用。静态内部类不持有外部类的引用,通过弱引用指向 Activity,即使 Activity 销毁,弱引用也能在垃圾回收时被正常回收。示例代码如下:
public class MainActivity extends AppCompatActivity {
private static class MyHandler extends Handler {
private WeakReference<MainActivity> weakReference;
public MyHandler(MainActivity activity) {
weakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = weakReference.get();
if (activity != null) {
// 处理消息
}
}
}
private MyHandler handler = new MyHandler(this);
@Override
protected void onDestroy() {
handler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
Looper 阻塞为何不会造成 ANR
Looper 的阻塞特性与 ANR(Application Not Responding)的触发机制存在本质区别。Looper 通过 loop () 方法实现消息循环,其内部通过 native 层的 pollOnce () 方法实现阻塞等待。当 MessageQueue 中没有待处理消息时,pollOnce () 会使线程进入休眠状态,此时线程并未真正阻塞,而是处于 “可唤醒” 的等待状态,系统资源消耗极低。一旦有新消息通过 enqueueMessage () 插入队列,内核会唤醒该线程,继续处理消息。
ANR 的触发条件是主线程(UI 线程)在规定时间内无法响应特定事件。例如,输入事件(如点击、滑动)需要在 5 秒内处理完成,前台 Service 的 onStartCommand () 需在 20 秒内执行完毕,后台 Service 则为 200 秒,广播接收器的 onReceive () 在前台场景下超过 10 秒、后台超过 60 秒未完成都会触发 ANR。系统通过 Watchdog 机制监控主线程的消息处理情况,当特定事件的处理时间超过阈值时,认为应用无响应,从而弹出 ANR 对话框。
Looper 的阻塞属于 “良性阻塞”,它不会影响主线程对消息的响应能力,因为阻塞状态是暂时的,且阻塞期间线程可以被快速唤醒。而 ANR 的本质是主线程在处理某个任务时被长时间阻塞,导致无法及时响应用户输入或系统事件。例如,在主线程中执行耗时的网络请求或大量计算时,会阻塞 Looper 的消息循环,使得输入事件无法被处理,超过 5 秒就会触发 ANR。
在广播中如何执行耗时操作
广播接收器(BroadcastReceiver)的生命周期非常短暂,onReceive () 方法的执行时间有限,若在其中直接执行耗时操作会导致 ANR。正确的做法是将耗时任务转移至其他组件处理,常见方案包括使用 IntentService、WorkManager 或启动 Service。
IntentService 是 Service 的子类,其内部通过 HandlerThread 处理消息,能自动处理异步任务并在任务完成后停止服务。使用时只需继承 IntentService 并实现 onHandleIntent () 方法,该方法在工作线程中执行,可安全处理耗时操作。示例如下:
public class DownloadIntentService extends IntentService {
public DownloadIntentService() {
super("DownloadIntentService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
// 执行下载等耗时操作
String url = intent.getStringExtra("url");
downloadFile(url);
}
}
在广播接收器中启动该服务:
Intent intent = new Intent(context, DownloadIntentService.class);
intent.putExtra("url", "http://example.com/file");
context.startService(intent);
对于 Android 5.0(API 21)以上版本,可使用 JobScheduler 实现周期性或条件触发的耗时任务。JobScheduler 基于系统调度,能根据网络状态、电量等条件优化任务执行时机,减少资源消耗。而 Android 8.0(API 26)推出的 WorkManager 则是更高级的封装,它结合了 JobScheduler、AlarmManager 等机制,支持任务调度、重试策略和周期性执行,且兼容低版本系统。
需要注意的是,广播接收器中获取 Context 时应使用 Application Context,避免因持有 Activity Context 导致内存泄漏。此外,对于有序广播,若在 onReceive () 中启动 Service,需注意在 Android 8.0 后,前台 Service 的启动受到限制,需明确声明通知渠道;对于后台服务,Android 10(API 29)后进一步限制了后台服务的使用,此时更推荐使用 WorkManager。
ANR 出现的条件,阻塞多久会触发 ANR
ANR 的触发源于主线程(UI 线程)在规定时间内无法完成特定任务,其核心条件包括以下几类场景,不同场景的超时阈值不同:
输入事件处理超时
当用户进行点击、滑动等输入操作时,系统会向应用发送输入事件,应用需在 5 秒内完成事件处理。若主线程因耗时操作阻塞,无法在 5 秒内响应输入事件,就会触发 ANR。例如,在主线程中执行复杂的数据库查询或文件操作,导致输入事件堆积无法处理。
Service 操作超时
- 前台 Service:调用 startService () 后,onStartCommand () 方法需在 20 秒内执行完毕;绑定服务时,onBind () 方法需在 20 秒内返回。
- 后台 Service:onStartCommand () 的超时时间为 200 秒,后台场景下系统对服务的响应时间要求更宽松,但过长的处理时间仍会导致 ANR。
广播接收器超时
- 前台广播:当广播接收器通过 registerReceiver () 动态注册,或在 Manifest 中声明为优先级别较高的广播时,onReceive () 方法需在 10 秒内完成。
- 后台广播:通过 Manifest 静态注册的普通广播,onReceive () 的超时时间为 60 秒。若在广播处理中执行网络请求、数据库操作等耗时任务,超过对应阈值就会触发 ANR。
ContentProvider 超时
当应用通过 ContentProvider 提供数据时,query ()、insert ()、update ()、delete () 等方法需在 20 秒内完成。若这些方法中存在耗时操作,如复杂的游标查询或大量数据写入,会导致 ContentProvider 超时。
系统通过 Watchdog 机制检测 ANR,该机制由系统守护进程监控,定期检查主线程的消息循环是否卡顿。当特定事件的处理时间超过阈值时,系统会收集相关堆栈信息,生成 ANR 日志(存储在 /data/anr/traces.txt),并向用户显示 ANR 对话框。此外,Binder 调用超时也可能触发 ANR,例如应用向系统服务发送请求后,超过特定时间未收到响应,也会被判定为无响应。
Android 中的布局类型、布局优化方法及常用标签
布局类型及特点
Android 提供多种布局容器,适用于不同的界面结构需求:
- LinearLayout:线性布局,子视图按水平或垂直方向排列,优点是结构简单,缺点是嵌套层级过多时性能较差。适用于简单的单行或单列布局。
- RelativeLayout:相对布局,子视图通过相对位置(如相对于父容器或其他视图)确定位置,可减少嵌套层级。但布局逻辑复杂时可读性较差,适用于中等复杂度的界面。
- ConstraintLayout:约束布局,Google 推荐的高性能布局,通过约束关系确定视图位置,支持平面化布局(减少嵌套),并提供可视化编辑器。适用于复杂界面,可替代多层嵌套的 LinearLayout 和 RelativeLayout。
- FrameLayout:帧布局,后添加的视图覆盖前一个视图,适用于层叠显示的场景,如图片叠加蒙层、Fragment 容器。
- TableLayout:表格布局,以行列形式排列子视图,每个单元格可放置一个视图,适用于表格类数据展示,但性能较低,不推荐大量使用。
- GridLayout:网格布局,比 TableLayout 更灵活,支持跨列、跨行等特性,可通过设置权重分配空间,适用于网格状布局。
布局优化方法
优化布局性能的核心是减少视图层级、避免冗余渲染:
- 减少嵌套层级:使用 ConstraintLayout 替代多层嵌套的 LinearLayout 或 RelativeLayout,例如用单层 ConstraintLayout 实现复杂布局,避免 LinearLayout 的垂直 + 水平嵌套。
- 使用 Merge 标签:在 include 引入布局时,用 Merge 作为根标签,消除多余的层级。例如,当引入的布局根节点是 LinearLayout,而父布局也是 LinearLayout 时,使用 Merge 可合并层级。
- ViewStub 延迟加载:对于不常用的布局模块,使用 ViewStub 标签声明,它在初始时不占用内存,仅在调用 inflate () 时才加载视图,可提高布局初始化速度。
- 移除不必要的背景:视图的背景会增加渲染开销,若无需背景,应设置为透明(android:background="@android:color/transparent")。
- 使用标签分组:通过 <androidx.constraintlayout.widget.ConstraintSet> 或<merge>标签预处理布局结构,减少运行时的计算量。
- 启用硬件加速:在 Activity 或 Application 中开启硬件加速(android:hardwareAccelerated="true"),将部分渲染工作转移到 GPU,提升流畅度。
常用布局标签
- 基础容器标签:LinearLayout、RelativeLayout、ConstraintLayout、FrameLayout、TableLayout、GridLayout。
- 优化相关标签:
<merge>
:合并布局层级,消除多余父容器。<ViewStub>
:延迟加载的轻量级视图,初始时不渲染。<include>
:引入外部布局,可重复使用布局模板。
- 常用子视图标签:TextView、Button、ImageView、EditText、RecyclerView、ListView、CheckBox、RadioButton 等。
- ConstraintLayout 特有标签:
<androidx.constraintlayout.widget.Guideline>
:辅助线,用于定义约束参考位置。<androidx.constraintlayout.widget.ChainStyle>
:链式布局,控制一组视图的排列方式。<androidx.constraintlayout.widget.Barrier>
:屏障,根据一组视图的边界创建约束参考。
布局优化需结合实际场景,例如在列表项布局中,应优先使用 LinearLayout 或 ConstraintLayout,避免 RelativeLayout 的复杂约束;对于需要动态加载的布局模块,合理使用 ViewStub 减少初始渲染耗时,从而提升应用启动速度和界面响应性能。
Android 中解决多线程冲突的方法
在 Android 开发中,多线程冲突通常源于多个线程同时访问共享资源,导致数据不一致或程序异常。解决这类问题需要结合 Java 并发编程机制与 Android 平台特性,常见的解决方案如下:
1. 同步代码块与关键字(synchronized)
synchronized
是 Java 内置的同步机制,可作用于方法或代码块,确保同一时刻仅有一个线程执行被修饰的内容。其原理是通过对象监视器(Monitor)实现锁的获取与释放,例如:
private final Object lock = new Object();
void updateData() {
synchronized (lock) {
// 访问共享资源的代码
}
}
特点:自动加锁和释放锁,使用简单,但可能存在锁竞争导致的性能开销,且无法中断锁等待。
2. Lock 接口与实现类(ReentrantLock)
java.util.concurrent.locks.Lock
是一个接口,提供更灵活的锁控制,如可中断锁、公平锁、尝试获取锁等。ReentrantLock
是其常用实现,支持重入特性(线程可多次获取同一把锁):
private final ReentrantLock lock = new ReentrantLock();
void process() {
lock.lock();
try {
// 操作共享资源
} finally {
lock.unlock(); // 确保锁释放
}
}
优势:可通过 tryLock()
避免死锁,通过 lockInterruptibly()
响应中断,适合复杂场景下的精细控制。
3. 原子类(AtomicXXX)
java.util.concurrent.atomic
包中的原子类(如 AtomicInteger
、AtomicReference
)利用硬件级别的原子操作(CAS,Compare-And-Swap)实现线程安全,无需加锁即可保证操作的原子性。例如:
private AtomicInteger count = new AtomicInteger(0);
void increment() {
count.getAndIncrement(); // 原子性自增
}
原理:CAS 操作包含三个参数(内存值、预期值、新值),仅当内存值等于预期值时才更新为新值,避免了锁的开销,适合高频轻量级操作。
4. 线程安全集合(ConcurrentHashMap 等)
Java 并发包提供了线程安全的集合类,如 ConcurrentHashMap
、CopyOnWriteArrayList
,内部通过锁分段、写时复制等机制实现并发安全。例如:
ConcurrentHashMap
在 JDK 1.8 中通过 CAS 和 synchronized 结合,将锁粒度细化到链表或红黑树的头节点;CopyOnWriteArrayList
每次修改时复制底层数组,读操作无锁,适合读多写少场景。
5. Android 特有的线程通信机制
Android 中的 Handler
、Looper
、MessageQueue
机制虽主要用于线程通信,但也可间接解决冲突。例如,通过 Handler
将操作发送到主线程或特定工作线程,避免多线程同时访问 UI 组件:
private Handler mainHandler = new Handler(Looper.getMainLooper());
void updateUIOnMainThread() {
mainHandler.post(() -> {
// 更新 UI 组件
});
}
6. 信号量(Semaphore)与 CountDownLatch
- Semaphore:控制同时访问资源的线程数量,例如限制线程池最大并发数:
private Semaphore semaphore = new Semaphore(3); // 最多3个线程同时访问 void accessResource() throws InterruptedException { semaphore.acquire(); try { // 访问资源 } finally { semaphore.release(); } }
- CountDownLatch:允许一个线程等待其他线程完成操作,常用于多线程任务的同步。
选择策略
- 轻量级操作优先使用原子类或 CAS;
- 复杂逻辑或需要公平性控制时选择
ReentrantLock
; - 集合操作直接使用并发包中的线程安全类;
- Android 中涉及 UI 更新时通过
Handler
切换线程。
Android 图片缓存与加载的实现方式
在 Android 开发中,图片加载与缓存是性能优化的关键环节,合理的实现可避免内存溢出、减少网络请求消耗。其核心思想是构建 “三级缓存” 体系,并结合图片压缩、按需加载等策略。
一、三级缓存架构
1. 内存缓存(Memory Cache)
- 作用:直接从内存获取图片,响应速度最快,适合频繁访问的图片。
- 实现方式:
- LruCache(Least Recently Used):基于最近最少使用原则淘汰旧数据,通过设定最大内存占用(如
Runtime.getRuntime().maxMemory() / 8
)控制缓存大小:int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); int cacheSize = maxMemory / 8; LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { return bitmap.getByteCount() / 1024; // 计算缓存项大小(KB) } };
- WeakReference:弱引用缓存曾被使用,但因容易被 GC 回收,目前已较少单独使用,多与 LruCache 结合。
- LruCache(Least Recently Used):基于最近最少使用原则淘汰旧数据,通过设定最大内存占用(如
2. 磁盘缓存(Disk Cache)
- 作用:内存缓存失效时,从磁盘读取图片,避免重复下载。
- 实现方式:
- DiskLruCache:Android 官方提供的磁盘缓存库,支持按文件大小、时间戳淘汰数据,适合存储缩略图或原图:
File cacheDir = new File(context.getCacheDir(), "image_cache"); DiskLruCache diskCache = DiskLruCache.open(cacheDir, 1, 1, 50 * 1024 * 1024); // 50MB 缓存空间
- FileOutputStream + 哈希算法:自定义缓存时,可通过 MD5 对图片 URL 加密生成文件名,便于管理和查找。
- DiskLruCache:Android 官方提供的磁盘缓存库,支持按文件大小、时间戳淘汰数据,适合存储缩略图或原图:
3. 网络加载(Network Load)
- 作用:当内存和磁盘均无缓存时,从网络获取图片。
- 优化点:
- 按需请求:根据 ImageView 尺寸加载对应分辨率的图片(如 WebP 格式或服务端缩略图);
- 断点续传:大图片下载时支持断点续传,避免网络波动导致重复请求;
- 缓存策略:设置 HTTP 响应头
Cache-Control
或Expires
,控制客户端缓存时长。
二、图片加载库的核心原理
1. 主流库对比
库 | 特点 | 适用场景 |
---|---|---|
Glide | 支持动图、WebP、内存缓存优化(Bitmap 复用),Android 兼容性强 | 通用场景,推荐首选 |
Picasso | 简洁易用,支持请求队列和优先级,适合小项目 | 轻量级图片加载 |
Fresco | 独立图片内存池,适合加载超大图片,性能开销较大 | 新闻、电商等大图场景 |
2. Glide 核心机制
- 三级缓存实现:内存缓存(LruCache + ActiveResources)、磁盘缓存(DiskLruCache)、网络加载;
- 图片解码优化:使用
BitmapRegionDecoder
按需解码大图局部,减少内存占用; - 生命周期绑定:通过
ActivityLifecycleCallbacks
监听页面生命周期,自动取消不可见页面的加载请求。
三、内存优化策略
- 图片压缩:根据 ImageView 尺寸计算采样率(
inSampleSize
),避免加载全尺寸图片:BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, options); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inJustDecodeBounds = false; Bitmap scaledBitmap = BitmapFactory.decodeFile(path, options);
- 图片格式选择:WebP 格式相比 JPG/PNG 有更小的文件体积,且支持透明通道;
- 内存泄漏防范:避免在 Activity/Fragment 中持有长生命周期的图片引用,结合
onDestroy
清理缓存。
四、实战场景优化
- 列表场景:滑动时暂停加载,停止滑动后恢复,通过
RecyclerView.OnScrollListener
控制; - 圆角图片:避免在适配器中频繁创建圆角 Bitmap,可使用
RoundedBitmapDrawable
或自定义Transformation
缓存处理结果; - 夜间模式:根据主题切换加载不同色调的图片,可通过缓存键(Key)区分主题版本。
Collection 和 Collections 的使用区别
在 Java 编程中,Collection
和 Collections
是两个极易混淆的概念,前者是集合框架的核心接口,后者是操作集合的工具类,二者在功能、用途和实现上存在本质差异。
一、Collection:集合框架的基础接口
1. 接口定义与层级
java.util.Collection
是 Java 集合框架的根接口之一,定义了集合(如列表、集合、队列)的通用操作方法,例如:
add(E e)
:添加元素;remove(Object o)
:删除元素;contains(Object o)
:判断是否包含元素;size()
:获取元素数量;iterator()
:返回迭代器。
2. 子接口与实现类
- List:有序、可重复的集合,实现类包括
ArrayList
、LinkedList
、Vector
; - Set:无序、不可重复的集合,实现类包括
HashSet
、TreeSet
、LinkedHashSet
; - Queue:队列接口,实现类包括
LinkedList
、PriorityQueue
。
3. 核心特性
- 定义了集合的基本操作规范,但不直接实现具体数据结构;
- 所有实现类需实现接口中的抽象方法,例如
ArrayList
基于数组实现,LinkedList
基于链表实现。
二、Collections:集合工具类
java.util.Collections
是一个包含静态方法的工具类,用于操作集合或创建特殊集合,其方法大致可分为以下几类:
1. 排序与查找
- 排序:
sort(List<T> list)
对列表进行自然排序,sort(List<T> list, Comparator<? super T> c)
自定义排序规则:List<String> names = new ArrayList<>(); names.add("Bob"); names.add("Alice"); Collections.sort(names); // 按字母顺序排序
- 查找与替换:
binarySearch(List<? extends Comparable<? super T>> list, T key)
二分查找元素位置,replaceAll(List<T> list, T oldVal, T newVal)
替换所有指定元素。
2. 线程安全包装
通过 synchronizedList
、synchronizedSet
等方法将非线程安全的集合转为线程安全版本:
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
// 访问时需手动同步
synchronized (safeList) {
for (String item : safeList) {
// 操作元素
}
}
3. 不可变集合与空集合
- 不可变集合:
unmodifiableList
、unmodifiableSet
等方法创建不可修改的集合,防止外部篡改:List<String> original = new ArrayList<>(); List<String> immutable = Collections.unmodifiableList(original); // immutable.add("element") 会抛出 UnsupportedOperationException
- 空集合:
emptyList
、emptySet
返回类型安全的空集合,避免null
指针异常。
4. 集合操作辅助方法
reverse(List<?> list)
:反转列表元素顺序;shuffle(List<?> list)
:随机打乱列表元素;max/ min(Collection<? extends T> coll)
:获取集合中的最大 / 最小值。
三、核心区别对比
维度 | Collection | Collections |
---|---|---|
本质 | 接口(interface) | 工具类(class,且构造方法私有) |
功能 | 定义集合的基本操作规范 | 提供操作集合的静态工具方法 |
是否可实例化 | 不可(接口无实现) | 不可(构造方法私有,仅含静态方法) |
典型用法 | 作为参数类型(如 void process(Collection<T> data) ) | 对集合进行排序、同步、不可变包装等 |
四、开发中的实际应用
- 当需要定义集合类型时,使用
Collection
接口作为参数或返回值,体现面向接口编程:// 良好实践:方法参数使用接口而非实现类 void processData(Collection<String> data) { // 处理集合数据 }
- 当需要对集合进行操作时,使用
Collections
工具类,例如对列表排序、创建线程安全集合:// 对学生列表按成绩排序 Collections.sort(students, (s1, s2) -> s2.getScore() - s1.getScore());
HashMap 的底层结构实现
HashMap 是 Java 中常用的键值对存储容器,其底层实现结合了数组、链表和红黑树(JDK 1.8 及之后),通过哈希算法实现高效的查找与插入。理解其底层结构对性能优化和问题排查至关重要。
一、JDK 1.7 与 JDK 1.8 的结构差异
1. JDK 1.7:数组 + 链表
- 核心结构:
Entry<K, V>
数组,每个元素是一个链表的头节点,链表存储哈希冲突的键值对; - 哈希冲突处理:采用头插法(新元素插入链表头部),冲突严重时链表变长,查找时间复杂度退化为 O (n)。
2. JDK 1.8:数组 + 链表 + 红黑树
- 核心结构:
Node<K, V>
数组,当链表长度超过阈值(默认 8)且数组容量超过 64 时,链表转换为红黑树,查找时间复杂度优化为 O (log n); - 哈希冲突处理:改用尾插法(避免头插法在多线程环境下的环形链表风险),红黑树进一步提升极端情况下的性能。
二、核心属性与参数
transient Node<K,V>[] table; // 存储数据的数组,又称“桶”(bucket)
transient int size; // 键值对数量
int threshold; // 扩容阈值,等于 capacity * loadFactor
final float loadFactor; // 负载因子,默认 0.75f,控制数组扩容时机
- 负载因子(loadFactor):决定数组利用率与哈希冲突的平衡:
- 取值越大,数组扩容频率越低,但哈希冲突概率越高;
- 取值越小,数组扩容更频繁,空间利用率低但冲突概率降低。
- 扩容阈值(threshold):当 size 超过阈值时,数组容量翻倍(如从 16 扩容到 32),并重新哈希所有元素。
三、关键操作的底层实现
1. 哈希值计算(hash (key) 方法)
JDK 1.8 中,哈希值计算如下:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
- 通过
key.hashCode()
获取哈希码,再与右移 16 位的自身异或,使高位特征也参与低位计算,减少哈希冲突。
2. 插入操作(put (K key, V value))
步骤如下:
- 计算 key 的哈希值,确定数组下标
i = (n - 1) & hash
(n 为数组长度,必须是 2 的幂,保证(n-1)&hash
等价于hash % n
,且运算更快); - 若下标 i 处为空,直接插入新节点;
- 若下标 i 处已有节点,判断 key 是否相等:
- 相等则覆盖 value;
- 不等则遍历链表(或红黑树),若链表长度超过阈值(8)则转为红黑树,否则插入链表尾部。
3. 查找操作(get (Object key))
- 计算 key 的哈希值,确定数组下标;
- 若下标处无元素,返回 null;
- 若下标处元素的 key 匹配,直接返回;
- 否则遍历链表(或红黑树)查找匹配的 key。
四、扩容机制(resize () 方法)
当 HashMap 中的元素数量超过扩容阈值时,会触发扩容:
- 创建新数组,容量为原数组的 2 倍;
- 遍历原数组中的每个链表,重新计算每个元素在新数组中的下标(由于容量是 2 的幂,
(n-1)&hash
结果仅取决于 hash 值新增的高位,可优化为if ((e.hash & oldCap) == 0) 位置不变,否则位置为原下标 + oldCap
); - JDK 1.7 中,链表在扩容时按头插法重新排列,多线程环境下可能形成环形链表,导致死循环;
- JDK 1.8 改用尾插法,避免了环形链表风险,但仍不支持并发扩容(需通过
ConcurrentHashMap
实现)。
五、性能与缺陷
1. 优势
- 平均查找、插入时间复杂度为 O (1),适用于高频读写场景;
- JDK 1.8 引入红黑树,优化了极端情况下(哈希冲突严重)的性能。
2. 不足
- 非线程安全,多线程并发操作可能导致数据不一致或异常;
- 扩容时需要重建哈希表,耗时与元素数量成正比,大集合扩容可能引发卡顿;
- key 为 null 时,哈希值固定为 0,所有 null key 会存入数组下标 0 的链表中。
六、典型应用场景
- 缓存场景:利用 HashMap 的快速查找特性实现本地缓存;
- 统计场景:如统计字符串中各字符出现的次数;
- 映射关系:如将用户 ID 映射到用户信息对象。
ConcurrentHashMap 的底层结构实现
ConcurrentHashMap 是 Java 并发包中提供的线程安全哈希表,相比 HashMap,它通过更精细的锁策略和数据结构优化,在高并发场景下实现了高效的读写操作。其底层实现随 JDK 版本迭代发生了显著变化。
一、JDK 1.7:分段锁(Segment + 数组 + 链表)
1. 核心结构
- Segment 数组:将哈希表分为 16 个 Segment(默认),每个 Segment 是一个独立的锁,实现 “锁分段” 机制;
- 每个 Segment 内部:包含一个
HashEntry<K, V>
数组,结构类似 JDK 1.7 的 HashMap,通过链表处理哈希冲突。
2. 关键属性
final Segment<K,V>[] segments; // Segment 数组
transient volatile int size; // 元素总数
3. 并发控制
- 读操作:大部分读操作无需加锁(如通过
volatile
修饰的引用直接获取值),仅在获取迭代器等场景下需要; - 写操作(如 put):先定位到对应的 Segment,对该 Segment 加锁,再执行插入操作,锁粒度为 Segment,而非整个哈希表。
4. 优势与不足
- 优势:锁分段机制将并发冲突概率降低 16 倍(默认),相比
Hashtable
的全表锁性能显著提升; - 不足:Segment 数组大小固定(初始化后不可变),并发度受限于 Segment 数量,且空间利用率较低。
二、JDK 1.8:CAS + synchronized + 数组 + 链表 + 红黑树
1. 结构优化
- 摒弃 Segment 数组,采用与 HashMap 1.8 类似的
Node<K, V>
数组 + 链表 + 红黑树结构; - 锁粒度进一步细化到链表或红黑树的头节点,使用
synchronized
替代 Segment 锁,结合 CAS 操作实现高效并发控制。
2. 核心属性
transient volatile Node<K,V>[] table; // 存储数据的数组
private transient volatile int sizeCtl; // 控制数组初始化和扩容的状态值
3. 关键操作实现
(1)初始化(initTable ())
- 通过 CAS 操作竞争初始化数组的权限,避免多线程重复初始化:
if ((tab = table) == null || tab.length == 0) { int h = threadLocalRandomProbe.get(); int rs = 0; // 通过 CAS 尝试设置 sizeCtl 为 -1(表示正在初始化) if (U.compareAndSwapInt(this, SIZECTL, 0, -1)) { try { // 初始化数组 } finally { sizeCtl = n; // n 为数组容量 } } }
(2)插入操作(put (K key, V value))
步骤如下:
- 计算 key 的哈希值(高位参与运算,减少冲突);
- 若数组未初始化,先初始化;
- 定位到数组下标,若该位置为空,通过 CAS 插入新节点;
- 若该位置已有节点,对节点头节点加
synchronized
锁,判断是链表还是红黑树,执行插入操作; - 插入后检查链表长度,超过阈值(8)且数组容量超过 64 时,转换为红黑树;
- 通过
addCount()
方法更新元素数量,可能触发扩容。
(3)扩容操作(transfer ())
- 支持并发扩容,将原数组元素迁移到新数组时,通过
ForwardingNode
标记已处理的桶,其他线程遇到该标记会协助扩容; - 扩容过程中,读操作可直接访问原数组或新数组,写操作会被引导至新数组。
(4)读操作(get (Object key))
- 无需加锁,直接通过哈希值定位节点,若遇到链表或红黑树,遍历查找;
- 由于节点的 value 用
volatile
修饰,保证可见性。
三、与 HashMap 的核心区别
维度 | HashMap | ConcurrentHashMap |
---|---|---|
线程安全性 | 非线程安全 | 线程安全,支持高并发读写 |
锁策略 | 无锁 | JDK 1.7 分段锁,JDK 1.8 细粒度 synchronized + CAS |
null key/value | 支持 key 和 value 为 null | 不支持 key 为 null(会抛出 NullPointerException),value 可为 null |
扩容机制 | 单线程扩容 | 支持并发扩容,通过 ForwardingNode 协作迁移 |
迭代器 | fail-fast(遍历时修改会抛异常) | 弱一致性迭代器,遍历时允许其他线程修改 |
四、性能优化点
- 锁粒度细化:JDK 1.8 中锁的粒度从 Segment 缩小到节点,减少锁竞争;
- 无锁读操作:读操作无需加锁,通过
volatile
和 CAS 保证数据可见性与原子性; - 并发扩容:扩容时允许多线程协作,避免单线程长时间阻塞;
- 红黑树优化:哈希冲突严重时转为红黑树,提升查找效率。
五、适用场景
- 高并发场景下的缓存容器,如数据库连接池、线程池的元数据存储;
- 多线程环境下的计数器,如统计在线用户数、请求量;
- 需要频繁读写的映射关系,如分布式系统中的路由表。
六、注意事项
- 避免将 ConcurrentHashMap 用于需要强一致性的场景(如金融交易),其迭代器和统计方法(如 size ())仅提供弱一致性;
- JDK 1.8 中,当 key 为 null 时会抛出异常,需提前处理;
- 虽然支持高并发,但仍需根据实际场景评估容量和负载因子,避免频繁扩容。
HashMap 与 Hashtable 的区别
HashMap 和 Hashtable 均为 Java 中存储键值对的集合类,但在设计理念、线程安全性和功能特性上存在显著差异,这些差异源于它们的历史背景和应用场景。
一、线程安全性:核心差异
- HashMap:非线程安全,多线程环境下并发操作可能导致数据不一致(如扩容时的环形链表问题)。其方法未使用同步关键字修饰,适用于单线程或外部加锁的场景。
- Hashtable:线程安全,其主要方法(如
put
、get
)均被synchronized
修饰,确保同一时刻仅有一个线程访问集合。但这种全表锁的方式在高并发场景下性能开销较大。
二、对 null 键值的支持
- HashMap:允许
key
和value
为null
,但null key
只能有一个(因为哈希值固定为 0)。例如:HashMap<String, Integer> map = new HashMap<>(); map.put(null, 1); // 合法 map.put("key", null); // 合法
- Hashtable:不允许
key
或value
为null
,若尝试插入null
,会抛出NullPointerException
。这是因为其put
方法在插入前会检查键值是否为null
:Hashtable<String, Integer> table = new Hashtable<>(); table.put(null, 1); // 抛出 NullPointerException
三、继承关系与接口实现
- HashMap:继承自
AbstractMap
,实现了Map
、Cloneable
和Serializable
接口,支持克隆和序列化。 - Hashtable:继承自
Dictionary
(Java 早期的集合接口),实现了Map
、Cloneable
和Serializable
接口,但Dictionary
已被Map
替代,属于遗留类。
四、初始化与扩容机制
维度 | HashMap | Hashtable |
---|---|---|
初始容量 | 默认 16 | 默认 11 |
负载因子 | 0.75(可自定义) | 0.75(可自定义) |
扩容策略 | 容量达到 threshold (容量 × 负载因子)时,新容量为原容量的 2 倍 | 容量达到 threshold 时,新容量为原容量的 2 倍 + 1 |
扩容效率 | 采用位运算 (n - 1) & hash 计算下标,效率更高 | 直接使用 hash % size ,性能略低 |
五、性能与应用场景
- HashMap:性能更优,尤其在单线程环境下,适合大多数日常开发场景(如缓存、映射关系)。
- Hashtable:由于线程安全的特性,仅在需要线程安全且无法外部加锁时使用(如早期 Java 代码),现代开发中更推荐使用
ConcurrentHashMap
。
六、迭代器差异
- HashMap 的迭代器支持快速失败(fail-fast)机制,遍历时若集合被修改会抛出
ConcurrentModificationException
; - Hashtable 的枚举器(Enumerator)不支持快速失败,遍历时允许集合被修改,但可能导致不一致的遍历结果。
ArrayList 与 LinkedList 的区别
ArrayList 和 LinkedList 均为 Java 中 List
接口的实现类,但底层数据结构不同,导致它们在性能特性和适用场景上存在明显差异。
一、底层数据结构
- ArrayList:基于动态数组实现,元素连续存储在内存中,通过索引直接访问。数组容量不足时会创建新数组并复制元素,扩容成本较高。
- LinkedList:基于双向链表实现,每个元素包含前驱和后继指针,无需连续内存空间,节点插入和删除仅需修改指针指向。
二、核心操作性能对比
操作类型 | ArrayList | LinkedList |
---|---|---|
随机访问 | 时间复杂度 O (1),直接通过索引获取元素 | 时间复杂度 O (n),需遍历链表查找 |
头部插入 / 删除 | 时间复杂度 O (n),需移动后续元素 | 时间复杂度 O (1),仅修改指针 |
中间插入 / 删除 | 时间复杂度 O (n),需移动相邻元素 | 时间复杂度 O (1)(已知节点位置) |
尾部插入 | 平均时间复杂度 O (1)(无需扩容时) | 时间复杂度 O (1) |
三、内存占用与扩容机制
- ArrayList:
- 内存紧凑,元素连续存储,每个元素仅存储值本身;
- 扩容时新容量为原容量的 1.5 倍(通过
Arrays.copyOf
实现),例如初始容量 10,扩容后为 15。
- LinkedList:
- 每个节点需存储前驱指针、后继指针和值,内存占用更高;
- 无固定扩容机制,节点数量可动态增减,无需预分配空间。
四、线程安全性与特殊方法
- 线程安全性:两者均非线程安全,多线程环境下需外部同步(如使用
Collections.synchronizedList
包装)。 - 特殊方法:
- LinkedList 额外提供
addFirst
、addLast
、getFirst
、getLast
等方法,适合作为队列或栈使用; - ArrayList 支持通过
ensureCapacity
预分配容量,减少扩容次数。
- LinkedList 额外提供
五、适用场景
- ArrayList:
- 频繁随机访问的场景(如数组下标查找元素);
- 数据量可预估,能通过
ensureCapacity
优化性能; - 元素添加删除主要发生在尾部(如日志记录)。
- LinkedList:
- 频繁在头部或中间插入删除的场景(如链表结构的缓存淘汰);
- 数据量不确定,需要动态扩展的场景;
- 作为队列(
Queue
接口)或双端队列(Deque
接口)使用时(LinkedList 实现了这两个接口)。
六、性能优化实践
- 若已知 ArrayList 数据量,使用
new ArrayList<>(initialCapacity)
预分配容量,避免多次扩容; - 对 LinkedList 进行遍历时,使用迭代器(
Iterator
)而非普通 for 循环,减少指针跳转次数; - 在多线程场景下,优先使用
CopyOnWriteArrayList
替代同步包装的 ArrayList/LinkedList,提升读性能。
线程安全的队列有哪些
Java 并发包(java.util.concurrent
)提供了多种线程安全的队列实现,根据功能特性可分为阻塞队列和非阻塞队列,适用于不同的并发场景。
一、阻塞队列(BlockingQueue)
阻塞队列支持线程阻塞等待,当队列为空时获取元素的线程会阻塞,当队列满时插入元素的线程会阻塞,常用于生产者 - 消费者模式。
1. ArrayBlockingQueue
- 特点:有界队列,基于数组实现,使用
ReentrantLock
实现线程安全,插入和获取操作均为阻塞操作。 - 构造参数:容量和是否公平锁(默认非公平)。
- 适用场景:固定容量的任务缓冲,如线程池的工作队列。
2. LinkedBlockingQueue
- 特点:可选有界(指定容量)或无界(默认 Integer.MAX_VALUE),基于链表实现,插入和获取操作分别使用不同的锁,减少竞争。
- 适用场景:生产者速度远低于消费者的场景,或不确定任务量的缓冲。
3. PriorityBlockingQueue
- 特点:无界优先队列,元素按优先级排序(实现
Comparable
或自定义Comparator
),获取操作返回优先级最高的元素。 - 注意事项:插入操作不阻塞(队列理论上无界),但获取操作会在队列为空时阻塞。
- 适用场景:任务优先级调度,如线程池处理优先级任务。
4. DelayQueue
- 特点:无界队列,元素需实现
Delayed
接口,只有到期后才能被获取。 - 适用场景:延迟任务处理,如定时任务、超时订单取消。
5. SynchronousQueue
- 特点:容量为 0,每个插入操作必须等待一个获取操作,反之亦然,不存储元素。
- 适用场景:线程间直接传递数据,如
ExecutorService
的newCachedThreadPool
底层使用该队列。
二、非阻塞队列
非阻塞队列使用 CAS(Compare-And-Swap)操作实现线程安全,不会阻塞线程,但可能需要重试操作。
1. ConcurrentLinkedQueue
- 特点:基于链表的无界非阻塞队列,插入和获取操作均通过 CAS 保证原子性,适用于高并发场景下的高频读写。
- 适用场景:日志收集、事件通知等无需阻塞等待的场景。
2. ConcurrentLinkedDeque
- 特点:双端队列,支持从头部和尾部插入 / 获取元素,同样基于 CAS 实现非阻塞操作。
- 适用场景:需要双端操作的并发场景,如任务调度中的双向队列。
三、其他线程安全队列
1. LinkedTransferQueue
- 特点:继承自
BlockingQueue
,支持transfer
方法(插入元素并等待消费者获取),性能优于SynchronousQueue
。 - 适用场景:生产者 - 消费者之间需要快速传递数据的场景。
2. LinkedBlockingDeque
- 特点:双端阻塞队列,支持从两端插入 / 获取元素,适用于需要双向操作的阻塞场景(如工作窃取算法)。
四、队列选择策略
- 需要阻塞等待:优先使用
ArrayBlockingQueue
(有界)或LinkedBlockingQueue
(无界),根据任务量选择队列类型; - 高并发非阻塞:使用
ConcurrentLinkedQueue
,避免锁竞争; - 优先级任务:使用
PriorityBlockingQueue
; - 延迟任务:使用
DelayQueue
; - 线程间直接通信:使用
SynchronousQueue
或LinkedTransferQueue
。
StringBuffer 和 StringBuilder 的区别
StringBuffer 和 StringBuilder 均为 Java 中用于处理可变字符串的类,它们共享相同的底层实现,但在线程安全性和性能表现上存在显著差异,适用于不同的编程场景。
一、线程安全性:核心差异
- StringBuffer:线程安全,其所有公共方法(如
append
、insert
)均被synchronized
修饰,确保多线程环境下操作的原子性。例如:StringBuffer buffer = new StringBuffer(); buffer.append("hello"); // 同步方法,线程安全
- StringBuilder:非线程安全,方法未加同步关键字,单线程环境下性能更优。例如:
StringBuilder builder = new StringBuilder(); builder.append("world"); // 非同步方法,单线程安全
二、底层实现与性能
- 共同底层:两者均继承自
AbstractStringBuilder
,使用字符数组char[] value
存储字符串,默认容量为 16,超过容量时通过Arrays.copyOf
扩容(新容量为原容量的 2 倍 + 2)。 - 性能对比:
- StringBuilder 由于无需同步开销,在单线程环境下比 StringBuffer 快 30%~50%;
- StringBuffer 的同步机制在多线程环境下能保证数据一致性,但会带来锁竞争开销。
三、适用场景
- StringBuffer:
- 多线程环境下的字符串拼接(如网络请求参数拼接);
- 需要线程安全的公共组件(如框架中的字符串处理工具)。
- StringBuilder:
- 单线程环境下的所有字符串操作(如循环内的字符串拼接);
- 临时字符串处理(如日志输出、SQL 语句拼接)。
四、与 String 的对比
维度 | String | StringBuffer | StringBuilder |
---|---|---|---|
不可变性 | 不可变,每次操作生成新对象 | 可变,直接修改字符数组 | 可变,直接修改字符数组 |
线程安全 | 无 | 安全 | 不安全 |
性能 | 低(频繁生成新对象) | 中(同步开销) | 高(无同步开销) |
适用场景 | 常量字符串、不可变场景 | 多线程可变字符串 | 单线程可变字符串 |
五、性能优化实践
- 在循环中拼接字符串时,避免使用
+
操作符(会生成多个String
对象),应使用StringBuilder
:// 低效做法 String result = ""; for (int i = 0; i < 1000; i++) { result += i; // 每次拼接生成新 String 对象 } // 高效做法 StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) { sb.append(i); // 直接修改字符数组,避免对象创建 }
- 若已知字符串大致长度,使用
StringBuilder(int capacity)
预分配容量,减少扩容次数; - 在多线程场景下,若无法避免使用
StringBuilder
,可通过synchronized
手动加锁:StringBuilder builder = new StringBuilder(); synchronized (builder) { builder.append("data"); }
单例模式的具体实现
单例模式是一种创建型设计模式,确保类在全局范围内仅有一个实例,并提供统一的访问入口。Java 中实现单例模式需解决线程安全、延迟初始化、序列化安全等问题,常见实现方式如下。
一、饿汉式单例(静态常量初始化)
核心思想:类加载时直接初始化实例,确保线程安全。
public class HungrySingleton {
// 静态常量直接初始化实例
private static final HungrySingleton instance = new HungrySingleton();
// 私有构造函数,防止外部实例化
private HungrySingleton() {}
// 公共访问方法
public static HungrySingleton getInstance() {
return instance;
}
}
特点:
- 线程安全(类加载由 JVM 保证线程安全);
- 简单直接,但无论是否使用都会初始化实例,可能浪费资源。
二、懒汉式单例(线程不安全版本)
核心思想:延迟初始化,首次使用时创建实例。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
// 未同步的获取方法,线程不安全
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton(); // 可能被多个线程同时创建
}
return instance;
}
}
风险:多线程环境下可能创建多个实例,需配合同步机制改进。
三、懒汉式单例(同步方法版本)
核心思想:通过 synchronized
保证线程安全。
public class SynchronizedSingleton {
private static SynchronizedSingleton instance;
private SynchronizedSingleton() {}
// 同步方法,线程安全但性能开销大
public static synchronized SynchronizedSingleton getInstance() {
if (instance == null) {
instance = new SynchronizedSingleton();
}
return instance;
}
}
缺点:每次获取实例都需加锁,并发场景下性能较低。
四、双重检查锁定(DCL)单例
核心思想:通过双重 null 检查和 volatile
关键字优化同步性能。
public class DoubleCheckSingleton {
// 必须使用 volatile 防止指令重排序
private static volatile DoubleCheckSingleton instance;
private DoubleCheckSingleton() {}
public static DoubleCheckSingleton getInstance() {
if (instance == null) { // 第一次检查,避免无意义加锁
synchronized (DoubleCheckSingleton.class) {
if (instance == null) { // 第二次检查,确保唯一实例
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
关键细节:
volatile
禁止指令重排序,确保 instance 初始化的可见性;- 双重检查减少加锁次数,兼顾线程安全和性能。
五、静态内部类单例
核心思想:利用类加载机制实现延迟初始化和线程安全。
public class InnerClassSingleton {
private InnerClassSingleton() {}
// 静态内部类在外部类被访问时才加载
private static class SingletonHolder {
private static final InnerClassSingleton INSTANCE = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
原理:
- JVM 保证类加载过程的线程安全,且静态内部类仅在首次被访问时加载;
- 实现了延迟初始化(lazy loading)和线程安全的完美结合。
六、枚举单例
核心思想:利用枚举的特性实现单例,简洁且安全。
public enum EnumSingleton {
INSTANCE;
// 可添加自定义方法
public void doSomething() {
// 业务逻辑
}
}
优势:
- 自动支持序列化和反序列化(枚举实例由 JVM 保证唯一);
- 防止反射攻击(枚举构造函数不能被反射调用);
- 代码极简,是官方推荐的单例实现方式。
七、各实现方式对比
实现方式 | 线程安全 | 延迟初始化 | 抗反射攻击 | 抗序列化攻击 | 推荐场景 |
---|---|---|---|---|---|
饿汉式 | 是 | 否 | 是 | 是 | 实例占用资源少,需提前初始化 |
懒汉式(同步) | 是 | 是 | 是 | 否 | 简单场景,性能要求不高 |
DCL | 是 | 是 | 是 | 否 | 高性能并发场景 |
静态内部类 | 是 | 是 | 是 | 否 | 通用场景,推荐使用 |
枚举 | 是 | 否 | 是 | 是 | 需支持序列化,推荐使用 |
八、单例模式的潜在问题
- 内存泄漏:若单例持有长生命周期对象(如 Activity 上下文),可能导致上下文泄漏,应使用 Application Context;
- 并发问题:非线程安全的实现(如早期懒汉式)在多线程环境下可能创建多个实例;
- 序列化安全:普通单例需实现
readResolve()
方法防止反序列化时创建新实例,枚举单例自动解决此问题。
双检锁的具体含义,以及两次检查的意义分别是什么
双检锁(Double-Checked Locking)是单例模式的一种实现技巧,其核心目的是在保证线程安全的同时提升性能。具体实现中,会通过两次判断实例是否为空(第一次不锁,第二次加锁后再判断),避免频繁加锁带来的开销。
双检锁的典型代码结构如下:
public class Singleton {
private volatile static Singleton instance; // 声明volatile防止指令重排序
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:未加锁时判断,减少锁竞争
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查:加锁后再次判断,避免多线程创建多个实例
instance = new Singleton(); // 这里可能发生指令重排序
}
}
}
return instance;
}
}
两次检查的意义:
- 第一次检查(instance == null):在未获取锁之前先判断实例是否存在。若实例已存在,直接返回,避免进入同步代码块,减少加锁和解锁的开销,提升性能。这在高并发场景下尤为重要,因为锁操作是昂贵的系统调用。
- 第二次检查(同步块内的 instance == null):确保在多线程环境下只有一个线程能创建实例。例如,当两个线程同时通过第一次检查并进入同步块时,其中一个线程创建实例后,另一个线程再次检查会发现实例已存在,从而避免重复创建,保证单例的唯一性。
此外,volatile
关键字在此处必不可少。因为instance = new Singleton()
并非原子操作,可能被 JVM 优化为以下步骤:
- 分配内存空间;
- 初始化对象;
- 将内存地址赋值给 instance 引用。
若没有volatile
,步骤 2 和 3 可能发生重排序(先赋值再初始化)。此时,若一个线程获取到未完全初始化的 instance,访问时可能引发异常。volatile
禁止指令重排序,确保实例初始化完成后才被其他线程访问。
对 Java 多线程的理解
Java 多线程是指在一个程序中同时运行多个执行流,每个线程独立执行任务,从而提升程序的并发处理能力。理解多线程需从以下几个维度切入:
线程与进程的区别
- 进程:是操作系统分配资源的基本单位,拥有独立的内存空间和系统资源(如文件句柄、CPU 时间片)。
- 线程:是进程内的执行单元,共享进程的内存空间和资源,切换开销远小于进程。
线程的生命周期
Java 线程通过Thread
类管理,其状态包括:
- 新建(New):创建
Thread
对象但未调用start()
方法。 - 就绪(Runnable):调用
start()
后进入就绪队列,等待 CPU 调度。 - 运行(Running):获取 CPU 时间片执行代码。
- 阻塞(Blocked):因等待锁、IO 操作等暂停执行,不占用 CPU 资源。
- 死亡(Terminated):线程执行完毕或异常终止。
并发与并行
- 并发(Concurrency):多个线程在同一时间段内交替执行,宏观上看似同时运行(如单核 CPU)。
- 并行(Parallelism):多个线程在同一时刻同时执行,需多核 CPU 支持。
多线程的优势与挑战
- 优势:
- 提升用户体验(如 Android 中 UI 线程与工作线程分离);
- 充分利用多核 CPU 资源;
- 异步处理耗时任务(如网络请求、文件读写)。
- 挑战:
- 线程安全问题(共享资源竞争导致数据不一致);
- 死锁风险(多个线程互相等待锁);
- 上下文切换开销(线程数量过多时性能下降)。
Java 线程的核心 API
Thread
类:封装线程操作,如start()
、join()
、interrupt()
;Runnable
接口:定义线程执行逻辑,推荐使用(避免继承限制);ExecutorService
:线程池框架,管理线程生命周期,避免频繁创建线程;Concurrent包
:提供线程安全的集合(如ConcurrentHashMap
)、同步工具(如CountDownLatch
)。
线程的实现方式有哪些
Java 中实现线程主要有以下四种方式,每种方式的适用场景和特性各不相同:
1. 继承 Thread 类
通过子类化Thread
类,重写run()
方法定义线程逻辑:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running");
}
}
// 使用方式
MyThread thread = new MyThread();
thread.start();
特点:
- 简单直接,可直接操作线程对象;
- 缺点是 Java 单继承限制,无法同时继承其他类。
2. 实现 Runnable 接口
定义实现Runnable
接口的类,将线程逻辑放入run()
方法:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable is running");
}
}
// 使用方式
Thread thread = new Thread(new MyRunnable());
thread.start();
特点:
- 避免单继承限制,更符合面向对象设计;
- 多个
Runnable
可共享同一资源(如计数器),适合资源共享场景。
3. 实现 Callable 接口(Java 5+)
Callable
接口支持泛型返回值和异常抛出,需配合FutureTask
使用:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 可抛出异常,返回计算结果
return "Callable result";
}
}
// 使用方式
Callable<String> callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
// 获取结果
try {
String result = futureTask.get(); // 阻塞等待结果
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
特点:
- 支持返回值和异常处理,功能更强大;
get()
方法会阻塞线程,需注意性能影响。
4. 使用 ExecutorService 线程池(推荐)
通过ExecutorService
框架管理线程,避免手动创建线程的开销:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
// 创建线程池(固定大小为5)
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交Runnable任务
executor.submit(new Runnable() {
@Override
public void run() {
System.out.println("Task from thread pool");
}
});
// 提交Callable任务
executor.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
// 执行任务
return null;
}
});
// 关闭线程池
executor.shutdown();
特点:
- 复用线程,减少创建和销毁开销;
- 控制线程数量,避免 OOM(如
FixedThreadPool
、CachedThreadPool
); - 提供任务调度、结果管理等功能,是实际开发中的首选方式。
线程之间的通信方式
线程通信是多线程编程的核心问题,用于协调多个线程的执行顺序或共享数据。Java 和 Android 中提供了多种通信机制,适用于不同场景:
1. 共享内存(变量 + 同步机制)
通过共享变量传递信息,配合synchronized
、Lock
等同步工具保证线程安全:
private int count = 0;
private final Lock lock = new ReentrantLock();
// 线程A修改数据
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
// 线程B读取数据
lock.lock();
try {
int value = count;
} finally {
lock.unlock();
}
适用场景:简单数据交换,需注意原子性和可见性(如使用volatile
修饰变量)。
2. wait/notify 机制(Object 类方法)
通过wait()
让线程等待,notify()
/notifyAll()
唤醒等待线程,需在synchronized
块中使用:
private final Object lock = new Object();
// 线程A等待数据
synchronized (lock) {
while (dataNotReady) {
lock.wait(); // 释放锁并进入等待队列
}
processData();
}
// 线程B准备好数据后唤醒
synchronized (lock) {
prepareData();
dataNotReady = false;
lock.notify(); // 唤醒一个等待线程
}
特点:底层基于 JVM 监视器(Monitor)实现,是最基础的线程通信方式,但需手动处理等待条件和唤醒逻辑。
3. CountDownLatch(Java 并发包)
允许一个线程等待其他线程完成任务后再执行,通过计数器控制:
import java.util.concurrent.CountDownLatch;
CountDownLatch latch = new CountDownLatch(3); // 计数器初始化为3
// 线程1-3完成任务后计数减1
new Thread(() -> {
doTask();
latch.countDown();
}).start();
// 主线程等待计数器归零
latch.await(); // 阻塞直到计数器为0
System.out.println("All tasks completed");
适用场景:一次性等待多个线程完成,如初始化阶段等待资源加载。
4. CyclicBarrier(Java 并发包)
多个线程互相等待,直到所有线程到达屏障点后再一起执行:
import java.util.concurrent.CyclicBarrier;
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("All threads arrived");
});
// 三个线程各自执行到barrier.await()时等待
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
doTask();
barrier.await(); // 等待其他线程
continueTask();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
特点:计数器可重置(Cyclic),适合重复使用的场景,如多阶段任务调度。
5. Semaphore(信号量)
控制同时访问资源的线程数量,常用于限流:
import java.util.concurrent.Semaphore;
Semaphore semaphore = new Semaphore(2); // 允许2个线程同时访问
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可,无许可时阻塞
accessResource();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
}).start();
}
适用场景:控制数据库连接数、文件句柄等有限资源的访问。
6. BlockingQueue(阻塞队列)
通过队列实现线程间数据传递,支持阻塞操作(队满时插入阻塞,队空时取出阻塞):
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// 生产者线程
new Thread(() -> {
while (true) {
String data = produceData();
try {
queue.put(data); // 队满时阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 消费者线程
new Thread(() -> {
while (true) {
try {
String data = queue.take(); // 队空时阻塞
consumeData(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
特点:天然支持生产者 - 消费者模式,代码结构清晰,是推荐的通信方式之一。
7. Handler(Android 特有的线程通信)
在 Android 中,通过Handler
实现主线程(UI 线程)与子线程的通信:
// 在主线程创建Handler
Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (msg.what == 1) {
updateUI((String) msg.obj); // 更新UI
}
}
};
// 子线程发送消息
new Thread(() -> {
String data = fetchDataFromNetwork();
Message message = Message.obtain();
message.what = 1;
message.obj = data;
handler.sendMessage(message); // 发送消息到主线程Looper
}).start();
适用场景:Android 中跨线程更新 UI,底层依赖Looper
和MessageQueue
机制。
守护线程的介绍
守护线程(Daemon Thread)是 Java 中一种特殊的线程,其生命周期依赖于用户线程(Non-Daemon Thread),主要用于执行后台支持性任务,如垃圾回收、日志监控等。
守护线程与用户线程的核心区别
特性 | 守护线程 | 用户线程 |
---|---|---|
生命周期 | 随最后一个用户线程结束而终止 | 独立运行,需主动结束 |
用途 | 后台辅助任务(如 GC) | 执行具体业务逻辑 |
启动方式 | 需在start() 前设置为守护线程 | 直接启动 |
终止条件 | 无用户线程时自动终止 | 任务完成或主动中断 |
如何设置守护线程
通过Thread.setDaemon(true)
方法将线程标记为守护线程,且必须在start()
之前调用:
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
try {
System.out.println("Daemon thread running...");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start(); // 启动线程
try {
Thread.sleep(3000); // 主线程运行3秒后结束
} catch (InterruptedException e) {
e.printStackTrace();
}
// 主线程结束后,守护线程自动终止
}
}
注意事项:
- 若在
start()
后调用setDaemon(true)
,会抛出IllegalThreadStateException
; - 守护线程中不能持有需要释放的资源(如文件句柄、数据库连接),否则可能导致资源泄漏,因为守护线程终止时不会执行
finally
块或shutdown
逻辑。
守护线程的应用场景
- 垃圾回收(GC)线程:JVM 的 GC 线程是典型的守护线程,负责回收内存垃圾,随 JVM 启动而运行,不影响程序退出。
- 日志记录线程:后台定期刷新日志缓冲区,程序退出时无需等待日志完全写入。
- 定时任务调度:如周期性检查更新的线程,当程序无其他用户线程时自动停止。
- 事件监听线程:监听端口或文件变化的线程,主线程结束后自动终止。
守护线程的限制与风险
- 不能执行关键业务逻辑:因守护线程的终止时机不可控,若持有业务数据或执行未完成的任务,可能导致数据不一致或任务中断。
- 异常处理需谨慎:守护线程中的未捕获异常不会终止 JVM,但可能导致线程默默终止,影响后台任务稳定性。
- 线程同步问题:若守护线程与用户线程共享资源,需注意同步机制,避免守护线程终止时用户线程仍在访问资源。
死锁的概念及避免方法
死锁是指在多线程或多进程环境中,两个或多个进程因争夺资源而造成的一种互相等待的状态,若无外力干涉则这些进程永远无法推进。死锁的发生必须同时满足四个必要条件:互斥条件(资源不能被共享,一次只能被一个进程使用)、请求与保持条件(进程已获得资源,同时又对其他资源发出请求)、不剥夺条件(进程已获得的资源在未使用完前不能被剥夺)、循环等待条件(存在一种进程资源的循环等待链)。
避免死锁的核心在于破坏这四个必要条件中的任意一个,常见方法包括:
- 资源有序分配法:为资源分配一个全局的序列号,进程必须按顺序请求资源,避免循环等待。例如,进程若已获得序列号为 3 的资源,后续只能请求大于 3 的资源,若需要序列号为 2 的资源,需先释放已获得的资源。
- 资源一次性分配法:进程在启动时一次性申请所有需要的资源,若无法满足则不分配,避免进程持有部分资源又等待其他资源的情况。
- 避免占有并等待:要求进程在申请资源时不能持有其他资源,若需要新资源,需先释放已持有的资源,再重新申请所有资源。
- 可剥夺资源分配:当进程请求的资源被其他进程占用时,系统可剥夺后者的资源分配给前者,适用于资源状态容易保存和恢复的场景(如 CPU 时间片)。
实际开发中,需通过合理的资源管理策略和代码设计来预防死锁,例如数据库事务中按固定顺序加锁、避免嵌套锁等。
volatile 和 synchronized 的详解及两者区别
volatile 的作用与原理:
volatile 是 Java 中的轻量级同步机制,用于保证变量的可见性和有序性,但不保证原子性。当变量被 volatile 修饰时,线程对该变量的读取会直接从主内存获取,写入时会立即刷新到主内存,避免线程本地缓存导致的可见性问题。此外,volatile 会禁止指令重排序,确保代码执行顺序与书写顺序一致(仅针对 volatile 变量的操作)。例如,在双重检查锁定单例模式中,volatile 用于防止指令重排序导致的初始化异常。
synchronized 的作用与原理:
synchronized 是 Java 中的内置锁,可修饰方法、代码块,用于保证代码的原子性、可见性和有序性。其底层通过 JVM 的 Monitor 机制实现,当线程获取锁时,会标记对象头的 Monitor 记录,执行完同步代码块后释放锁,其他线程才能获取。synchronized 是重量级锁,涉及线程状态切换和操作系统内核态与用户态的转换,性能开销较大,但在 JDK1.6 后通过偏向锁、轻量级锁等优化,性能有所提升。
两者的核心区别:
维度 | volatile | synchronized |
---|---|---|
原子性 | 不保证,仅保证可见性和有序性 | 保证代码块或方法的原子性 |
锁范围 | 修饰变量 | 修饰方法或代码块 |
性能开销 | 开销小,无线程阻塞 | 开销大,可能导致线程阻塞 |
底层实现 | JVM 内存模型规范 | 依赖 Monitor 机制(对象头标记) |
适用场景 | 多线程读多线程写的变量可见性 | 多线程竞争资源的同步操作 |
Java 的 GC 算法
Java 的垃圾回收(GC)算法用于识别和回收不再使用的对象,释放内存空间。常见的 GC 算法包括:
标记 - 清除算法(Mark-Sweep)
- 流程:首先标记所有需要回收的对象,然后统一清除这些标记的对象。
- 优点:实现简单,无需移动对象。
- 缺点:会产生内存碎片,导致大对象无法分配足够连续内存,需额外的碎片整理步骤。
复制算法(Copying)
- 流程:将内存分为大小相等的两块,每次只使用一块。当一块内存满时,将存活对象复制到另一块,然后清除原有块的所有对象。
- 优点:不会产生内存碎片,回收效率高,适合存活对象少的场景(如新生代)。
- 缺点:内存利用率低,仅能使用一半内存。
标记 - 整理算法(Mark-Compact)
- 流程:先标记存活对象,然后将所有存活对象向内存一端移动,最后清除边界外的内存。
- 优点:解决了标记 - 清除的碎片问题,同时避免了复制算法的内存浪费。
- 缺点:需要移动对象,涉及对象引用地址的更新,开销较大。
分代收集算法(Generational Collection)
- 核心思想:根据对象存活周期将内存分为新生代、老年代和永久代(JDK8 后为元空间),针对不同代采用不同的 GC 算法。
- 新生代:对象存活时间短,采用复制算法,分为 Eden 区和两个 Survivor 区,每次 GC 时将 Eden 和 Survivor 中存活的对象复制到另一 Survivor 区,提高回收效率。
- 老年代:对象存活时间长,采用标记 - 整理算法,减少碎片和移动开销。
- 元空间:存储类元数据,GC 频率低,采用标记 - 清除或标记 - 整理算法。
分代收集的典型流程
当新生代 Eden 区满时触发 Minor GC,用复制算法回收;当老年代空间不足时触发 Major GC(Full GC),用标记 - 整理算法回收。分代收集通过对不同生命周期的对象采用针对性算法,平衡了回收效率和内存利用率。
Java 的引用类型,弱引用的作用、引用队列
Java 中有四种引用类型,按强度从高到低依次为:
强引用(Strong Reference)
- 定义:最常见的引用类型,如
Object obj = new Object();
,只要强引用存在,对象就不会被 GC 回收。 - 场景:日常开发中大部分对象引用都是强引用,若使用不当可能导致内存泄漏(如静态集合持有对象强引用)。
软引用(Soft Reference)
- 定义:通过
SoftReference
类实现,当系统内存不足时,才会回收软引用指向的对象。 - 场景:适用于缓存场景,如图片缓存,当内存紧张时自动释放缓存,避免 OOM。
SoftReference<Bitmap> softRef = new SoftReference<>(bitmap);
Bitmap bitmap = softRef.get(); // 需判空
弱引用(Weak Reference)
- 定义:通过
WeakReference
类实现,弱引用的对象会在下次 GC 时被回收,无论内存是否充足。 - 作用:防止内存泄漏,例如在 HashMap 中使用弱引用作为键,避免键对象无法被回收;或在 Activity 中使用弱引用持有 Context,防止 Activity 被长生命周期对象引用导致无法销毁。
- 引用队列(ReferenceQueue):当弱引用对象被 GC 回收时,弱引用会被加入关联的引用队列,可通过队列监控对象的回收情况,进行后续处理(如清除缓存)。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> weakRef = new WeakReference<>(obj, queue);
虚引用(Phantom Reference)
- 定义:通过
PhantomReference
类实现,最弱的引用类型,无法通过虚引用获取对象实例,仅用于对象被回收时接收通知。 - 场景:主要用于对象回收时的资源清理,需与引用队列配合使用,例如 DirectByteBuffer 的内存释放。
引用类型对比
引用类型 | GC 时是否回收 | 对象可达性 | 典型应用场景 |
---|---|---|---|
强引用 | 否 | 始终可达 | 普通对象引用 |
软引用 | 内存不足时回收 | 内存充足时可达 | 缓存(如图片、数据缓存) |
弱引用 | 下次 GC 时回收 | 仅弱引用存在时 | 防止内存泄漏、弱键 HashMap |
虚引用 | 立即回收 | 不可达 | 对象回收通知、资源清理 |
TCP 和 UDP 的区别
TCP(传输控制协议)和 UDP(用户数据报协议)是 TCP/IP 协议栈中两种核心的传输层协议,在设计理念和应用场景上有显著差异:
连接性与可靠性
- TCP:面向连接的协议,通信前需通过三次握手建立连接(客户端发送 SYN→服务器回复 SYN+ACK→客户端回复 ACK),通信结束后通过四次挥手释放连接。TCP 提供可靠传输,通过序列号、确认应答、超时重传、流量控制(滑动窗口)和拥塞控制(慢启动、拥塞避免)等机制,确保数据无丢失、无重复、按序到达。
- UDP:无连接协议,无需建立连接即可发送数据,不保证数据的可靠性,可能出现丢包、重复或乱序,也没有流量控制和拥塞控制机制。
传输方式与效率
- TCP:基于字节流传输,数据被分割成多个数据包,每个数据包包含序列号和确认信息,接收方需按序组装数据。TCP 的头部开销较大(20 字节固定头部 + 可选字段),传输效率相对较低,但稳定性高。
- UDP:基于数据报传输,每个数据报独立传输,头部开销小(8 字节固定头部),传输效率高,适合对实时性要求高但允许少量丢包的场景。
应用场景
- TCP 的典型应用:
- 网页浏览(HTTP/HTTPS):需要可靠传输网页数据。
- 文件传输(FTP):确保文件完整无缺地传输。
- 电子邮件(SMTP、POP3、IMAP):保证邮件内容准确送达。
- 远程登录(SSH、Telnet):需要稳定的连接和按序数据传输。
- UDP 的典型应用:
- 实时音视频通信(WebRTC、VoIP):允许少量丢包但要求低延迟。
- 直播与流媒体(RTMP、RTSP):实时性优先于数据完整性。
- 游戏网络通信:如 FPS 游戏,需要快速传输位置信息,少量丢包可通过客户端预测补偿。
- 传感器数据传输:如物联网设备发送实时数据,部分丢包不影响整体业务。
核心区别对比
维度 | TCP | UDP |
---|---|---|
连接性 | 面向连接(三次握手、四次挥手) | 无连接 |
可靠性 | 可靠传输(保证顺序和完整性) | 不可靠传输(不保证交付) |
传输单位 | 字节流(Byte Stream) | 数据报(Datagram) |
头部开销 | 20 字节(固定)+ 可选字段 | 8 字节(固定) |
流量控制 | 支持(滑动窗口) | 不支持 |
拥塞控制 | 支持(慢启动、拥塞避免) | 不支持 |
实时性 | 较低(连接建立和重传开销) | 较高(无连接和重传机制) |
应用场景 | 对可靠性要求高的场景 | 对实时性要求高的场景 |
OSI 五层结构
OSI(Open System Interconnection)五层模型是计算机网络通信的理论框架,将网络通信的过程划分为不同层次,每层负责特定功能,各层之间相互独立又协同工作。具体如下:
应用层
应用层是模型的最高层,直接为用户应用程序提供服务,处理用户接口、数据格式转换等。例如 HTTP、FTP、SMTP 协议,分别用于网页浏览、文件传输和电子邮件。应用层关注的是应用程序的逻辑,如浏览器如何解析 HTML 文档,邮件客户端如何发送邮件。
传输层
传输层负责端到端的数据传输,确保数据可靠传输或高效传输。主要协议包括 TCP 和 UDP。TCP(传输控制协议)提供面向连接的可靠传输,通过三次握手建立连接,使用确认机制和重传策略确保数据无误;UDP(用户数据报协议)是无连接的不可靠传输,适合对实时性要求高的场景,如视频流、直播。传输层通过端口号区分不同应用程序,如 HTTP 默认端口 80,HTTPS 默认端口 443。
网络层
网络层负责网络间的路由选择和 IP 地址寻址,将数据从源主机路由到目标主机。主要协议是 IP 协议,规定了数据包的格式和路由规则。此外,ICMP(互联网控制报文协议)用于网络诊断,如 ping 命令;ARP(地址解析协议)用于将 IP 地址映射为 MAC 地址。网络层解决的是 “在哪里” 的问题,确保数据能从一个网络传输到另一个网络。
数据链路层
数据链路层在物理层之上,负责相邻节点间的数据传输,将比特流组织成帧,并处理错误检测和流量控制。常见协议有 PPP(点对点协议)、Ethernet(以太网协议)。它通过 MAC 地址标识设备,实现局域网内的数据传输,例如交换机通过学习 MAC 地址来转发数据帧。数据链路层还会进行帧同步,确保接收方正确解析数据。
物理层
物理层是模型的最底层,负责二进制比特流的传输,定义物理设备的电气特性、接口标准等。例如网线的接口类型、信号的电压范围、光纤的传输方式。物理层不关心数据的内容,只负责将数据转化为电信号或光信号在介质中传输,是网络通信的物理基础。
HTTP 报文的组成
HTTP(Hypertext Transfer Protocol)报文是客户端与服务器之间通信的载体,分为请求报文和响应报文,两者结构相似但内容不同。
请求报文的组成
- 请求行:包含请求方法、请求 URL 和 HTTP 版本,用空格分隔,结尾用 CRLF(回车换行)。
例如:GET /index.html HTTP/1.1
,其中 GET 是方法,/index.html
是路径,HTTP/1.1 是版本。 - 请求头部:由多个键值对组成,描述请求的附加信息,如客户端支持的格式、认证信息等。
User-Agent
:标识客户端类型,如浏览器信息。Accept
:客户端接受的响应数据类型,如text/html
。Cookie
:携带服务器之前设置的 Cookie 信息。Content-Type
:请求体的数据类型,如application/json
。
- 空行:请求头部和请求体之间用空行分隔,标识头部结束。
- 请求体:可选部分,包含请求提交的数据,如 POST 请求提交的表单数据。
响应报文的组成
- 状态行:包含 HTTP 版本、状态码和状态描述,如
HTTP/1.1 200 OK
。- 状态码常见分类:2xx(成功)、3xx(重定向)、4xx(客户端错误)、5xx(服务器错误)。
- 响应头部:类似请求头部,提供响应的附加信息。
Content-Type
:响应体的数据类型。Content-Length
:响应体的字节长度。Set-Cookie
:服务器设置 Cookie,让客户端后续请求携带。Cache-Control
:缓存控制策略。
- 空行:分隔响应头部和响应体。
- 响应体:服务器返回的实际数据,如 HTML 页面、JSON 数据。
示例对比
类型 | 组成部分 | 示例内容 |
---|---|---|
请求报文 | 请求行 | GET /api/data HTTP/1.1 |
请求头部 | User-Agent: Mozilla/5.0<br>Accept: application/json | |
空行 | \r\n | |
请求体 | {"key": "value"} (仅 POST 等方法存在) | |
响应报文 | 状态行 | HTTP/1.1 200 OK |
响应头部 | Content-Type: application/json<br>Content-Length: 1024 | |
空行 | \r\n | |
响应体 | {"code": 200, "message": "success"} |
HTTP 报文通过 ASCII 码编码,头部字段不区分大小写,键值对用冒号分隔。了解报文结构有助于分析网络请求问题,如抓包工具(Fiddler、Charles)中显示的报文内容即为此格式。
HTTP 请求获取数据的两种方式
HTTP 协议定义了多种请求方法,其中 GET 和 POST 是最常用的两种获取数据的方式,两者在设计理念、使用场景上有明显区别。
GET 方法
GET 请求的核心目的是 “获取资源”,将请求参数附加在 URL 中,格式为URL?参数名=参数值&参数名=参数值
。例如:https://example.com/api/user?userId=123&type=admin
。
- 特点:
- 参数暴露在 URL 中,可见且可能被缓存,安全性较低,不适合传输敏感数据(如密码)。
- 受 URL 长度限制(不同浏览器和服务器限制不同,通常约 2000 字节),不能传递大量数据。
- 是幂等操作(多次请求结果相同),适合获取静态资源或查询数据。
- 使用场景:
- 网页搜索(如百度搜索关键词)、获取文章列表、查询商品信息等。
POST 方法
POST 请求将参数放在请求体中,URL 仅表示资源路径,不包含参数。例如:URL 为https://example.com/api/user
,请求体为{"userId": 123, "type": "admin"}
。
- 特点:
- 参数不在 URL 中显示,相对安全,适合传输敏感数据或大量数据(如文件上传)。
- 无固定长度限制,请求体大小取决于服务器配置。
- 不是幂等操作(多次提交可能产生不同结果,如重复下单)。
- 使用场景:
- 提交表单(如注册、登录)、上传文件、创建资源(如发布文章)等。
核心区别对比
维度 | GET 方法 | POST 方法 |
---|---|---|
参数位置 | URL 查询字符串 | 请求体 |
安全性 | 低(参数可见) | 高(参数在请求体) |
幂等性 | 是 | 否 |
长度限制 | 有(受 URL 限制) | 无(受服务器限制) |
缓存支持 | 支持 | 通常不支持 |
数据类型 | 文本(适合简单参数) | 任意类型(如 JSON、文件) |
此外,HTTP 还有 PUT(更新资源)、DELETE(删除资源)、HEAD(获取头部)等方法,但 GET 和 POST 最常用。在 Android 开发中,通常使用 OkHttp 或 Retrofit 库发送请求,例如:
// GET请求示例(Retrofit)
@GET("api/user")
Call<User> getUser(@Query("userId") int userId);
// POST请求示例(Retrofit)
@POST("api/user")
Call<Result> saveUser(@Body User user);
选择 GET 或 POST 需根据业务场景:若仅需获取数据且参数简单,用 GET;若需提交数据或参数复杂,用 POST。
粘包分包产生的原因及解决方法
在 TCP 通信中,粘包(多个数据包合并)和分包(一个数据包被拆分)是常见问题,本质原因是 TCP 作为流协议,没有明确的消息边界。
产生原因
- TCP 流特性:TCP 将应用数据视为无边界的字节流,不关心应用层的消息边界。例如,客户端发送两条消息 “Hello” 和 “World”,服务器可能一次性接收 “HelloWorld”(粘包),或分多次接收部分数据(分包)。
- 缓冲区机制:
- 发送缓冲区:客户端数据先写入发送缓冲区,当缓冲区满或达到 MSS(最大段大小)时,TCP 将数据封装成数据包发送。若两条消息间隔时间短,可能被合并成一个数据包。
- 接收缓冲区:服务器接收数据包后存入接收缓冲区,应用层读取数据时可能未读完整个数据包,导致剩余数据与下一个数据包合并(粘包),或一次读取超过一个数据包(分包)。
- MTU 限制:网络层的 MTU(最大传输单元)限制数据包大小。若数据包超过 MTU,会被拆分成多个分片传输,到达接收端后再重组,可能导致分包。
解决方法
- 定长包头法:在每个数据包前添加固定长度的头部,包含消息总长度。接收方先读取头部获取长度,再按长度读取完整消息。
- 示例:头部占 4 字节(int 类型表示长度),消息 “Hello” 的总长度为 5,头部为
00000101
(二进制),接收方先读 4 字节得长度 5,再读 5 字节消息。
- 示例:头部占 4 字节(int 类型表示长度),消息 “Hello” 的总长度为 5,头部为
- 分隔符标记法:用特定字符(如
\r\n
)作为消息边界。HTTP 协议即采用此方法,请求头和请求体用空行(\r\n
)分隔。- 实现:发送方在每条消息末尾添加分隔符,接收方按分隔符解析。例如,消息 “Hello\nWorld\n”,用
\n
作为分隔符。
- 实现:发送方在每条消息末尾添加分隔符,接收方按分隔符解析。例如,消息 “Hello\nWorld\n”,用
- 长度字段法:在消息中显式包含长度字段,类似定长包头,但长度字段可放在消息任意位置(如开头或结尾)。
- 优点:比定长包头更灵活,适合长度不固定的消息。
- 应用层协议封装:自定义协议格式,将消息分为头部和主体,头部包含版本、类型、长度等信息。例如,JSON 消息可封装为
{"length": 1024, "data": "..."}
,接收方先解析 length 字段再读数据。 - Nagle 算法处理:Nagle 算法会合并小数据包以减少网络开销,可能导致粘包。可通过设置
TCP_NODELAY
参数禁用该算法,强制立即发送数据,适用于实时性要求高的场景(如游戏)。
Android 中的实现示例
在 Socket 通信中,可通过自定义协议处理粘包分包:
// 读取消息(假设头部4字节表示长度)
public byte[] readMessage(InputStream inputStream) throws IOException {
byte[] lengthBytes = new byte[4];
inputStream.read(lengthBytes); // 先读长度
int length = bytesToInt(lengthBytes);
byte[] message = new byte[length];
inputStream.read(message); // 再读完整消息
return message;
}
// 发送消息
public void sendMessage(OutputStream outputStream, byte[] data) throws IOException {
byte[] lengthBytes = intToBytes(data.length); // 转换长度为4字节
outputStream.write(lengthBytes);
outputStream.write(data);
outputStream.flush();
}
解决粘包分包问题的核心是在应用层建立消息边界,让接收方能够正确解析每个独立的消息,确保数据完整性和准确性。
在下载过程中如何显示进度条?若下载操作耗时且数据大小不确定,该如何处理
下载进度条的显示涉及 UI 更新和耗时操作处理,而数据大小不确定时需特殊处理,确保用户体验。
常规下载(数据大小确定)的进度条实现
- 获取总大小与实时进度:
- 通过 HTTP 响应头中的
Content-Length
字段获取文件总大小(单位:字节)。 - 下载过程中实时计算已下载字节数,通过
(已下载/总大小)*100%
得到进度百分比。
- 通过 HTTP 响应头中的
- 异步下载与 UI 更新:
- 不能在主线程执行下载(会阻塞 UI 导致 ANR),需用后台线程(如
AsyncTask
、Service
或协程
)。 - 通过
Handler
或回调机制将进度传递给主线程更新 UI。
- 不能在主线程执行下载(会阻塞 UI 导致 ANR),需用后台线程(如
- Android 中的实现示例(以 AsyncTask 为例):
public class DownloadTask extends AsyncTask<String, Integer, String> { private ProgressBar progressBar; private TextView progressText; private Context context; public DownloadTask(Context context, ProgressBar progressBar, TextView progressText) { this.context = context; this.progressBar = progressBar; this.progressText = progressText; } @Override protected String doInBackground(String... urls) { try { URL url = new URL(urls[0]); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.connect(); if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { int totalSize = connection.getContentLength(); progressBar.setMax(totalSize); InputStream inputStream = connection.getInputStream(); FileOutputStream outputStream = new FileOutputStream("downloaded_file"); byte[] buffer = new byte[1024]; int len; int downloaded = 0; while ((len = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, len); downloaded += len; // 计算进度并通知UI更新 int progress = (int) ((downloaded * 100) / totalSize); publishProgress(progress); } outputStream.close(); inputStream.close(); return "下载完成"; } } catch (Exception e) { e.printStackTrace(); return "下载失败"; } return "下载取消"; } @Override protected void onProgressUpdate(Integer... values) { super.onProgressUpdate(values); progressBar.setProgress(values[0]); progressText.setText(values[0] + "%"); } @Override protected void onPostExecute(String result) { super.onPostExecute(result); Toast.makeText(context, result, Toast.LENGTH_SHORT).show(); } }
- 进度条组件选择:
ProgressBar
:Android 原生组件,支持水平进度(style="?android:attr/progressBarStyleHorizontal"
)和圆形进度(默认)。- 自定义 View:若需特殊样式,可继承
View
或ProgressBar
自定义绘制逻辑。
数据大小不确定时的处理方案
当服务器未提供Content-Length
(如流式响应、动态生成的数据),或下载内容为分段数据(如直播流),无法获取总大小时,需采用以下策略:
- 使用不确定模式进度条:
- 将
ProgressBar
设置为indeterminate
模式(圆形旋转或水平波浪动画),表示 “正在加载”,不显示具体百分比。
<ProgressBar android:layout_width="match_parent" android:layout_height="wrap_content" android:indeterminate="true" />
- 将
- 动态估算总大小:
- 基于已下载数据量和时间估算总大小,例如假设下载速度稳定,用 “已下载大小 + 估算剩余大小” 显示进度。但此方法误差较大,仅作参考。
- 实现:记录开始下载的时间和已下载数据量,计算平均速度(如 1MB / 秒),假设总大小为 “已下载 + 速度 * 剩余时间”,但需定期更新估算值。
- 分段显示进度:
- 若数据可分段处理(如分块下载),可显示当前块的进度和总块数(如 “下载第 3 块,共 10 块”)。
- 实时反馈下载状态:
- 显示已下载的字节数和下载速度(如 “已下载 5.2MB,速度 1.2MB/s”),让用户感知下载进度,即使没有百分比。
- 结合进度条与状态文本:
- 用
ProgressBar
的不确定模式配合文本提示(如 “正在加载数据...”),避免用户认为程序卡顿。
- 用
耗时下载的优化
无论数据大小是否确定,下载操作都应在后台执行,并处理异常情况(如网络中断、用户取消):
- 使用
Service
配合BroadcastReceiver
或LiveData
,确保下载不随 Activity 销毁而中断。 - 支持断点续传,通过
HttpURLConnection
的setRequestProperty("Range", "bytes=" + 已下载 + "-")
设置续传位置。 - 对于大文件下载,使用分块读取(如每次读取 1024 字节),避免内存溢出。
总之,确定数据大小时用百分比进度条,不确定时用不确定模式或实时状态反馈,核心是让用户明确感知操作在进行,提升交互体验。
MVP 模式与 MVC 模式的对比
MVP(Model-View-Presenter)和 MVC(Model-View-Controller)是 Android 开发中常见的架构模式,两者在职责划分和交互方式上有明显差异。
核心结构与职责
MVC 模式中,Model 负责数据存储和业务逻辑,View 负责界面展示,Controller 作为中间层处理用户输入并更新 Model 和 View。但在实际开发中,View 常直接操作 Model,导致 Controller 职责模糊,形成 “Massive View” 问题。例如,Android 中的 Activity 常同时处理界面逻辑和数据交互,违背单一职责原则。
MVP 模式将 Controller 改为 Presenter,View 不再直接操作 Model,而是通过 Presenter 与 Model 交互。Presenter 持有 View 和 Model 的引用,负责数据转换和逻辑处理。例如,在登录功能中,Activity 作为 View 只处理界面渲染,Presenter 接收用户输入,调用 Model 的登录接口,并将结果返回给 View 更新界面,避免 View 与 Model 的强耦合。
差异对比
维度 | MVC 模式 | MVP 模式 |
---|---|---|
交互方式 | View 可直接访问 Model,Controller 调度 | View 与 Model 无直接交互,通过 Presenter 通信 |
测试性 | View 与 Model 耦合度高,难以单元测试 | Presenter 可独立测试,View 通过接口模拟 |
职责清晰度 | Controller 职责可能重叠 | 三层职责更明确,符合单一职责原则 |
Android 适配 | Activity 常兼任 View 和 Controller | View 通常为接口,Activity 实现接口 |
优缺点与适用场景
MVC 的优势在于结构简单,适合小型项目,但在复杂场景下易导致代码臃肿。MVP 通过接口隔离使代码更易维护,适合中大型项目,但会增加类的数量。例如,在电商 App 的商品列表页面,MVP 模式可将数据加载、列表渲染逻辑分离到 Presenter 中,Activity 仅处理界面更新,提升代码可维护性。
观察者模式的应用场景
观察者模式定义了对象间的一对多依赖关系,当一个对象(主题)状态改变时,所有依赖它的对象(观察者)都会得到通知并自动更新。在 Android 开发中,该模式常用于实现数据与界面的解耦和实时更新。
典型应用场景
- 广播机制(BroadcastReceiver):系统或应用发送广播时,注册的 Receiver 作为观察者会收到通知。例如,网络状态变化时,应用通过注册 ConnectivityReceiver 监听变化并更新界面提示。
- 数据绑定(Data Binding):Android Jetpack 的 Data Binding 库中,数据对象作为主题,View 作为观察者,数据变化时自动更新界面。例如,ViewModel 中的 LiveData 对象变更时,观察它的 Activity/Fragment 会收到通知并刷新 UI。
- 事件总线(EventBus):第三方库如 EventBus 通过观察者模式实现跨组件通信。例如,在 Fragment 与 Activity 之间传递事件,避免直接引用导致的耦合。
- Adapter 与数据集:ListView/RecyclerView 的 Adapter 观察数据集合的变化,当数据新增或删除时,通过 notifyDataSetChanged () 通知 Adapter 更新列表。
优势与实现要点
观察者模式的核心优势是解耦,主题和观察者无需知道对方的具体实现,仅通过接口交互。例如,在天气应用中,WeatherModel 作为主题存储天气数据,多个 Activity/Fragment 作为观察者订阅数据更新,当后台获取新数据时,自动通知所有观察者刷新界面。实现时需注意避免内存泄漏,如在 Activity 销毁时取消观察者注册,否则主题持有观察者的引用会导致 Activity 无法被回收。
装饰者模式的原理
装饰者模式(Decorator Pattern)属于结构型设计模式,其核心思想是动态地为对象添加新的行为,而不修改原有类的代码。它通过创建一个包装对象(装饰者)来包裹真实对象,从而在不改变原对象的情况下扩展功能。
模式结构与工作原理
装饰者模式包含四个角色:
- Component(组件接口):定义对象的基本行为,如 Java 的 InputStream 接口。
- ConcreteComponent(具体组件):实现组件接口的具体对象,如 FileInputStream。
- Decorator(装饰者抽象类):持有 Component 引用,实现与 Component 相同的接口,用于包装具体组件。
- ConcreteDecorator(具体装饰者):继承 Decorator,添加额外功能。
以 Java 的 IO 流为例,BufferedInputStream 是 FileInputStream 的装饰者:
// 组件接口
InputStream inputStream = new FileInputStream("data.txt");
// 装饰者包装组件,添加缓冲功能
inputStream = new BufferedInputStream(inputStream);
// 继续包装,添加加密功能(假设存在加密装饰者)
inputStream = new EncryptedInputStream(inputStream);
每次包装都会添加新功能,且装饰者与组件遵循相同接口,因此可多次包装,灵活性高。
与继承的区别
继承是静态的扩展方式,一旦子类继承父类,功能便固定;而装饰者模式是动态扩展,可在运行时选择不同装饰者组合。例如,TextView 需要同时支持红色字体和粗体,若用继承需创建 RedBoldTextView 类,但若功能更多(如下划线、阴影),继承会导致类爆炸。而装饰者模式可通过 TextDecorator 装饰者动态添加不同样式,如 RedDecorator、BoldDecorator,避免类数量膨胀。
数据库左连接和右连接的区别及其关键字
在 SQL 中,连接(Join)用于组合两个或多个表的数据,左连接(LEFT JOIN)和右连接(RIGHT JOIN)是两种常见的连接方式,其核心区别在于保留哪张表的全部记录。
基本定义与关键字
- 左连接(LEFT JOIN):关键字为
LEFT JOIN
或LEFT OUTER JOIN
,查询结果保留左表的所有记录,右表中匹配的记录会被合并,若右表无匹配记录,则对应字段为 NULL。 - 右连接(RIGHT JOIN):关键字为
RIGHT JOIN
或RIGHT OUTER JOIN
,保留右表的所有记录,左表中匹配的记录会被合并,无匹配时左表字段为 NULL。
示例说明
假设存在两张表:学生表(Student)
和课程表(Course)
,结构如下:
Student: id, name
Course: id, student_id, course_name
- 左连接查询:
SELECT s.name, c.course_name FROM Student s LEFT JOIN Course c ON s.id = c.student_id;
结果会包含所有学生,无论是否选课程。若学生未选课程,course_name
为 NULL。 - 右连接查询:
SELECT s.name, c.course_name FROM Student s RIGHT JOIN Course c ON s.id = c.student_id;
结果会包含所有课程,无论是否有学生选课。若课程无学生选,name
为 NULL。
对比与应用场景
连接类型 | 保留表 | NULL 出现位置 | 适用场景 |
---|---|---|---|
左连接 | 左表 | 右表不匹配的记录 | 查询所有用户及其订单(即使无订单) |
右连接 | 右表 | 左表不匹配的记录 | 查询所有订单及其用户(即使无用户) |
实际开发中,左连接更为常用,例如在 Android 的 Room 数据库中,查询所有用户及其关联的地址信息时,使用左连接可确保用户记录不丢失,地址为空时表示用户未填写地址。
算法与数据结构
算法和数据结构是计算机科学的基础,两者相互依存:数据结构定义了数据的组织形式,算法则定义了操作这些数据的方法。
基本概念与关系
- 数据结构:指数据在计算机中的存储和组织方式,目的是提高数据的访问和操作效率。例如:
- 线性结构:数组、链表、栈、队列,数据元素呈线性排列。
- 非线性结构:树、图,数据元素存在多对多关系。
- 算法:解决特定问题的一系列步骤,需要依赖数据结构实现。例如排序算法(冒泡排序、快速排序)操作数组或链表,搜索算法(二分查找)基于有序数组实现。
常见类型与 Android 应用
数据结构 | 特点 | Android 应用场景 |
---|---|---|
数组 | 连续内存存储,随机访问高效 | ListView/RecyclerView 的适配器数据源 |
链表 | 非连续存储,插入删除高效 | 事件分发链(View 的事件传递链表) |
哈希表 | 键值对存储,查找时间复杂度 O (1) | IntentFilter 的匹配(通过哈希表快速查找) |
树 | 层级结构,如二叉树、红黑树 | Bitmap 缓存 LRU 算法(用树结构管理缓存顺序) |
算法在 Android 中也有广泛应用:
- 排序算法:如 RecyclerView 的 item 排序,使用归并排序或快速排序优化性能。
- 查找算法:联系人搜索功能中,使用二分查找在有序列表中快速定位。
- 图形算法:图片压缩、滤镜处理(如高斯模糊算法)。
性能优化中的意义
合理选择数据结构和算法可显著提升应用性能。例如,在联系人列表中,若使用数组存储联系人,查找操作需遍历所有元素(O (n)),而使用二叉搜索树(O (log n))或哈希表(O (1))可大幅提升效率。此外,Android 的布局测量过程使用递归算法遍历 View 树,若 View 层级过深会导致测量耗时,因此优化布局结构(减少层级)也是算法优化的体现。
给定银行卡前缀(如前 4 位或前 5 位),如何查找银行卡号所属银行?该查找操作的时间复杂度是多少(java 代码完整实现)
要根据银行卡前缀查找所属银行,核心在于建立前缀与银行信息的映射关系。银行卡号的前几位(称为发卡行识别码,BIN)由国际标准化组织分配,不同银行对应特定的前缀范围。
实现思路
- 数据存储:使用
HashMap<String, String>
存储常见银行的 BIN 前缀与银行名称的映射,例如 “622202” 对应 “工商银行”。 - 前缀匹配:由于不同银行的 BIN 长度可能不同(如 4 位或 5 位),需按最长前缀优先匹配的原则,避免短前缀覆盖长前缀的情况(例如 “6222” 和 “622202” 同时存在时,优先匹配后者)。
- 时间复杂度:使用 HashMap 存储和查询的时间复杂度均为 O (1),但需遍历前缀长度从长到短的顺序匹配,最坏情况下遍历次数为最长前缀长度(如 6 位),因此整体时间复杂度仍接近 O (1)。
Java 代码实现
import java.util.HashMap;
import java.util.Map;
public class BankCardFinder {
// 存储银行卡前缀与银行名称的映射,按前缀长度从长到短排序存储
private final Map<String, String> binBankMap;
public BankCardFinder() {
binBankMap = new HashMap<>();
// 初始化常见银行的BIN前缀(实际应用中可从数据库或配置文件加载)
binBankMap.put("622202", "中国工商银行");
binBankMap.put("622848", "中国农业银行");
binBankMap.put("622700", "中国建设银行");
binBankMap.put("621226", "中国银行");
binBankMap.put("622575", "招商银行");
binBankMap.put("621700", "中国建设银行"); // 长前缀优先存储
binBankMap.put("6222", "中国工商银行"); // 短前缀作为后备
binBankMap.put("6228", "中国农业银行");
}
/**
* 根据银行卡号查找所属银行
* @param cardNumber 银行卡号(至少包含前几位前缀)
* @return 银行名称,未找到返回null
*/
public String findBankByCardNumber(String cardNumber) {
if (cardNumber == null || cardNumber.length() < 4) {
return "银行卡号长度不足";
}
// 按前缀长度从长到短遍历,优先匹配长前缀
for (int length = Math.min(cardNumber.length(), 6); length >= 4; length--) {
String prefix = cardNumber.substring(0, length);
if (binBankMap.containsKey(prefix)) {
return binBankMap.get(prefix);
}
}
return "未找到对应银行";
}
/**
* 新增或更新银行前缀映射
*/
public void addBankMapping(String binPrefix, String bankName) {
binBankMap.put(binPrefix, bankName);
}
// 测试方法
public static void main(String[] args) {
BankCardFinder finder = new BankCardFinder();
System.out.println(finder.findBankByCardNumber("6222021234567890123")); // 应输出"中国工商银行"
System.out.println(finder.findBankByCardNumber("6217001234567890")); // 应输出"中国建设银行"
System.out.println(finder.findBankByCardNumber("62257512345")); // 应输出"招商银行"
}
}
实现说明
- 前缀匹配策略:代码中按前缀长度 6 到 4 的顺序遍历,确保长前缀优先匹配,避免短前缀误判(如 “622202” 比 “6222” 更长,优先匹配前者)。
- 扩展性:实际项目中可将 BIN 数据存储在数据库或本地文件中,通过接口动态加载,避免硬编码。
- 性能优化:若 BIN 数据量极大,可使用 Trie 树(前缀树)优化查询,时间复杂度仍为 O (k),k 为前缀长度,比 HashMap 更节省内存。
JRE、JDK、JVM 的区别
JRE、JDK、JVM 是 Java 技术体系中三个核心概念,彼此关联但功能迥异,需从定义、组成和用途三个维度区分。
定义与功能
-
JVM(Java Virtual Machine,Java 虚拟机)
是 Java 程序的运行容器,负责将字节码(.class 文件)转换为机器码执行。它屏蔽了底层操作系统和硬件的差异,实现了 “一次编写,到处运行” 的跨平台特性。JVM 包含类加载器、执行引擎、内存区域等核心组件,不同平台(如 Windows、Linux、Android)有对应的 JVM 实现(如 HotSpot、Dalvik/ART)。 -
JRE(Java Runtime Environment,Java 运行时环境)
是运行 Java 程序所需的最小环境,包含 JVM、核心类库(如 java.lang、java.util)和其他支持文件。用户若仅需运行 Java 程序,安装 JRE 即可,无需安装开发工具。JRE 的目录结构中,lib
文件夹存放核心类库,bin
文件夹包含 Java 运行时工具(如 java.exe)。 -
JDK(Java Development Kit,Java 开发工具包)
是 Java 开发的完整工具集,包含 JRE 和开发所需的工具,如编译器(javac)、调试器(jdb)、文档生成工具(javadoc)等。开发者通过 JDK 编写、编译和调试 Java 程序,最终生成可在 JRE 环境中运行的字节码文件。JDK 的bin
目录下包含这些工具,lib
目录存放工具的支持库。
三者关系与依赖
JDK 包含 JRE,JRE 包含 JVM。三者的依赖关系如下:
JDK → JRE → JVM
- 开发阶段:开发者使用 JDK 编写代码,通过 javac 编译为字节码,此时 JDK 中的工具依赖 JRE 的类库。
- 运行阶段:字节码文件在 JRE 环境中的 JVM 上执行,JVM 依赖操作系统的底层支持。
应用场景对比
组件 | 用途 | 包含内容 | 用户群体 |
---|---|---|---|
JVM | 执行字节码,提供运行时环境 | 类加载器、执行引擎、内存管理 | 底层开发者 |
JRE | 运行 Java 程序 | JVM + 核心类库 + 运行时工具 | 普通用户 |
JDK | 开发 Java 程序 | JRE + 编译器 + 调试工具 + 开发库 | Java 开发者 |
开发过程中遇到的异常类型及处理方法
Java 异常体系以Throwable
为根,分为Exception
和Error
两大类,其中Exception
又分为Checked Exception
和Unchecked Exception
(运行时异常)。在开发中,合理处理异常能提升程序的健壮性和用户体验。
异常类型分类与常见示例
-
Checked Exception(编译时异常)
- 定义:必须在编译期显式处理(使用
try-catch
或throws
声明),否则编译失败。 - 常见类型:
IOException
:文件读写、网络连接等 I/O 操作异常,如FileNotFoundException
。SQLException
:数据库操作异常,如 SQL 语法错误、连接超时。ClassNotFoundException
:类未找到异常,如动态加载类时路径错误。
- 定义:必须在编译期显式处理(使用
-
Unchecked Exception(运行时异常)
- 定义:无需强制处理,通常由程序逻辑错误导致,如空指针、数组越界等。
- 常见类型:
NullPointerException
:访问 null 对象的方法或属性,如String str = null; str.length();
。ArrayIndexOutOfBoundsException
:数组下标越界,如int[] arr = new int[5]; arr[10] = 0;
。IllegalArgumentException
:参数不合法,如传入负数作为数组长度。ClassCastException
:类型转换错误,如Object obj = "string"; int i = (int) obj;
。
-
Error(错误)
- 定义:指程序无法处理的系统级错误,如内存溢出(
OutOfMemoryError
)、栈溢出(StackOverflowError
),一般无需捕获,应通过优化代码或调整 JVM 参数解决。
- 定义:指程序无法处理的系统级错误,如内存溢出(
异常处理方法与最佳实践
-
try-catch-finally 块
- 用于捕获并处理异常,
finally
块中的代码无论是否发生异常都会执行,常用于资源释放(如关闭文件流、数据库连接)。
FileInputStream fis = null; try { fis = new FileInputStream("data.txt"); // 读取文件内容 } catch (FileNotFoundException e) { System.err.println("文件不存在"); e.printStackTrace(); // 开发阶段打印堆栈信息,便于调试 } catch (IOException e) { System.err.println("文件读取失败"); } finally { if (fis != null) { try { fis.close(); // 在finally中关闭资源,确保资源释放 } catch (IOException e) { e.printStackTrace(); } } }
- 用于捕获并处理异常,
-
throws 声明
- 方法可通过
throws
关键字声明可能抛出的异常,由调用者处理。
public void readFile() throws IOException { // 可能抛出IOException的代码 }
- 方法可通过
-
异常包装与自定义异常
- 当底层异常无法表达业务含义时,可包装为自定义异常:
public class UserService { public void register(String username) throws UserExistsException { if (isUsernameExists(username)) { throw new UserExistsException("用户名已存在"); } // 注册逻辑 } } // 自定义Checked异常 class UserExistsException extends Exception { public UserExistsException(String message) { super(message); } }
异常处理原则
- 不捕获所有异常:避免使用
catch (Exception e)
捕获所有异常,应根据异常类型分别处理,防止隐藏真正的问题。 - 异常信息清晰:异常信息应包含足够的上下文,便于定位问题(如 “用户 ID 为 null,无法查询” 而非 “操作失败”)。
- 资源释放优先:使用
try-with-resources
语法(Java 7+)自动释放实现了AutoCloseable
接口的资源,简化代码:try (FileInputStream fis = new FileInputStream("data.txt")) { // 读取文件,无需手动关闭fis } catch (IOException e) { // 处理异常 }
如何实现 ImageLoader
ImageLoader 是 Android 开发中加载和管理图片的核心组件,需解决异步加载、内存缓存、磁盘缓存、图片压缩等关键问题,避免 OOM 和 UI 卡顿。以下从架构设计和核心功能两方面说明实现思路。
架构设计与核心模块
- 任务调度模块:使用线程池(如
ExecutorService
)管理图片加载任务,避免创建大量临时线程。Android 中可结合HandlerThread
或IntentService
处理后台任务。 - 缓存模块:
- 内存缓存:使用
LruCache
(最近最少使用算法)存储常用图片,根据应用内存情况设置缓存大小(如Runtime.getRuntime().maxMemory() / 8
)。 - 磁盘缓存:使用
DiskLruCache
存储内存中未命中的图片,减少网络请求或文件读取频率。
- 内存缓存:使用
- 图片处理模块:根据 ImageView 的尺寸对图片进行压缩,使用
BitmapFactory.Options
设置inSampleSize
,避免加载全尺寸图片导致内存溢出。 - 回调模块:通过
Handler
或Callback
将加载完成的图片传递给 UI 线程,更新 ImageView。
核心功能实现步骤
-
内存缓存的实现
// 基于LruCache实现内存缓存 private LruCache<String, Bitmap> mMemoryCache; public void initMemoryCache() { // 获取最大可用内存的1/8作为缓存大小 int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { @Override protected int sizeOf(String key, Bitmap bitmap) { // 返回Bitmap的占用内存大小(单位:KB) return bitmap.getByteCount() / 1024; } }; } public void addToMemoryCache(String key, Bitmap bitmap) { if (getFromMemoryCache(key) == null) { mMemoryCache.put(key, bitmap); } } public Bitmap getFromMemoryCache(String key) { return mMemoryCache.get(key); }
-
磁盘缓存的实现
// 基于DiskLruCache实现磁盘缓存(需处理IOException) private DiskLruCache mDiskCache; private static final int DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB private static final String DISK_CACHE_DIR = "image_cache"; public void initDiskCache(Context context) { File cacheDir = new File(context.getCacheDir(), DISK_CACHE_DIR); if (!cacheDir.exists()) { cacheDir.mkdirs(); } try { mDiskCache = DiskLruCache.open(cacheDir, 1, 1, DISK_CACHE_SIZE); } catch (IOException e) { e.printStackTrace(); } } public void addToDiskCache(String key, Bitmap bitmap) { if (mDiskCache == null) return; String filename = key.hashCode() + ".jpg"; try { DiskLruCache.Editor editor = mDiskCache.edit(filename); if (editor != null) { OutputStream os = editor.newOutputStream(0); bitmap.compress(Bitmap.CompressFormat.JPEG, 80, os); editor.commit(); mDiskCache.flush(); } } catch (IOException e) { e.printStackTrace(); } } public Bitmap getFromDiskCache(String key) { if (mDiskCache == null) return null; String filename = key.hashCode() + ".jpg"; try { DiskLruCache.Snapshot snapshot = mDiskCache.get(filename); if (snapshot != null) { InputStream is = snapshot.getInputStream(0); return BitmapFactory.decodeStream(is); } } catch (IOException e) { e.printStackTrace(); } return null; }
-
图片加载与异步处理
使用Handler
或AsyncTask
在后台线程加载图片,完成后切换到主线程更新 UI:// 简化的图片加载方法 public void displayImage(final String imageUrl, final ImageView imageView) { // 先从内存缓存获取 String key = imageUrl; Bitmap bitmap = getFromMemoryCache(key); if (bitmap != null) { imageView.setImageBitmap(bitmap); return; } // 内存未命中,从磁盘缓存获取 bitmap = getFromDiskCache(key); if (bitmap != null) { imageView.setImageBitmap(bitmap); addToMemoryCache(key, bitmap); return; } // 磁盘未命中,发起网络请求(实际需结合OkHttp或Volley) imageView.setImageResource(R.drawable.place_holder); // 设置占位图 loadImageFromNetwork(imageUrl, imageView, key); } private void loadImageFromNetwork(final String url, final ImageView imageView, final String key) { // 使用线程池加载图片 new Thread(new Runnable() { @Override public void run() { try { // 模拟网络请求获取图片(实际需用OkHttp等库) Bitmap bitmap = downloadBitmap(url); if (bitmap != null) { // 压缩图片 bitmap = compressBitmap(bitmap, imageView.getWidth(), imageView.getHeight()); // 存入缓存 addToMemoryCache(key, bitmap); addToDiskCache(key, bitmap); // 更新UI(通过Handler切换到主线程) mHandler.post(new Runnable() { @Override public void run() { ImageView currentView = mImageViewMap.get(key); if (currentView == imageView && bitmap != null) { imageView.setImageBitmap(bitmap); } } }); } } catch (Exception e) { e.printStackTrace(); } } }).start(); }
进阶优化点
- 图片压缩策略:根据 ImageView 的实际尺寸计算压缩比例,避免加载过大图片,可通过
BitmapFactory.Options.inJustDecodeBounds
先获取图片原始尺寸。 - 列表滑动优化:在 ListView/RecyclerView 滑动时暂停图片加载,避免大量任务并发导致卡顿,可通过
ScrollListener
监听滑动状态。 - 内存泄漏处理:使用弱引用(
WeakReference<ImageView>
)存储 ImageView,防止 Activity 销毁后图片任务仍持有引用。 - 三级缓存策略:内存缓存→磁盘缓存→网络加载,形成多级缓存体系,提升加载效率。
实际开发中,可参考成熟的开源库(如 Glide、Picasso、Fresco)的实现原理,这些库已处理了图片加载的复杂场景,如动图支持、圆角裁剪、高斯模糊等,比自定义实现更高效和稳定。