Java
一、jvm模块
-
jvm是什么?
是一用用于计算设备的规范,虚构出来的计算机,在计算机上仿真模拟各种计算机功能来实现
-
jvm 作用是什么?
java中所有类必须装载jvm中才能运行,这个装载工作有jvm装载器完成,.class类型文件能在jvm虚拟器中运行,但不能直接在系统中运行,需要jvm解释给系统,同时需要java类库,这就是人机交互,jvm作用
-
jvm内存模型

方法区(线程共享)
常量,静态变量 以及方法信息(修饰符,方法名,返回值,参数等)、类信息等
堆(线程共享)
实例对象, 内存最大的一块
栈(虚拟机栈-线程私有)
生命周期、线程结束,栈内存就释放了,主要存储 8大基本类型 + 对象饮用 + 实例方法
本地方发栈 线程隔离
主要就是Java里面 native修饰的方法,指责与虚拟机栈一样,只不过针对的是 c++曾
程序计数器 线程隔离
保存的是 jvm指令集,程序计数器总是指向下一条指令地址,生命周期与线程生命周期一样 -
类加载器都有哪些?这里只介绍java
bootstrap classloader: 引导类加载器 c++编写,用于家在java核心库,jir/lib目录下jar包,无法直接获取
Extension classloader:扩展类加载器 负责jir/lib/ext目录下jar包 或 -D java.ext dirs 制定目录下的jar包
Application classloader 系统类加载器,负责 java-classpath 或 -D java classpath制定目录下的jar包
Custom classloader 自定义类加载器 -
类加载器作用?
将class字节码加载到内存,并将这些动态数据转换成方法区运行时数据结构,然后在堆中形成这个类的class对象,作为方法区中数据访问路口
本质就是 将字节码加载到内存里面 -
java文件加载过程?


- 双亲委派

-
双亲委派优缺点
优点: 保证类的安全性,不管被哪个类加载,都会委托给引导类加载器加载,也就是父类加载器,只有父类加载不了,才交给子类, 这样能保证创建的对象是一个
缺点:子类加载器可以使用父类加载器创建的类,而父类加载器不能使用子类加载器创建的类 (所以要破坏双亲委派机制) -
破坏双亲委派机制的方式
自定义类加载器, 重写 loadclass 方法
使用线程上下文类 serviceloader ,使父类加载器可以加载子类
二、 Java内存模型
-
什么是Java内存模型?JMM
就是一种符合内存模型的规范,屏蔽硬件和操作系统系统的差异,保证Java在各个平台内存运行的效果一致
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。 -
JMM作用
工作内存与主内存之间数据同步过程
-
JMM 实现
java提供了一系列和并发处理相关的关键字,比如 volatile synchronized final 等 其实就是 原子性,可见性,有序性
-
java对象模型
三、GC相关
-
垃圾算法有哪些?
复制算法:
将内存分为大小相等的两块内存区域,每次只使用一块,当这一块内存存满后将存活的对象复制到另一块内存上,将剩余的已使用的内存回收
优点:算法简单,内存效率高,不易产生内存碎片
缺点:内存使用率被缩减了原来的一半,存入对象增多时,效率大大降低标记-清除:
分为两个阶段:
1.标记所有需要回收的对象
2.回收被标记对象所占内存空间
缺点:算法效率较低, 会产生大量不连续内存碎片,存储大对象时找不到可利用空间等问题标记整理:
标记阶段和标记清除很像,但后面不是清理对象,而是将存活对象移至内存的一端,然后清除边界外内容
分代回收:

新生代 : 老年代 = 1:2 默认
新生代里面又分为, eden , survivor from, survivor to
按照 8:1:1的关系分配,
每次只使用eden和一块survivor空间, 当eden和srvivor from 内存不足时,会出发一次GC,将这两块存活下来的对象复制到另一块survivo to 中, 然后清理eden和 survivor from 内存空间
如果 srvivor to内存空间无法存储某个对象时,则将对象存入老年代 (空间担保);此时 survivor from 和 survivor to互换,如此循环,当一个对象经历过一次GC未被清除, 该对象年龄+1,默认 15 次 移至老年代
新生代 一般是 复制算法 / 标记-清除
老年代 标记-整理算法分区回收:
-
垃圾回收器有哪些?

