Java进阶篇
1. List、Set 以及 Map 的区别,它们之间有什么关系?
(1)List集合是一个元素有序、可重复的集合,集合中每个元素都有其对应的顺序索 引。
(2)Set集合是无序,不可重复的
(3)Map 是有映射关系的键值对
2. ArrayList 和 LinkedList 的区别?
(1)ArrayList是基于数组的数据结构,分配的是一段连续的内存空间。 新增数据的时候效率低,因为新增一个元素的时候都要考虑数组的容量,即扩容判断,而后进行新增元素; 修改数据同样根据index索引查找修改; 删除数据的时候效率低,因为是连续的内存空间,除删除最后一个元素外,删除任一元素都会使数组中的元素进行移动; 查找数据可根据index索引快速查找;
(2)LinkedList是基于链表的数据结构 对比以上: 优点: 不需要考虑扩容的问题和连续的内存空间问题,在新增和删除的效率更好 缺点: 缺少了索引,降低了查找元素的效率,对应也降低了修改元素的效率
3. 请说说对HashMap的理解
(1)HashMap是基于Map接口的实现,存储键值对时,它可以接收null的键值,是非同步的, HashMap存储着Entry(hash, key, value, next)对象
(2)通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put 方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过 Load Facotr 则resize为原来的 2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket 位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通 过链表将产生碰撞冲突的元素组织起来,在Java 8中,如果一个bucket中碰撞冲突 的元素超过某个限制(默认是8),则使用红黑树来替换链表,从而提高速度
(3)通过对key的hashCode()进行hashing,并计算下标( (n-1) & hash ),从而获得 buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应 的节点
(4)在Java 1.8的实现中,是通过hashCode()的高16位异或低16位实现的: (h = k.hashCode()) ^ (h >>> 16) ,主要是从速度、功效、质量来考虑的,这么做 可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中, 同时不会有太大的开销。
(5)如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HashMap, 并且重新调用hash方法。
4. HashMap, LinkedHashMap 和 TreeMap有啥区别?
(1)HashMap不保证数据有序
(2)LinkedHashMap保证数据可以保持插入顺序
(3)TreeMap可以保持key的大小顺序
5. HashMap 与 HashTable 的区别
(1)HashMap 是非 synchronized 的,性能更好
(2)HashMap 可以接受为 null 的 key-value,而Hashtable 是线程安全的,比 HashMap 要慢,不接受 null 的 key-value
6. 谈谈对于 ConcurrentHashMap 的理解
(1)首先HashMap是线程不安全的,其主要体现:
第一,在jdk1.7中,使用头插法,在多线程环境下,扩容时会造成环形链或数据丢失。
第二,在jdk1.8中,在多线程环境下,会发生数据覆盖的情况 在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,会出现数据覆盖的情况;
(2)Hashtable线程安全,但是效率低下,体现在:
Hashtable是用synchronized关键字来保证线程安全的,由于synchronized的机制是在同一时刻只能有一个线程操作,其他的线程阻塞或者轮询等待,在线程竞争激烈的情况下,这种方式的效率会非常的低下。
(3)ConcurrentHashMap实现分析
从类图可以看出:ConcurrentHashMap由Segment和HashEntry组成。
Segment是可重入锁,它在ConcurrentHashMap中扮演分离锁的角色;
HashEntry主要存储键值对;
CurrentHashMap包含一个Segment数组,每个Segment包含一个HashEntry数组并且守护它,当修改HashEntry数组数据时,需要先获取它对应的Segment锁;而HashEntry数组采用开链法处理冲突,所以它的每个HashEntry元素又是链表结构的元素
更多了解查看 超详细!从HashMap到ConcurrentMap,我是如何一步步实现线程安全的!
7. 创建线程的方式有哪些?
(1)继承Thread类,重写run方法
(2)实现Runnable接口,并实现该接口的run方法
(3)实现Callable接口,重写call方法
(4)使用线程池
8. 线程的状态有哪些?
(1)New:新创建状态。线程被创建,还没有调用start方法
(2)Runnable: 可运行状态。调用starth后进入当前状态
(3)Blocked: 阻塞状态
(4)Waiting: 等待状态
(5)Timed waiting: 超时等待状态
(6)Terminated: 终止状态
9. synchronized 和 volatile 关键字的区别
(1)volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
(2)synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
(3)volatile仅能使用在变量级别
(4)synchronized则可以使用在变量、方法、和类级别的
(5)volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
(6)volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
(7)volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
10. 如何保证线程安全
当多个线程要共享一个实例对象的值得时候,那么在考虑安全的多线程并发编程时就要保证下面3个要素:
(1)原子性(Synchronized, Lock)
(2)有序性(Volatile,Synchronized, Lock)
(3)可见性(Volatile,Synchronized,Lock)
由于synchronized和Lock保证每个时刻只有一个线程执行同步代码,所以是线程安全的,也可以实现这一功能,但是由于线程是同步执行的,所以会影响效率。
原子性: 指的是一个或多个操作要么全部执行成功要么全部执行失败。
可见性: 指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性:程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
11. ThreadLocal 用法和原理
ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
(1)ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
(2)ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
(3)ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
(4)ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。
12. 谈一谈线程 sleep() 和 wait() 方法的区别?
(1)sleep 是Thread类的方法,wait是Object类的方法
(2)sleep不释放锁,wait释放锁
(3)sleep不需要Synchronized ,wait需要Synchronized
(4)sleep不需要唤醒,wait需要唤醒(除wait(int time))
13. 谈谈 Java 线程中 notify 和 notifyAll 方法有什么区别
notify:只会唤醒等待该锁的其中一个线程。 notifyAll:唤醒等待该锁的所有线程。 既然notify会唤醒一个线程,并获取锁,notifyAll会唤醒所有线程并根据算法选取其中一个线程获取锁
14. Java 中有哪些常见的锁
(1)synchronized
(2)ReentrantLock
(3)ReentrantReadWriteLock
(4)AtomicInteger
补充: 在java中可以通过锁和循环CAS的方式来实现原子操作。Java 中 java.util.concurrent.atomic 包相关类就是 CAS的实现,atomic包里包括 以下类:
15. 什么是悲观锁和乐观锁?
(1)乐观锁:默认为,某个线程在自己处理共享资源的时候,不会出现同一时刻来修改此资源的前提,只在处理完毕,最后写入内存的时候,检测是否此资源在之前未被修改。类似于读写锁的读锁就是乐观锁。
(2)悲观锁:默认为,某个线程在自己处理共享资源的时候,一定会出现同一时刻来修改此资源,所以刚拿到这个资源就直接加锁,不让其他线程来操作,加锁在逻辑处理之前。类似,synchronized关键字,条件锁,数据库的行锁,表锁等就是悲观锁。
16. 线程池的优点有哪些,都有哪些参数?
线程池的优点:
(1)重用线程池中的线程,避免因为线程的创建和销毁带来性能开销。
(2)能有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致的 阻塞现象。
(3)能够对线程进行管理,并提供定时执行以及定间隔循环执行等功能。
线程池的构造方法如下:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
ThreadPoolExecutor的参数包括:
- corePoolSize:核心线程数,表示线程池维护线程的最少数量。
- maximumPoolSize:最大线程数,表示线程池维护线程的最大数量。
- workQueue:阻塞队列,表示如果任务数量超过核心池大小,多余的任务添加到阻塞队列中。
- keepAliveTime:线程池维护线程所允许的空闲时间。
- unit:线程池维护线程所允许的空闲时间的单位。
- threadFactory:线程工厂,用于创建新的线程。
- handler:拒绝策略,当任务队列已满时,用于处理无法执行新任务的情况。
17. 线程池的种类有哪些?
- 固定线程池(FixedThreadPool):只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结果的有界队列,控制线程的最大并发数。
- 可缓冲线程池(CachedThreadPool):无核心线程,非核心线程数量无限,执行完闲置60s回收,任务队列为不存储元素的阻塞队列,执行大量、耗时少的任务。
- 单线程池(SingleThreadExecutor):只有一个核心线程,无非核心线程,执行完立即回收,任务队列为链表结果的有界队列,不适合做并发但可能引起io阻塞及影响ui线程响应的操作,如数据库操作、文件操作等。
- 定时器线程池(ScheduledThreadPool):核心线程数量固定,非核心线程数量无限,执行完闲置10s后回收,任务队列为延时阻塞队列,执行定时或周期性任务。
18. 引用类型有哪些?
(1)强引用(StrongReference) 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
(2)软引用(SoftReference) 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。 软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
(3)弱引用(WeakReference) 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列 ReferenceQueue 联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
(4)虚引用(PhantomReference) “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 ReferenceQueue 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
19. Java 中反射的理解
Java中的反射首先是能够获取到Java中要反射类的字节码, 获取字节码有三种方法:
1.Class.forName(className)
2.类名.class
3.this.getClass()
然后将字节码中的方法,变量,构造函数等映射成相应的 Method、Filed、Constructor 等类,这些类提供了丰富的方法可以被我们所使用。
20. 简述 JVM 中类的加载机制与加载过程?
java中的类加载机制
Java语言系统自带有三个类加载器:
- Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib下的rt.jar、 resources.jar、charsets.jar和class等。另外需要注意的是可以通过启动jvm时指定-Xbootclasspath和路径来改变Bootstrap ClassLoader的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的bootstrap路径中。
- Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。还 可以加载-D java.ext.dirs选项指定的目录。
- Appclass Loader也称为SystemAppClass 加载当前应用的classpath的所有类。
我们需要知道这三个加载器的加载顺序
- Bootstrap CLassloder
- Extention ClassLoader
- AppClassLoader
然后需要知道这个加载顺序具体执行策略 双亲委托机制
一个类加载器查找class和resource时,是通过“委托模式”进行的,它首先判断这个class是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到Bootstrap ClassLoader,如果Bootstrap classloader找到了,直接返回,如果没有找到,则一级一级返回,最后到达自身去查找这些对象。这种机制就叫做双亲委托。
21. JVM、Dalvik、ART 三者的原理和区别?
1、JVM指Java虚拟机,能够运行Java字节码的虚拟机器,有多种实现
2、Dalvik虚拟机是Google设计用于Android平台的虚拟机,支持已转换为.dex格式的Java程序的运行。与JVM的区别:
- Dalvik基于寄存器,JVM基于堆栈
- Dalvik有自己的字节码,不使用Java字节码
- Android2.2开始,Dalvik支持JIT即时编译
3、ART虚拟机是现行的Android虚拟机,与Dalvik的区别:
- ART采用的是Ahead-of-time AOT预编译技术,Dalvik采用的是JIT即时编译技术
- AOT预编译会在应用安装过程中,将所有的字节码编译成机器码,应用运行时无需实时编译了,直接调用即可
- JIT即时编译在应用启动时,通过性能分析来优化代码的执行;在应用运行时,实时将字节码编译成机器码
所以,ART虚拟机提高了程序的运行效率;首次安装需要预编译,所以安装时间比Dalvik中略长,机器码占用空间大,应用占用空间也会略大。
22. 请谈谈 Java 的内存回收机制?
在Java中,它的内存管理包括两方面:内存分配(创建Java对象的时候)和内存回收,这两方面工作都是由JVM自动完成的,降低了Java程序员的学习难度,避免了像C/C++直接操作内存的危险。但是,也正因为内存管理完全由JVM负责。
23. 什么是 JMM?它存在哪些问题?该如何解决?
java内存模型:定义了共享内存系统中多线程程序读写操作行为的规范,Java内存模型也就是为了解决这个并发编程问题而存在的 怎么解决:内存模型解决并发问题主要采取两种方式,分别是限制处理器优化,另一种是使用了内存屏障。 而对于这两种方式,Java底层其实已经封装好了一些关键字,我们这边只需要用起来就可以了。 关于解决并发编程中的原子性问题,Java底层封装了Synchronized的方式,来保证方法和代码块内的操作都是原子性的; 而至于可见性问题,Java底层则封装了Volatile的方式,将被修饰的变量在修改后立即同步到主内存中。 至于有序性问题,其实也就是我们所说的重排序问题,Volatile关键字也会禁止指令的重排序,而Synchroinzed关键字由于保证了同一时刻只允许一条线程操作,自然也就保证了有序性。
24. Java中的垃圾收集算法有哪些?
- 标记-清除(Mark-Sweep) :这是最基本的垃圾收集算法。它分为两个阶段:标记阶段和清除阶段。在标记阶段,垃圾收集器会遍历所有对象,找出并标记活动的对象。在清除阶段,垃圾收集器会清理掉所有未被标记的对象。这种算法的一个主要问题是会产生大量碎片化的内存。
- 复制(Copy) :这种算法将内存分为两个区域,每次只用其中一个区域。当进行垃圾收集时,将活动的对象从当前区域复制到另一个区域,然后清除当前区域的所有对象。这种算法避免了内存碎片化,但需要两倍的内存空间。
- 标记-压缩(Mark-Compact) :这是标记-清除算法的改进版。它在清除阶段后,会将所有活动的对象压缩到内存的一端,然后直接清除边界以外的所有内存。这种算法解决了内存碎片化的问题,但需要额外的空间来存储活动对象。
- 分代收集(Generational GC) :这种算法将内存分为几个不同的代(如新生代和老年代),根据对象的存活时间和分配位置进行不同的收集策略。新生代中的对象存活时间短,可以快速地进行标记和清除;而老年代中的对象存活时间长,可以采用更复杂的标记-压缩算法。这种算法可以提高垃圾收集的效率,但需要更复杂的内存管理。
25. 内存分配策略
Java 程序运行时的内存分配策略有三种,分别是静态分配,栈式分配,和堆式分配,对应的,三种存储策略使用的内存空间主要分别是静态存储区(也称方法区)、栈区和堆区。
(1)静态存储区(方法区):主要存放静态数据、全局 static 数据和常量。这块内 存在程序编译时就已经分配好,并且在程序整个运行期间都存在。
(2)栈区 :当方法被执行时,方法体内的局部变量都在栈上创建,并在方法执行结 束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于 处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)堆区 : 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存。 这部分内存在不使用时将会由 Java 垃圾回收器来负责回收。
26. 栈与堆的区别
-
存储位置:栈是位于方法调用栈上的内存区域,而堆是位于Java虚拟机中的内存区域。
-
生命周期:栈的生命周期与线程相同,当线程启动时,栈被创建,当线程结束时,栈被销毁。而堆的生命周期与应用程序相同,当应用程序启动时,堆被创建,当应用程序结束时,堆被销毁。
-
内存分配:栈的内存分配和回收速度较快,因为栈的大小在创建时就确定,并且栈的内存分配和回收是由系统自动管理的。而堆的内存分配和回收速度较慢,因为堆的大小在运行时动态调整,并且堆的内存分配和回收是由程序员手动管理的。
-
内存溢出:如果程序在运行过程中不断向栈中压入数据,而栈的大小有限,那么就会发生栈溢出。如果程序在运行过程中不断向堆中申请内存空间,而堆的大小有限,那么就会发生堆溢出。
-
内存泄漏:如果程序在运行过程中不断向堆中申请内存空间,但是并没有及时释放这些内存空间,那么就会发生内存泄漏。而栈不会发生内存泄漏。