安卓入门一 Java基础

一、团队效率提升

  1. 版本更新

Android 增量更新与热修复

增量更新的目的是为了减少更新app所需要下载的包体积大小,常见如手机端游戏,apk包体积为几百M,但有时更新只需下载十几M的安装包即可完成更新。

增量更新demo地址:https://github.com/po1arbear/bsdiff-android

热修复一般是用于当已经发布的app有Bug需要修复的时候,开发者修改代码并发布补丁,让应用能够在不需要重新安装的情况下实现更新,主流方案有Tinker、AndFix等。

管理软件更新

1.自然升级 2.引导升级

进度感知(用户可以看见更新进度),提醒链路(导航栏打开更新弹窗)。

提醒更新策略原则 不阻塞行为操作

2023年工信部发布了《关于进一步提升移动互联网应用服务能力》的通知,其中第四条更是明确表示"窗口关闭用户可选",所以强更弹窗并不是我们的最佳选择。

主要字段

标题

内容

最新版本号

取消倒计时时长

是否提醒

是否强更

双端最低支持的系统版本

最大提醒次数

未更新最大版本间隔等等

版本治理

  1. 编译优化
  1. 编译流程(需要了解)
  2. 全量编译/增量编译
  3. 有效方案:
  1. 升级硬件
  2. Moudle arr化
  3. 构建缓存
  1. 其他方案(作用不大)

1.使用最新 版本工具2.Debug环境只运行编译需要的资源3.版本将图片转为WebP4.格式停用PNG5.开启gradle缓存6.开启kotlin增量和并行编译7.使用静态依赖项版本8.合理调整堆大小9.kapt优化10.使用增量注解处理器

  1. 检测编译耗时

Build Analyaer模块查看

Gradle命令

$ gradlew app:assemble --profile (Windows)

$ ./gradlew app:assembleDebug --profile (Ubuntu,Mac)

scan命令,扫描后在网址中打开查看性能(推荐使用)

$ gradlew app:assemble --scan(Windows)

$ ./gradlew app:assembleDebug --scan(Ubuntu,Mac)

Android studio 配置修改:https://juejin.cn/post/7094198918065422350

Moudle arr开源方案参考:https://juejin.cn/post/7038157787976695815,这个插件可以直接引用到项目中。

只编译文件变更位置,两种实现方式,通过git上传记录或者本地window变更文件找到需要重新编译的位置。

  1. 内存优化

1. 内存概念

虚拟内存(用户空间 内核空间)

虚拟内存是计算机系统内有管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分限成多个物理内存碎片,还有部分暂时存储在外部总盘存健客上,在需要时进行数据交换。

Java堆:堆实际上是一块匿名共享内存,Android虚拟机仅仅只是把它封装成一个mSpace。由底层C库管理,仍然可以使用libc提供的函数malloc和free分配和释放内存。

虚拟机:早期Dalvik,现在ART。不同点:lmage Space存放一些预加载类,在Zygote进程和应用程序进程之间共享Large Object Space离散地址的集合,分配一些大对象,用于提高GC的管理效率和整体性能

Native堆

2. 内存优化

目的

1.降低OOM,抖动,泄漏 提升稳定性

2.减少内存占用 接高应用后台运行时的存活率

3.减少卡顿 提高流畅性

背景

Java GC **回收:**1.判斯对象是否回收的可达性分析算法2.强软弱虚4种引用类型3.GC回收算法

**内存泄漏:**内存泄漏指的是一块内存没有被使用且无法被GC回收

**内存抖动:**内存抖动意味着短时间频繁创建对象与回收,容易触发GC而当GC时所有线程都会停止,因此可能导致卡顿

**内存溢出:**申请的内存超出可用的内存,即OOM

  • Java堆内存超限1堆内存单次分配过大2多次分配累计过大3内存累计分配触顶4无足够连续内存空间
  • 文件描述符(fd)数目超限
  • pthread create 1线程数超限 2虚拟空间不足

内存优化工具

Android Profiler、MAT推荐、VisualVM

内存监控工具

线下LeakCanary、线上Matrix、Koom

内存优化常规

  • 合适的代码设计,数据结构
  • 对象复用,SimplePool与SynchronizedPool
  • 资源,复用,序列化Parcelable
  • 避免内存泄漏,抖动,溢出... 图片

图片优化

  • BitMap内存演变
  • 图片优化

统一图片库使用glide coil 低端机使用Fresco

设备分极优化策略 比如低端机单进程

  • 大图监控 Glide--SingleRequest--onResourceReady、Epic
  • 重复图片检测

重复图片检测的原理:使用内存Hprof分析工具,自动将重复Bitmap的图片和引用堆找输出。

  • 建立全局的线上Btmap监控 ASM编译插桩-Gradle Transform

内存泄漏

已动态分配的堆内存由于未知原因未释放或无法释放

  • 永久性泄漏 泄漏的内存永远不会回收
  • 临时性泄漏 泄漏的内存未来某些时间可能被回收

泄漏场景

  • 资源未关闭 finish关闭
  • 监听器 成对出现
  • 系统Bug InputMethodManager mCurRootView/AccountManager使用不当导致的泄漏等
  • 集合泄漏 主动clear null
  • webview泄漏 ondestory
  • 第三方库泄漏
  • 线程

Thread 不持有

HandlerThread

1.onDestroy方法中调用HandlerThread的quit方法2.将HandlerThread定义为静态内部类3.使用ApplicationContext

  • Handler

1 onDestroy执行时,调用Handler的removeCallbacksAndMessage,Runnable(Thread)泄漏则可通过终止线程

2 将Handler,Runnable(Thread)定义为静态内部类

3 使用弱引用,或者使用ApplicationContext

4 耗时操作jobschedule

  • Static

Activity

找到static变量泄漏的Activity的时机将其引用链剪断

将static变量回复为非static变量,使其可以正常GC

如果确实需要context.需要保持static.则可使用Application Context

  • View

通过设计改变static为普通变量 不要在Android中时用static修饰View

onDestroy时将static view 置为null

  • 内部类持有外部类引用

非静态内部类 静态内部类 匿名内部类 尽量不要使用

内存溢出

00M 原因

  • 进程虚拟内存不足
  • Java堆内存不足 一般是内存泄缩或者内存使用不合理导致
  • 连续内存不足 相对较少
  • FD数量超过系统限制
  • 线程数量超过系统限刚
  • 手机物理内存不足

Native 内存和内存监控

内存指标监控

二、Java基础

1.1 并发和并行、同步和异步
  • 并发:同一时间段,多个任务交替执行,时间片轮法共享系统资源(Synchronized)
  • 并行:单位时间内,多个任务在多个独立处理单元上同时执行(分布式锁)
  • 同步:任务依次执行,任务需要依次执行(阻塞)
  • 异步:可以同时进行多个任务,任务的调用和完成是独立的(非阻塞)
1.2 并发编程三大特性

可见性:是指一个线程对共享变量进行修改,另一个线程立即得到修改后的最新值。

原子性:在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行。

有序性:解决了重排序。重排序:Java在编译时和运行时会对代码进行优化,会导致程序最终的执行顺序不一定就是我们编写代码时的顺序。

锁搞定了多线程中的重排序,单线程中怎么解决重排序?使用as-if-serial--语句

1.3 锁类型
    1. 线程要不要锁住同步资源? a.锁住 悲观锁 b.不锁住 乐观锁