serial垃圾回收器
单核或单线程,采用复制算法,垃圾回收的同时,暂停所有工作,直至垃圾回收结束, 简单高效,没有线程交互开销,可以获得最高效率的单线程垃圾收集效率, client模式下默认的新生代垃圾回收器
PartNew垃圾回收器
serial 垃圾回收器的多线程版本, 同样采用复制算法,在垃圾回收过程中同样会暂停其他所有工作,直至垃圾回收完成,jvm servicer端新生代垃圾回收器
ParNew收集器默认开启和CPU数目相同的线程数,可以通过-XX:ParallelGCThreads参数限制垃圾收集器的线程数。parrallel scavenge 垃圾回收器
新生代垃圾回收器,多线程, 同样采用复制算法,重点关注的是 程序达到一个可控制的吞吐量,可以高效率的利用cpu, 主要适用于后台运算不需要太多交互任务,自适应调节策略也是 parallel scavenge 与 partNew 的重要区别
吞吐量 = 运行用户代码时间 / 运行用户代码时间 + 垃圾回收时间serial old 垃圾回收器
单线程垃圾回收器,serial 老年代版本,采用标记-整理算法 jvm client 默认老年代垃圾回收器
parallel old 垃圾回收器
paraelle scavenge 的老年代版本,使用多线程 标记-整理算法
CMS垃圾回收器
老年代垃圾回收器,主要目标最短垃圾回收停顿时间,和其他老年代回收算法 标记-整理不同的是,它采用 多线程 标记-清除算法, 最短的垃圾回收回收停顿时间可以给人机交互最好的体验
CMS工作机制
初始标记:标记一下gcroot 能直接关联的对象,速度很快,同样暂停其他所有工作
并发标记:进行GCRoot跟踪的过程,和用户线程一起工作,不需要暂停所有工作
重新标记:修正并发标记过程中,由于程序运行导致标记产生变动的那一部分记录,仍然暂停所有工作的线程
并发清除:清除GCRoot 不可达对象和工作线程一起,不需要暂停所有线程工作G1垃圾回收器
不会像传统垃圾回收器一样等内存满了时候进行垃圾回收,而是没满时就进行了回收,这样保证了垃圾回收最短停顿时间,最大吞吐量
应用场景:面向多cpu和大内存服务场景,例如:电商秒杀服务器,多路直播服务器
特征:弱化分代,强化分区,能实现一些更细更复杂的功能
分区的好处
1.垃圾回收过程和其他工作线程能够并行工作,避免STW
2.不同区域可同时回收,并发性高,更适应多核服务器
3.可以先回收一部分,回收更快
4.可以建立停顿预测模型,用户可以设定垃圾回收最长时间 -
判断可回收对象的方法有哪些?
应用计数法
一种内存管理技术,用于跟踪每个对象被引用的次数。当某个对象的引用次数变为0时,表示没有其他对象再引用它,此时该对象可以被回收。这种方法通过在对象头中添加一个计数器来记录对象的被引用次数,从而实现自动资源管理
引用计数法实现流程
1. 分配对象:当新对象被分配时,其引用计数初始化为1。例如,在Java中,当一个新对象被创建时,它的引用计数会被设置为1。
2. 更新引用:每当一个对象的引用发生变化时,相应的引用计数也会更新。例如,如果有一个对象A被另一个对象B引用,那么对象A的引用计数会增加1;如果B不再引用A,那么A的引用计数会减少1。
3. 回收对象:当某个对象的引用计数变为0时,表示没有其他对象再引用它,此时该对象可以被回收。例如,如果对象A的引用计数变为0,那么A可以被回收。
引用计数法的优缺点
优点:
实现简单:引用计数法的实现相对简单,不需要复杂的垃圾回收算法。
效率较高:由于没有复杂的垃圾回收过程,程序的运行效率较高。
缺点:
循环引用问题:在有循环引用的情况下,即使两个对象都不再被使用,它们的引用计数也不会变为0,导致这些对象无法被回收。
内存泄漏:在某些情况下,由于引用计数的错误管理,可能会导致内存泄漏。可达性分析算法
通过一些列成为GCRoot对象最为根节点,从这些节点开始,向下搜索,搜索过程所走过的路径叫引用链,如果某个对象到GCRoot间没有任何引用链相连,称之为 不可达,也就是这个对象不再被使用
-
Gc root 对象
- 虚拟机栈中应用的对象, 例:各个线程调用堆栈中 参数,局部变量,临时变量等
- 方法区中静态属性引用的对象,例:Java引用类型静态变量
- 方法区中常量引用对象,例:字符串常量池中引用的对象
- 本地方法栈中JNI引用的对象
- Java虚拟机内部引用,例:基本类型对应的Class对象,一些常驻的异常对象
- 所有被同步锁持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
-
对象什么时候进入老年代?
- 经过几轮 yung GC仍然存活 ,或者触发了动态年龄判读 默认是15
- 存活对象 在S区放不下,都会让对象进入老年代 (空间担保机制)
-
G1 垃圾收集器了解么?
G1垃圾收集器的设计目标是在停顿时间可控的情况下,最大化系统吞吐量,它旨在提供更可控、更高效的垃圾回收性能。
以下是G1垃圾收集器的一些特点和工作原理:
a). 区域化内存布局:G1收集器将堆内存划分为多个大小相等的区域(region)。每个区域可以是Eden区、Survivor区或Old 区。这种内存布局有助于提高垃圾收集的效率。
b). 并行与并发 : G1收集器使用并行和并发的方式执行垃圾回收操作。它通过并行处理来加快标记和复制阶段的速度,同时利用并发处理来减少垃圾回收对应应用程序的停顿时间。
c).垃圾优先策略: G1收集器使用Garbage-First策略来确定优先处理哪些区域中的垃圾。它会根据区域中的垃圾数量、回收成本等因素来选择下一个要回收的区域,以最大程度地提高垃圾回收的效率。
d).混合回收: G1收集器执行混合回收,即同时处理新生代和老年代的垃圾回收。相比于传统的的分代式回收,它可以均衡地处理整个堆内存,避免长时间的FullGC暂停。
e).可预测的停顿时间: G1收集器使用一种停顿预测模型的机制,通过控制每次垃圾回收的时间目标来实现可预测的停顿时间。开发人员可以通过设置最大停顿时间来控制G1收集器的行为。 -
工作原理

