深入理解 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,避免内存泄漏

相关推荐
烟花落o7 小时前
指针深入第二弹--字符指针、数组指针、函数指针、函数指针数组、转移表的理解加运用
c语言·开发语言·笔记·vscode·算法
熊猫_豆豆7 小时前
嫦娥号地月轨道、环月(一个月)MATLAB仿真
开发语言·matlab
wjs20247 小时前
MongoDB Java:深入解析与应用实践
开发语言
散峰而望7 小时前
基本魔法语言数组 (二) (C语言)
c语言·开发语言·github·visual studio
大G的笔记本7 小时前
用 Redis 的 List 存储库存队列,并通过 LPOP 原子性出队来保证并发安全案例
java·数据库·redis·缓存
太过平凡的小蚂蚁8 小时前
适配器模式:让不兼容的接口协同工作
java·前端·javascript
ljh_learn_from_base8 小时前
【spring boot 使用apache poi 生成和处理word 文档】
java·spring boot·word·apache
逻极8 小时前
Rust之结构体(Structs):构建自定义数据类型
开发语言·后端·rust
小二·8 小时前
深入解析 Rust 并行迭代器:Rayon 库的原理与高性能实践
开发语言·算法·rust