基础
一、算法
(一)二分查找
源码:
public static int binarySearch(int[] array, int num) {
//1. 首先排序
int temp;
for (int i = array.length - 1; i >= 0; i--) {
for (int j = 0; j < i; j++) {
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
//2. 进行二分查找
int l = 0, r = array.length - 1, m;
while (l <= r) {
m = (l + r) / 2;
if (array[m] == num) {
return m;
}
if (array[m] > num) {
r = m - 1;
} else {
l = m + 1;
}
}
return -1;
}
当L和R的值过大时,相加可能超过int能存储的最大范围,导致整数溢出问题
方法一:(l+r)/2>>l+(r-l)/2
方法二:移位运算(l+r)>>>1
补充:java移位运算
(二)冒泡排序
(1)冒泡排序改进版1.0
public static void bubbleSort(int[] array) {
int temp;
for (int i = 0; i < array.length - 1; i++) {
//设置flag,只要某一次冒泡没有发生交换,就表明数组已经排好序,直接退出循环
boolean flag = false;
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
flag = true;
}
}
if (!flag)
break;
System.out.println("第" + i + "轮冒泡排序:" + Arrays.toString(array));
}
}
(2)冒泡排序改进版2.0
public static void bobbleSort02(int[] array) {
int n = array.length - 1;
while (true) {
//记录最后一次交换的索引位置,初始值0表示假设现有数组全部有序
int last = 0, temp;
for (int i = 0; i < n; i++) {
if (array[i] > array[i + 1]) {
temp = array[i];
array[i] = array[i + 1];
array[i + 1] = temp;
//如果发生了交换,就记下交换的索引位置
last = i;
}
}
//下一次冒泡的结束索引位置
n = last;
if (n == 0)
break;
}
}
(三)选择排序
选择排序: 首先在未排序的数组中找到最小(or最大)元素,然后将其存放到数组的起始位置;接着,再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到未排序数组的起始位置。以此类推,直到所有元素均排序完毕。
public static void selectionSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
int min = i;
for (int j = i + 1; j < array.length; j++) {
if (array[min] > array[j]) {
min = j;
}
}
if (min != i) {
//最小元素和第一个元素换位置
int temp = array[i];
array[i] = array[min];
array[min] = temp;
System.out.println("第" + i + "轮排序:" + Arrays.toString(array));
}
}
}
稳定性:
- 在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
- 简单来说,就是两个元素相同时,冒泡不会交换其顺序,而选择会交换两者顺序。
(三)插入排序
二、集合
(一)ArrayList
- new一个无参arrayList时,初始容量是0;当有第一个内容添加时,初始容量变为10。
- ArrayList底层是数组,扩容后新数组会替换掉旧数组,旧数组没引用就会被垃圾回收掉。
- ArrayList的扩容不是直接*1.5,而是通过移位实现,如10+10>>1。
- 对于
.addAll(Collection<E> c)
方法的扩容机制:
A. 无参构造时:下一次默认的扩容容量大小和当前添加元素个数之间进行比较,取较大值来扩容。
如添加11个元素,下一次默认扩容大小10,而元素长度11,这时就会取元素长度作为集合扩容后的大小。
(1)FailFast&FailSafe
这两个是对于集合迭代器(iterator)而言。
failFast
:一旦发现遍历的同时有其他人来修改,则立刻抛异常。
failSafe
:发现遍历的同时有其他人来修改,应当能有应对策略,如牺牲一致性来让整个遍历运行完成。
ArrayList属于failFast
CopyOnWriteArrayList属于failSafe
(2)ArrayList对比LinkedList
局部性原理:CPU缓存读取一个数据时,会附带读取该数据相邻的数据(内存地址必须相邻,所以ArrayList能利用缓存,而LinkedList不能)。
(二)HashMap(重点)
HashMap默认容量16,当元素超过3/4时(不包含)会扩容,每次扩容2倍。
不同的哈希码算出相同的下标Index,就会导致哈希碰撞,一旦发生哈希碰撞,HashMap的查找效率就会从O(1)退化成O(n)或者O(logn)
链表树化为红黑树条件(同时满足):
- 链表长度>8(如果长度没超过64,首先会尝试扩容来减少链表长度)
- 数组长度>=64
红黑树特点 :父节点左边都是比父节点小的数,右边都是比父节点大的数。
↑删除树中某个key时,才进行是否退化检查(删除前检查,而不是删除后做检查)。
Q:介绍下HashMap的数据结构
A:
- HashMap 的底层实现基于一个数组,这个数组的每个元素是一个桶(bucket)。每个桶中存储的是链表或红黑树组成的节点(没有hash冲突时桶直接存Node对象)。如果发生哈希冲突(多个键映射到同一个桶),这些节点会以链表或红黑树的形式存储在同一个桶中。
// Node结构
static class Node<K, V> {
final int hash; // 键的哈希值
final K key; // 键
volatile V val; // 值
volatile Node<K, V> next; // 指向下一节点的指针
}
// 红黑树结构
static final class TreeNode<K, V> extends Node<K, V> {
TreeNode<K, V> parent; // 父节点
TreeNode<K, V> left; // 左子节点
TreeNode<K, V> right; // 右子节点
TreeNode<K, V> prev; // 前驱节点
boolean red; // 节点颜色(红/黑)
}
+-------------------+
| HashMap |
+-------------------+
| |
| +---------------+ +---------------+ +---------------+
| | Bucket 0 | --> | Node (k1,v1)| --> | Node (k3,v3)| ...
| +---------------+ +---------------+ +---------------+
| |
| +---------------+ +---------------+
| | Bucket 1 | --> | Node (k2,v2)| ...
| +---------------+ +---------------+
| |
| +---------------+
| | Bucket 2 | --> null
| +---------------+
| |
| ... |
| |
| +---------------+
| | Bucket n-1 | --> +---------------+ +---------------+
| +---------------+ | Node (k4,v4)| --> | Node (k5,v5)| ...
| +---------------+ +---------------+
+---------------------------+
- HashMap使用键的hash值来确定它应该存储在哪个桶中,计算过程:
- 调用键的
hashCode()
方法,得到一个整数 - 使用该整数与数组长度进行取模运算,得到桶索引
- 调用键的
Q:HashMap是线程安全的吗,为什么?
A:
因为HashMap里的方法没有使用任何同步机制(比如synchronized或volatile)。
在多线程环境下,多个线程同时操作HashMap可能导致数据不一致或扩容死链的问题。
- 比如在
put()
时,多个线程同时插入数据,可能导致数据覆盖或丢失。 - 在扩容时,如果多个线程同时触发
resize()
方法,可能导致链表形成环,从而引发扩容死链。
Q:介绍下安全的HashMap
A:
- 通过
Collections.synchronized()
将HashMap包装成线程安全的Map。 - 使用
HashTable
,但性能较差(因为所有方法都用synchronized修饰)。 - 使用
ConcurrentHashMap
,专为高并发场景设计的线程安全Map,性能优于HashTable
。
通过分段锁(JDK 7)或CAS+synchronized
(JDK 8)实现线程安全。
Q:介绍下Collections.synchronized()
是如何包装HashMap线程安全的。
A:
注:仅适合低并发环境
- 互斥锁(mutex)
- 默认使用当前对象本身作为互斥锁(mutex),但可以通过构造方法传入自定义锁对象。
- 方法级别的同步
- 每个方法都被
synchronized
修饰,确保同一时间只有一个线程可以访问Map。 - 存在的问题:方法级别的锁,在高并发场景下性能下降。
- 复合操作仍然是非线程安全的,如
if (!synchronizedMap.containsKey("key")) synchronizedMap.put("key", "value");
- 使用iterator遍历Map时仍然是非线程安全的,需要给遍历加锁。
- 每个方法都被
- 使用装饰器模式(Decorator Pattern)
Synchronized
是对原始Map的包装,所有操作都委托给原始Map执行。
Collections.synchronizedMap
的核心实现
public static <K, V> Map<K, V> synchronizedMap(Map<K, V> m) {
return new SynchronizedMap<>(m);
}
// 内部静态类 SynchronizedMap
private static class SynchronizedMap<K, V> implements Map<K, V>, Serializable {
private final Map<K, V> m; // 被包装的 Map
final Object mutex; // 互斥锁对象
SynchronizedMap(Map<K, V> m) {
this.m = Objects.requireNonNull(m);
this.mutex = this; // 使用当前对象作为锁
}
// 所有方法都使用 synchronized 修饰
public int size() {
synchronized (mutex) { return m.size(); }
}
public boolean isEmpty() {
synchronized (mutex) { return m.isEmpty(); }
}
public boolean containsKey(Object key) {
synchronized (mutex) { return m.containsKey(key); }
}
public boolean containsValue(Object value) {
synchronized (mutex) { return m.containsValue(value); }
}
public V get(Object key) {
synchronized (mutex) { return m.get(key); }
}
public V put(K key, V value) {
synchronized (mutex) { return m.put(key, value); }
}
public V remove(Object key) {
synchronized (mutex) { return m.remove(key); }
}
public void putAll(Map<? extends K, ? extends V> map) {
synchronized (mutex) { m.putAll(map); }
}
public void clear() {
synchronized (mutex) { m.clear(); }
}
// 其他方法省略...
}
Q:介绍下ConcurrentHashMap
是如何利用CAS+synchronized
保证线程安全的
A:
(三)单例模式
单例模式常见五种实现方式?
- 饿汉式
public class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
- 枚举饿汉式
public enum Singleton {
INSTANCE;
// 自定义成员变量
private String field01;
public String getField01() {
return field01;
}
public void setField01(String field01) {
this.field01 = field01;
}
// 其他需要的方法
public void doSomething() {
System.out.println(field01);
}
}
- 懒汉式(问题:当有多线程环境时,可能会被创建多个对象,导致单例模式实际上是失效的,所以在方法上加锁)
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- DCL(Double Check Lock双检锁)懒汉式(首次使用对象时加入同步锁,二次使用后不加锁)
这里有两个if语句,也是DCL名称的来源(如果不加第二个if,实际上还是会创建多个对象,因为等待解锁的线程在synchronized上等待完成后,进入不再次判断的话就会创建多个对象)
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
6. 内部懒汉式(推荐)
public class Singleton {
private Singleton() {
}
private static final class SingletonInner {
private static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonInner.instance;
}
}
jdk中有哪些地方体现了单例模式?
- Runtime(饿汉式)
- System的Console对象(DCL懒汉式)
并发
临界区
- 临界区是指一个程序片段或代码块,其中包含了对临界资源的访问或修改操作。
临界资源
- 临界资源是指一次只能被一个进程访问的资源。
- 临界资源和临界区的区别:
- 临界资源:只允许一个进程进行访问的资源,比如打印机、消息缓冲队列。
- 临界区:使用临界资源的代码区。
- 共享资源包含临界资源,临界资源是共享资源的子集。
- 如何判断共享资源和临界资源(前提首先这个资源是共享资源):在一段时间内能否允许被多个进程访问(并发使用),不能则为临界资源,能则为共享资源。
- 共享资源且不可并发:临界资源。
共享资源且可并发:共享资源。
共享资源
- 共享资源是指多个进程或线程可以同时访问的资源,它通常存储着数据或状态信息。
- 共享资源可以是临界资源,也可以是非临界资源。临界资源需要特别的同步机制来保护,而非临界资源可能不需要同步。
参考链接:
临界区、临界资源、共享资源、临界调度原则
临界资源和共享资源
OS 共享变量、临界区、临界资源之间的联系
并发编程三要素:共享数据、互斥访问和同步机制
[多线程问题需要满足哪些条件]https://www.cnblogs.com/bangiao/p/13195689.html#%E5%A4%9A%E7%BA%BF%E7%A8%8B%E9%97%AE%E9%A2%98%E9%9C%80%E8%A6%81%E6%BB%A1%E8%B6%B3%E5%93%AA%E4%BA%9B%E6%9D%A1%E4%BB%B6?#()
互斥访问
同步机制
(一)线程状态
(1)线程有哪些状态?
Java线程有哪几个状态?
Java的线程分成六种状态。视频讲解
操作系统的线程分成五种状态(笔记上有)
Java线程状态之间的转换?
如上图
六种状态和五种状态
六种是Java层面的线程状态;五种是操作系统层面的线程状态,如上图
(2)线程池的核心参数?(重点)
执行流程:提交任务后,如果有空闲核心线程就直接执行,不进任务队列;如果核心线程全部在忙,就进入任务队列排队等待(先进先出)。
救急线程:当核心线程全部正在执行任务,同时任务队列也已经满了,这时又添加了一个任务就会启动救急线程,来执行这个溢出的任务。
handler的四种拒绝策略:
- AbortPolicy:直接抛出异常,告知新任务不能被执行。
- CallerRunsPolicy:让调用者自己去执行这个新任务(谁提交的任务谁去执行)。
- DiscardPolicy:默默丢弃新任务,不报异常和通知。
- DiscardOldestPolicy:将任务队列的第一个任务丢弃掉(也就是最先加入任务队列的),然后将新任务加入到队列尾中。
代码示例:
(3)sleep() VS wait()
(4)lock VS synchronized
(5)悲观锁和乐观锁
(6)ConcurrentHashMap
1.8之前
1.8之前是由多个Segment组成,每个Segment就是一把分段锁,通过局部的上锁实现了整体的线程安全问题,理论上有多少个Segment就支持多少线程并发。
默认Segment容量是16,可以在初始化时指定Segment数量,但是一旦初始化完成后,Segment不可以再扩容;每个Segment下的数据结构和HashMap类似。
1.8之后
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性,也引入了红黑树。
HashTable VS ConcurrentHashMap
对比
(7)ThreadLocal
线程并发:在多线程并发的场景下
传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
线程隔离:每个线程的变量都是独立的,不会互相影响
ThreadLocal的数据结构类似于HashMap,可以保存键值对,但是一个ThreadLocal只能保存一个键值对;
在ThreadLocalMap中,也是初始化一个大小为16的数组,但是数组下没有链表的情况,每一个数组只保存一个键值对,ThreadLocal把自己当作key,存储的值为value,放入ThreadLocalMap;
如果在存入时发生hash碰撞的情况,那么ThreadLocal会去找下一个相邻的数组,看是否为空,如果为空就放入,否则继续找下一个。
- 强引用:就算内存不足,JVM也不会回收该对象;
- 软引用:只有在内存不足时,JVM才会回收该对象;
- 弱引用:一旦被JVM发现弱引用对象,不管内存足够与否,JVM都会回收该对象;
虚拟机
一、JVM内存结构
JVM的内存结构有哪几部分?
- 方法区里,存放类加载的信息,比如类名,类继承关系,类成员变量等信息。
- 堆里,存放对象信息。
- 程序计数器里,用于指明当前程序运行到哪一行了。
- 虚拟机栈,存放局部变量或者对象实例的引用地址。
- 本地方法栈,存放引用的系统库。
哪些部分会出现内存溢出?
二、垃圾回收
JVM的垃圾回收算法?
标记:不能被垃圾回收的对象,将打上标记