主题
Java多线程与Lambda表达式完全指南
目录
一、多线程基础概念
1.1 进程与线程
进程:进入到内存运行的应用程序
线程:进程中的一个执行单元,负责当前进程中程序的运行
关键点:
- 一个进程中至少有一个线程
- 一个进程可以有多个线程,这样的应用程序称为多线程程序
- 简单理解:进程中的一个功能就需要一条线程去执行


1.2 并发与并行
| 概念 | 定义 | 比喻 |
|---|---|---|
| 并行 | 同一时刻,多个指令在多个CPU上同时执行 | 多个厨师在炒多个菜 |
| 并发 | 同一时刻,多个指令在单个CPU上交替执行 | 一个厨师在炒多个菜 |


重要细节:
- 之前CPU是单核,执行多个程序时好像同时执行,原因是CPU在多个线程之间做高速切换
- 现在CPU都是多核多线程(如2核4线程),可以同时运行4个线程,超出部分仍需切换
- 现代CPU执行程序时,并发和并行都存在
1.3 CPU调度
1. 分时调度
- 让所有线程轮流获取CPU使用权
- 平均分配每个线程占用CPU的时间片
2. 抢占式调度(Java采用)
- 多个线程抢占CPU使用权
- 优先级越高的线程,先抢到CPU使用权的几率越大
- 但不是每次都是优先级高的线程先抢到,只是几率更大
1.4 主线程
定义:专门为main方法服务的线程

1.5 多线程使用场景
- 软件中的耗时操作:拷贝大文件、加载大量资源
- 所有的聊天软件
- 所有的后台服务器
- 优势:多线程程序同时干多件事,提高了CPU使用率
二、创建线程的方式
创建多线程有四种方式:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池
2.1 方式一:继承Thread类
实现步骤:
- 创建自定义类继承Thread
- 重写Thread类中的run方法,设置线程任务
- 创建自定义线程类的对象
- 调用Thread类中的start方法开启线程
代码示例:
java
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("MyThread正在执行..." + i);
}
}
}java
public class Demo01Thread {
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
for (int i = 0; i < 5; i++) {
System.out.println("Main正在执行......" + i);
}
}
}注意事项:
- 如果直接调用run方法,并不代表将线程开启,仅仅是简单的调用方法
- 只有调用start方法,线程才会真正开启
- 同一个线程对象只能调用一次start,想开新线程需要重新new对象
2.2 多线程内存运行原理

