JAVA之路(16)—— 线程
1. 线程基本概念1.1 线程和进程概念本质资源分配调度单位关系进程运行中的程序操作系统分配独立内存空间资源分配单位一个进程可以包含多个线程线程进程内的执行路径共享进程的堆、方法区资源栈、程序计数器私有CPU 调度执行单位线程由进程创建是进程的子实体简单类比进程 工厂线程 工厂里的工人共享工厂的设备和原料每个工人有自己的工作台和工具。Java 线程属于内核级线程和操作系统原生线程一一对应线程的调度由操作系统内核完成。JVM 启动时会自动创建一个主线程负责执行 main 方法是程序的入口。Java 中用java.lang.Thread类抽象描述线程所有线程对象都是 Thread 类或其子类的实例。1.2 并发 vs 并行并发单核 CPU 下多个线程交替执行造成 “貌似同时” 的错觉。宏观同时微观串行。并行多核 CPU 下多个线程同时执行真正的物理层面同时运行。Java 的多线程是「并发 并行」的混合模式多核并行、单核并发最终由 CPU 调度器统一调度。2. 线程的创建方式2.1 继承 Thread 类步骤定义类继承Thread类重写run()方法编写线程执行的业务逻辑创建子类对象调用start()方法启动线程class Cat extends Thread { int times 0; Override public void run() { // 重写run方法写业务逻辑 while (times 80) { System.out.println(喵喵我是小猫咪 (times)); try { Thread.sleep(1000); // 休眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Thread01 { public static void main(String[] args) { Cat cat new Cat(); cat.start(); // 启动线程底层创建新线程调度执行run() // cat.run(); // ❌ 错误普通方法调用不会开新线程会阻塞主线程 } }注意调用start()会启动新线程java在底层调用 native 方法start0()由 JVM 向操作系统申请创建线程线程就绪后自动执行run()run()方法本身并不会开辟新线程。继承 Thread 类实现售票系统package com.hspedu.ticket; /** * 方式1继承Thread类实现售票系统 */ public class ThreadStyleSell { public static void main(String[] args) { // 每个窗口都是独立的线程对象 SellTicketThread window1 new SellTicketThread(窗口1); SellTicketThread window2 new SellTicketThread(窗口2); SellTicketThread window3 new SellTicketThread(窗口3); // 启动3个售票线程 window1.start(); window2.start(); window3.start(); } } // 售票线程类继承Thread class SellTicketThread extends Thread { // ✅ 重点1必须加 static让所有线程对象共享同一份总票数 // ❌ 不加static的话每个线程对象各有100张票总共会卖出300张不是共享库存 private static int ticketNum 100; // ✅ 重点2锁对象也必须加 static保证全局只有一把锁 // ❌ 不加static的话每个线程自己有一把独立的锁各锁各的同步完全失效 private static final Object LOCK new Object(); // 构造器设置线程名窗口名称 public SellTicketThread(String name) { super(name); } Override public void run() { while (true) { // 所有线程抢同一把静态锁 synchronized (LOCK) { // 没票了就退出 if (ticketNum 0) { break; } // 模拟售票操作耗时 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } // 输出售票信息 System.out.println(Thread.currentThread().getName() 售出第 ticketNum 张票); ticketNum--; } // 出了同步代码块释放锁下一个线程可以抢 } } }票数必须加static继承 Thread 方式每个线程都是独立对象不加 static 每个线程各有一份 100 张票不是共享同一份库存。锁对象必须加static如果锁不是静态每个线程持有自己的锁互不干扰同步完全失效会出现超卖。2.2 实现 Runnable 接口步骤定义类实现Runnable接口实现run()方法创建 Runnable 实现类对象将该对象作为参数传入 Thread 构造器创建 Thread 对象调用 Thread 的start()启动线程class Dog implements Runnable { int count 0; Override public void run() { while (count 10) { System.out.println(小狗汪汪叫 (count)); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Thread02 { public static void main(String[] args) { Dog dog new Dog(); // 必须包装成Thread对象才能启动 Thread thread new Thread(dog); thread.start(); } }Thread类本身就是一个Runnable的代理持有一个Runnable target属性run()方法内部调用target.run()。 传入的Runnable实现类就是真正的任务执行者Thread负责线程管理。实现 Runnable 接口实现售票系统package com.hspedu.ticket; /** * 方式2实现Runnable接口实现售票系统推荐写法 */ public class RunnableStyleSell { public static void main(String[] args) { // 只创建1个售票任务对象 SellTicketRunnable task new SellTicketRunnable(); // 3个线程共享同一个任务对象 Thread window1 new Thread(task, 窗口1); Thread window2 new Thread(task, 窗口2); Thread window3 new Thread(task, 窗口3); window1.start(); window2.start(); window3.start(); } } // 售票任务类实现Runnable接口 class SellTicketRunnable implements Runnable { // ✅ 不用加static同一个Runnable对象被多个线程共享天然只有一份票数 private int ticketNum 100; // ✅ 不用加static同一个Runnable实例只有一个锁对象天然全局唯一 private final Object LOCK new Object(); Override public void run() { while (true) { // 所有线程抢同一个lock对象 synchronized (LOCK) { if (ticketNum 0) { break; } // 模拟售票耗时 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() 售出第 ticketNum 张票); ticketNum--; } } } }天然数据共享同一个 Runnable 实例的成员变量被多个线程共享无需 static 修饰更符合面向对象设计。锁对象天然唯一同一个任务只有一把锁不会出现「各锁各的」的低级错误。2.3 两种创建方式对比对比维度继承 Thread实现 Runnable继承限制受单继承限制不能再继承其他类不受单继承限制还可以继承其他父类资源共享多个线程共享数据需要加 static同一个 Runnable 对象可以传给多个 Thread天然共享数据耦合度任务和线程绑定在一起任务和线程管理分开更灵活推荐程度简单场景可用不推荐✅ 开发标准写法推荐使用3. 线程的常用方法3.1 线程类的基础信息方法方法作用注意事项getName()获取当前线程的名字主线程默认叫main子线程默认叫Thread-0、Thread-1...setName(String name)设置线程名字方便调试区分线程getPriority()获取线程优先级默认优先级是 5setPriority(int priority)设置线程优先级范围 1~10数字越大被调度概率越高⚠️ 只是概率更高不保证一定先执行3.2 线程类的控制方法1sleep (long millis) 线程休眠作用让当前线程暂停执行指定毫秒数。是谁的方法静态方法写在哪个线程里就休眠哪个线程。异常会抛出InterruptedException必须 try-catch。⚠️重点sleep 过程中不会释放锁后面同步部分会再讲。2yield () 线程礼让作用当前线程主动让出 CPU 时间片回到就绪状态让其他线程先跑。特点礼让不一定成功CPU 可能马上又调度它只是提供一种 “谦让” 的可能性。不会释放锁。3join () 线程插队作用t1.join()写在哪个线程里哪个线程就阻塞一直等到 t1 线程全部执行完再继续。通俗理解插队。你正在干活有人插队你就得等他干完你再干。示例主线程每隔 1 秒输出 hi一共 10 次输出到第 5 次的时候启动子线程子线程插队输出 10 次 hello子线程走完主线程再继续。public class ThreadMethodExercise { public static void main(String[] args) throws InterruptedException { Thread t3 new Thread(new T3()); for (int i 1; i 10; i) { System.out.println(hi i); if(i 5) { // 第5次hi的时候 t3.start(); // 启动子线程 t3.join(); // ✅ 子线程插队主线程阻塞等它 } Thread.sleep(1000); } } } class T3 implements Runnable { private int count 0; Override public void run() { while (true) { System.out.println(hello (count)); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if(count 10) break; } } }4interrupt () 中断线程注意不是杀死线程不是终止线程真正作用打断线程的休眠 / 等待状态让它从 sleep/wait 中醒过来抛出InterruptedException。5正确停止线程的方式标记位法❌ 不要用stop()方法已废弃会强制释放锁导致数据错乱。 ✅ 推荐方式用一个布尔变量做标记外部修改标记让 run 方法自己退出。class AThread implements Runnable { private boolean loop true; // 循环标记 Override public void run() { while (loop) { // 标记为true就一直跑 // 业务逻辑 } } // 外部调用这个方法就能停止线程 public void setLoop(boolean loop) { this.loop loop; } }3.3 守护线程用户线程也叫工作线程任务做完才会退出。守护线程后台服务线程为用户线程服务。所有用户线程都结束了守护线程自动结束。典型例子垃圾回收器GC后台默默清理垃圾程序退出它就走。设置thread.setDaemon(true)必须在start()之前设置。4. 线程的状态4.1 线程的五种状态状态英文什么时候进入这个状态新建NEWThread t new Thread()刚创建完还没调用 start可运行RUNNABLE调用 start () 之后、正在 CPU 上运行、yield 礼让之后阻塞BLOCKED抢锁抢不到等着拿锁的时候等待WAITING调用 wait ()、join () 无参版无限期等别人通知计时等待TIMED_WAITINGsleep ()、wait (时间)、join (时间)等时间到自动醒终止TERMINATEDrun 方法执行完、异常退出4.2 线程状态的转换新建线程对象 → NEW 状态调用 start () → RUNNABLE就绪CPU 调度执行 → RUNNABLE运行运行中抢不到 synchronized 锁 → BLOCKED抢到锁后回到 RUNNABLE运行中调用 wait () → WAITING被 notify () 唤醒后先抢锁抢到后回到 RUNNABLE运行中调用 sleep (long)/wait (long) → TIMED_WAITING时间到 / 被唤醒后抢锁回到 RUNNABLErun () 执行完毕 / 异常退出 → TERMINATED面试考点Java 线程没有单独的 “运行态”RUNNABLE 包含了就绪和运行因为 Java 线程是内核级线程线程的调度由操作系统完成JVM 不做细分。5. 线程安全与同步5.1 为什么需要同步当多个线程同时操作同一个共享资源且操作不是原子操作时会出现数据不一致的问题就是线程安全问题。class TicketTask implements Runnable { private int ticketCount 100; // 共享资源100张票 Override public void run() { while (true) { if (ticketCount 0) { try { Thread.sleep(10); // 模拟出票耗时 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() 卖出第 ticketCount-- 张票); } else { break; } } } } public class ThreadUnsafeDemo { public static void main(String[] args) { TicketTask task new TicketTask(); // 3个窗口同时卖票 new Thread(task, 窗口1).start(); new Thread(task, 窗口2).start(); new Thread(task, 窗口3).start(); } }运行后会出现超卖、重复卖票的问题这就是典型的线程不安全。5.2 synchronized 关键字synchronized是 Java 内置的互斥锁用于保证同一时间只有一个线程执行同步代码解决线程安全问题。它是可重入锁同一个线程可以多次获取同一把锁。1同步代码块锁的是括号里指定的对象class TicketTask implements Runnable { private int ticketCount 100; private final Object lock new Object(); // 锁对象 Override public void run() { while (true) { // 同步代码块锁是lock对象 synchronized (lock) { if (ticketCount 0) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() 卖出第 ticketCount-- 张票); } else { break; } } } } }2同步实例方法锁的是当前对象this整个方法在同步范围内。// 非静态同步方法锁对象是 this当前对象 public synchronized void sell() { // 代码 } // 静态同步方法锁对象是 类名.class整个类的类对象 public static synchronized void test() { // 代码 }注意锁的是对象不是代码。多个线程抢同一个对象的锁同步才生效。5.3 释放锁 vs 不释放锁的方式操作是否释放锁同步代码 / 方法执行完毕✅ 释放同步代码中 return、break✅ 释放同步代码中抛出未捕获的异常✅ 释放同步代码中调用锁对象的 wait ()✅ 释放同步代码中调用 Thread.sleep ()❌ 不释放抱着锁睡觉同步代码中调用 Thread.yield ()❌ 不释放6. 死锁多个线程互相拿着对方需要的锁都不肯先放导致所有线程永远卡住程序僵住。生活例子妈妈你先写完作业才让你玩手机小明你先让我玩手机我才写作业俩人僵住谁也不让步 → 死锁示例public class DeadLock_ { public static void main(String[] args) { DeadLockDemo A new DeadLockDemo(true); A.setName(A线程); DeadLockDemo B new DeadLockDemo(false); B.setName(B线程); A.start(); B.start(); } } class DeadLockDemo extends Thread { static Object o1 new Object(); // 锁1 static Object o2 new Object(); // 锁2 boolean flag; public DeadLockDemo(boolean flag) { this.flag flag; } Override public void run() { if(flag) { // A线程先拿o1再想拿o2 synchronized (o1) { System.out.println(Thread.currentThread().getName() 拿到o1); synchronized (o2) { System.out.println(Thread.currentThread().getName() 拿到o2); } } } else { // B线程先拿o2再想拿o1 synchronized (o2) { System.out.println(Thread.currentThread().getName() 拿到o2); synchronized (o1) { System.out.println(Thread.currentThread().getName() 拿到o1); } } } } }