**初始标记:**停顿所有的应用程序的线程,识别出GcRoots直接关联的对象,并标记这些对象。
**并发标记:**从第一步得到的标记点继续向下遍历对象图,标记所有被引用的存活对象,此步骤与应用程序并发运行。
**最终标记:**在并发标记完成后,再次停顿所有的应用程序线程,重新标记被改变的对象和整理存活对象的布局。
**筛选回收:**根据用户设定的回收目标,选取一个或多个Region进行垃圾回收,将这些Region中的存活对象(还有其他Region中被引用的对象)复制到新的Region中,即称为筛选回收。G1垃圾收集器适用于具有大内存需求和低暂停时间要求的应用程序。但需要注意的是,G1收集器也有一定的局限性,比如在处理大量临时垃圾对象或存在大量跨区域引用时可能会导致性能下降。总的来说,G1垃圾收集器在java应用程序中的使用越来越广泛,特别适合那些对停顿时间敏感的大型服务端应用程序。
-
FullGC场景
从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
从老年代分区转移存活对象时,无法找到可用的空闲分区
分配巨型对象时在老年代无法找到足够的连续分区 -
从对象创建了解JVM内存模型
- VM在创建对象时首先会试图在Eden区->Eden区内存不足则进行minor GC回收不活跃的对象->回收后的Eden区内存够不够->不够则执行下一步
- survivor区内存够?->够则将Eden区的部分复制到s区,申请空间成功。不够则查看old区,old区够则将S区的部分活跃对象复制到OLD区,Eden区部分活跃对象复制到S区,申请对象空间成功。->old区不够,则触发FullGC->fullGC后的OLD区还是不够则触发OM(OutOfMemoryError)
注:minorGC–新生代GC majorGC–老年代GC --fullGC=minorGC+majorGC
-
为什么要有survivor区,只有Eden区行嘛?
如果只有Eden区那么就会有很多存活对象经过minorGC后被保存到Old区,这样Old区很快就满了,当Old区内存不足就会触发Full Gc(因为MajorGC一般都是伴随着minorGC的,所以可以当为fullGC),FullGc时间较长,效率较低,会影响程序性能
如果Old区内存过小会频繁触发FullGc,如果增大Old区内存,则一次FUllGc时间更长
注:FullGC消耗时间比较长主要是Old区采用标记清除和标记清理算法,且Old区一般对象数量庞大,一次GC的消耗时间自然要比minorGC长 -
为什么需要两个Survibor区?
为了解决内存碎片化问题,Eden区满了就会把存活对象复制到Surfier区, 下一次Gc的时候Eden和survivor区都可能有一部分存活对象,此时把eden区对象复制到survivor区,这两区内存可能是不连续的,这就导致内存碎片问题,而分成两个区可以永远保证一个survivor区是空的,另一个survivor区是连续的,相当于浪费一个survivor区的资源来解决内存碎片化问题
-
为什么Eden 区,s0, s1 区按照8:1:1这个比例来分配
对象朝生夕死,只有Eden区内存足够大,才能保证对象尽量在Eden区回收
-
堆内存中都是线程共享的吗?
不是的,jvm默认会为每个线程开辟一个buffer区,用来加速对象分配 这个称之为 TLAB(ThreadLocal Allocation Buffer),TLAB内存区域较小,对象分配默认是在TLAB中分配,如果对象比较大,则是在堆共享内存区域中分配
四、线程相关
-
线程创建方式有哪些?
- 继承Thread 类
- 实现Runnable 接口
- 实现Callable接口
- 创建线程池
区别
-
采用 Runnable 和 Callable 方式
优势: 线程类只是实现了Runnable 或Callable接口,同样还可以实现其他类,可以共享同一个target对象,多线程处理同一份数据,这样 cpu 代码 数据 分开 模型清晰,很好体现Java面向对象的思想
劣势: 编程稍微复杂,如果访问当前线程,则必须使用Thread.currentThread()方法 -
采用继承Thread的方式
优势:编程简单,如果访问当前线程无需使用Thread.currentThread()方法,直接使用this即可
劣势:继承了Thread了, 不能在继承其他类 -
Runnable 和 Callable 区别
- Runnable 重写的run 方法, Callable 重写的是 call方法
- Callable 执行结束后可以返回值同样可以抛出异常,Runnable方法没有返回值,不能抛出 异常
- 运行callable任务可以拿到一个futter对象,表示异步计算结果,它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
- 线程池后面说
-
线程的状态有哪些?
六种状态 new,ready,running, time_waiting, waiting, blocked, terminated
-
NEW
处于 NEW 状态的线程此时尚未启动。这里的尚未启动指的是还没调用 Thread 实例的start()方法
反复调用同一个线程的 start 方法不可行,会抛出 IllegalThreadStateException异常 -
RUNNABLE, (ready, running)
表示当前线程正在运行中。处于 RUNNABLE 状态的线程在 Java 虚拟机中运行,也有可能在等待 CPU 分配资源。包括了操作系统线程的 ready 和 running 个状态 -
BLOCKED
阻塞状态。处于 BLOCKED 状态的线程正等待锁的释放以进入同步区。 -
WAITING
等待状态。处于等待状态的线程变成 RUNNABLE 状态需要其他线程唤醒。调用下面这 3 个方法会使线程进入等待状态:
Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;
Thread.join():等待线程执行完毕,底层调用的是 Object 的 wait 方法;
LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。 -
TIMED_WAITING
超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。调用如下方法会使线程进入超时等待状态:
Thread.sleep(long millis):使当前线程睡眠指定时间;
Object.wait(long timeout):线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;
Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;
LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;
LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间; -
TERMINATED;
终止状态。此时线程已执行完毕。
-
-
线程状态如何转换的?
Object.wait()
调用wait()方法前线程必须持有对象的锁。线程调用wait()方法时,会释放当前的锁,直到有其他线程调用notify()/notifyAll()方法唤醒等待锁的线程。
需要注意的是,其他线程调用notify()方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用wait()方法的线程。
同样,调用notifyAll()方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
Thread.join()
调用join()方法,会一直等待这个线程执行完毕(转换为 TERMINATED 状态)。Thread.sleep(long)
使当前线程睡眠指定时间。需要注意这里的“睡眠”只是暂时使线程停止执行,并不会释放锁。时间到后,线程会重新进入 RUNNABLE 状态。Object.wait(long)
wait(long)方法使线程进入 TIMED_WAITING 状态。这里的wait(long)方法与无参方法 wait()相同的地方是,都可以通过其他线程调用notify()或notifyAll()方法来唤醒。不同的地方是,有参方法wait(long)就算其他线程不来唤醒它,经过指定时间 long 之后它会自动唤醒,拥有去争夺锁的资格。
Thread.join(long)
join(long)使当前线程执行指定时间,并且使线程进入 TIMED_WAITING 状态。线程中中断
线程中断机制是一种协作机制。通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。简单介绍下 Thread 类里提供的关于线程中断的几个方法:
Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为 true(默认是 flase);
Thread.isInterrupted():测试当前线程是否被中断。
Thread.interrupted():检测当前线程是否被中断,与 isInterrupted() 方法不同的是,这个方法如果发现当前线程被中断,会清除线程的中断状态。
在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为 true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己决定,可以在合适的时机中断请求,也可以完全不处理继续执行下去。
线程池相关
-
线程池优劣势?
优势
1. 降低资源损耗, 线程池内部线程复用,避免重复创建
2. 提高响应速度 接到任务时候,减少线程创建的内存消耗,和cpu时间片
3. 提高线程的可管理性 又线程统一 管理,良好的线程数量控制,线程调度,可以增加系统的稳定性
4. 提高可拓展性, 提供更多可能 -
线程池核心参数有哪些?
corePoolSize: 核心线程数
maximumPoolSize:最大线程数
keepAliveTime: 超出核心线程后的线程 或者 所有线程的最大保活时间 取决于配置
unit:保活时间的单位
workQueue:任务队列
threadFactory: 线程创建工厂
handler: 拒绝策略 当队列已满或者超过线程池最大线程数 会调用这方法提交优先级: 从核心线程数 队列 最大线程数
执行优先级:核心线程数 最大线程数 队列 -
决绝策略有哪些?
- AbortPolicy: 不执行新任务,直接抛异常,提示队列已满
- DisCardPolicy:不执行新任务,也不抛异常
- DisCardOldSetPolicy:将队列中第一个任务替换为当前任务 执行
- CallerRunsPolicy:直接调用 excuse 执行任务
-
创建线程池的方式有哪些?使用哪些场景?
-
newCacheThreadPool
底层
核心线程数为0, 只有工作线程且 最大线程数 是 Inter.max, 保活时间是60秒,workQueqe 采用是 SynchronousQueue 同步队列
通俗
当有新任务到来,则插入到同步队列中,会在线程池寻找可用线程, 有空闲线程 直接使用, 没有则创建新线程去执行, 线程达到线程最大保活时间后自行销毁
适用场景
比较适合时间短的异步小程序, 或负载较轻的服务器 可能造成 cpu占用100% -
newFixedThreadPool
底层
固定数量的线程池, 返回ThreadPoolExecutor,固定数量的核心线程数,没有最大线程数限制,
保活时间无限,当所有核心线程都在执行任务,则新任务添加到阻塞队列(LinkedBlockingQueue() )中等待
通俗
创建可容纳固定数量线程的池子,每个线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
使用场景- 长周期执行的任务,性能会好很多
- 并发线程数控制方面比较友善
-
newSingleThreadExecutor
底层
核心线程数是1 最大线程数也是1,保活实现无线, 队列是 无界阻塞队列 LinkedBlockingQueue
通俗
就是线程池只有一个线程且保活时间无限,当该线程繁忙时, 新任务加入到无界阻塞队列中等待
使用场景
一个一个任务执行场景,顺序执行的场景 -
newScheduleThreadPool
底层
固定数量的核心线程数,最大线程数是 inter.max_value 保活时间是10秒,支持定时任务(ScheduleTask)workqueue 采用的是 DelayWorkQueue 队列 是一个按照时间升序队列
通俗
就是创建一个固定数量的核心线程的线程池,线程池内线程存活时间是 无限的,可以支持定时或者周期性任务,如果线程池内线程都在执行任务,则加入DelayWorkQueue,按照超时时间数序排序
使用场景
定时任务或者周期性执行的任务场景
-
-
队列都有哪些?
-
ArrayBolckQueue
有界阻塞队列,基于数组实现,FIFO 先进先出对元素进行排序,队列头部是存放时间最长的任务,新插入元素放入队尾,队列获取工作是从队列头部开始特点:队列有一个固定的容量限制,当队列满时,尝试添加新任务的操作会被阻塞,直到队列中有空间可用。
适用场景:适用于需要控制任务数量,防止资源耗尽的场景。通过调整队列大小和线程池大小,可以灵活控制任务的并发执行。
-
LienkedBlockQueue
无界阻塞队列,基于链表结构实现 FIFO 先进先出的方式对元素进行排序,默认容量是integer.max_value 如果不设置 就等于是无界的 线程池设置最大线程数 就失去了意义, 比如, 线程没有达到核心线程数,则去创建核心线程去执行任务,当超过核心线程数 小于最大线程数的时候 任务会加入队列,当核心线程空闲的时候 才去处理队列任务提示:只有在线程池队列满且仍有待处理任务时,线程池才会增加至最大线程数。否则则会一直使用核心线程。
特点:队列容量理论上是无限的,但受限于JVM的内存。当内存耗尽时,会抛出OutOfMemoryError。
适用场景:适用于任务量非常大,且任务执行时间较长,生产者生成任务的速度不会超过消费者处理任务的速度的场景。但需注意内存溢出的风险。
-
priorityBlockQueue
优先级 无界阻塞队列 使用二进制堆实现,可以使用比较器或者元素自然顺序排序, 不允许 null元素,无论何时插入元素可以按照优先级对元素进行排序, 优先的元素放在最前面如果你试图对队列进行排序,或者对最小元素调用 remove(x) 或者 contains(x),那么使用的是按优先级遍历的元素,而不是队列的元素遍历。因为它是无界的,所以添加操作(add、offer、put 方法)永远不阻塞,而在队列为空时,获取操作(remove、poll、take)才阻塞。特点:队列中的元素会根据其优先级进行排序,优先级高的元素会先被取出执行。
适用场景:适用于需要按照任务优先级顺序执行的场景。通过调整任务的优先级,可以确保重要任务得到优先处理。
-
SynchronousQueue
直接提交队列 一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue 是一个特殊的阻塞队列,它没有任何内部容量,甚至连一个队列的容量都没有。也就是说,该队列在任何时候,甚至在 put 操作后,都不能保证任何元素存在于队列中,因为这些元素可能已经被获取了。它是线程之间进行任务转移的一个载体。静态工厂方法Executors.newCachedThreadPool使用了这个队列
特点:这种队列实际上并不存储任何元素,每个插入操作必须等待另一个线程的相应删除操作(也就是:要添加新任务必须得有空闲的线程才能添加),反之亦然。即一个线程尝试向队列中添加元素时,必须有另一个线程正在等待接收这个元素。
适用场景:适用于任务处理时间较短,且生产者和消费者速度大致匹配的场景。它可以有效减少任务在队列中的等待时间,提高系统的响应速度。
出处链接和本声明。
-
-
队列 选择建议
在选择线程池中的队列时,需要根据具体的应用场景和需求来决定:
任务类型和特点:如果任务处理时间较长,且任务量不确定,可以选择无界队列;如果任务量较大且需要控制并发数,可以选择有界队列。
系统资源:考虑系统的内存和CPU资源。无界队列虽然可以处理大量任务,但存在内存溢出的风险;有界队列可以避免内存溢出,但需要合理设置队列大小和线程池大小。
性能要求:如果要求系统响应速度快,且任务处理时间较短,可以选择直接提交队列或优先级队列。
任务优先级:如果任务有明确的优先级要求,可以选择优先级队列。
线程池中的队列选择需要根据实际情况进行权衡和决策。在设计和配置线程池时,应充分考虑任务类型、系统资源和性能要求等因素。 -
线程池执行流程是什么?
- 首先判断当前传入的任务是否为空,若为空,则抛出空指针异常;
- 获取线程状态量,判断工作线程数是否小于核心线程数。若小于核心线程数,则启动一个核心线程处理任务,方法结束;
- 若核心线程数此时已达到上限,或者(步骤2)添加核心线程失败,进入阻塞队列部分;
- 判断当前线程池状态是否为RUNNING运行状态;
- 若是,则尝试将当前任务添加进阻塞队列中;
- 若阻塞队列已满,则任务添加失败,此时尝试启动一个非核心线程执行任务;
- 若工作线程已经达到最大线程数,非核心线程启动失败,执行拒绝策略;
- 若阻塞队列未满,则(步骤5)添加成功后,再次判断线程池状态是否为RUNNING;
- 若不是,则从队列中移除刚刚添加的任务,并执行拒绝策略;
- 若是,则进一步判断工作线程数是否为0,若为0,添加一个非核心线程执行该任务。
-
自定义线程池步骤?
- 创建线程池对象,并设定线程池的核心线程数、最大线程数、存活时间等参数。
- 创建工作队列,线程池中的任务都会被放入这个队列中。
- 创建线程工厂,设定创建线程的具体细节,比如线程的名称、优先级等。
- 创建拒绝策略,当工作队列满了,且线程池中的线程数已达最大值时,如何处理新进来的任务。
注意事项
-
当创建一个线程池时,需要考虑工作队列的大小和线程数的设定。如果设置不当,可能会导致资源浪费或任务执行延迟。
-
对于线程工厂,不仅可以自定义线程的创建,还可以在创建线程时添加一些额外的操作,如记录日志等。
-
在设定拒绝策略时,需要考虑应用的具体需求,选择最适合的策略。
-
在使用线程池时,务必确保所有的任务都能得到正确的执行。当线程池关闭时,一定要等待所有的任务都已经完成后再进行下一步操作。
-
需要合理控制线程池大小,避免因线程过多导致系统压力过大。
-
一般情况下,应优先使用JDK提供的线程池,如:newCachedThreadPool, newFixedThreadPool, newSingleThreadExecutor等。因为手动创建线程池需要考虑更多的因素,比较复杂。(前提是你要了解这些线程池的优劣,不要盲目使用!)
-
线程池如何合理配置线程数?
CPU密集型
定义:CPU密集型的意思就是该任务需要大量运算,而没有阻塞,CPU一直全速运行。CPU密集型任务只有在真正的多核CPU上才可能得到加速(通过多线程)。
CPU密集型任务配置尽可能少的线程数。
CPU密集型线程数配置公式:(CPU核数+1)个线程的线程池
IO密集型
定义:IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型任务会导致浪费大量的CPU运算能力浪费在等待。
所以IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要利用了被浪费掉的阻塞时间。
第一种配置方式:
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程。
配置公式:CPU核数 * 2。
第二种配置方式:
IO密集型时,大部分线程都阻塞,故需要多配置线程数。
配置公式:CPU核数 / (1 – 阻塞系数)(0.8~0.9之间)
比如:8核 / (1 – 0.9) = 80个线程数
-
线程间通信方式有哪些?
- 共享变量 使用volatile、final、synchronized 、lock 等同步手段
- 使用 wait 、notify/notifyAll 等 Object 方法 通过线程等待和通知手段
- 使用condition接口,没用过 据说比 wait / notify更灵活,通常与ReentrantLock 一起使用
- 使用Blockqueue 线程安全的 队列 方式 生产者 - 消费者 模式 实现线程安全
- 其实吧,只要只用线程安全的 集合 都可以保障 线程安全,方法有很多种,没必要纠结列出很多种
-
那么问题来了, 为什么要线程通信? 比较sb的问题, 一般都是水货会问
- 资源共享 可能有多多个线程同时操作和访问共享资源,为了避免资源竞争,确保数据安全所以需要线程间通信来协调对资源的访问
- 任务分工 不同线程可以负责不同的任务或子任务,线程之间协作和通信来完成整体任务,通信可以是单向传达结果,也可以是双向信息和状态交换
- 同步操作 线程间通信可以借助同步操作, 例如:某个线程执行需要上一个线程执行完的某个条件,后者告知其他线程现在状态
- 数据传输 线程间通过通信来传递数据和信息,方便协调工作、共享信息和通知时间
- 提高性能 通信协作可以提高程序性能 ,例如线程池执行并发任务时,避免线程的创建和销毁
- 实现某些模型和算法有些并发模型和算法需要线程之间的协作和通信,如生产者消费者模型、并发队列等
参考文档:
说出Java创建线程的三种方式及对比
Java线程的六种状态
java四种线程池创建
线程池知多少?
一文搞懂Java线程池
【多线程】线程池中的队列有哪些,应该如何选择
java线程池任务执行过程 | java线程池原理探究 | 线程池源码
JAVA两个线程之间是如何通信的?
java 线程之间通信-volatile 和 synchronized
五、 JVM内置关键字都有哪些?
-
final 如何理解?
Java内置关键字 可以修饰 类、方法、变量
修饰变量:常量,初始化后 不能修改, 只读
修饰方法:不能重写,且 final修饰的方法比非final方法要快,因为编译的时候已经绑定,不需要在运行期的时候在动态绑定
修饰类: 不能继承
总结
好处:
1. 提高性能, jvm和Java都会缓存java final变量
2. final变量可以在多线程环境下进行共享, 不需要额外同步开销
3. 使用final 修饰的变量,JVM会对 变量 方法 类进行优化面试关注点:
- final关键字可以用于成员变量、本地变量、方法以及类。
- final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。
- 你不能够对final变量再次赋值。
- 本地变量必须在声明时赋值。
- 在匿名类中所有变量都必须是final变量。
- final方法不能被重写。
- final类不能被继承。
- final关键字不同于finally关键字,后者用于异常处理。
- final关键字容易与finalize()方法搞混,后者是在Object类中定义的方法,是在垃圾回收之前被JVM调用的方法。
- 接口中声明的所有变量本身是final的。
- final和abstract这两个关键字是反相关的,final类就不可能是abstract的。
- final方法在编译阶段绑定,称为静态绑定(static binding)。
- 没有在声明时初始化final变量的称为空白final变量(blank final variable),它们必须在构造器中初始化,或者调用this()初始化。不这么做的话,编译器会报错“final变量(变量名)需要进行初始化”。
- 将类、方法、变量声明为final能够提高性能,这样JVM就有机会进行估计,然后优化。
- 按照Java代码惯例,final变量就是常量,而且通常常量名要大写。
- 对于集合对象声明为final指的是引用不能被更改,但是你可以向其中增加,删除或者改变内容。
原理
对于final域,编译器和处理器要遵守两个重排序规则:- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。(先写入final变量,后调用该对象引用)
原因:编译器会在final域的写之后,插入一个StoreStore屏障 - 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。(先读对象的引用,后读final变量)
原因:编译器会在读final域操作的前面插入一个LoadLoad屏障
-
volatile
优缺点
优点
- 可见性: volatile变量的值在多线程之间可见,一个线程对volatile变量的修改会立即被其他线程看到,从而避免了数据不一致问题。
- 有序性:volatile变量的读取和写入操作具有顺序性,保证了写入操作先于读取操作,从而避免了出现脏读、幻读等问题。
- 轻量级:相比于synchronized关键字或者Lock接口,volatile关键字没有锁定的机制,因此在多线程交互的场景下,使用volatile变量可以提高程序的执行效率。
缺点
- 不支持原子性 :虽然volatile变量可以保证其读取和写入操作的顺序性和可见性,但是它并不能保证原子性。当多个线程同时对一个volatile变量进行写入操作时,仍然可能出现竞态条件,从而导致数据不一致问题。
- 不能替代synchronized关键字:虽然volatile关键字可以提高程序的执行效率,但是在一些复杂的多线程交互场景下,还是需要使用synchronized关键字或者Lock接口来保证线程安全。
保证可见性原理
处理器lock前缀指令和处理器的缓存一致性协议
-
Sychorized 关键字
作用
- 原子性
- 有序性
- 可见性
作用域
- 修饰代码块: 大括号括起来的代码,作用于调用对象
// synchronized(obj){…} - 修饰方法: 整个方法,作用于调用对象
// public synchronized void test(){…} - 修饰静态方法:整个静态方法,作用于所有对象
// public static synchronized void print(int j){…} - 修饰类:括号括起来的部分,作用于所有对象。
实现原理
Java 对象头:在 Java 对象的内存布局中,每个对象都有一个头部信息。对象头是对象实例的一部分。它包含了对象的元数据信息,如对象的哈希码、锁状态标志等。在synchronized实现中,对象头被用来作为锁的标识。当一个线程执行synchronized方法时,它需要获取该方法所在对象的对象头锁。如果其他线程已经持有该锁,那么当前线程就会进入阻塞状态,直到其他线程释放了它所持有的锁