2.3 Thread类常用方法
| 方法 | 说明 |
|---|---|
void run() | 设置线程任务,这个线程能干啥 |
void start() | 使该线程开始执行;JVM调用该线程的run方法 |
void setName(String name) | 给线程设置名字 |
String getName() | 获取线程名字 |
static Thread currentThread() | 获取当前正在执行的线程对象 |
static void sleep(long millis) | 线程睡眠,设置毫秒值,超时后自动醒来继续执行 |
代码示例:
java
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":MyThread正在执行..." + i);
}
}
}java
public class Demo01Thread {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
t1.setName("广坤");
t1.start();
for (int i = 0; i < 5; i++) {
Thread.sleep(2000L);
System.out.println(Thread.currentThread().getName() + ":Main正在执行......" + i);
}
}
}问题:为什么run方法中有编译时期异常只能try,不能throws?
答案:Thread类中的run方法没有抛异常,重写后就不能抛,只能try
2.4 方式二:实现Runnable接口
实现步骤:
- 创建自定义线程类,实现Runnable接口
- 重写run方法,设置线程任务
- 利用Thread类中的构造方法:
Thread(Runnable r)Thread(Runnable r, String name)可以设置线程名字
- 调用Thread类中的start方法开启线程
代码示例:
java
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":正在执行..." + i);
}
}
}java
public class Demo01Runnable {
public static void main(String[] args) throws InterruptedException {
MyRunnable myRunnable = new MyRunnable();
Thread t1 = new Thread(myRunnable);
t1.start();
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":Main正在执行......" + i);
}
}
}2.5 两种方式对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| 继承Thread类 | 代码简单 | Java单继承,有局限性 |
| 实现Runnable接口 | 接口可多实现,还可继承其他类,局限性小 | 代码稍复杂 |
推荐使用:实现Runnable接口方式
2.6 匿名内部类创建多线程
java
public class Demo02Runnable {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":正在执行......" + i);
}
}
}, "广坤").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ":正在执行......" + i);
}
}
}, "刘能").start();
}
}
三、线程安全与同步机制
3.1 线程安全问题场景
发生场景:多个线程共享同一个资源的时候
经典案例:卖票问题
java
public class MyTicket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}
}java
public class Demo01Ticket {
public static void main(String[] args) {
MyTicket myTicket = new MyTicket();
Thread t1 = new Thread(myTicket, "广坤");
Thread t2 = new Thread(myTicket, "刘能");
Thread t3 = new Thread(myTicket, "赵四");
t1.start();
t2.start();
t3.start();
}
}问题:可能出现重票、错票等线程安全问题
3.2 解决方案一:同步代码块
格式:
java
synchronized (任意对象) {
线程不安全的代码
}说明:
- 任意对象就是锁对象
- 线程先抢到锁才能进入同步代码块执行,其他线程等待
- 线程出了同步代码块自动释放锁,其他线程才能抢锁
代码示例:
java
public class MyTicket implements Runnable {
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}
}
}3.3 解决方案二:同步方法
3.3.1 普通同步方法(非静态)
格式:
java
修饰符 synchronized 返回值类型 方法名(形参) {
方法体
return 结果
}默认锁:this
代码示例:
java
public class MyTicket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
method01();
}
}
public synchronized void method01() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}等价于:
java
public void method01() {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}3.3.2 静态同步方法
格式:
java
修饰符 static synchronized 返回值类型 方法名(形参) {
方法体
return 结果
}默认锁:当前类.class
代码示例:
java
public class MyTicket implements Runnable {
private static int ticket = 100;
@Override
public void run() {
while (true) {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
method01();
}
}
public static synchronized void method01() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}等价于:
java
public static void method01() {
synchronized (MyTicket.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
}
}
}四、单例模式
4.1 概念
- 单:一个
- 例:实例 → 对象
- 目的:让一个类只产生一个对象供外界使用
4.2 饿汉式
特点:我好饿呀,我很急需一个对象,所以赶紧让对象new出来
实现要点:
- 构造方法私有化
- 创建静态私有对象
- 提供公共静态获取方法
代码示例:
java
public class Singleton {
private Singleton() {
}
private static Singleton singleton = new Singleton();
public static Singleton getSingleton() {
return singleton;
}
}测试:
java
public class Test01 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Singleton singleton = Singleton.getSingleton();
System.out.println(singleton);
}
}
}4.3 懒汉式
特点:不需要提前new对象,等到用的时候再new,同时保证只产生一个对象
实现要点:
- 构造方法私有化
- 声明静态私有对象(不初始化)
- 提供公共静态获取方法(双重检查锁定)
代码示例:
java
public class Singleton {
private Singleton() {
}
private static Singleton singleton = null;
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}测试:
java
public class Test01 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton singleton = Singleton.getSingleton();
System.out.println(singleton);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
Singleton singleton = Singleton.getSingleton();
System.out.println(singleton);
}
}).start();
}
}4.4 饿汉式 vs 懒汉式
| 特性 | 饿汉式 | 懒汉式 |
|---|---|---|
| 对象创建时机 | 类加载时立即创建 | 首次使用时创建 |
| 线程安全 | 天然安全 | 需要同步处理 |
| 效率 | 高(无同步开销) | 较低(双重检查锁定) |
| 内存占用 | 可能造成内存浪费 | 按需创建,节省内存 |
五、Lambda表达式
5.1 函数式编程思想
面向对象编程思想:
- 强调先new对象,然后调用方法
- 过多强调new对象这个过程
函数式编程思想:
- 强调目的(调用对象的方法),不强调过程(new对象)
5.2 Lambda表达式格式
格式:() -> {}
解释说明:
():重写方法的参数位置->:将重写方法的参数传递给重写方法的方法体{}:重写方法的方法体
代码示例:
java
public class Demo01Lambda {
public static void main(String[] args) {
// 匿名内部类方式
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("正在执行...");
}
}).start();
System.out.println("==========================");
// Lambda表达式方式
new Thread(() -> System.out.println("正在执行...")).start();
}
}5.3 Lambda表达式使用前提
前提条件:
- 必须是函数式接口做方法参数传递或者返回值返回
- 函数式接口:必须有且只能有一个抽象方法的接口
- 使用
@FunctionalInterface注解标识函数式接口
代码示例:
java
@FunctionalInterface
public interface USB {
void open();
}java
public class Demo02Lambda {
public static void main(String[] args) {
// 匿名内部类方式
method(new USB() {
@Override
public void open() {
System.out.println("usb打开了");
}
});
System.out.println("========================");
// Lambda表达式方式
method(() -> System.out.println("lambda打开"));
}
public static void method(USB usb) {
usb.open();
}
}5.4 Lambda表达式省略规则
涛哥秘籍:
- 先观察,是否是函数式接口做方法参数传递或者返回值返回
- 如果是,调用方法,以匿名内部类传参或者返回
- 从new接口开始到重写方法的方法名结束,选中,删除,然后别忘记多删除一个右半个大括号
- 在重写方法的参数和重写方法的方法体之间加
->
省略规则:
- 重写方法的参数类型可以省略
- 重写方法的参数如果只有一个,数据类型和所在的小括号可以省略
- 如果重写方法的方法体只有一句,所在的大括号和分号可以省略
- 如果重写方法的方法体只有一句,并且带return,那么所在的大括号、分号以及return都可以省略
代码示例:
java
@FunctionalInterface
public interface USB {
String open(String name);
}java
public class Test01 {
public static void main(String[] args) {
// 匿名内部类方式
method(new USB() {
@Override
public String open(String name) {
return name + "打开了";
}
});
System.out.println("========================");
// Lambda表达式方式(省略后)
method(name -> name + "打开了");
System.out.println("===================");
// Lambda作为返回值
String result = method02().open("键盘");
System.out.println(result);
}
public static void method(USB usb) {
String result = usb.open("鼠标");
System.out.println(result);
}
public static USB method02() {
return name -> name + "打开了";
}
}六、常见面试题
6.1 基础概念题 ⭐
Q1:进程和线程的区别是什么?
答案:
- 进程是正在运行的程序,是系统进行资源分配和调度的独立单位
- 线程是进程中的一个执行单元,是CPU调度和分派的基本单位
- 一个进程至少有一个线程,也可以有多个线程
- 进程之间相互独立,线程之间共享进程资源
Q2:并行和并发的区别是什么?
答案:
- 并行:同一时刻,多个指令在多个CPU上同时执行
- 并发:同一时刻,多个指令在单个CPU上交替执行
- 现代多核CPU中,并行和并发都存在
Q3:Java采用什么调度方式?
答案:
- Java采用抢占式调度
- 多个线程抢占CPU使用权
- 优先级高的线程抢到CPU使用权的几率更大,但不是绝对的
6.2 线程创建题 ⭐⭐
Q4:创建线程有哪几种方式?
答案:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池
Q5:继承Thread类和实现Runnable接口哪种方式更好?为什么?
答案:
- 实现Runnable接口方式更好
- 原因:
- Java单继承,继承Thread类有局限性
- 接口可以多实现,还可以继承其他类
- 适合多线程共享同一资源的情况
Q6:start()和run()的区别?
答案:
start():启动线程,JVM会自动调用run方法run():只是普通方法调用,不会启动新线程- 直接调用run方法不会创建新线程,只是在当前线程中执行
6.3 线程安全题 ⭐⭐⭐
Q7:什么是线程安全问题?如何解决?
答案:
- 线程安全问题:多个线程共享同一资源时,可能出现数据不一致等问题
- 解决方案:
- 同步代码块:
synchronized (锁对象) { 代码 } - 同步方法:在方法声明上加
synchronized - Lock锁(后续学习)
- 同步代码块:
Q8:同步代码块和同步方法的锁对象分别是什么?
答案:
- 同步代码块:任意对象,需要手动指定
- 普通同步方法:this(当前对象)
- 静态同步方法:当前类.class(类对象)
Q9:同步代码块的执行流程是什么?
答案:
- 线程先抢到锁才能进入同步代码块执行
- 其他线程等待
- 线程执行完同步代码块后自动释放锁
- 其他线程抢锁,抢到执行,抢不到继续等待
6.4 单例模式题 ⭐⭐⭐
Q10:什么是单例模式?有哪些实现方式?
答案:
- 单例模式:让一个类只产生一个对象供外界使用
- 实现方式:
- 饿汉式:类加载时就创建对象
- 懒汉式:首次使用时创建对象
Q11:饿汉式和懒汉式的区别?
答案:
| 特性 | 饿汉式 | 懒汉式 |
|---|---|---|
| 对象创建时机 | 类加载时 | 首次使用时 |
| 线程安全 | 天然安全 | 需要同步处理 |
| 效率 | 高 | 较低 |
| 内存占用 | 可能浪费 | 按需创建 |
Q12:懒汉式如何保证线程安全?
答案:
- 使用双重检查锁定(Double-Check Locking)
- 两次判断singleton == null
- 中间加synchronized同步代码块
- 锁对象为当前类.class
6.5 Lambda表达式题 ⭐⭐
Q13:什么是函数式接口?
答案:
- 有且只有一个抽象方法的接口
- 可以使用
@FunctionalInterface注解标识 - 是Lambda表达式使用的前提
Q14:Lambda表达式的格式是什么?各部分代表什么?
答案:
- 格式:
() -> {} ():重写方法的参数列表->:箭头,将参数传递给方法体{}:重写方法的方法体
Q15:Lambda表达式可以省略哪些内容?
答案:
- 参数类型可以省略
- 只有一个参数时,小括号可以省略
- 方法体只有一句时,大括号和分号可以省略
- 方法体只有一句且有return时,return也可以省略
七、经典练习题
7.1 基础练习
练习1:创建三个线程,分别打印"线程A"、"线程B"、"线程C"
要求:使用继承Thread类的方式
参考答案:
java
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "正在执行");
}
}
}
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread("线程A");
MyThread t2 = new MyThread("线程B");
MyThread t3 = new MyThread("线程C");
t1.start();
t2.start();
t3.start();
}
}练习2:使用Runnable接口实现三个窗口卖票
要求:共100张票,三个窗口同时卖票
参考答案:
java
public class Ticket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
} else {
break;
}
}
}
}
}
public class Test {
public static void main(String[] args) {
Ticket ticket = new Ticket();
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}7.2 进阶练习
练习3:使用Lambda表达式简化代码
要求:将以下匿名内部类改写为Lambda表达式
java
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行");
}
}).start();参考答案:
java
new Thread(() -> System.out.println("线程执行")).start();练习4:实现一个线程安全的单例模式
要求:使用懒汉式,保证线程安全
参考答案:
java
public class Singleton {
private Singleton() {
}
private static Singleton singleton = null;
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}练习5:使用同步方法实现卖票系统
要求:使用同步方法替代同步代码块
参考答案:
java
public class Ticket implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if (!sellTicket()) {
break;
}
}
}
public synchronized boolean sellTicket() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");
ticket--;
return true;
}
return false;
}
}八、学习建议
8.1 学习重点
- 线程创建方式:重点掌握继承Thread类和实现Runnable接口两种方式
- 线程安全:理解线程安全问题的产生原因,掌握同步代码块和同步方法
- 单例模式:理解饿汉式和懒汉式的区别,掌握懒汉式的线程安全实现
- Lambda表达式:理解函数式编程思想,掌握Lambda表达式的格式和省略规则
8.2 学习方法
- 多写代码:线程相关代码需要多动手实践,理解并发执行的效果
- 画图理解:使用图示理解线程执行流程、内存原理等
- 对比学习:对比两种线程创建方式、两种单例模式的区别
- 面试准备:重点理解面试题中的核心概念和实现原理
8.3 常见误区
误区一:直接调用run方法就能启动线程
- 正确:必须调用start方法才能启动线程
误区二:同步代码块的锁对象必须是this
- 正确:可以是任意对象,但要保证多个线程使用同一把锁
误区三:Lambda表达式可以用于任何接口
- 正确:只能用于函数式接口(只有一个抽象方法)
8.4 扩展学习
下一章预告:
- 线程生命周期
- 线程通信(wait/notify)
- 线程池
- Lock锁
附录:关键概念速查表
| 概念 | 定义 |
|---|---|
| 进程 | 正在运行的程序 |
| 线程 | 进程中的执行单元 |
| 并行 | 多个CPU同时执行多个指令 |
| 并发 | 单个CPU交替执行多个指令 |
| 同步 | 控制多个线程按顺序访问共享资源 |
| 函数式接口 | 只有一个抽象方法的接口 |
| Lambda表达式 | 简化匿名内部类写法的语法糖 |