悲观锁:悲观锁的策略是在访问数据之前先获取锁,这表示它悲观地认为在整个数据操作过程中都会有并发修改的风险。因此,在悲观锁的机制下,当一个线程访问数据时,会先加锁,其他线程要想访问该数据就必须等待已持有锁的线程释放锁才能继续操作。典型的悲观锁实现包括数据库中的行级锁、Java中的synchronized关键字等。

乐观锁:相对于悲观锁,乐观锁则采取一种更加乐观的态度,它假设在大多数情况下数据不会发生冲突。因此,乐观锁在更新数据时并不加锁,而是先读取数据版本信息,然后在写回数据时检查数据版本信息是否发生变化。典型的乐观锁实现方式包括版本号机制和CAS(Compare And Swap)等。

悲观锁适合写操作频繁的场景,适合于长事务,而乐观锁适合读操作频繁的场景,适合于短事务。在实际应用中,选择合适的锁策略需要根据具体的业务场景和性能需求进行权衡。

    1. 锁住同步资源失败,线程要不要阻塞? a.阻塞 b.不阻塞 自旋锁 适应性自旋锁

自旋锁是一种基于忙等待的锁机制,它在获取锁时不会立即放弃CPU的执行时间片,而是通过循环进行空转等待其他线程释放锁。这样可以减少线程切换的开销,适用于短时间内竞争锁的情况。

自旋锁的基本思想是,当一个线程发现该锁已被其他线程占用时,它会不断地在循环中尝试获取锁,直到获取到为止。如果在循环中一直无法获取到锁,该线程可能会消耗较多的CPU资源。

为了提高自旋锁的效率,有时候可以采用适应性自旋锁(Adaptive Spin Lock)。适应性自旋锁是根据之前获取锁的历史记录来调整自旋等待的时间。如果一个线程在过去经常能够快速获取到锁,那么它的自旋等待时间会较短;相反,如果一个线程在过去很少能够快速获取到锁,那么它的自旋等待时间会较长。

适应性自旋锁可以根据线程的历史获取情况来动态调整自旋等待时间,更有效地利用CPU资源。例如,如果一个线程经常能够快速获取到锁,那么它的自旋时间可以较短,减少无谓的空转等待时间;如果一个线程经常无法快速获取到锁,那么它的自旋时间可以适当延长,减少不必要的锁竞争。

适应性自旋锁的具体实现方式可以根据不同的编程语言和平台而有所不同。在Java中,JVM会使用一些统计信息来调整自旋等待时间,以提高自旋锁的性能。

    1. 多个线程竞争同步资源的流程细节有没有区别?
  1. 不锁住资源,多个线程中只有一个能修改资源成功,其它线程会重试 无锁
  2. 同一个线程执行同步资源时自动获取资源 偏向锁
  3. 多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放 轻量级锁
  4. 多个线程竞争同步资源时,没有获取资源的线程阻塞等待唤醒 重量级锁

偏向锁:偏向锁是为了解决大多数情况下都是单线程访问同步块的情况。当一个线程首次访问同步块时,偏向锁会将对象头部的标记设置为指向该线程,并且记录下该线程的ID。此后,如果同步块再次被相同的线程访问,无需进行同步操作,可以直接进入临界区执行,从而减少了获取锁和释放锁的开销。

轻量级锁:轻量级锁是针对多线程竞争同步块的情况进行优化的,它通过CAS操作来尝试获取锁。当只有一个线程在竞争同步块时,轻量级锁使用CAS操作将对象头部的标记替换为指向锁记录的指针,从而避免了传统的互斥量操作。

重量级锁:重量级锁是指当多个线程竞争同步块时,锁会膨胀为重量级锁,在这种情况下会引入互斥量,使得其他线程阻塞,等待持有锁的线程释放锁。

这三种锁在Java中的实现是为了在不同的多线程场景下提供最佳的性能和资源利用率。偏向锁适用于大多数情况下都是单线程访问同步块的场景,轻量级锁适用于少量线程竞争同步块的场景,而重量级锁适用于多个线程竞争同步块的场景。 Java虚拟机在运行时会根据实际情况动态地选择合适的锁机制来提高程序的性能。

    1. 多个线程竞争锁时要不要排队?

a.排队 公平锁 b.先尝试插队,插队失败再排队 非公平锁
在并发编程中,公平锁和非公平锁是两种不同的锁获取策略。它们影响了线程在竞争锁时的获取顺序和公平性。

公平锁:公平锁是指当多个线程按照申请锁的顺序来获取锁,即先来先得的原则。当一个线程发出获取锁的请求时,如果当前锁被其他线程占用,该线程会进入等待队列,按照先后顺序等待锁的释放。当锁释放时,等待时间最长的线程会获得锁。公平锁的特点是能够保证锁的获取是按照线程的申请顺序进行的,避免了线程饥饿的情况。

非公平锁:非公平锁是指当多个线程竞争锁时,不考虑申请锁的顺序,允许"插队",即如果一个线程发出获取锁的请求时,如果当前锁没有被其他线程占用,那么该线程可以直接获取到锁,而无需进入等待队列。这样可能导致某些线程会一直获取到锁,而其他线程长时间得不到执行,出现线程饥饿的情况。

在实际应用中,选择公平锁或非公平锁取决于具体的业务场景和性能需求。公平锁能够保证线程按照申请顺序获取锁,但可能会带来一定的性能开销;而非公平锁可能会提高系统的吞吐量,但可能造成某些线程长时间得不到执行的情况。因此,在使用锁的时候需要根据实际情况进行选择,以达到最佳的性能和公平性。

    1. 一个线程中的多个流程能不能获取同一把锁? 能可重入锁 不能非可重入锁

可重入锁:可重入锁是指当一个线程持有锁时,可以再次获取同一个锁,而不会被自己所持有的锁所阻塞。也就是说,可重入锁允许同一个线程多次获得同一个锁,而不会出现死锁的情况。这种特性使得线程可以在执行同步代码块时,调用其他需要获取同一把锁的方法,而不会造成阻塞。

可重入锁的实现通常是通过给每个锁关联一个获取计数器和一个持有线程的标识来实现的。当线程第一次获取锁时,计数器+1,并将持有线程标记为当前线程。当同一个线程再次获取锁时,计数器再次+1,线程继续持有锁。只有当计数器归零时,锁才会被完全释放。

非可重入锁:非可重入锁是指当一个线程持有锁时,再次获取同一个锁时会被自己所持有的锁所阻塞,从而导致死锁。也就是说,非可重入锁不允许同一个线程多次获取同一把锁,否则会造成阻塞。

非可重入锁的设计通常是没有关联的获取计数器,因此同一个线程在获取锁之后再次获取锁时,会因为无法通过获取检查而被阻塞,导致死锁的发生。在实际应用中,可重入锁是最常见和常用的锁机制。它允许线程多次获取同一把锁,可以提高代码的灵活性和可维护性。而非可重入锁很少使用,因为它容易导致死锁问题,不利于并发编程的实现。

    1. 多个线程能不能共享一把锁 能共享锁 不能 排他锁

共享锁:共享锁允许多个事务同时对同一份数据进行读取操作,但不允许任何事务对该数据进行写入操作。也就是说,共享锁能够提供并发的读取能力,多个事务可以同时读取相同的数据,而不会相互影响。在共享锁下,读取操作不会阻塞其他读取操作,但会阻塞写入操作。

排他锁:排他锁是一种独占锁,它在被一个事务获取后会阻塞其他事务对同一份数据的读取和写入操作。也就是说,排他锁只允许一个事务对数据进行写入操作,并且在该事务持有锁期间,其他事务无法对该数据进行读取或写入操作。排他锁确保了在修改数据时的一致性,但可能会降低并发性能。

