《JAVA八股真解》三、线程与锁

#JAVA##JAVA面经##JAVA内推#

1. 线程的状态

alt

Java中的线程生命周期由Thread类的getState()方法返回,共有六种状态:

状态 说明
NEW(新建) 线程刚被创建但尚未启动,处于初始状态。
RUNNABLE(运行中) 线程已启动并正在执行或准备执行任务。此状态包括“就绪”和“运行”两个阶段,取决于是否获取到CPU时间片。
BLOCKED(阻塞) 线程因等待锁而暂停执行,例如在synchronized代码块中无法获得锁时进入该状态。
WAITING(等待) 线程主动调用wait()join()LockSupport.park()等方法后进入无限期等待状态,直到被其他线程唤醒。
TIMED_WAITING(计时等待) 类似于WAITING,但等待时间有限,如调用sleep(long)wait(long)join(long)等方法后进入该状态。
TERMINATED(终止) 线程已完成执行,退出运行状态。

注意:线程从一个状态转换到另一个状态的过程是自动完成的,开发人员通常无需手动干预。

2. 创建线程的方式

Java提供了多种创建线程的方法,常见的有以下几种:

(1)继承Thread

public class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running!");
    }
}

// 使用
MyThread thread = new MyThread();
thread.start();

(2)实现Runnable接口

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread running!");
    }
}

// 使用
Thread thread = new Thread(new MyRunnable());
thread.start();

(3)使用内部类

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                System.out.println("Thread running!");
            }
        };
        thread.start();
    }
}

(4)使用Lambda表达式(JDK 8+)

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("Thread running!"));
        thread.start();
    }
}

推荐:优先使用RunnableCallable接口,避免继承带来的单继承限制。

3. 线程池核心参数

线程池是管理线程资源的重要工具,通过复用线程减少创建开销,提升系统性能。ThreadPoolExecutor是线程池的核心实现类。

ThreadPoolExecutor(
    int corePoolSize,           // 核心线程数
    int maximumPoolSize,        // 最大线程数
    long keepAliveTime,         // 空闲线程存活时间
    TimeUnit unit,              // 时间单位
    BlockingQueue<Runnable> workQueue, // 工作队列
    ThreadFactory threadFactory, // 线程工厂
    RejectedExecutionHandler handler // 拒绝策略
)

参数详解:

  • corePoolSize:核心线程数,即使空闲也不会被回收。
  • maximumPoolSize:最大线程数,当工作队列满且当前线程数小于最大值时,会创建新线程。
  • keepAliveTime:非核心线程的空闲超时时间,超过该时间会被回收。
  • workQueue:任务队列,用于存放待处理的任务。
  • threadFactory:用于创建新线程的工厂。
  • handler:拒绝策略,当线程池无法接受新任务时的处理方式。

常见拒绝策略

  • AbortPolicy:抛出异常。
  • DiscardPolicy:丢弃任务。
  • DiscardOldestPolicy:丢弃最老的任务。
  • CallerRunsPolicy:由调用者线程执行任务。

4. 如何创建线程池?

方式一:使用Executors工具类(不推荐)

ExecutorService executor = Executors.newFixedThreadPool(10);

缺点:容易导致资源耗尽,因为默认无界队列可能导致内存溢出。

方式二:手动创建ThreadPoolExecutor(推荐)

ExecutorService executor = new ThreadPoolExecutor(
    10,                    // corePoolSize
    20,                    // maximumPoolSize
    60L,                   // keepAliveTime
    TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100), // workQueue
    new ThreadPoolExecutor.CallerRunsPolicy() // handler
);

建议:明确设置线程池参数,避免资源泄漏。

5. 线程池工作原理

alt

线程池的工作流程如下:

  1. 提交任务:将任务放入工作队列。
  2. 检查核心线程数:如果当前线程数小于corePoolSize,则创建新线程执行任务。
  3. 检查队列容量:如果线程数已达corePoolSize,且队列未满,则将任务加入队列。
  4. 检查最大线程数:如果队列已满且当前线程数小于maximumPoolSize,则创建新线程。
  5. 拒绝任务:如果线程数达到maximumPoolSize且队列已满,则触发拒绝策略。

图示

提交任务 → 检查核心线程 → 添加到队列 → 创建新线程 → 执行任务 → 完成后回收

