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();
    }
}

8 线程的生命周期