在Java中,synchronized的实现是通过对象头中的Mark Word来实现的。Mark Word是Java对象头中的一个重要组成部分,它包含了对象的哈希码、类型信息、锁状态等信息。当一个线程执行synchronized方法时,JVM会通过CAS(Compare and Swap)操作来尝试获取该对象的锁。如果获取成功,那么该线程就可以执行synchronized方法;如果获取失败,那么该线程就会进入阻塞状态。优缺点
优点:- 简单易用:
synchronized关键字的语法简单,易于理解和使用,可以方便地确保多个线程对共享资源的安全访问。 - 内置支持:作为Java语言的一部分,
synchronized关键字得到了JVM层面的支持,避免了用户自行实现线程同步机制的复杂性。 - 可重入性:synchronized锁是可重入的,一个线程可以多次获得同一个锁,而不会造成死锁。
缺点:
- 粒度粗:使用 synchronized 关键字进行同步时,通常是对整个方法或代码块进行同步,这可能会导致一些不必要的等待,降低并发性能。
- 无法中断:一旦进入 synchronized 代码块,除非获取到锁否则无法被中断,这可能会导致线程挂起的时间过长。
- 性能开销:在某些情况下,使用 synchronized 可能会引入一定的性能开销,特别在高并发的场景下,这种开销可能会更加显著。
- 局限性:synchronized 的锁是基于对象的,因此如果需要对不同的资源进行管理,就需要创建不同的对象锁,这可能会增加复杂性。
-
锁升级
当执行 synchronized 修饰的方法或代码块时,根据对象的状态,JVM 会进行如下处理:
- 无锁状态:当对象没有被任何线程锁定时,进入 synchronized 代码块的线程将会尝试获取对象的锁。
- 偏向锁状态:如果对象的锁处于无锁状态且没有竞争,那么进入 synchronized 代码块的线程可以直接获取锁,并将对象头中的线程ID更新为自己的ID,此时对象处于偏向锁状态。
- 轻量级锁状态:如果对象处于偏向锁状态但出现了竞争,JVM 会尝试使用轻量级锁来实现同步。它通过CAS(比较并交换)操作来尝试获取锁,如果获取成功,则执行 synchronized 代码块;否则进入重量级锁状态。
- 重量级锁状态:当多个线程争用同一个对象的锁时,JVM 会将对象的状态升级为重量级锁状态,此时线程会被阻塞,并加入到对象的等待队列中。只有拥有锁的线程释放锁后,等待队列中的线程才能被唤醒。

