深入理解 Java 多线程与线程池 —— 从原理到实战

一、为什么要使用多线程?

在现代系统中(如电商秒杀、日志分析、物联网设备数据处理等),单线程执行任务 往往无法满足性能需求。

多线程的核心目标是:提升资源利用率与系统吞吐量

🚫 单线程的不足

  • 任务串行执行,CPU 大部分时间处于空闲;

  • 无法并行计算;

  • 遇到 I/O 阻塞性能急剧下降;

  • 无法支撑高并发访问。

⚡ 多线程的优势

  • 可同时处理多个任务;

  • 充分利用多核 CPU;

  • 提升系统响应速度;

  • 提高 I/O 吞吐能力。

二、Java 实现多线程的四种方式

1️⃣ 继承 Thread

class MyThread extends Thread {

@Override

public void run() {

System.out.println("线程执行:" + Thread.currentThread().getName());

}

}

public class ThreadDemo {

public static void main(String[] args) {

new MyThread().start();

}

}


2️⃣ 实现 Runnable 接口

class MyRunnable implements Runnable {

@Override

public void run() {

System.out.println("Runnable线程执行:" + Thread.currentThread().getName());

}

}

public class RunnableDemo {

public static void main(String[] args) {

new Thread(new MyRunnable()).start();

}

}


3️⃣ 实现 Callable 接口(可返回结果)

复制代码
import java.util.concurrent.*;

public class CallableDemo {

public static void main(String[] args) throws Exception {

ExecutorService executor = Executors.newSingleThreadExecutor();

Future<Integer> future = executor.submit(() -> {

System.out.println("计算中...");

return 42;

});

System.out.println("计算结果:" + future.get());

executor.shutdown();

}

}

4.使用线程池创建

三、多线程

进程和线程的区别?

进程是正在运行程序的实例,一个进程有许多线程,每个线程执行一个任务

进程独占一个内存空间,进程中的线程共享该内存空间

Runnable和Callable的区别?

1.Runnable重写的是run方法,Callable重写的是call()方法

2.Runnable不能抛出异常,只能在内部消化,Callable可以向上抛出异常

3.Runnable的run方法不能返回结果,Callable的call()方法可以返回结果,配合future使用获取结果

线程包括哪些状态,状态之间是如何变化的?

在JDK中的Thread类中的枚举State里面定义了6中线程的状态分别是:新建、可运行、终结、阻塞、等待和有时限等待六种。

关于线程的状态切换情况比较多。我分别介绍一下

当一个线程对象被创建,但还未调用 start 方法时处于新建 状态,调用了 start 方法,就会由新建 进入可运行 状态。如果线程内代码已经执行完毕,由可运行 进入终结状态。当然这些是一个线程正常执行情况。

如果线程获取锁失败后,由可运行 进入 Monitor 的阻塞队列阻塞 ,只有当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞 线程,唤醒后的线程进入可运行状态

如果线程获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行 状态释放锁等待 状态,当其它持锁线程调用 notify() 或 notifyAll() 方法,会恢复为可运行状态

还有一种情况是调用 sleep(long) 方法也会从可运行 状态进入有时限等待 状态,不需要主动唤醒,超时时间到自然恢复为可运行状态

在 java 中 wait 和 sleep 方法的不同?

1.wait可以被notify唤醒,sleep需要等待到时才会被唤醒

2.wait是用于线程间通信,sleep用于线程短暂休眠

3.wait是Object类的方法 只能在synchronized中使用,sleep是Thread的静态方法 可以在任何地方使用

四、线程池

线程池的核心参数?

corePoolSize 核心线程数:线程池中常驻的核心线程数

maximumPoolSize 最大线程数目:核心线程数+救急线程数

keepAliveTime 空闲存活时间:非核心线程到达空闲时间会被销毁

unit 时间单位:分钟,秒等

workQueue 任务队列:当没有空闲核心线程时,新创建的任务假如队列中排队,队列满了会创建救急线程执行任务

threadFactory 线程工厂:用于创建线程,可以自定义线程名,是否为守护线程等

handler 拒绝策略:当队列满了且线程数达到maximumPoolSize,执行拒绝策略。执行策略有四种,第一种是抛出异常(默认),第二种是由调用者执行任务,第三种是丢弃当前任务,第四种是丢弃最早排队的任务

五,java各种锁

synchronized底层原理