在数据库系统中,共享锁和排他锁通常用于实现事务隔离级别。例如,在读取数据时可以使用共享锁,以提高并发性能;在修改数据时可以使用排他锁,以确保数据的一致性。合理地使用共享锁和排他锁可以有效地控制并发访问,保证数据的完整性和一致性。

1.4 Synchronized锁的特性(同步锁的特性)

可重入性:线程从锁A到锁B之后,还能到锁A里去。

为什么可以重入/如何判断一个对象现在有没有锁:对象的头存着锁信息(计数器)count,加了锁就++,从锁出去就--,当count==0时,就是没有锁。

不可中断性:一个线程获得锁后,另一个线程想要获得资源,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断。

不可打断性:一旦进入了这个锁,必须要执行完任务后出来。

1.5 Synchronized原理

监视器------monitor

1.monitorenter

当 JVM 执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应的monitor的所有权。过程如下:

  • 若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为monitor的owner(所有者)。
  • 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1。
  • 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

2.monitorexit

  • 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  • 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权 。
1.6 Synchronized使用方式

(悲观锁,非公平锁,可重入锁)

修饰实例方法:作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁。

修饰静态方法:给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表明这是该类的一个静态资源,不管new了多少对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法占用的锁是当前类的,而访问非静态synchronized方法占用的锁是当前实例对象锁。

修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块先获取给定对象的锁

尽量不要使用synchronized(String a)因为JVM中,字符串常量池具有缓存功能。

1.7 Volatile使用方式

Volatile只能作用于变量,不能作用于方法和代码块,是线程同步的轻量级实现,性能比synchronized好,与synchronized是互补关系。

线程

2.1 进程和线程
  • 进程:操作系统的独立执行单元,可以类比于一个应用程序运行的实体,进程间资源不共享(可以跨进程实现需求)。
  • 线程:进程的子任务,CPU调度基本单元,资源共享。
  • 进程间通信ipc:数据传输、资源共享、事件通知、进程控制。

进程详解:

在linux系统中,通过task_struct这个结构体抽象表示一个进程。

这里mm*的指针用来指向一块虚拟内存,当程序想要运行的时候,操作系统就会把程序里这些重要的段加载到虚拟内存里进行运行。

BSS段不用直接加载,通常是指用来存放程序中未初始化的或者初始化为0的全局变量静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。

随着程序运行,它会使用各种资源,task_struct这个结构体用来管理这些资源和程序

注:可以理解为程序就是静态文件,进程就是程序运行着的实体

2.2 创建线程方式(七种)

java创建和启动线程较为常用的方式有++继承Thread类、实现Runnable接口和匿名内部类++ 的方式,带返回值的方式实现++implements Callable<返回值类型>++。

继承Thread类

重写run方法,start()方法来启动,线程间执行时线程是以抢占式的方式运行。

1.Thread.currentThread(),是Thread类的静态方法,该方法返回当前正在执行的线程对象。

2.getName():该方法是Thread类的实例方法,该方法返当前正在执行的线程的名称。

在默认情况下,主线程的名称为main,用户启动的多线程的名称依次为Thread-0,Thread-1,Thread-3..Thread-n等。

实现Runnable接口

重写run()方法,调用该线程对象的start()方法启动该线程。

匿名内部类

本质上也是一个类实现了Runnable接口,重写了run方法,只不过这个类没有名字,直接作为参数传入Thread类。(适用于创建启动线程次数较少的环境)

实现implements Callable<返回值类型>

以上方式,都没有返回值且都无法抛出异常。

Callable和Runnbale一样代表着任务,只是Callable接口中不是run(),而是call()方法,但两者相似,即都表示执行任务,call()方法的返回值类型即为Callable接口的泛型。

定时器方法java.util.Timer

Timer有不可控的缺点,当任务未执行完毕或我们每次想执行不同任务时候,实现起来比较麻烦。

Lambda 表达式的实现(parallelStream)

计算后的结果为21,事实证明是并行执行

线程池方式java.util.concurrent.Executor接口

2.3 线程池(四种)

1.Executor 框架

ThreadPoolExecutor类(推荐使用)

线程池实现类ThreadPoolExecutor是Executor框架最核心的类。

  • corePoolSize : 核心线程数线程数。
  • maximumPoolSize : 最大线程数。
  • workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
  • keepAliveTime:当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime才会被回收销毁;
  • unit : keepAliveTime参数的时间单位。
  • threadFactory :工厂。
  • handler :饱和策略。

参数设计

核心线程数设计为二八原则

队列长度设计为核心线程数除以(每个线程执行需要的时间*2

最大线程数= (最大任务数- 任务队列长度)* 单个任务执行时间

最大空闲时间根据系统调整参数

Ctl是一个原子类的常量,为了保证线程安全,它存储了线程池状态,以及工作的线程数(二进制方式)3bit+29bit

2.SingleThreadExecutor 只有一个线程的线程池

不推荐使用 SingleThreadExecutor?

SingleThreadExecutor使用无界队列 LinkedBlockingQueue作为线程池的工作队列。会对线程池带来的影响,导致OOM。

3.CachedThreadPool 是一个会根据需要创建新线程的线程池

不推荐使用CachedThreadPool?

CachedThreadPool可能会创建大量线程,导致OOM。

4.ScheduledThreadPoolExecutor 主要用来在给定的延迟后运行任务,或者定期执行任务。

2.4 阿里巴巴Java开发手册规范

在《阿里巴巴 Java 开发手册》"并发处理"章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。

另外,《阿里巴巴Java开发手册》中强制线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor构造函数的方式,这样的处理方式规避资源耗尽的风险。

2.5 常见问题

为什么调用start()方法时会执行run()方法,为什么不能直接调用run()方法?

调用 start 方法方可启动线程并使线程进入就绪状态,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

线程池中为什么要使用阻塞队列的原因?

线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。

另外一方面,如果新任务的到达速率超过了线程池的处理速率,那么新到来的请求将累加起来,这样的话将耗尽资源。

什么是线程死锁如何避免线程死锁?

什么是线程死锁

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

产生死锁必须具备以下四个条件:

  • 互斥条件:该资源任意一个时刻只由一个线程占用。
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁?

  • 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  • 破坏请求与保持条件 :一次性申请所有的资源。
  • 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

说说sleep()方法和wait()方法区别和共同点?

两者最主要的区别在于:sleep方法没有释放锁,而wait方法释放了锁。

两者都可以暂停线程的执行。

Wait通常被用于线程间交互/通信,sleep通常被用于暂停执行。

wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify() 或者notifyAll()方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

现有线程T1、T2和T3。怎样确保T2线程在T1之后执行,T3线程在T2后执行?

可以用Thread类的join方法实现这一效果、Wait 、线程池

多线程的线程安全问题怎么解决?有哪几种方法?

(悲观锁)synchronized代码块、synchronized方法、Lock锁。

(乐观锁)假设不会发生并发冲突,每次不加锁而是假设没有冲突而去完成某项操作,只在提交操作时检查是否违反数据完整性。如果因为冲突失败就重试,直到成功为止。CAS算法。

Lock 和 Synchronized 区别?

  • lock 是一个接口,而synchronized是java的一个关键字,synchronized是内置的语言实现;
  • synchronized在发生异常时候会自动释放占有的锁,因此不会出现死锁;而lock发生异常时候, 不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。);
  • lock等待锁过程中可以用interrupt来终端等待,synchronized 只能等待锁的释放,不能响应中 断;
  • lock可以通过try lock来知道有没有获取锁,而synchronized不能;
  • Lock可以提高多个线程进行读操作的效率(可以通过readwritelock实现读写分离)。