-
synchronized 和 volatile 的区别?
synchronized 和 volatile 都是 Java 中用于保证多线程程序正确性的关键字,虽然它们的作用有所不同,但可以作为互补。
synchronized 关键字用于实现原子性操作和互斥访问。使用 synchronized 修饰的代码块或方法,在同一时间只允许一个线程进入临界区,其他线程需要等待当前线程执行完毕后才能进入。因此,synchronized 能够保证多个线程对共享资源的安全访问,并防止数据竞争和不一致性。
volatile 关键字用于保证可见性和禁止指令重排序。使用 volatile 修饰的变量,在多个线程之间保持可见性,当一个线程修改了变量的值,其他线程能够立即看到最新的值。此外,volatile 还能够禁止编译器和处理器对代码的优化,确保指令按照程序的顺序执行,避免出现意外的结果。
需要注意的是,volatile 不能保证原子性,如果需要进行复合操作,例如自增、自减、比较并交换等,仍然需要使用 synchronized 或 Lock 等机制来确保原子性。
-
synchronized与Lock的区别?

-
什么是内存屏障?
内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。
-
为什么需要内存屏障?
我们知道,在多CPU(核)场景下,为了充分利用CPU,会通过流水线将指令并行进行。为了能并行执行,又需要将指令进行重排序以便进行并行执行,那么问题来了,那些指令不是在所有场景下都能进行重排,除了本身的一些规则(如Happens Before 规则)之外,我们还需要确保多CPU的高速缓存中的数据与内存保持一致性, 不能确保内存与CPU缓存数据一致性的指令也不能重排,内存屏障正是通过阻止屏障两边的指令重排序来避免编译器和硬件的不正确优化而提出的一种解决办法。
-
硬件层的内存屏障?
Intel硬件提供了一系列的内存屏障,主要有:
- lfence,是一种Load Barrier 读屏障
- sfence, 是一种Store Barrier 写屏障
- mfence, 是一种全能型的屏障,具备ifence和sfence的能力
- Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
-
内存屏障的主要类型
不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。
Java内存屏障主要有Load和Store两类。
对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据
对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存对于Load和Store,在实际使用中,又分为以下四种:
LoadLoad 屏障
序列:Load1,Loadload,Load2
确保Load1所要读入的数据能够在被Load2和后续的load指令访问前读入。通常能执行预加载指令或/和支持乱序处理的处理器中需要显式声明Loadload屏障,因为在这些处理器中正在等待的加载指令能够绕过正在等待存储的指令。 而对于总是能保证处理顺序的处理器上,设置该屏障相当于无操作。StoreStore 屏障
序列:Store1,StoreStore,Store2
确保Store1的数据在Store2以及后续Store指令操作相关数据之前对其它处理器可见(例如向主存刷新数据)。通常情况下,如果处理器不能保证从写缓冲或/和缓存向其它处理器和主存中按顺序刷新数据,那么它需要使用StoreStore屏障。LoadStore 屏障
序列: Load1; LoadStore; Store2
确保Load1的数据在Store2和后续Store指令被刷新之前读取。在等待Store指令可以越过loads指令的乱序处理器上需要使用LoadStore屏障。StoreLoad 屏障
序列: Store1; StoreLoad; Load2
确保Store1的数据在被Load2和后续的Load指令读取之前对其他处理器可见。StoreLoad屏障可以防止一个后续的load指令 不正确的使用了Store1的数据,而不是另一个处理器在相同内存位置写入一个新数据。正因为如此,所以在下面所讨论的处理器为了在屏障前读取同样内存位置存过的数据,必须使用一个StoreLoad屏障将存储指令和后续的加载指令分开。Storeload屏障在几乎所有的现代多处理器中都需要使用,但通常它的开销也是最昂贵的。它们昂贵的部分原因是它们必须关闭通常的略过缓存直接从写缓冲区读取数据的机制。这可能通过让一个缓冲区进行充分刷新(flush),以及其他延迟的方式来实现。 -
java内存屏障使用介绍
- 通过 Synchronized关键字包住的代码区域,当线程进入到该区域读取变量信息时,保证读到的是最新的值.这是因为在同步区内对变量的写入操作,在离开同步区时就将当前线程内的数据刷新到内存中,而对数据的读取也不能从缓存读取,只能从内存中读取,保证了数据的读有效性.这就是插入了StoreStore屏障
- 使用了volatile修饰变量,则对变量的写操作,会插入StoreLoad屏障.
- 其余的操作,则需要通过Unsafe这个类来执行.
UNSAFE.putOrderedObject类似这样的方法,会插入StoreStore内存屏障
Unsafe.putVolatiObject 则是插入了StoreLoad屏障
Java并发(十九):final实现原理
Java中volatile关键字详解(详细图文)
优化Java多线程程序的性能:使用volatile关键字的技巧
java之synchronized作用域
Java中的synchronized关键字
Java中的内存屏障
java内存屏障的原理与应用
六、 锁相关