利用jvm中的montor判断是否获得了锁,montor位于java对象的对象头中,这也是为什么java对象可以作为锁的原因,montor内部维护了两个变量分别是EntryList等待锁的队列和Owner持有锁的线程,只有一个montor可以设置成功owner,因为一个montor只能有一个owner,在上锁的过程中,会有其他线程来抢锁,则会进入EntryList阻塞,当线程执行完会唤醒EntryList中线程竞争,竞争的时候是非公平的

synchronized锁升级

有偏向锁,轻量级锁,重量级锁

1.当只有一个线程锁一个对象时,属于偏向锁

2.此时又来一个线程与上一次线程交替锁同一对象,没有竞争,属轻量级锁

3.当多个线程锁同一对象时,属于重量级锁

ReentrantLock底层

是可重入的锁,调用lock方法获取锁,再次调用lock时不会阻塞,内部直接增加可重入次数,表示已经重复获取一把锁了,调用unlock释放锁,底层通过CAS和AQS实现,支持公平锁和非公平锁,内部通过构造方法传一个可选公平参数,设置true时为公平锁,默认是非公平锁

volatile

1.保证线程的可见性

一个线程对变量进行了修改,则该变量对其他线程是立即可见的

2.禁止指令重排序

通过插入一个内存屏障在内存屏障前后的指令禁止重排序优化

CAS

修改前,先判断数据是否与期望的一致,一致则修改,

不一致则放弃修改

AQS

1.是阻塞锁和相关工具的框架

1.内部维护了一个state表示持有锁的状态,默认为0表示没有获取锁,1表示获取到锁,通过cas机制设置state的状态

2.通过FIFO阻塞队列管理线程

synchronized和Lock有什么区别 ?

1.synchronized是由C++语言实现,代码执行完锁释放;Lock是java语言实现,调用unlock释放锁

2.两者都是悲观锁,具有互斥,同步,锁重入等功能,但是Lock具备synchronized没有的功能,比如公平锁,可超时,可等待等等

3.synchronized有锁升级,在没有竞争,使用偏向锁,轻量级锁,性能不错;有竞争时,Lock性能比较好

六,并发安全

ConcurrentHashMap底层

jdk1.7的ConcurrentHashMap

默认长度是16,一旦初始化中间不能扩容

底层是分段的数组+链表,内部维护了一个Segment数组,Segment与HashMap结构类似也是数组+链表每个Segment对应一个HashEntry数组,每个HashEntry是链表结构上的元素,segment是可重入的锁ReentrantLock 每个Segment守护一个HashEntry数组中的元素,当HashEntry数组中的元素被修改时,需要先获取对应segment锁

jdk1.8

数据结构与1.8的HashMap结构一致,通过CAS+synchronized实现并发安全,用cas控制数组节点的添加,synchronized只锁定链表或者链表的头结点,只要Hash不冲突,就不会产生并发安全

ThreadLocal

一是实现了资源隔离, 二是实现了线程内资源共享

底层是每个ThreadLocal内部维护了一个ThreadLocalMap,

.set时是把ThreadLocal作为key,要隔离的资源作为value存入map中

.get时是把ThreadLocal作为key,从map中取出value

.remove时是把ThreadLocal作为key,从map中移除value

为什么会发生内存泄漏?

因为ThreadLocalMap中key是弱引用,value是强引用,

当ThreadLocal被GC时,key会被GC,但是value是强引用在ThreadLocalMap上,所以无法被GC,只有当线程执行完才会被GC,因此要通过remove移除value,避免内存泄漏

相关推荐
心疼你的一切2 分钟前
Unity开发Rokid应用之离线语音指令交互模型
android·开发语言·unity·游戏引擎·交互·lucene
N***73852 分钟前
JavaScript物联网案例
开发语言·javascript·物联网
IT方大同10 分钟前
C语言的组成部分
c语言·开发语言
BINGCHN11 分钟前
流量分析进阶(一):RCTF2025-Shadows of Asgard
开发语言·python
G***669129 分钟前
Java区块链开发
java·开发语言·区块链
Java天梯之路29 分钟前
上篇讲坑,这篇讲 “根”!Java 数据类型底层逻辑全解析
java·面试
悟空码字30 分钟前
手把手搭建Java微服务:从技术选型到生产部署
java·后端·微服务
leonardee31 分钟前
MySQL----case的用法
java·后端
慧慧吖@42 分钟前
Zustand
开发语言·javascript·ecmascript
8***B43 分钟前
Java自然语言处理
java·开发语言·自然语言处理