
🎁个人主页:User_芊芊君子
🎉欢迎大家点赞👍评论📝收藏⭐文章
🔍系列专栏:JavaEE


文章目录:
- 前言
- 一、认识线程
- 二、Thread类及常见方法
-
- 1.创建线程
- 2.Thread其他用法
-
- [2.1 Thread常见构造方法](#2.1 Thread常见构造方法)
- [2.2 Thread常见属性](#2.2 Thread常见属性)
- [2.3 启动一个线程-start()](#2.3 启动一个线程-start())
- [2.4 中断/终止线程](#2.4 中断/终止线程)
-
- (1)使用标志位来实现
- [(2)使用 Interrupt方法](#(2)使用 Interrupt方法)
- [2.5 线程等待](#2.5 线程等待)
- [2.6 获取线程引用](#2.6 获取线程引用)
- [2.7 休眠线程](#2.7 休眠线程)
- 三、线程的状态
- 四、线程安全
-
-
- [4.1 线程安全产生原因](#4.1 线程安全产生原因)
- [4.2 加锁](#4.2 加锁)
- [4.3 可重入锁](#4.3 可重入锁)
- [4.4 死锁的场景](#4.4 死锁的场景)
- [4.5 死锁产生原因](#4.5 死锁产生原因)
- [4.6 Java标准库中的线程安全类](#4.6 Java标准库中的线程安全类)
- [4.7 内存可见性问题----volatile](#4.7 内存可见性问题----volatile)
- [4.8 wait/notify](#4.8 wait/notify)
- [4.9 wait和sleep的区别(经典面试题)](#4.9 wait和sleep的区别(经典面试题))
-
- 五、总结
前言
Java 多线程是后端开发和面试的高频重点,很多同学刚接触时,会被线程概念、创建方式、线程安全这些知识点绕晕。其实从 "什么是线程" 到 "怎么保证线程安全",是一条完整的学习路径。今天就用直白的方式,把线程基础和核心安全机制梳理清楚,帮你快速入门多线程开发。

一、认识线程
1.什么是线程
一个线程就是一个"执行流"。一个方法中包含很多指令,执行流会执行这些指令,此时,这个执行流就可以放到一个cpu核心上执行,多个执行流之间,就会采用"并行""并发"的方式执行。
2.线程和进程的区别
- 线程比进程更轻:
- 创建线程的开销比创建进程小很多;
- 销毁线程的开销比销毁进程小很多;
同时,使用多线程也能解决并发编程的问题,so,使用多线程就可以替代多进程的模型。
3.线程与进程的关系
- 进程包含线程;
- 线程是资源分配的基本单位,进程是调度执行的基本单位;
- 每个进程都有自己独立的资源,一个进程的多个线程之间共用一份资源;
- 进程与线程之间是隔离的,一个进程出问题,不影响别的进程。同一个进程的线程之间是共享资源的,好处是节省线程的创建和销毁所消耗的资源,坏处是容易冲突,一个线程出问题,容易把别的线程带走。
二、Thread类及常见方法
Java对操作系统的各种API进行了封装,Java的标准库,就提供了Thread类,封装了多线程的相关操作。
- 使用
jconsole命令观察线程
先找到你jdk的路径



1.创建线程
方法一:继承Thread类
java
class MyThread extends Thread{
//重写run方法,run代表进程的入口,自动调用
@Override
public void run(){
while(true){
System.out.println("hello thread");
}
}
}
创建MyThread类的实例
java
MyThread myThread = new MyThread();
调用start方法启动线程
java
myThread.start();//调用操作系统的API,执行到创建线程逻辑,--》run
【注意】run 方法中的sleep只能try catch,不能throws,因为父类run()没有抛出异常,子类不能抛出父类没有的异常或者比父类多

方法二:实现Runnable接口
Java不支持多继承,所有需要引入接口
实现Runnable接⼝
java
class MyRunnable implements Runnable{
@Override
public void run(){
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
java
MyRunnable runnable = new MyRunnable();
Thread t = new Thread(runnable);
java
t.start();
方法三:继承Thread,重写run,使用匿名内部类(一次性)
java
public static void main(String[] args) {
//创建Thread的匿名子类
Thread t = new Thread(){
@Override
public void run(){
while(true){
System.out.println("hello thread");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
方法四:使用匿名内部类基于Runnable
java
public static void main(String[] args) throws InterruptedException {
//创建Runnable的子类
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
//定义子类的实例
Thread t = new Thread(runnable);
t.start();
while (true){
System.out.println("hello world");
Thread.sleep(1000);
}
}
方法五:基于lambda表达式(最推荐的写法)
lambda表达式本质上是匿名函数,表示一个一次性的使用逻辑
java
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while (true){
System.out.println("hello world");
Thread.sleep(1000);
}
}
2.Thread其他用法
2.1 Thread常见构造方法
| 方法 | 说明 |
|---|---|
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用Runnable对象创建线程对象 |
| Thread(String name) | 创建线程对象,并命名 |
| Thread(Runnable target, String name) | 使用Runnable对象创建线程对象,并命名 |
| Thread(ThreadGroup group, Runnable target) | 线程可以被用来分组管理,分好的组即为线程组 |
2.2 Thread常见属性
| 属性 | 获取方法 |
|---|---|
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |
(1)代码中创建的线程默认是前台线程,包括main线程,JVM自带的线程是后台线程
- 前台线程:这个线程如果没执行完,此时进程不会结束,后台线程会阻止进程结束;
- 后台线程:这个线程执行完或者没完,都不会影响进程的结束,后台线程不会阻止进程结束
(2)不存活:未开始执行/执行结束
- 描述操作系统内核里是不是有这个线程。
- 操作系统的线程执行完毕,不代表Thread对象销毁。Thread对象的生命周期可以比系统内部的线程更长
2.3 启动一个线程-start()
重写run方法创建一个线程后,并没有真正的运行,只有调用start()方法之后,才真正在操作系统底层创建出一个线程。(一个Thread对象不能被调用多次)
2.4 中断/终止线程
Java中,只能通过使线程的入口方法尽快return来让线程结束,没有强制终止的操作。(操作系统原生的api可以强制终止)
(1)使用标志位来实现
java
private static boolean running = true;
public static void main(String[] args) {
Thread t = new Thread(()->{
while (running){
System.out.println("hello thread");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("thread 结束");
});
t.start();
Scanner scanner = new Scanner(System.in);
System.out.println("输入任意内容,控制t线程结束");
scanner.next();
running = false;
}

【注意】
lambad + 局部变量 = 不能修改

java
private static boolean running = true;
so:这里running的定义不能在main里面,这就属于局部变量了
(2)使用 Interrupt方法
- 可以唤醒阻塞方法;
- 如果线程没有执行到阻塞操作,还可以设置内置标志位,进行线程结束的判定
java
public static void main(String[] args) {
boolean running = true;
Thread t = new Thread(()->{
//通过isInterrupted()判定线程是否被终止
//此处不能直接使用t,因为t的初始化在最后执行,这个时候不能使用
//使用Thread.currentThread()来替代
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
sleep(1000);
} catch (InterruptedException e) {
//throw new RuntimeException(e);
//1.立即终止
//break;
//2.稍后处理
//先执行其他操作,然后break;
//3.不处理,忽略终止请求
continue;
}
}
System.out.println("线程t终止");
});
t.start();
System.out.println("输入任意内容,终止t线程");
Scanner scanner = new Scanner(System.in);
//终止t线程
t.interrupt();
}
2.5 线程等待
等待线程结束。因为线程的调度顺序是随机的,所以需要一些方法来对线程的顺序产生干预。(后结束的线程等待先结束的线程)
java
private static int result = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
//在这个线程中从1加到1000,主线程打印结果
int sum = 0;
for (int i = 0;i<=1000;i++){
sum+=i;
}
result = sum;
});
t.start();
t.join();
System.out.println(result);
}
- 由于main和t线程是并发执行,很可能main执行打印的时候t还没执行完毕。
- sleep的话不好控制时间,定义太长造成浪费。若使用while还会使main被反复调度。
t.join(): 在main代码中调用的,就是main等待t执行完。也可以放在t中,就变成t等待main.
| 方法 | 说明 |
|---|---|
| public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标志位 |
| public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
| public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
无参数则为"死等",一般使用带超时时间的版本,达到时间上限就不等了
2.6 获取线程引用
返回当前线程的引用
java
public static Thread currentThread();
2.7 休眠线程
- sleep核心是让线程阻塞(不参与调度),操作系统会计时,在这个时间内,将PCB从就绪队列放入阻塞队列,结束后放回去。
- sleep(0):不休眠,但主动放弃了cpu的执行权
- 获取到系统的时间戳(毫秒)
java
public static void main(String[] args) throws InterruptedException {
long beg = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
System.out.println(end-beg);
}
三、线程的状态
| 状态 | 通俗解释 | 核心场景 |
|---|---|---|
| NEW | 安排了工作,还未开始行动 | 线程对象已创建,未调用start() |
| RUNNABLE | 可工作的,分为正在工作中、即将开始工作 | 包含操作系统运行中 、就绪等待CPU调度两种情况 |
| BLOCKED | 排队等着其他事情 | 等待获取synchronized锁,阻塞 |
| WAITING | 排队等着其他事情 | 无限等待,如wait()、join(),需手动唤醒 |
| TIMED_WAITING | 排队等着其他事情 | 限时等待,如sleep()、wait(超时),时间到自动唤醒 |
| TERMINATED | 工作完成了 | Thread对象还在,但系统中的线程已经销毁 |
java
public static void main(String[] args) {
Thread t = new Thread(()->{
while (true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
System.out.println(t.getState());
t.start();
}

四、线程安全
有些代码在单线程环境下执行没有问题,但在多线程环境下就会有bug。这就是线程不安全。
4.1 线程安全产生原因
1.【根本】线程调度是随机的,一个线程执行到任何一个指令都可能被从cpu上调度走;
2.多个线程同时修改同一个变量;解决:
- 一个线程先操作,另一个后操作;
- 多个线程同时读取这个变量,不操作;
- 多个线程同时修改不同变量
3.【直接】针对变量的修改操作不是原子:count++有三个指令
eg:count++对应三个cpu指令
load:把内存中的数据加载到cpu寄存器里;
add:把cpu寄存器中的值+1;
save:把寄存器的值写回内存
线程的调度是随机的,一个线程在cpu上执行,随时可能被从cpu上调走,比如线程一执行到add时(但也是会执行完add这个指令,不会执行一半被调走),被线程二再进行操作,这样就存在了bug,如下图,本来应该是自增两次为2,结果却还是1

t1回到cpu时会恢复上下文(存档),切换出cpu时,把所有寄存器的值都保存到了PCB的内存中
java
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
//创建两个线程,针对同一个变量进行循环自增
Thread t1 = new Thread(()->{
for (int i = 0;i<50000;i++){
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0;i<50000;i++){
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = "+count);
}

结果应该是100000,实际却不一样。
4.2 加锁
-
通过锁,一个变量只能被一个线程修改,这个过程中,其他线程只能等。
-
锁不是让线程停止调度,加锁过程中,线程仍然可以被调度出cpu。而是只要持有锁,别人就不能用。
-
Java中,通过synchronized关键字,搭配代码块的方式实现的。
java
synchronized(锁对象){
count++;
}
- 锁对象可以是任何对象(String,HashMap,Object...)
- 锁对象的用途:用来判定多个线程之间是否存在"锁竞争"(两个线程针对同一个对象加锁,此时,一个线程先拿到锁,另一个线程只能阻塞等待,等待锁被释放)
java
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
//创建两个线程,针对同一个变量进行循环自增
//定义一个锁对象,任意
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 0;i<50000;i++){
synchronized (locker){
count++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0;i<50000;i++){
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = "+count);
}

- t1线程lock之后,t2lock时,锁就已经被占用,此时lock就会产生阻塞等待,此时线程进入BLOCKED状态,直到另一个线程解锁;
- t1解锁之后,操作系统才会告诉t2锁空闲了,t2就被唤醒,可以参与调度了,调度到cpu上就可以拿到锁了,然后继续t2操作;
- 通过锁把load,add,save变成了"串行执行",安全问题解决,但会影响性能
4.3 可重入锁
一个线程针对一把锁加两次或者多次,不会产生死锁,如下代码:打印在第二个锁中,但仍能打印出来。
java
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(()->{
synchronized (locker){
synchronized (locker){
System.out.println("thread");
}
}
});
t1.start();
}

- 可重入锁的判定逻辑:
记录哪个线程持有了这把锁,当收到加锁请求时,判定加锁的线程是不是已经持有了锁的线程,如果是,则无事发生,如果不是,产生锁竞争,产生阻塞
4.4 死锁的场景
- 一个线程,一把锁;对锁加锁两次。(可重入锁已解决);
- 两个线程,两把锁:
(1)线程1对锁A加锁,同时线程2对锁B加锁;
(1)线程1再对锁B加锁,线程2再对锁A加锁
- M个线程,N把锁
4.5 死锁产生原因
- 锁是"互斥"的;
- 锁不可被抢占(线程1先获取到锁,线程2只能阻塞等待) ;
- 保持再请求(一个线程,获取到锁A的情况下再去获取锁B);
- 循环等待(多个线程等待锁的顺序出现循环)如下代码:
t1要想获取到lockerB,就需要等待t2释放lockerB;t2要想获取到lockerA,就需要等待t1释放lockerA。这样就进入了永久阻塞
java
public static void main(String[] args) {
Object lockerA = new Object();
Object lockerB = new Object();
Thread t1 = new Thread(()->{
synchronized (lockerA){
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lockerB){
System.out.println("t1");
}
}
});
Thread t2 = new Thread(()->{
synchronized (lockerB){
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lockerA){
System.out.println("t2");
}
}
});
t1.start();
t2.start();
}

- 如何解决?
只要约定好多个线程按照固定的顺序加锁,就可以避免等待(比如:对锁进行编号,按照从小到大或从大到小的顺序)
4.6 Java标准库中的线程安全类
下面这些类是线程不安全的,可能涉及到多线程修改共享数据,又没有任何加锁措施
- ArrayList;
- LinkedList;
- HashMap;
- TreeMap;
- HashSet;
- TreeSet;
= StringBuilder
下面的是线程安全的,使用一些锁机制来控制.核心方法中都有synchronized
- Vector;
- HashTable;
- ConcurrentHashMap;
- StringBuffer
4.7 内存可见性问题----volatile
(1)什么是内存可见性问题
编译器会自动对代码进行优化,使效率变高。如下代码,线程1在读取,线程2在修改。多线程环境中,会产生误判导致优化后的逻辑不一样。(大部分情况下逻辑都不会变化,多线程就有可能)
java
private static boolean flag = true;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while (flag){
//无操作
}
System.out.println("t1结束");
});
Thread t2 = new Thread(()->{
//用户输入修改flag
System.out.println("输入任意内容,终止线程t1");
Scanner scanner = new Scanner(System.in);
scanner.next();
//通过修改flag让t1结束
flag = false;
System.out.println("flag = "+flag);
});
t1.start();
t2.start();
}

- 线程1并没有结束,这就产生了bug;
- 在线程1的while循环中,会对load(读取内存)进行优化,本来是读内存,改成了读cpu寄存器/cpu缓存。
- 优化的原因:load读取内存,开销很大;循环中每次load的结果一样;线程1中没有修改操作
(2)volatile
给flag变量加上volatile修饰,编译器就会避免进行优化了。
java
private static volatile boolean flag = true;

4.8 wait/notify
线程的调度是随机的。我们就可以使用 wait / notify(等待 / 通知)来控制谁先执行,谁后执行,这两个方法都属于Object类
(1)wait
java
Object object = new Object();
synchronized (object){
System.out.println("wait 之前");
object.wait();//阻塞
System.out.println("wait 之后");
}
【注意事项:】
- 先释放锁才能wait,所有就得先加上锁;
- 加锁的锁对象和wait的对象必须一致;
(2)notify
notify也得搭配锁使用
java
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new Thread(()->{
synchronized (locker){
System.out.println("t1 wait 之前");
try {
locker.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("t1 wait 之后");
});
Thread t2 = new Thread(()->{
Scanner scanner = new Scanner(System.in);
System.out.println("输入任意内容唤醒锁");
scanner.next();
synchronized (locker) {
locker.notify();
}
});
t1.start();
t2.start();
}

wait的好处: 避免"线程饿死"。
- eg:线程1释放锁后,所有线程(包括线程1)开始拿到锁,并无顺序,可能每次都是线程1拿到,那其他的线程就会饿死。
- wait 中,释放锁后,会先阻塞等待。(这两步是"原子操作"),notify之后,接触阻塞,重新获取锁,继续执行。
- 如果拆分前两步,会产生bug
4.9 wait和sleep的区别(经典面试题)
(1)
- wait 提供两个版本:死等和带有超时时间的;
- sleep只有带时间版本
(2)
- wait 可以通过notify唤醒,也可以通过Interrupt提前终止;
- sleep 只能通过Interrupt提前终止;
(3)
- wait 阻塞时会释放锁(强制要求搭配 synchronized使用);
- sleep和锁无关。
五、总结
简单来说,线程是进程内的执行单元,Java 提供了多种创建线程的方式,其中 Lambda 写法最简洁高效;而线程状态流转、加锁机制、volatile 关键字、wait/notify 这些,则是解决线程安全问题的核心手段。吃透这些基础,不仅能写对并发代码,也能轻松应对面试中的多线程高频问题。