-
synchorized 作用域及原理
修饰实例方法(锁当前对象实例)
给当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
修饰静态方法 (锁当前类)
给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 当前 class 的锁。
修饰代码块 (锁指定对象/类)
对括号里指定的对象/类加锁:
synchronized(object) 表示进入同步代码库前要获得 给定对象的锁。
synchronized(类.class) 表示进入同步代码前要获得 给定 Class 的锁。Synchronized的底层原理

分析 用例1:
-
打开类对应目录下的终端

-
输入javac -encoding UTF-8 test.java
-
输入javap -c -s -v -l test.class
-
开始查看字节码:

从上面我们可以看出:synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
-
-
为什么会有两个monitorexit指令?为什么保证同步代码块不管是正常执行发生执行期间发生异常锁都可以被正确释放。
当执行 monitorenter 指令时,线程试图获取锁也就是获取 对象监视器 monitor 的持有权。每个 Monitor 内部都有一个计数器,用于跟踪锁的重入次数。当线程第一次获取锁时,计数器设为1。如果同一个线程再次获取同一把锁,计数器会增加。当线程退出同步代码块时,计数器减1,只有当计数器回到0时,锁才会被完全释放。
如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。分析


-
synchronized 和 Retrnlock 区别




七、集合相关
Hashmap
-
Hashmap 数据结构
使用数组+链表的数据结构进行数据的存储,后来演变到jdk8之后,为了提高插入和查询效率,在插入的数据超过某个阈值之后,会将数组+链表的结构转化成数组+红黑树的数据结构
-
负载因子
从源代码层面看,只是对内部一个叫做loadFactor的属性进行了初始化0.75,那么这个loadFactor参数是做什么用的呢?
我们前言中说过,HashMap是用的一个数组+链表的形式进行数据的存储,那么这个数组的长度是如何决定的呢?什么时候要对数组进行扩充,什么时候又要对数组进行缩减呢?那么这个判断依据就是根据loadFactor来决定的。具体来说就是:当插入数据的个数 / 数组的长度 >= loadFactor时,我们就要对数组的长度进行扩展啦。因为当插入的数据越来越大的时候,在数组长度不变的情况下,hash冲突会越来越严重,数组节点下的数据就会越来越多,进而导致查询效率越来越差。
另外需要说明的是:在扩展数组的时候,数组的长度都是2n个,在初始化的时候,数组长度是24 = 16,每一次扩展,都增加一倍。
-
为什么采用2^n 扩容?


