1 进程与线程
进程:操作系统进行资源分配的基本单位 线程:操作系统进行CPU 调度的最小单位
2 并发与并行
并发:同一执行单元上,多个任务轮流执行。 并行:多个执行单元上,多个任务同时执行。
3 线程的创建
3.1 继承 Thread 类
- 创建一个类继承
Thread类 - 在类中覆写
run()方法 - 创建对象调用
start()方法
// 方式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) {
MyThread t1 = new MyThread();
t1.start(); // 启动线程,自动执行 run()
}
}缺点:Java 单继承,无法再继承其他类;不能共享同一个任务对象(多个线程执行不同任务时需创建多个对象)。
3.2 实现 Runnable 接口
Note
创建一个类实现
Runnable接口 在类中实现run()方法 将该类的实例作为参数传给Thread对象 调用start()方法
// 方式2:实现 Runnable 接口(推荐)
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 正在执行");
}
}
public class RunnableDemo {
public static void main(String[] args) {
MyRunnable task = new MyRunnable();
Thread t1 = new Thread(task);
t1.start();
// 也可以使用 Lambda 简化
Thread t2 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " Lambda 方式");
});
t2.start();
}
}优点:避免了单继承的局限性,一个任务对象可以被多个线程共享(适合多线程处理同一资源)。
3.3 实现 Callable 接口
特点:与 Runnable 类似,但:
- 可以有返回值
- 可以抛异常
- 需要配合
FutureTask获取结果
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 模拟计算
System.out.println(Thread.currentThread().getName() + " 开始计算");
Thread.sleep(2000);
return 100; // 返回结果
}
}
public class CallableDemo {
public static void main(String[] args) throws Exception {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread t = new Thread(ft);
t.start();
// 获取线程执行结果(会阻塞等待)
Integer result = ft.get();
System.out.println("计算结果:" + result);
}
}3.4 创建线程池
使用
Executors工具类或ThreadPoolExecutor创建线程池,避免频繁创建和销毁线程。
import java.util.concurrent.*;
public class ThreadPoolDemo {
public static void main(String[] args) {
// 方式1:使用 Executors 工具类(简单但不推荐用于生产)
ExecutorService pool = Executors.newFixedThreadPool(3);
// 方式2:手动创建 ThreadPoolExecutor(推荐,更可控)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<Runnable>(10), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
// 提交任务
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " 执行任务");
});
// 关闭线程池
executor.shutdown();
}
}4 Thread 类常用方法
5 线程安全
5.1 同步代码块
// 这是一个卖票系统,演示线程安全问题及同步代码块的解决方法
public class TicketDemo {
public static void main(String[] args) {
// 创建1个卖票任务(共享同一个资源)
TicketTask task = new TicketTask();
// 创建3个线程,模拟3个售票窗口
Thread window1 = new Thread(task, "窗口1");
Thread window2 = new Thread(task, "窗口2");
Thread window3 = new Thread(task, "窗口3");
// 3个窗口同时开始卖票
window1.start();
window2.start();
window3.start();
}
}
// 卖票任务类(这个类被多个线程共享)
class TicketTask implements Runnable {
// 共享资源:总共10张票(多个线程会同时访问这个变量)
private int ticketCount = 10;
// 锁对象:用来给代码块上锁(任意对象都可以当锁)
// 注意:必须是同一个对象,多个线程才能互斥
private final Object lock = new Object();
@Override
public void run() {
// 每个窗口(线程)会一直卖,直到票卖完
while (true) {
// ================= 关键代码开始 =================
// 【线程安全问题发生在这里】
// 如果不加锁,多个线程可能同时进入下面的if判断
synchronized (lock) { // ← 同步代码块:拿钥匙
// 这个区域内的代码,一次只能有一个线程执行
// 其他线程必须等当前线程执行完才能进
// 1. 先检查还有没有票
if (ticketCount > 0) {
// 2. 模拟卖票的业务处理(比如打印票、收钱等)
try {
// 故意让线程睡一会,模拟真实卖票的耗时操作
// 这样更容易暴露线程安全问题
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 3. 票数减1
ticketCount--;
// 4. 输出卖票结果
System.out.println(Thread.currentThread().getName()
+ "卖出一张票,还剩" + ticketCount + "张票");
} else {
// 没票了,结束这个线程
System.out.println(Thread.currentThread().getName() + "发现票已售罄");
break;
}
} // ← 同步代码块结束:还钥匙
// ================= 关键代码结束 =================
}
}
}5.2 同步方法
6 生产者和消费者
6.1 概览
Note
- 生产者,负责生产.当生产完之后【到达一定数量】,则需要等待.【wait】
- 消费者,负责消费.当没有产品的时候,【数量为 0】, 需要等待.【wait】
- 唤醒操作,当产品数量为 0 的时候,需要生产者生产的时候,唤醒生产者【notifyAll】
- 唤醒操作,当产品数量到达了一定数量的时候,需要消费者消费的时候,唤醒消费者 【nofityAll】
6.2 资源类
package com.nb.thread.demo07;
public class JianBing {
private static int num = 0; // 煎饼的数量
/**
* 生产方法
*/
public void makeJianBing() throws InterruptedException {
synchronized (this) {
while (num >= 5) {
// 在睡觉前打印提示,而不是在睡觉后
System.out.println(Thread.currentThread().getName() + "煎饼够了[5个],生产线暂停…唤醒消费者!");
this.wait();
}
num++; // 先加数量
System.out.println(Thread.currentThread().getName() + "生产了1个,当前库存: " + num + " 个");
this.notifyAll(); // 唤醒消费者
}
}
/**
* 消费方法
*/
public void buyJianBing() throws InterruptedException {
synchronized (this) {
// 核心修正:必须用 while 防止虚假唤醒和逻辑越界
while (num <= 0) {
System.out.println(Thread.currentThread().getName() + "没煎饼了,吃货被迫等待…唤醒生产者!");
this.wait();
}
// 核心修正:先打印当时消费的那一个,再做扣减,或者打印扣减前的状态
System.out.println(Thread.currentThread().getName() + "成功买走1个,当前剩余库存: " + (num - 1) + " 个");
num--;
this.notifyAll(); // 唤醒生产者
}
}
}6.3 测试代码
7 Lock 锁
7.1 Lock 接口
Lock 它是一个接口,当中定义了: 获取锁、释放锁、Condition 对象的获取方法.
public interface Lock {
void lock(); // 获取锁方法
boolean tryLock(); // 尝试获取锁
void unlock(); // 释放锁
Condition newCondition(); // 获取Condition对象
}7.2 Condition 接口
public interface Condition {
void await() throws InterruptedException; // 类似于wait, 线程等待
void signal(); // 相当于notify,唤醒一个线程
void signalAll(); // 相当于之前学过的nofityAll方法,唤醒所有线程.
}7.3 实现 Lock 接口的类 ReentrantLock
public class ReentrantLock implements Lock, java.io.Serializable {}标准用法
煎饼例子
package com.nb.thread.demo09;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 煎饼店资源类:完美融合 Lock 锁、Condition 分组、try-finally 契约以及 while 防虚假唤醒
*/
public class JianBingStore {
private int num = 0; // 核心共享资源:煎饼的库存数量
// 1. 定义显式锁(Lock 接口的经典实现类),保证内存地址唯一且不被篡改
private final Lock lock = new ReentrantLock();
// 2. 利用 lock.newCondition() 划分出两个高档、独立的专属休息室
private final Condition prodCondition = lock.newCondition(); // 生产者的专属休息室
private final Condition consCondition = lock.newCondition(); // 消费者的专属休息室
/**
* 生产煎饼的方法
*/
public void makeJianBing() {
// 核心铁律:lock() 必须紧贴在 try 的外面!确保拿锁成功才开启保护罩
lock.lock();
try {
// 核心铁律:必须使用 while 循环检查条件,防止线程被虚假唤醒后逻辑越界
while (num >= 5) {
System.out.println(Thread.currentThread().getName() + "库存已满[5个],生产线暂停…去【生产者休息室】睡觉!");
// 3. 生产者精准地去自己的房间躺下,并自动交出锁
prodCondition.await();
}
// 生产逻辑
num++;
System.out.println(Thread.currentThread().getName() + "摊好了一个煎饼!当前库存: " + num + " 个");
// 4. 核心优势:我生产完了,有货了,所以我【精准唤醒消费者】起来吃煎饼
// 绝不打扰另外那些还在睡觉的生产者,这就是 signalAll() 的威力
consCondition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 核心铁律:无论 try 里发生什么意外,临走前必须在 finally 里完好无损地交还钥匙
lock.unlock();
}
}
/**
* 购买/消费煎饼的方法
*/
public void buyJianBing() {
lock.lock();
try {
// 核心铁律:使用 while 检查,没货就死死卡住门槛
while (num <= 0) {
System.out.println(Thread.currentThread().getName() + "没煎饼了,吃货被迫等待…去【消费者休息室】睡觉!");
// 5. 消费者精准地去消费者的房间躺下
consCondition.await();
}
// 消费逻辑:打印扣减前的库存状态,或者计算扣减后的状态,保证语序正常
System.out.println(Thread.currentThread().getName() + "成功买走1个,当前剩余库存: " + (num - 1) + " 个");
num--;
// 6. 核心优势:我吃掉了一个,位置空出来了,我【精准唤醒生产者】起来揉面做煎饼
prodCondition.signalAll();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}测试代码
package com.nb.thread.demo09;
public class TestStore {
public static void main(String[] args) {
// 核心要点:全场只 new 一个煎饼店实例,保证所有线程访问的是同一个堆内存地址!
JianBingStore store = new JianBingStore();
// 创建两个生产者线程任务
Runnable producerTask = new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(800); // 生产者动作稍慢,800毫秒做一个
store.makeJianBing();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 创建两个消费者线程任务
Runnable consumerTask = new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(200); // 消费者是个饿货,吃得极快,200毫秒就想吃一个
store.buyJianBing();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 直接在 Thread 构造方法中改名,优雅地解耦
Thread p1 = new Thread(producerTask, "大壮师傅");
Thread p2 = new Thread(producerTask, "强子师傅");
Thread c1 = new Thread(consumerTask, "吃货小明");
Thread c2 = new Thread(consumerTask, "吃货小红");
// 全员全线启动!
p1.start();
p2.start();
c1.start();
c2.start();
}
}