集合

3.1 集合基础、数据结构
  • 时间复杂度:算法执行所需时间的增长率。
  • 空间复杂度:算法执行所需的存储空间的增长率。
  • List:可以有多个元素引用相同的对象,有序。
  • Set:不允许重复的集合。不会有多个元素引用相同的对象。
  • Map: 使用键值对存储。Key不能重复,两个Key可以引用相同的对象。

#### 集合相关

1 Set 集合去重之后会存在哪些问题?

经set去重后,列表会变成乱序,和原列表比较的话,会显示两者不等所以set去重后不能直接比较再次排一次序就可以了,两个就相等了。

2 ArrayList 怎么去重?

声明2个ArrayList,分别为listA与listB,listA为待去重list,listB保存去重之后数据。遍历listA,然后判断listB中是否包含各个元素,若不包含,把此元素加入到listB中。

利用set集合进行去重。

3 Arraylist 和LinkedList数据结构?

ArrayList底层是一个数组 ,所以查询快,LinkedList底层是一个链表,所以增删快。

LinkedList是一个实现了List接口和Deque接口的双向链表。

ArrayList 的底层是数组队列,相当于动态数组。

4 Arraylist 与LinkedList区别?

1.是否保证线程安全:两者都是不同步的,不保证线程安全;

2.底层数据结构:Arraylist底层使用的是Object数组;LinkedList底层使用的是双向链表;

3.是否支持快速随机访问:LinkedList不支持高效的随机元素访问,ArrayList支持。快速随机访问就是通过元素的序号快速获取元素对象get(int index)方法;

4.内存空间占用:ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

5 ArrayList 的拓容机制?

ArrayList每次扩容都为原先容量1.5倍。

6 ArrayList 和Vector的区别?

这两个类都实现了List接口(List接口继承了Collection接口),他们都是有序集合。

(1)同步性:

Vector是线程安全的,也就是说是它的方法之间是线程同步的,而ArrayList是线程序不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用ArrayList,因为它不考虑线程安全,效率会高些;如果有多个线程会访问到集合,那最好是使用Vector,因为不需要我们自己再去考虑和编写线程安全的代码。

(2)数据增长:

ArrayList与Vector都有一个初始的容量大小,当存储进它们里面的元素的个数超过了容量时,就需要增加ArrayList与Vector的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要取得一定的平衡。

即Vector增长原来的一倍,ArrayList增加原来的0.5倍。

#### 数据结构相关

排序算法

  • 冒泡排序:通过多次遍历数组,比较相邻元素的大小并交换位置,将较大的元素逐渐"冒泡"到数组的末尾。时间复杂度为O(n^2),属于稳定排序算法。冒泡排序可以应用于对象排序,需要在比较时定义对象间的大小关系。
  • 选择排序:每次从未排序的部分中选择最小(或最大)的元素,并将其放置在已排序部分的末尾。时间复杂度为O(n^2),属于不稳定排序算法。选择排序同样可以应用于对象排序,需要在比较时定义对象间的大小关系。
  • 插入排序:将数组分为已排序和未排序两部分,每次从未排序部分选择一个元素插入到已排序部分的正确位置。时间复杂度为O(n^2),属于稳定排序算法。插入排序同样适用于对象排序,需要在比较时定义对象间的大小关系。

高级排序算法

  • 快速排序:通过选定一个基准元素,将数组分割成两个子数组,然后对子数组分别进行快速排序。时间复杂度平均情况为O(nlogn),属于不稳定排序算法。快速排序通常比较高效,在大多数情况下都优于冒泡排序、选择排序和插入排序。
  • 归并排序:采用分治法,将数组递归地分成更小的数组,然后合并已经排序的子数组。时间复杂度始终为O(nlogn),属于稳定排序算法。归并排序的优点是可以应用在链表上,并且性能稳定。
  • 希尔排序:也称作缩小增量排序,是插入排序的一种更高效改进版本。希尔排序通过将相距较远的元素进行比较和交换,从而使得元素能够更快地回到合适的位置。时间复杂度取决于选择的增量序列,最差情况下为O(n^2),但是在一般情况下要好于简单插入排序。

查找算法

  • 线性查找:线性查找是一种基本的查找算法,也称为顺序查找。它从数组或列表的起始位置开始逐个比较元素,直到找到目标元素或遍历完整个数据结构。时间复杂度为O(n),其中n是元素的数量。线性查找适用于小规模的数据或无序数据。
  • 二分查找:二分查找是一种高效的查找算法,但要求待查找的数据结构必须是有序的。该算法通过将目标元素与数组或列表的中间元素进行比较,从而将查找范围缩小一半。如果目标元素小于中间元素,则在左半部分继续查找;如果目标元素大于中间元素,则在右半部分继续查找;如果目标元素等于中间元素,则找到了目标元素。时间复杂度为O(log n),其中n是元素的数量。
  • 哈希查找:哈希查找利用哈希函数将关键字映射到特定的存储位置,从而快速定位目标元素。在哈希表中,通过给定的关键字计算出对应的哈希值,然后访问该位置的元素。哈希查找的平均时间复杂度为O(1),但在最坏情况下可能达到O(n),其中n是元素的数量。
  • 二叉查找树:二叉查找树是一种二叉树结构,其中每个节点的左子树上的所有节点都小于该节点的值,右子树上的所有节点都大于该节点的值。通过比较目标值与当前节点的值,可以在二叉查找树上进行快速查找。二叉查找树的平均查找时间复杂度为O(log n),但在最坏情况下可能退化为链表,导致查找时间复杂度为O(n)。
3.2 HashMap详解

1 hashCode() 介绍

定义在JDK的Object.java中的方法,返回一个int整数,这就意味着 Java中的任何类都包含有hashCode()函数。

作用是获取哈希码,确定该对象在哈希表中的索引位置。

2 为什么要有hashCode

我们以"HashSet如何检查重复"为例子来说明为什么要有 hashCode:

把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同 时也会与其他已经加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。

3 hashMap 和HashSet的区别

HashSet底层调用的是 HashMap。

HashMap储存键值对 HashSet仅仅存储对象

使用 put()方法将元素放入map中 使用add()方法将元素放入

HashMap中使用键对象key来计算hashcode值

HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以 equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false

4 HashMap 的数据结构;线程安全么?为什么?

答:不安全,可能造成死循环,具体表现链表的循环指向;应该使用 ConcurrentHashMap。

5 谈一下HashMap的底层原理是什么?

HashMap的主干是Entry数组。

Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。

HashMap由数组+链表组成的。

数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。

如果定位到的数组索引位置不含链表,那么对于查找,添加等操作很快,

如果定位到的数组包含链表,对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

基于hashing的原理,jdk8后采用数组+链表+红黑树的数据结构。我们通过put和get存储和获取对象。当我们给put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。

6 谈一下HashMap中put是如何实现的?

1.计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)

2.如果散列表为空时,调用resize()初始化散列表

3.如果没有发生碰撞,直接添加元素到散列表中去

4.如果发生了碰撞(hashCode值相同),进行三种判断

4.1若key地址相同或者equals后内容相同,则替换旧值

4.2如果是红黑树结构,就调用树的插入方法

4.3链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。

5.如果桶满了大于阀值,则resize进行扩容

7 谈一下HashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?

调用场景:

1.初始化数组table

2.当数组table的size达到阙值时即++size > load factor * capacity 时,也是在putVal函数中

实现过程:

扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。

8 谈一下HashMap中get是如何实现的?

对key的hashCode进行hashing,与运算计算下标获取bucket位置,如果在桶的首位上就可以找到就直接返回,否则在树中找或者链表中遍历找,如果有hash冲突,则利用equals方法去遍历链表查找节点。