6. 线程池大小如何设定?

合理设置线程池大小对性能至关重要。

计算公式:

  • CPU密集型任务线程数 ≈ CPU核心数 + 1
    • 原因:避免过多线程争抢CPU资源,造成上下文切换开销。
  • IO密集型任务线程数 ≈ CPU核心数 × (1 + IO等待时间 / CPU计算时间)
    • 原因:IO操作期间线程处于等待状态,可利用空闲时间处理其他任务。

示例:假设服务器有4核CPU,平均每个任务需要1秒CPU时间和2秒IO等待,则线程数约为:

4 × (1 + 2/1) = 12

7. 线程池的拒绝策略有哪些?

拒绝策略 行为描述
AbortPolicy 抛出RejectedExecutionException异常,阻止任务提交。
DiscardPolicy 直接丢弃任务,不抛异常。
DiscardOldestPolicy 丢弃队列中最老的任务,再尝试提交新任务。
CallerRunsPolicy 由调用线程直接执行任务,适用于负载较低场景。

选择依据

  • 若希望及时响应错误,选AbortPolicy
  • 若允许部分任务丢失,选DiscardPolicy
  • 若需保证关键任务执行,选CallerRunsPolicy

8. 线程池是一个服务还是一个组件?为什么?

线程池更像是一种服务而非简单的组件。

原因:

  1. 提供长期运行能力:线程池可以持续接收和处理任务,类似于Web服务器的请求处理。
  2. 资源管理:它负责线程的生命周期管理、任务调度、异常处理等,具备完整的生命周期。
  3. 可扩展性:支持动态调整线程数量、队列容量等参数,适应不同负载需求。

对比:普通组件通常是短生命周期的,而线程池作为后台服务,贯穿整个应用运行过程。

9. synchronized 和 lock 的区别

特性 synchronized Lock
是否可中断 不可中断 可中断(如lockInterruptibly()
是否公平 非公平锁(默认) 支持公平锁
是否可重入 可重入 可重入
是否支持条件变量 不支持 支持(Condition
是否需要显式释放 自动释放 必须手动释放(unlock()
是否支持超时 不支持 支持(tryLock(timeout)

推荐:在需要复杂同步逻辑时使用Lock;简单场景下使用synchronized即可。

10. synchronized 的锁种类

synchronized关键字支持三种锁类型:

  1. 对象锁(实例锁):

    • 锁住当前对象实例,同一时刻只能有一个线程访问。
    • 示例:synchronized(this)synchronized(obj)
  2. 类锁(静态锁):

    • 锁住整个类,所有线程共享同一个锁。
    • 示例:synchronized(MyClass.class)static synchronized method()
  3. 方法锁

    • 在方法前加synchronized修饰符,等价于在方法体内加锁。
    • 示例:public synchronized void doSomething()

注意synchronized作用于对象时,锁的是对象本身;作用于类时,锁的是类的字节码。

11. 线程安全问题及解决方案

常见问题:

  • 竞态条件(Race Condition):多个线程同时修改共享数据,导致结果不可预测。
  • 死锁(Deadlock):两个或多个线程互相等待对方释放锁,形成循环依赖。
  • 活锁(Livelock):线程不断重复相同的操作,却无法前进。

解决方案:

  • 使用synchronizedReentrantLock保证临界区互斥。
  • 使用volatile关键字确保可见性。
  • 使用原子类(如AtomicInteger)替代基本类型。
  • 使用线程安全集合(如ConcurrentHashMap)。

12. 线程池最佳实践

  1. 避免使用Executors工具类创建线程池,应手动配置参数。
  2. 合理设置线程池大小,根据任务类型选择合适的线程数。
  3. 设置合理的队列容量,防止内存溢出。
  4. 选择合适的拒绝策略,根据业务需求决定是否丢弃任务。
  5. 监控线程池状态,定期检查活跃线程数、队列长度等指标。
  6. 优雅关闭线程池,使用shutdown()shutdownNow()
#Java##JAVA面经##JAVA秋招#

本专栏在精不在多,内容分为八股文、大厂真实面经,面试通过后将offer和面试题私发给我,可退还专栏的收益部分费用。欢迎大家共建专栏

全部评论

相关推荐

02-26 09:15
已编辑
蚌埠学院 golang
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务