Array List底层是用动态扩展的数组实现的;
ArrayList初始容量为0,当第一次添加数据的时候才初始容量为10;
在进行扩展时容量是原来的1.5倍,每次扩展都需要拷贝数据;
在添加数据的时候有以下情况:首先判断当前数组是否有足够容量存储新数据,如果容量不足,将调用grow方法进行扩容(原来的1.5倍),确保新增数据有地方存储后,将新元素添加到位于size的位置上,返回添加成功布尔值。
为什么ArrayList的插入有时比LinkedList快?
A:当插入位置靠近尾部且不需要扩容时,ArrayList的System.arraycopy()
在现代CPU上的效率可能高于LinkedList的内存分配和指针操作,尤其在小数据集时。
何时LinkedList实际插入复杂度不是O(1)?
A:当需要先定位插入位置时(如list.add(index, element)
),需要O(n)遍历时间,实际操作为O(n)+O(1)
核心思想:多线程就是让程序能“同时”干好几件事(虽然CPU可能在快速切换)。关键问题在于怎么让它们别打架(线程安全),怎么排队(锁),怎么互相打招呼(协作),怎么高效干活(线程池)。
1. 线程安全:为啥要“锁”?
i++
这个操作。它看起来简单,实际是三步:读i的值 -> 加1 -> 写回i。如果两个线程同时做这个操作,可能它们都读到同一个值(比如10),都加1变成11,然后都写回去。结果应该是12,但实际只有11!这就是竞态条件。HashMap
为啥线程不安全?ConcurrentHashMap
怎么解决的?
HashMap
就像个没管理员也没排队规则的菜市场摊位,一群人(线程)同时抢着往本子上记东西(修改内部结构),很容易把本子撕坏(内部链表环化导致死循环)或者记错(数据丢失/错误)。ConcurrentHashMap
聪明多了:它给菜市场分了很多小隔间(分段锁/JDK8后的Node+CAS),大家只在抢同一个隔间的东西时才需要排队,抢不同隔间的可以同时进行,效率高多了。或者用更精细的“无锁”方式(CAS)来记账。2. 锁:怎么“排队”和“管理钥匙”?
synchronized
(内置锁/监视器锁):
synchronized
-> 钥匙就是这个方法所属的对象本身(对于实例方法) 或 这个类的Class对象(对于静态方法)。synchronized(某个对象) { ... }
-> 钥匙就是你指定的那个对象(锁对象)。synchronized
方法或代码块),不会被自己卡在外面。就像你有钥匙,进大门后进里面的小门不需要再找钥匙。ReentrantLock
(可重入锁):
synchronized
的升级版。还是那把钥匙,但功能更强:
lockInterruptibly()
) 然后去做别的事。tryLock(timeout)
)。Condition
): 更精细的“等待室”。比如生产者线程发现仓库满了,就去“满仓等待室”等着;消费者拿走东西后,可以去那个“满仓等待室”喊一声“有位置了!”,只唤醒在等仓库空位的生产者,而不是乱喊把所有人都吵醒(wait/notify
是唤醒所有在同一个锁上等待的线程)。ReentrantLock
必须手动解锁!通常放在 finally
块里。synchronized
是自动释放的。synchronized
和 ReentrantLock
的基本用法、区别(公平性、灵活性、需手动释放)、可重入性。知道 ReentrantLock
的高级功能(公平、中断、超时、条件)。3. volatile
:这个变量大家都能看见最新版!
volatile
的作用:
volatile
变量,新值立刻对其他所有线程可见(强制写回主存,其他线程读时强制从主存读)。volatile
能防止这种重排序跨越它(建立内存屏障)。volatile
不保证原子性! 它只保证单次读/写是原子的(比如读一个long)。像 i++
(读+改+写)这种复合操作,volatile
管不了。要保证 i++
原子性,得用 synchronized
或 AtomicInteger
。boolean running = true;
需要 volatile
),单例模式双重检查锁定(DCL)里的实例引用。volatile
解决了什么问题(可见性、有序性),不解决什么问题(原子性)。常见使用场景。4. 线程间通信:你干完了叫我一声!
wait()
, notify()
, notifyAll()
(必须在 synchronized
块里用):
条件不满足
)。
wait()
:A把钥匙挂回去(释放锁),然后去厕所旁边的“等待室”坐着睡觉。notify()
:线程B(比如送纸工)拿到钥匙进去送完纸,出来时可以喊一声“纸来啦!”(notify()
),这会随机唤醒等待室里的一个线程(比如A)。notifyAll()
:喊“纸来啦!”,唤醒等待室里所有线程。被唤醒的线程会重新竞争钥匙。wait()
前必须持有锁(在 synchronized
块里)。wait()
会释放锁。wait()
返回前必须重新拿到锁。while(条件不满足) { wait(); }
来防止虚假唤醒(没被 notify
也可能醒来)。Condition
(配合 ReentrantLock
使用):
ReentrantLock
可以有多个独立的“等待室”(Condition)。
notFull.await()
(仓库满了,去“不满等待室”等着) -> 消费者取货后:notFull.signal()
(喊“不满啦!”唤醒生产者)。notEmpty.await()
(仓库空了,去“不空等待室”等着) -> 生产者放货后:notEmpty.signal()
(喊“不空啦!”唤醒消费者)。wait/notify
更精细,能定向唤醒特定条件的线程。wait/notify
机制(为什么要在 synchronized
里?为什么 wait
会释放锁?虚假唤醒?)。知道 Condition
提供了更灵活的等待/通知方式。5. 原子类 (java.util.concurrent.atomic
):不用锁也能安全加减?
i++
不安全,用 synchronized
太重(排队慢)。AtomicInteger
, AtomicLong
等。synchronized
高很多,尤其在低竞争场景。get()
, set()
, getAndIncrement()
(i++), incrementAndGet()
(++i), compareAndSet(expect, update)
(核心CAS) 等。6. 线程池:别老创建销毁线程,太浪费!
corePoolSize
(核心线程数): 池子里长期保留的工人数,即使他们闲着。maximumPoolSize
(最大线程数): 池子最多能容纳的工人数(核心 + 临时工)。workQueue
(工作队列): 任务太多时,排队的队伍。常用 LinkedBlockingQueue
(无界/有界), ArrayBlockingQueue
(有界), SynchronousQueue
(直接交接,不排队)。keepAliveTime
(空闲线程存活时间): 临时工(超出核心线程数的那些)如果闲着超过这个时间,就被解雇(销毁)。threadFactory
(线程工厂): 用来创建新线程(可以设置线程名、优先级等)。RejectedExecutionHandler
(拒绝策略): 当池子满了(线程数达max且队列也满了),新任务咋办?常见策略:
AbortPolicy
(默认):直接抛异常 RejectedExecutionException
。CallerRunsPolicy
:让提交任务的线程(比如main线程)自己来执行这个任务。DiscardPolicy
:默默丢掉新任务,不通知。DiscardOldestPolicy
:丢掉队列里最老的任务,然后尝试把新任务加进去。FixedThreadPool
: 固定大小线程池。核心=最大线程数,队列无界。可能导致OOM。CachedThreadPool
: 核心线程数=0,最大线程数巨大(几乎无限制),队列是 SynchronousQueue
。来一个任务,如果有空闲线程就用,没有就创建新线程。线程空闲60秒后被回收。适合大量短生命周期的异步任务。可能导致创建过多线程。SingleThreadExecutor
: 只有一个线程的池子。保证任务按提交顺序串行执行。如果这个线程挂了,会创建一个新的。ScheduledThreadPool
: 能执行定时或周期性任务的线程池。7. 其他高频点:
Thread
vs Runnable
vs Callable
:
Thread
:代表一个线程对象。可以直接继承 Thread
并重写 run()
,但Java是单继承,不推荐。Runnable
:最常用。定义一个任务(run()
方法),没有返回值,不能抛受检异常。任务可以被提交给 Thread
或线程池执行。Callable
:类似 Runnable
,但它的 call()
方法有返回值,并且可以抛出受检异常。通常配合 Future
/FutureTask
使用,由线程池 (ExecutorService.submit()
) 执行。Future
/ FutureTask
: 代表一个异步计算的结果。你可以用它来:
isDone()
)。cancel()
)。get()
)。注意 get()
会阻塞,直到计算完成或超时。synchronized
和 ReentrantLock
的区别: 上面锁的部分已经讲了(公平性、灵活性、手动释放、条件变量)。tryLock(timeout)
)。ThreadLocal
: 给每个线程提供了一个变量的独立副本。线程内部任何地方都可以访问到这个副本。常用于保存线程上下文信息(如用户会话、数据库连接),避免参数传递。
remove()
!8. ThreadLocal 能给子线程传值吗?
核心答案:默认情况下,不能!
通俗解释:
ThreadLocal
是每个线程自己专属的“储物柜”(ThreadLocalMap
)。父线程往自己的储物柜里放的东西(设置值),子线程是完全看不到、拿不到的。子线程有自己的、全新的、空空的储物柜。ThreadLocal
的设计核心就是线程隔离,保证每个线程操作自己的变量副本,互不干扰。这是它的核心价值所在(避免同步,提高性能)。特殊情况:InheritableThreadLocal
ThreadLocal
的一个特殊子类。InheritableThreadLocal
会把它父线程中当前的值拷贝一份给新创建的子线程的 InheritableThreadLocal
。之后,父子线程各自修改自己的副本,互不影响。new Thread()
创建子线程的那一刻进行值拷贝。之后父线程再修改自己的值,子线程不会跟着变。InheritableThreadLocal
值。但是,当这个线程执行完一个任务,回到线程池待命,再被分配执行下一个任务时:
InheritableThreadLocal
值还在!InheritableThreadLocal
值不会自动覆盖线程池线程已有的值。线程池中如何“传值”?
InheritableThreadLocal
: 因为上述问题,在线程池场景下基本不可用。Runnable
或 Callable
任务的构造参数传入。这是最清晰、最安全的方式。Runnable
/Callable
在任务提交时捕获当前值,在任务执行时恢复值,并在执行后清理,完美适配线程池复用机制。(面试加分项!)总结: 普通 ThreadLocal
绝对不行。InheritableThreadLocal
只在 new Thread()
创建子线程时有效,严禁用于线程池。线程池传值首选显式参数传递,复杂场景考虑 TTL 等方案。
核心答案:强烈推荐使用 ThreadPoolExecutor
构造器手动创建!
为什么不用 Executors
工厂方法?
Executors.newFixedThreadPool()
和 Executors.newSingleThreadExecutor()
: 使用无界队列 (LinkedBlockingQueue
)。如果任务提交速度持续远大于处理速度,队列会无限增长,最终导致 OutOfMemoryError
(OOM)。Executors.newCachedThreadPool()
: 核心线程数为0,最大线程数是 Integer.MAX_VALUE
。如果任务提交过多且都是长任务,可能瞬间创建海量线程,耗尽系统资源 (CPU, 内存,线程数限制),同样可能导致崩溃。正确创建姿势:
import java.util.concurrent.*;
public class CustomThreadPool {
public static void main(String[] args) {
// 1. 定义核心参数 (下面会详细讲每个参数)
int corePoolSize = 5; // 核心线程数
int maximumPoolSize = 10; // 最大线程数
long keepAliveTime = 60L; // 空闲线程存活时间 (单位)
TimeUnit unit = TimeUnit.SECONDS; // 存活时间单位 (秒)
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100); // 工作队列 (有界队列,容量100)
ThreadFactory threadFactory = Executors.defaultThreadFactory(); // 线程工厂 (可自定义)
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy(); // 拒绝策略 (默认抛异常)
// 2. 使用构造器创建线程池
ExecutorService threadPool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
// 3. 提交任务
threadPool.execute(() -> {
System.out.println("执行任务...");
});
// ... 提交更多任务
// 4. 优雅关闭 (重要!)
threadPool.shutdown(); // 停止接收新任务,等待已提交任务执行完成
// threadPool.shutdownNow(); // 尝试立即停止所有正在执行的任务,返回未执行任务列表
}
}
10. 线程池有哪些参数呢?
ThreadPoolExecutor
构造器的 7 个核心参数 (非常重要!):
corePoolSize
(核心线程数):
maximumPoolSize
(最大线程数):
keepAliveTime
(空闲线程存活时间):
corePoolSize
时,那些多余的空闲线程在等待新任务时的最长存活时间。corePoolSize
的那些线程生效。核心线程默认一直存活 (allowCoreThreadTimeOut
可设置核心线程超时)。unit
。unit
(存活时间单位):
keepAliveTime
参数的时间单位。TimeUnit.SECONDS
(秒)、TimeUnit.MILLISECONDS
(毫秒) 等。workQueue
(工作队列):
ArrayBlockingQueue
: 基于数组的有界队列。需要指定容量。任务数超过容量且线程数达max,触发拒绝策略。推荐!防止OOM。LinkedBlockingQueue
: 基于链表的队列。默认构造是无界队列 (Integer.MAX_VALUE
),容易OOM。也可指定容量变成有界。SynchronousQueue
: 一个不存储元素的队列。每个 put
操作必须等待一个 take
操作,反之亦然。相当于直接交接。通常要求 maximumPoolSize
足够大,否则容易触发拒绝策略。Executors.newCachedThreadPool()
使用它。PriorityBlockingQueue
: 具有优先级的无界队列。按优先级出队。也有OOM风险。threadFactory
(线程工厂):
ThreadFactory
接口来自定义线程的名称、是否是守护线程、优先级等。Executors.defaultThreadFactory()
,创建的就是普通的、同组的、非守护线程。handler
(拒绝策略 - RejectedExecutionHandler):
maximumPoolSize
且 workQueue
已满)时,对新提交的任务采取的处理策略。AbortPolicy
(默认): 直接抛出 RejectedExecutionException
异常。最常用,明确知道任务被拒绝了。CallerRunsPolicy
: 将任务退回给调用者线程(提交任务的线程,比如 main 线程)去执行。这样提交任务的速度会下降,给线程池喘息时间。DiscardPolicy
: 默默丢弃新提交的任务,不做任何通知。DiscardOldestPolicy
: 丢弃工作队列中排队时间最久(队列头部)的那个任务,然后尝试重新提交当前这个新任务。(不保证成功,因为队列可能还是满的)。11. 线程数怎么设置呢?
Runtime.getRuntime().availableProcessors()
获取逻辑核心数。CPU核心数 * 2
开始测试,逐步增加,观察 CPU 利用率、响应时间、吞吐量变化,找到性能拐点。-Xss
调整)。大量线程会消耗可观的内存。PriorityBlockingQueue
。maximumPoolSize
和合适的队列大小 (ArrayBlockingQueue
) 以及拒绝策略 (CallerRunsPolicy
或自定义降级) 来应对。ThreadPoolExecutor
提供了 setCorePoolSize()
和 setMaximumPoolSize()
方法,但动态调整逻辑需要自己实现(例如基于监控指标)。ArrayBlockingQueue
) 并设置合理的容量。 这是防止 OOM 的关键。AbortPolicy
或 CallerRunsPolicy
通常是好的选择,让调用者感知到系统压力。CPU核心数 * 2
开始压测调整。压测!压测!压测! 是找到最佳线程数的唯一可靠方法。CPU核心数
或 CPU核心数 + 1
。总结关键点:
InheritableThreadLocal
只在新线程创建时有效,绝对不要用于线程池。线程池传值用参数传递或 TTL。ThreadPoolExecutor
手动创建,避免 Executors
的潜在 OOM 风险。corePoolSize
, maxPoolSize
, keepAliveTime
, unit
, workQueue
, threadFactory
, handler
) 的含义和作用,特别是有界队列和合适的拒绝策略。一句话总结核心: **多线程要安全,共享数据要保护(锁/原子类/volatile)。线程之间要协作(wait/notify/Condition)。线程创建销毁太贵,用池子管理(线程池)。