9 为什么不直接将key作为哈希值而是与高16位做异或运算?

因为数组位置的确定用的是与运算,仅仅最后四位有效,设计者将key的哈希值与高16为做异或运算使得在做&运算确定数组的插入位置时,此时的低位实际是高位与低位的结合,增加了随机性,减少了哈希碰撞的次数。

10 为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?

HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,必须是2的幂。

1.为了数据的均匀分布,减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)

2.输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字

11 谈一下当两个对象的hashCode相等时会怎么样?

产生哈希碰撞,key值相同则替换旧值,不然链接到链表后面,链表长度超过阙值8就转为红黑树存储

12 请解释一下HashMap的参数loadFactor,它的作用是什么?

loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor。

13 如果HashMap的大小超过了负载因子(load factor)定义的容量,怎么办?

超过阙值会进行扩容操作,概括的讲就是扩容后的数组大小是原数组的2倍,将原来的元素重新hashing放入到新的散列表中去。

14 传统HashMap的缺点(为什么引入红黑树)?

JDK 1.8以前HashMap的实现是 数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当HashMap中有大量的元素都存放到同一个桶中时,这个桶下有一条长长的链表,这个时候HashMap 就相当于一个单链表,假如单链表有n个元素,遍历的时间复杂度就是O(n),完全失去了它的优势。针对这种情况,JDK 1.8中引入了红黑树(查找时间复杂度为 O(logn))来优化这个问题。

15 使用HashMap时一般使用什么类型的元素作为Key?

择Integer,String这种不可变的类型,像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的。

16 HashMap 和HashTable和ConcurrentMap的区别和联系?

HashMap是非线程安全的,HashTable是线程安全的。

HashMap的键和值都允许有null值存在,而HashTable则不行。

HashMap最多只允许一条记录的键为null,允许多条记录的值为null。

因为线程安全的问题,HashMap效率比HashTable的要高。

HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。

如果需要满足线程安全,可以用Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap,是HashMap的多线程版本。

3.3 LinkedHashMap详解

继承自HashMap,可以按照元素的插入顺序来遍历或访问元素。

双向链表。

特点

支持键值对的存储。

允许插入null键和null值。

保持插入顺序。

提供了按照访问顺序进行遍历的功能(通过构造函数设置accessOrder参数为true)。

3.4 ConcurrentHashMap(线程安全原理)

HashMap 和ConcurrentHashMap的区别

1、HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。

2、ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。

3、ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。

ConcurrentHashmap 原理

synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性查找,替换,赋值操作都使用CAS

锁:锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容。

Node的val和next使用volatile修饰,读写线程对该变量互相可见数组用volatile修饰,保证扩容时被读线程感知。

实际的数据结构是跟hashmap一样的,只是多了一些并发的控制。

LongAddr:作为ConcurrentHashMap统计元素总数数据结构。使用Cells数组进行多线程的值的累加,求和的时候把base+数组中每个元素的值

添加或者删除元素的时候,使用Synchronized锁住数组位置上的首元素。

3.5 SparseArray(Android)

介绍

以键值对形式进行存储,基于分查找,因此查找的时间复杂度为0(LogN);

由于SparseArray中Key存储的是数组形式,因此可以直接以int作为Key。避免了HashMap的装箱拆箱操作,性能更高且int的存储开销远远小于Integer;

采用了延迟删除的机制(针对数组的删除扩容开销大的问题的优化) ;

延迟删除机制

通过将删除KEY的Value设置DELETED,方便之后对该下标的存储进行复用;

使用二分查找,时间复杂度为O(LogN),如果没有查找到,那么取反返回左边界,再取反后,左边界即为应该插入的数组下标;

如果无法直接插入,则根据mGarbage标识(是否有潜在延迟删除的无效数据),进行数据清除,再通过System.arraycopy进行数组后移,将目标元素插入二分查找左边界对应的下标;

mSize小于等于keys.length,小于的部分为空数据或者是gc后前移的数据的原数据(也是无效数据),因此二分查找的右边界以mSize为准;mSize包含了延迟删除后的元素个数;

如果遇到频繁删除,不会触发gc机制,导致mSize 远大于有效数组长度,造成性能损耗;

根据源码,可能触发gc操作的方法有(1、put;2、与index有关的所有操作,setValueAt()等;3、size()方法;)

mGarbage为true不一定有无效元素,因为可能被删除的元素恰好被新添加的元素覆盖;

使用场景

key为整型;

不需要频繁的删除;

元素个数相对较少;

JVM

JVM 工作原理

跨平台,java源文件通过javac命令启动编译器,将java文件编译成class字 节码文件,class字节码文件通过java命令启动jvm虚拟机加载class字节码文件。

内存管理:线程共享和线程私有内存线程

共享内存:堆、方法区、常量池线程

私有内存:java栈、本地方法栈

类加载机制:父类委托模式

垃圾回收:大部分的GC都是采用分代收集算法的