参考:
八、Java基础搜集
-
java 多态理解
多态就是同一个方法在不同的对象上可以有不同的行为
优点
代码可扩展性: 可以通过增加新的子类或实现新的接口来扩展系统的功能,而不需要修改已有的代码。
代码可维护性: 多态使得代码更加模块化,易于维护和修改。
代码可读性: 多态使得代码更加简洁易懂。 -
oop pop aop 有什么区别?
面向过程编程 pop (Procedural Oriented Programming)
它将程序分解为一系列相关联的过程或函数,每个过程负责执行特定的任务,并通过输入输出与其他过程交互,核心在于过程的调用和参数的传递
面向切面编程 aop (Aspect Oriented Programming)
它是一种解决面向对象编程中横切关注点问题的一种编程方式,跨越多对象或模块的功能,例如:日志记录、事务管理、性能监控、安全性检查、缓存管理、异常处理、权限控制等。
面向对象编程(Object Oriented Programming)
它将现实世界的事务抽象形成对象,每个对象都具有属性与方法,对象之间通过消息传递进行交互,从而实现程序的功能。
-
方法的重载和重写
重载(overload):类中的函数的名称相同,参数个数、类型、顺序不同。可以有不同的返回类型和访问修饰符,可以抛出不同的异常,可以根据参数区别不同函数。
编译时的多态性,在编译时根据参数类型或数量选择合适的方法执行。重写(override):子类对父类函数的重新实现,函数名与参数列表相同,行为不同。
运行时的多态性,在运行时根据对象实际类型决定调用哪个方法,通过虚函数和动态绑定实现。 -
面向对象的五大基本原则
单一职责原则:一个类最好只做一件事情,提高可维护性,减少代码修改的影响。
开放封闭原则:对扩展开放,对修改封闭。促进可扩展性,降低风险。
里氏替换原则:子类必须能替换其基类,提高代码的可互换性,增加代码可重用性。
依赖倒置原则:程序依赖于抽象接口,而不是具体实现。提高代码可测试性,减少耦合。
接口隔离原则:使用多个小专门的接口,而不使用大接口。减少系统耦合,提高灵活性稳定性。
-
内部类
成员内部类:成员内部类定义在外部类的成员位置,可以访问外部类的所有成员变量和方法。使用格式为:外部类名.内部类名 in = 外部类对象.new 内部类名();
局部内部类:局部内部类定义在方法或作用域内部,只能在所在的方法或作用域中访问。局部内部类可以定义在代码块内部,对外部类的访问权限与它们所在的方法或作用域相同。
匿名内部类:匿名内部类没有名字,直接在创建对象的地方进行定义和实例化。一般用来简化代码,减少类的定义。匿名内部类常用于实现接口或继承抽象类。
静态内部类:静态内部类是在一个类内部定义的静态类,它不依赖于外部类的实例,可以直接使用外部类的静态成员,而不需要创建外部类对象。
区别与联系:
内部类是有名字的,可以独立存在。而匿名内部类没有名字,必须依附于其他类存在。
内部类可以有自己的构造方法和成员变量,而匿名内部类不能有构造方法和成员变量。
内部类可以被继承和实例化,而匿名内部类只能被实例化一次。
内部类使用起来相对灵活,可以随意访问外部类的成员。而匿名内部类通常用于简化代码,减少类的定义。
-
抽象类和接口
抽象类
1.抽象类不能直接实例化对象
2.抽象方法不能是 private 的
3.抽象方法要被子类重写,所以抽象方法不能被final和static修饰
4.抽象类必须被继承,并且继承后子类要重写父类中的抽象方法,否则子类也是抽象类,必须要使用 abstract 修饰
5.抽象类中不一定包含抽象方法,但是有抽象方法的类一定是抽象类
6.抽象类中可以有构造方法,供子类创建对象时初始化父类的成员变量接口
1.接口类型是一种引用类型,但是不能直接new接口的对象
2.接口中每一个方法都是public的抽象方法, 即接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)
3.接口中的方法是不能在接口中实现的,只能由实现接口的类来实现,但如果接口当中的方法被static或default修饰,则可以有具体的实现
4.重写接口中方法时,不能使用默认的访问权限
5.接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量
6.接口中不能有静态代码块和构造方法
7.接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是.class
8.如果类没有实现接口中的所有的抽象方法,则类必须设置为抽象类
9.jdk8中:接口中还可以包含default方法 -
List、ArrayList、LinkedList


-
ArrayMap HashMap SparseArray
HashMap



SparseArray稀疏矩阵

ArrayMap


-
HashMap


为什么是扩容是2的n次方

扩容机制


-
CurrenHashMap
-
HashTable
-
HashMap HashTable 区别
-
LinkHashMap
-
Set
-
深拷贝 浅拷贝
参考
- Java中的多态
- 面向过程、面向对象、面向切面
- Java抽象类与接口详解
- Java初阶~~四种内部类总结
- 深入理解Synchronized的使用和实现原理
- Synchronize和ReentrantLock区别
- List、ArrayList、LinkedList
- Java集合LinkList类和ArrayList常见使用和区别
- HashMap、SparseArray与ArrayMap比较
- HashMap底层实现原理?着这一篇就够了
- hashmap的实现原理

3893

被折叠的 条评论
为什么被折叠?



