一、为什么要使用多线程?
在现代系统中(如电商秒杀、日志分析、物联网设备数据处理等),单线程执行任务 往往无法满足性能需求。
多线程的核心目标是:提升资源利用率与系统吞吐量。
🚫 单线程的不足
-
任务串行执行,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,避免内存泄漏