GC调优[译]GC专家系列3-GC调优_maxgcpausemillis-CSDN博客

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区(JDK1.8之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈。

与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。

1 为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢

程序计数器主要用于改变程序计数器来依次读取指令用于流程控制,还有在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

2 堆和方法区

堆是进程中最大的一块内存,主要用于存放新创建的对象

方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据

4.1 内存模式

1.Java 对象的创建过程(五步,每一步虚拟机做了什么)

**类加载:**在程序首次使用该类时,虚拟机会通过类加载器加载对象的类文件,并进行类的初始化工作,包括静态变量的初始化和执行静态代码块。

**分配内存:**在类加载完成后,接下来虚拟机需要在堆(Heap)中为对象分配内存空间。虚拟机的分配方式有两种:指针碰撞(Bump the Pointer)和空闲列表(Free List)。在选择完合适的内存空间之后,虚拟机将会对内存空间进行零值化,确保对象的属性都能被正确初始化。

**初始化零值:**在分配内存空间后,虚拟机会对对象进行必要的零值初始化,包括对象头信息、实例变量等。

**执行构造方法:**接着,虚拟机会调用对象的构造方法来进行一些特定的初始化操作,包括初始化实例变量等。构造方法的执行可能会涉及到对象的初始化顺序、父类构造方法的调用等。

**返回对象地址:**最后一步是将对象的引用返回给程序,使得程序能够通过该引用来访问新创建的对象。

Java对象的创建过程包括类加载、内存分配、零值初始化、构造方法执行以及返回对象地址这五个步骤。这些步骤确保了对象的正确初始化和合理分配内存,从而让程序能够正常地使用新创建的对象。

2. 对象的访问定位的两种方式(句柄和直接指针两种方式)

句柄方式,Java堆中不直接存储对象实例数据,而是将对象存储在一个称为"堆外"的地方。在Java堆中则只存储对象实例的句柄,即对象的引用。这个句柄是一个具体的指针,指向堆外存储的对象实例数据的地址。当要访问对象实例时,首先通过句柄在Java堆中找到对象的引用,然后再根据引用中的指针定位到堆外的实际对象数据。

现代的Java虚拟机中,已经不再使用句柄,使用直接指针方式,Java堆中直接存储对象实例的数据,而对象引用就是指向对象实例数据的指针。这样,当需要访问对象时,可以直接通过引用指针来定位到对象实例的数据,而不需要额外的句柄进行间接定位。

4.2 垃圾回收算法

1.GC 垃圾回收的几种算法

标记-清除算法 该算法先标记,后清除,将所有需要回收的算法进行标记,然后清除;这种算法的缺点是:效率比较低;标记清除后会出现大量不连续的内存碎片,这些碎片太多可能会使存储大对象会触发GC回收,造成内存浪费以及时间的消耗。

复制算法 复制算法将可用的内存分成两份,每次使用其中一块,当这块回收之后把未回收的复制到另一块内存中,然后把使用的清除。这种算法运行简单,解决了标记-清除算法的碎片问题,但是这种算法代价过高,需要将可用内存缩小一半,对象存活率较高时,需要持续的复制工作,效率比较低。

标记整理算法 标记整理算法是针对复制算法在对象存活率较高时持续复制导致效率较低的缺点进行改进的,该算法是在标记-清除算法基础上,不直接清理,而是使存活对象往一端游走,然后清除一端边界以外的内存,这样既可以避免不连续空间出现,还可以避免对象存活率较高时的持续复制。这种算法可以避免 100%对象存活的极端状况,因此老年代不能直接使用该算法。

分代收集算法 分代收集算法就是目前虚拟机使用的回收算法,它解决了标记整理不适用于老年代的问题,将内存分为各个年代,在不同年代使用不同的算法,从而使用最合适的算法,新生代存活率低,可以使用复制算法。而老年代对象存活率搞,没有额外空间对它进行分配担保,所以只能使用标记清除或者标记整理算法。

2. 如何判断对象是否死亡(两种方法)

引用计数法:引用计数法是一种简单的内存管理技术,它通过对对象的引用计数进行跟踪来确定对象是否可以被回收。当对象被创建时,引用计数为1;每当有新的引用指向该对象时,引用计数加1;当引用失效或被销毁时,引用计数减1。当对象的引用计数变为0时,就意味着该对象已经没有被任何引用指向,可以被回收。

可达性分析法:可达性分析法是一种更为普遍和高效的内存管理技术,它是现代编程语言中常用的垃圾回收算法之一。可达性分析法从一组称为"根"的对象开始,递归地遍历所有通过引用和指针可达的对象。如果一个对象无法通过任何路径与"根"对象相连,那么这个对象就被认为是不可达的,即"死亡"的对象,可以被回收。

3. 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。

**强引用:**强引用是最常见的引用类型。当我们通过一个变量来引用一个对象时,这个引用就是强引用。只要强引用存在,垃圾回收器就无法回收被引用的对象。

**软引用:**软引用是一种相对强引用弱化的引用类型。如果一个对象只有软引用关联,垃圾回收器在内存不足时,会尝试回收这些对象。使用软引用可以实现缓存等功能,提高系统的性能和效率。

**弱引用:**弱引用比软引用更弱化。如果一个对象只有弱引用关联,那么在下一次垃圾回收时,无论内存是否充足,都会被回收。弱引用经常用于实现一些容器类,如WeakHashMap。

**虚引用:**虚引用是最弱化的引用类型。它无法通过虚引用来获取被引用的对象,也无法阻止对象被垃圾回收。虚引用主要用于跟踪对象被垃圾回收的状态。

软引用和弱引用的区别在于垃圾回收器对待它们的回收策略不同。软引用只有在内存不足时才会被回收,而弱引用则在下一次垃圾回收时就可能被回收。

使用软引用可以带来一些好处,例如:

  • 在内存紧张的情况下,垃圾回收器可以回收被软引用引用的对象,释放内存空间,避免内存溢出。
  • 软引用可以用于实现缓存机制,当内存不足时,自动清除缓存中的对象,避免大量占用内存。
  • 软引用还可以用于临时保存对象,当需要时再获取,避免频繁地创建和销毁对象。

4.HotSpot 为什么要分为新生代和老年代?

将堆内存划分为新生代和老年代有助于根据对象的生命周期特点,采用更合适的垃圾回收算法,并优化内存管理,从而提高垃圾回收效率和系统性能。

5. 常见的垃圾回收器有哪些,介绍一下CMS,G1收集器。

CMS 收集器:它是一种以获取最短回收停顿时间为目标的垃圾回收器。CMS收集器采用了并发标记和并发清除的方式,在垃圾回收期间,部分工作与应用线程并发执行,以减少回收停顿时间。

G1 收集器:它是一种面向服务器端应用的垃圾回收器,它的设计目标是更好地满足长时间运行的应用程序的性能需求。G1收集器采用了分代式的垃圾回收策略,并且具备增量并发标记和并发清理的能力。

6.Android 的GC和Java的GC之间区别

由于Android平台的特殊性和需求,Android的GC实现与Java的GC有一些差异。

GC **算法选择:**Android的GC实现使用了不同的垃圾回收算法。在早期版本的Android中,Dalvik虚拟机使用的是标记-清除(Mark and Sweep)算法。而在当前的Android版本中,使用的是基于分代的并发式复制(Generational Concurrent Copying)算法的垃圾回收器,称为G1 GC(Garbage First Garbage Collector)。

**堆结构和大小:**Android设备通常具有更有限的内存资源。因此,Android的GC会针对较小的堆空间进行优化,以提高效率和响应性能。相比之下,Java虚拟机通常运行在具有更大堆空间和更高内存容量的服务器或桌面环境中。

**并发和暂停时间:**Android的GC需要考虑到移动设备的响应性能,因此尽量减少GC暂停时间。Android的G1 GC是一种并发式的垃圾回收器,它可以在应用程序运行的同时进行垃圾收集,以减少暂停时间。相比之下,Java虚拟机的垃圾回收通常会导致较长的暂停时间,因为在回收过程中需要停止应用程序的执行。

**内存压缩:**Android的GC实现通常包含内存压缩功能。由于Android设备的内存有限且容易产生内存碎片,GC会尝试对空闲内存块进行整理和压缩,以提高内存利用率和性能。这种内存压缩功能在Java虚拟机中并不常见。

需要注意的是,Android的GC实现可能会因不同的Android版本和设备而有所差异。每个版本和设备都可能使用不同的GC策略和算法来优化内存管理和性能。

网络

5.1 网络模式

根据TCP/IP协议,各层的要实现的功能如下:

  • LLC层:处理传输错误;调节数据流,协调收发数据双方速度,防止发送方发送得太快而接收方丢失数据。主要使用数据链路协议。
  • 网络层:本层也被称为IP层。LLC层负责把数据从线的一端传输到另一端,但很多时候不同的设备位于不同的网络中(并不是简单的网线的两头)。此时就需要网络层来解决子网路由拓扑问题、路径选择问题。在这一层主要有IP协议、ICMP协议。
  • 传输层:由网络层处理好了网络传输的路径问题后,端到端的路径就建立起来了。传输层就负责处理端到端的通讯。在这一层中主要有TCP、UDP协议
  • 应用层:经过前面三层的处理,通讯完全建立。应用层可以通过调用传输层的接口来 编写特定的应用程序。而TCP/IP协议一般也会包含一些简单的应用程序如Telnet远程登录、 FTP文件传输、SMTP邮件传输协议。实际上,在发送数据时,经过网络协议栈的每一层,都会给来自上层的数据添加上一个数据包的头,再传递给下一层。在接收方收到数据时,一层层地把所在层的数据包的头去掉,向上层递交数据

tcp和udp的区别和应用场景

TCP 面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接。

TCP 提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP 尽最大努力交付,即不保证可靠交付。

TCP 面向字节流,实际上是 TCP 把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)

每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信

TCP 首部开销 20 字节;UDP 的首部开销小,只有 8 个字节 6、TCP 的逻辑通信信道是全双工的可靠信道,UDP 则是不可靠信道 应用场景:TCP 协议(如文件传输、重要状态的更新等);反之,则使用 UDP 协议(如视频传输、实时通信等)

5.2 TCP/IP协议栈

标准TCP/IP协议是用于计算机通信的一组协议,通常称为TCP/IP协议栈,通俗讲就是符合以太网通信要求的代码集合,一般要求它可以实现网络模式图中对应的协议,比如应用层的HTTP、FTP、DNS、SMTP协议,传输层的TCP、UDP协议、网络层的IP、ICMP协议等等。

为什么使用协议栈

物理层主要定义物理介质性质,MAC子层负责与物理层进行数据交接,这两部分是与 硬件紧密联系的,就嵌入式控制芯片来说,很多都内部集成了MAC控制器,完成MAC子 层功能,所以依靠这部分功能是可以实现两个设备数据交换,而时间传输的数据就是MAC数据包,发送端封装好数据包,接收端则解封数据包得到可用数据,这样的一个模型与使用USART控制器实现数据传输是非常类似的。但如果将以太网运用在如此基础的功能上,完全是大材小用,因为以太网具有传输速度快、可传输距离远、支持星型拓扑设备连接等等强大功能。功能强大的东西一般都会用高级的应用,这也是设计者的初衷。

5.3 HTTP

用于在客户端和服务器之间传输超文本数据的应用层协议。

http 的请求方式有哪些

opions 返回服务器针对特定资源所支持的 HTML 请求方法 或 web 服务器发送*测试服务器 功能(允许客户端查看服务器性能)

Get向特定资源发出请求(请求指定页面信息,并返回实体主体)

Post向指定资源提交数据进行处理请求(提交表单、上传文件),又可能导致新的资源的建立 或原有资源的修改

Put向指定资源位置上上传其最新内容(从客户端向服务器传送的数据取代指定文档的内容)

Head与服务器索与 get 请求一致的相应,响应体不会返回,获取包含在小消息头中的原信息(与get请求类似,返回的响应中没有具体内容,用于获取报头)

Delete请求服务器删除request-URL所标示的资源*(请求服务器删除页面)

Trace回显服务器收到的请求,用于测试和诊断

Connect HTTP/1.1 协议中能够将连接改为管道方式的代理服务器 http 服务器至少能实现 get、head、post 方法,其他都是可选的。

http 演进过程:1.0-->1.1-->2.0

HTTP 1.0:HTTP 1.0是最早的版本,于1996年发布。它采用短连接方式,每次请求都需要建立一个新的TCP连接,并在完成后断开连接。该版本功能相对简单,没有持久连接的概念,每个请求和响应都需要独立的连接。

HTTP 1.1:HTTP 1.1是在1999年发布的版本,目前仍然是最广泛使用的版本。它引入了持久连接(Keep-Alive)的概念,允许在单个TCP连接上发送多个请求和响应,减少了建立和断开连接的开销。此外,HTTP 1.1还引入了请求管线化(Pipeline)和分块传输编码(Chunked Transfer Encoding)等特性,提高了性能和效率。

HTTP 2.0:HTTP 2.0是在2015年发布的版本,旨在进一步提升性能和效率。它基于Google的SPDY协议进行扩展,引入了多路复用(Multiplexing)的特性,允许在同一个TCP连接上同时传输多个请求和响应。此外,HTTP 2.0还支持头部压缩(Header Compression)、服务器推送(Server Push)等功能,减少了网络延迟和带宽消耗。

每个版本都引入了新的特性和改进,旨在提高性能、效率和用户体验。

设计模式

  • 创建型模式:共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
  • 结构型模式:共7种:适配器模式、装饰器模式、代理模式、桥接模式、外观模式、组合模式、享元模式
  • 行为型模式:共11种:策略模式、模板方法模式、观察者模式、责任链模式、访问者模式、中介者模式、迭代器模式、命令模式、状态模式、备忘录模式、解释器模式
  • 还有两类:并发型模式和线程池模式

下图为设计模式之间关系:

6.1 设计模式的六大原则
  • 开闭原则

开闭原则指的是对扩展开放,对修改关闭。在对程序进行扩展的时候,不能去修改原有的代码,想要达到这样的效果,我们就需要使用接口或者抽象类

  • 依赖倒转原则

依赖倒置原则是开闭原则的基础,指的是针对接口编程,依赖于抽象而不依赖于具体

  • 里氏替换原则

里氏替换原则是继承与复用的基石,只有当子类可以替换掉基类,且系统的功能不受影响时,基类才能被复用,而子类也能够在基础类上增加新的行为。所以里氏替换原则指的是任何基类可以出现的地方,子类一定可以出现。

里氏替换原则是对 "开闭原则" 的补充,实现 "开闭原则"的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏替换原则是对实现抽象化的具体步骤的规范。

  • 接口隔离原则

使用多个隔离的接口,比使用单个接口要好,降低接口之间的耦合度与依赖,方便升级和维护方便

  • 迪米特原则

迪米特原则,也叫最少知道原则,指的是一个类应当尽量减少与其他实体进行相互作用,使得系统功能模块相对独立,降低耦合关系。该原则的初衷是降低类的耦合,虽然可以避免与非直接的类通信,但是要通信,就必然会通过一个"中介"来发生关系,过分的使用迪米特原则,会产生大量的中介和传递类,导致系统复杂度变大,所以采用迪米特法则时要反复权衡,既要做到结构清晰,又要高内聚低耦合。

  • 合成复用原则

尽量使用组合/聚合的方式,而不是使用继承。

6.2 工厂模式

建立一个工厂类,并定义一个接口对实现了同一接口的产品类进行创建。首先看下关系图:

6.3 单例模式

可以确保系统中某个类只有一个实例,该类自行实例化并向整个系统提供这个实例的公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

单例模式的优点在于:系统中只存在一个共用的实例对象,无需频繁创建和销毁对象,节约了系统资源,提高系统的性能。

可以严格控制客户怎么样以及何时访问单例对象。

单例模式的写法有好几种,主要有三种:懒汉式单例、饿汉式单例、登记式单例。

懒汉式懒汉式单例是指在第一次使用时才会创建单例实例。这意味着在多线程环境下,可能会出现并发安全性问题,需要通过加锁等手段来确保线程安全。通常使用synchronized关键字或者双重检查锁(double-checked locking)来实现懒汉式单例的线程安全性。

饿汉式单例: 饿汉式单例是指在类加载的时候就创建单例实例,因此在多线程环境下也可以保证线程安全,不需要额外的同步措施。但是,由于实例在类加载时就创建,因此如果该实例在后续运行过程中没有被使用,会造成资源浪费。

6.4 享元模式

享元模式通过共享技术有效地支持细粒度、状态变化小的对象复用,当系统中存在有多个相同的对象,那么只共享一份,不必每个都去实例化一个对象,极大地减少系统中对象的数量,从而节省资源。

享元模式的核心是享元工厂类,享元工厂类维护了一个对象存储池,当客户端需要对象时,首先从享元池中获取,如果享元池中存在对象实例则直接返回,如果享元池中不存在,则创建一个新的享元对象实例返回给用户,并在享元池中保存该新增对象,这点有些单例的意思。

工厂类通常会使用集合类型来保存对象,如HashMap、Hashtable、Vector等,在Java中,数据库连接池、线程池等都是用享元模式的应用。

享元模式的UML结构图如下:

Java中,String类型就是使用享元模式,String对象是final类型,对象一旦创建就不可改变。而Java 的字符串常量都是存在字符串常量池中的,JVM会确保一个字符串常量在常量池中只有一个拷贝。

下面是一个简单的享元模式实例,假设我们有一个游戏中的棋盘,棋盘上有许多棋子,我们希望在内存中只创建一个棋子对象,并在不同的位置上重复使用它们。

首先,我们定义一个抽象的棋子类 ChessPiece ,包含一个方法 move 用于移动棋子:

然后,我们创建具体的棋子类 ConcreteChessPiece ,继承自 ChessPiece ,并实现 move 方法:

接下来,我们创建一个工厂类 ChessPieceFactory ,用于管理和提供共享的棋子对象:

最后,我们可以在客户端代码中使用享元模式:

享元模式通过共享对象来减少内存使用,提高性能。它适用于需要创建大量细粒度对象,并且这些对象可以共享部分或全部状态的场景。

6.5 其他常用模式
  • 观察者模式:定义了一种一对多的依赖关系,使得多个观察者对象同时监听某一个主题对象。常用于当一个对象的状态改变时,需要通知所有依赖于它的对象进行更新操作。
  • 策略模式:定义了一簇算法,将每个算法封装成一个独立的类,并使它们可以相互替换。常用于在运行时动态选择算法,以适应不同的业务需求。
  • 适配器模式:将一个类的接口转换成客户端所期望的另一个接口。常用于解决两个不兼容接口之间的适配问题,使得原本不兼容的类可以合作。

其他问题

1 == 与equals区别?

== : 判断两个对象的地址是不是相等。判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。

equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

情况 1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过"=="比较这两个对象。

情况 2:类覆盖了equals()方法。内容相等,则返回 true (即,认为这两个对象相等)。

2 为什么重写equals时必须重写hashCode方法?

提高效率,采取重写hashcode方法,先进行hashcode比较,如果不同,那么就没必要在进行equals的比较了,这样就大大减少了equals比较的次数。

3 解决hash碰撞?

1、开防定址法2、再哈希法3、链地址法(Java的hashmap的解决办法就是这个)4、建立一个公共溢出区

4 hashCode 与equals?

两个对象的hashCode()相同,则equals()一定为true,对吗?

两个对象equals相等,则它们的hashcode必须相等,反之则不一定。

两个对象==相等,则其hashcode一定相等,反之不一定成立。

两个对象相等,对两个对象分别调用equals方法都返回true

因此,equals方法被覆盖,hashcode也必须被覆盖

5 Java 序列化中如果有些字段不想进行序列化,怎么办?

对于不想进行序列化的变量,使用transient关键字修饰。

transient关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。

6 描述深拷贝和浅拷贝?

浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

7 & 和&&的区别?

&和&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。

&&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式。

&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。

8 final 关键字修饰这三个地方:变量、方法、类,会有什么作用?

对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。

使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。

9 final, finally, finalize 的区别?

final用于声明属性,方法和类,分别表示属性不可变,方法不可覆盖,类不可继承。内部类要访问局部变量,局部变量必须定义成final类型。

finally是异常处理语句结构的一部分,表示总是执行。

finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,可以覆盖此方法提供垃圾收集时的其他资源回收,例如关闭文件等。但是JVM不保证此方法总被调用

10 值传递?

当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递

是值传递。Java语言的方法调用只支持参数的值传递。

11 为什么Java中只有值传递?

首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法 接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值 调用所对应的变量值。它用来描述各种程序设计语言(不只是Java)中方法参数传递方式。Java程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容。

12 值传递和引用传递有什么区别?

值传递:指的是在方法调用时,传递的参数是按值的拷贝传递,传递的是值的拷贝,也就是说传递后就互不相关了。

引用传递:指的是在方法调用时,传递的参数是按引用进行传递,其实传递的引用的地址,也就是变量所对应的内存空间的地址。传递的是值的引用,也就是说传递前和传递后都指向同一个引用(也就是同一个内存空间)。

13 String StringBuffer 和StringBuilder的区别是什么?

String为什么是不可变的? 可变性 简单的来说:String类中使用final关键字修饰字符数组来保存字符串,private final char value[],所以String对象是不可变的。补充:在Java 9之后,String类的实现改用

byte数组存储字符串private final byte[] value;而StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串char[]value但是没有用final关键字修饰,所以这两种对象都是可变的。

线程安全性String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是 StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、 append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的。 性能每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的 String对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。 相同情况下使用StringBuilder相比使用StringBuffer仅能获得10%~15%左右的性能提升,但却有多线程不安全的风险。

总结:1. 操作少量的数据: 适用String 2. 单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder 3. 多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer。

14String对象是否相等?

2.Java 与 C#区别

函数签名

Java泛型擦除(伪泛型) 在编译时把泛型替换掉

new ArrayList<String>();

编译过程中会把String擦除掉 C#是真泛型 传进去什么就是什么

真泛型会产生不同的对象 伪泛型会提高虚拟机的编译速度

3.接口和继承的区别

4.对String的理解

(内存优化)

String 1.8的时候是char数组 jdk9是字节byte[ ]数组 为什么这样设计?

为什么这样改进?存储少了一倍 char需要2个字符 byte需要1个

大部分String数据只需要一个字符 jdk9通过一个判断分配内存大小

String 为什么设计成final 但是通过反射也会改变

String str1 = "helloworld"

String str2 = "hello"+ "world"加号是StringBuilder做的

str1 == str2

true

5.synchronized(a){} 原子性 线程安全 ...

锁对象的级别 对象 类

锁的原理 字节码对象类的区别

锁升级过程 锁的性能1.6后没有什么问题了

锁三大特性

synchronized 和 joc 区别 当前锁的弊端 改进地方

单例的双锁模式

Class A;

A a = new A();懒汉式双重判断

if(a == null)当它为空就不用进入synchronized 了 加快cpu处理

synchronized (A.class){ 锁当前类对象

if(a == null) CPU排序导致的问题

new A();

}线程内存(栈) 主内存(虚拟内存) synchronized 在内存屏障保证原子性

volatile 指令重排序 可见性 不会通过内存屏障 直接通过栈写入主内存

1.泛型

泛型的边界 向上的边界是可读的 向下的边界是可写的

Kotlin 中的泛型支持变型,即协变(covariant)和逆变(contravariant)

基础知识:

Object类相关:Object类的几个关键函数、String涉及到的常量池概念,序列化 & 反序列化

内部类:内部类的分类、应用场景、内部类编译成class后是怎么样的。

抽象类 & 接口:区别、应用场景。

异常:异常体系、自定义异常。

注解:注解的基本概念、分类、编译时注解 & 运行时注解。

类加载的过程。

泛型:分类、通配符 & 上下边界、泛型擦除。

反射:使用。

动态代理

NIO:Android NIO 系列教程(一) NIO概述-CSDN博客

相关推荐
工程师老罗1 小时前
我用AI学Android Jetpack Compose之开篇
android·android jetpack
热心市民运维小孙1 小时前
Mysql8主从复制(兼容低高版本)
android·adb
佛系小嘟嘟1 小时前
Android Jetpack Compose开发小组件【入门篇】
android·开发语言·android jetpack·小组件
工程师老罗1 小时前
我用AI学Android Jetpack Compose之入门篇(1)
android·android jetpack
Java知识日历2 小时前
【内含例子代码】Spring框架的设计模式应用(第二集)
java·开发语言·后端·spring·设计模式
尘浮生4 小时前
Java项目实战II基于微信小程序的家庭大厨(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven
Java知识技术分享4 小时前
spring boot通过文件配置yaml里面的属性
java·spring boot·后端
军训猫猫头4 小时前
36.Add的用法 C#例子
开发语言·c#
一休哥助手5 小时前
深入解析Spring Boot项目的类加载与启动流程
java·spring boot·后端