Java多线程JUC
1. 什么是多线程?
多线程是一种程序执行方式,它允许一个程序在同一时间运行多个线程,每个线程可以看作是程序中的一个执行单元。多线程的目的是提高程序的并发性、利用多核CPU的性能、提升执行效率。
2. 并发和并行
- 并发:在同一时刻,有多个指令在单个CPU上交替执行
- 并行:在同一时刻,有多个指令在多个CPU上同时执行
3. 多线程的实现方式
3.1. 继承Thread类的方式进行实现
直接继承 java.lang.Thread 类,重写其中的 run() 方法,把线程要执行的代码写在里面。
- 优点:简单直观,适合小型程序
- 缺点:Java 单继承限制,如果继承了 Thread,就不能继承其他类,不灵活
package com.fg.thread1;public class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println(getName() + "hello juc");}}
}
package com.fg.thread1;public class ThreadDemo1 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.setName("线程1");t2.setName("线程2");t1.start();t2.start();}
}
3.2. 实现Runnable接口的方式进行实现
通过实现 Runnable 接口,把任务逻辑与线程解耦,使得任务可以被多个线程复用。
- 更加灵活,推荐使用
- 可与线程池配合使用
- 便于资源共享
package com.fg.thread2;public class MyRun implements Runnable {@Overridepublic void run() {for (int i = 0; i < 100; i++) {Thread t = Thread.currentThread();System.out.println(t.getName() + "hello juc");}}
}
package com.fg.thread2;public class ThreadDemo2 {public static void main(String[] args) {MyRun mr = new MyRun();Thread t1 = new Thread(mr);Thread t2 = new Thread(mr);t1.setName("t1");t2.setName("t2");t1.start();t2.start();}
}
3.3. 利用Callable接口和Future接口方式实现
Callable 接口与 Runnable 类似,但可以有返回值和抛出异常,配合 Future 对象获取异步执行结果。
-
有返回值,比 Runnable 更强大
-
异步编程的基础
package com.fg.thread3;import java.util.concurrent.Callable;public class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i < 100; i++) {sum += i;}return sum;}
}
package com.fg.thread3;import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class ThreadDemo3 {public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建MyCallable对象,表示多线程要执行的任务MyCallable mc = new MyCallable();// 创建FutureTask对象,作用管理多线程运行的结果FutureTask<Integer> ft = new FutureTask<>(mc);Thread t1 = new Thread(ft);t1.start();Integer result = ft.get();System.out.println(result);}
}
4. 常用成员方法
4.1. 线程的优先级
Java 中的线程优先级(Thread Priority)是指调度器(线程调度器)用来决定哪个线程应该先执行的一个提示性参数。它不能保证执行顺序,但可以影响执行倾向。
常量 | 值 | 含义 |
---|---|---|
Thread.MIN_PRIORITY | 1 | 最低优先级 |
Thread.NORM_PRIORITY | 5 | 默认优先级 |
Thread.MAX_PRIORITY | 10 | 最高优先级 |
Thread t1 = new Thread(() -> {System.out.println("低优先级线程");
});
t1.setPriority(Thread.MIN_PRIORITY); // 1Thread t2 = new Thread(() -> {System.out.println("高优先级线程");
});
t2.setPriority(Thread.MAX_PRIORITY); // 10
4.2. 守护线程(Daemon Thread)
守护线程是一种服务型线程,它在后台运行,为其他线程提供服务,比如垃圾回收线程。
当所有用户线程(非守护线程)都结束时,JVM会自动 退出,并杀掉所有守护线程。
Thread t = new Thread(() -> {while (true) {System.out.println("守护线程运行中...");}
});
t.setDaemon(true); // 设置为守护线程(必须在 start() 前)
t.start();
4.3. 礼让线程(Yield)
yield() 是一个静态方法,用于提示线程调度器:当前线程愿意“礼让”,让其他相同或更高优先级的线程先执行。
并不保证一定会让出CPU,也可能继续执行当前线程。
Thread t = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程运行:" + i);Thread.yield(); // 礼让}
});
t.start();
4.4. 插入线程(Join)
join() 方法表示:当前线程等待另一个线程执行完毕后再继续执行。
主要用于线程之间的依赖关系控制,比如:先下载再处理数据。
Thread t1 = new Thread(() -> {System.out.println("子线程执行中...");
});
t1.start();t1.join(); // 主线程等待 t1 执行完毕后再继续
System.out.println("主线程继续执行");
5. 线程的状态
- 新建状态(NEW):线程被创建但尚未启动。
- 就绪状态(RUNNABLE):线程可以在任意时刻运行。处于这个状态的线程可能正在运行,也可能正在等待CPU分配时间片。
- 阻塞状态(BLOCKED):线程被阻止执行,因为它正在等待监视器锁定。其他线程正在占用所需的锁定,因此线程被阻塞。
- 等待状态(WAITING):线程进入等待状态,直到其他线程显式地唤醒它。线程可以调用Object类的wait()方法、join()方法或Lock类的条件等待方法进入此状态。
- 计时等待(TIMED_WAITING):线程进入计时等待状态,等待一段指定的时间。线程可以调用Thread.sleep()方法、Object类的wait()方法、join()方法或Lock类的计时等待方法进入此状态。
- 结束状态(TERMINATED):线程完成了其任务,或者因为异常或其他原因而终止运行。
6. 线程的安全问题
6.1. 例子
例子:假如有100张票,有三个窗口售卖。
package com.fg.thread4;public class ThreadDemo4 {public static void main(String[] args) {MyThread t1 = new MyThread();MyThread t2 = new MyThread();MyThread t3 = new MyThread();t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}
package com.fg.thread4;public class MyThread extends Thread {static int ticket = 0;@Overridepublic void run() {while (true) {if (ticket < 100) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");} else {break;}}}
}
以上代码会出现线程安全问题:
- 同一张票被多个线程重复卖出(如两个线程都判断 ticket < 100 成立)
- 票号跳过或重复(非原子性导致结果错乱)
- 总票数超过100张
解决方法:
1. 使用 synchronized关键字加锁
- 使用
synchronized
锁定MyThread.class
(因为 ticket 是静态变量,多个线程共享) - 保证
if
判断 +ticket++
+输出
是一个原子操作
public class MyThread extends Thread {static int ticket = 0;@Overridepublic void run() {while (true) {// 同步代码块synchronized (MyThread.class) {if (ticket < 100) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");} else {break;}}}}
}
2. 使用ReentrantLock
ReentrantLock
是java.util.concurrent.locks
包中的一个类,它提供了与synchronized
类似的互斥锁功能- 使用
ReentrantLock
时,更适合在复杂多线程逻辑中替代synchronized
。
public class MyThread extends Thread {static int ticket = 0;static ReentrantLock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock(); // 加锁try {if (ticket < 100) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(getName() + "正在卖第" + ticket + "张票");} else {break;}} finally {lock.unlock(); // 确保释放锁}}}
}
6.2. 同步方法
就是把 synchronized 关键字加到方法上。
修饰符 synchronized 返回值类型 方法名(方法参数) {...}
- 同步方法是锁住方法里面所有的代码
- 锁对象不能自己指定,静态方法:当前类的字节码文件对象,非静态方法:this
package com.fg.thread5;public class ThreadDemo5 {public static void main(String[] args) {MyRunnable mr = new MyRunnable();Thread t1 = new Thread(mr);Thread t2 = new Thread(mr);Thread t3 = new Thread(mr);t1.setName("窗口1");t2.setName("窗口2");t3.setName("窗口3");t1.start();t2.start();t3.start();}
}
package com.fg.thread5;import com.fg.thread4.MyThread;public class MyRunnable implements Runnable {int ticket = 0;@Overridepublic void run() {while (true) {if (method()) break;}}private synchronized boolean method() {synchronized (MyThread.class) {if (ticket < 100) {try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}ticket++;System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket + "张票");} else {return true;}}return false;}
}
7. 死锁
死锁(Deadlock) 是指两个或多个线程在执行过程中,因为互相持有对方需要的资源而无限等待下去,导致程序无法继续执行的现象。
例子: 假设两个人(线程 A 和线程 B):
A 拿着筷子1,想要筷子2吃饭;
B 拿着筷子2,想要筷子1吃饭;
结果:谁也不肯放下自己手上的筷子,就这样一直等对方让出来,谁都吃不上饭 —— 这就是死锁!
public class DeadLockDemo {private static final Object resourceA = new Object();private static final Object resourceB = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (resourceA) {System.out.println("线程1 拿到了 resourceA 锁");try { Thread.sleep(100); } catch (InterruptedException ignored) {}synchronized (resourceB) {System.out.println("线程1 拿到了 resourceB 锁");}}});Thread t2 = new Thread(() -> {synchronized (resourceB) {System.out.println("线程2 拿到了 resourceB 锁");try { Thread.sleep(100); } catch (InterruptedException ignored) {}synchronized (resourceA) {System.out.println("线程2 拿到了 resourceA 锁");}}});t1.start();t2.start();}
}
两个线程:
t1 先拿 resourceA,等 resourceB
t2 先拿 resourceB,等 resourceA
就卡住了,相互等待,形成死锁。
8. 生产者和消费者
8.1. 等待唤醒机制
Java 中的“生产者-消费者模式”是多线程开发的经典案例,核心在于多个线程之间协作:
- 生产者不断“生产资源”
- 消费者不断“消费资源”
- 双方通过等待-唤醒机制
wait()/notify()
配合使用,避免资源争抢或空转
方法 | 说明 |
---|---|
wait() | 当前线程等待并释放锁,直到被唤醒 |
notify() | 唤醒等待中的一个线程 |
notifyAll() | 唤醒所有等待线程 |
例子:
- 厨师线程负责“炒菜”
- 吃货线程负责“吃菜”
- 一次只能炒一个菜(盘子最多只能放一个),吃了才能继续炒
package com.fg.thread6;public class Dish {private String food; // 菜名,null表示盘子空// 厨师做菜public synchronized void cook(String dishName) throws InterruptedException {while (food != null) {System.out.println("厨师正在等待,盘子中有菜:" + food);wait(); // 等待吃货吃菜}this.food = dishName;System.out.println("厨师做好菜了:" + dishName);notifyAll(); // 通知吃货吃}// 吃货吃菜public synchronized void eat() throws InterruptedException {while (food == null) {System.out.println("吃货正在等待,盘子中没有菜");wait(); // 等待厨师做菜}System.out.println("吃货吃掉菜:" + food);food = null;notifyAll(); // 通知厨师做菜}
}
package com.fg.thread6;public class ThreadDemo6 {public static void main(String[] args) {Dish dish = new Dish();// 厨师线程Thread chef = new Thread(() -> {String[] menu = {"宫保鸡丁", "鱼香肉丝", "红烧肉"};int index = 0;while (true) {try {String dishName = menu[index % menu.length];dish.cook(dishName);Thread.sleep(800);index++;} catch (InterruptedException e) {e.printStackTrace();}}}, "厨师");// 吃货线程Thread foodie = new Thread(() -> {while (true) {try {dish.eat();Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}}, "吃货");chef.start();foodie.start();}
}
8.2. 阻塞队列方式实现
BlockingQueue
是 Java 提供的线程安全的队列,具有以下特点:
- 它内部已经帮你实现了加锁、
wait() / notify()
等逻辑; - 如果队列满,
put()
会阻塞生产者; - 如果队列空,
take()
会阻塞消费者。
package com.fg.thread6;import java.util.concurrent.ArrayBlockingQueue;public class ThreadDemo6 {public static void main(String[] args) {// 创建一个盘子容量为3的阻塞队列ArrayBlockingQueue<String> dishQueue = new ArrayBlockingQueue<>(3);// 厨师线程Thread chef = new Thread(() -> {String[] menu = {"宫保鸡丁", "鱼香肉丝", "红烧肉"};int index = 0;try {while (true) {String dish = menu[index % menu.length];dishQueue.put(dish); // 如果满了,会阻塞等待System.out.println("厨师做好菜了:" + dish);Thread.sleep(500);index++;}} catch (InterruptedException e) {e.printStackTrace();}});// 吃货线程Thread foodie = new Thread(() -> {try {while (true) {String dish = dishQueue.take(); // 如果空了,会阻塞等待System.out.println("吃货吃菜了:" + dish);Thread.sleep(800);}} catch (InterruptedException e) {e.printStackTrace();}});chef.start();foodie.start();}
}
9. 练习
9.1. 抢红包案例
假设有100块,分成了3个红包,现在有5个人去抢。
package com.fg.thread7;import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Random;public class MyThread extends Thread {static BigDecimal money = new BigDecimal("100.00");static int count = 3;static final BigDecimal MIN = new BigDecimal("0.01"); // 最小金额static Random random = new Random();@Overridepublic void run() {synchronized (MyThread.class) {if (count == 0) {System.out.println("来晚了,红包已被抢光");return;}BigDecimal amount;if (count == 1) {amount = money;} else {// 红包最大值 = 剩余金额 - (剩余红包 - 1) * MINBigDecimal max = money.subtract(MIN.multiply(BigDecimal.valueOf(count - 1)));// 生成 0 到 max 的随机金额,再加 MIN 保底double rand = random.nextDouble();amount = max.multiply(BigDecimal.valueOf(rand)).add(MIN);amount = amount.setScale(2, RoundingMode.DOWN);}money = money.subtract(amount);count--;System.out.println(getName() + " 抢到了红包:" + amount + " 元,剩余:" + count + " 个,余额:" + money);}}public static void main(String[] args) {// 创建 5 个线程,抢 3 个红包for (int i = 1; i <= 5; i++) {new MyThread().start();}}
}
9.2. 抽奖案例
有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为{10,5,20,50,100,200,500,800,2,80,300,700};
“抽奖箱2’创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1",随机从抽奖池中获取奖项元素并打印在控制台上。
package com.fg.thread7;import java.util.Random;public class MyThread extends Thread {Random random = new Random();public MyThread(String name) {super(name);}@Overridepublic void run() {while (true) {int prize;synchronized (ThreadDemo7.prizePool) {if (ThreadDemo7.prizePool.isEmpty()) {break;}int index = random.nextInt(ThreadDemo7.prizePool.size());prize = ThreadDemo7.prizePool.remove(index);}System.out.println(getName() + "抽中了" + prize + "元大奖");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
package com.fg.thread7;import java.util.ArrayList;
import java.util.List;public class ThreadDemo7 {public static final List<Integer> prizePool = new ArrayList<>();static {int[] prizes = {10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700};for (int prize : prizes) {prizePool.add(prize);}}public static void main(String[] args) throws InterruptedException {MyThread t1 = new MyThread("抽奖箱1");MyThread t2 = new MyThread("抽奖箱2");t1.start();t2.start();}
}
在上一题基础上继续完成如下需求:
每次抽的过程中,不打印,抽完时一次性打印(随机)。
package com.fg.thread7;import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;public class MyThread extends Thread {private final Random random = new Random();private final List<Integer> myPrizes = new ArrayList<>();public MyThread(String name) {super(name);}@Overridepublic void run() {while (true) {int prize;synchronized (ThreadDemo7.prizePool) {if (ThreadDemo7.prizePool.isEmpty()) {break;}int index = random.nextInt(ThreadDemo7.prizePool.size());prize = ThreadDemo7.prizePool.remove(index);}myPrizes.add(prize);try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}public int getMaxPrize() {return myPrizes.isEmpty() ? 0 : Collections.max(myPrizes);}public void printSummary() {int total = myPrizes.stream().mapToInt(i -> i).sum();int max = myPrizes.isEmpty() ? 0 : Collections.max(myPrizes);System.out.println(getName() + "总共抽中" + myPrizes.size() + "个奖项");System.out.println("分别为:");for (int i = 0; i < myPrizes.size(); i++) {System.out.print(myPrizes.get(i));if (i < myPrizes.size() - 1) {System.out.print(",");}}System.out.println();System.out.println("最高奖项为" + max + "元,总计额为" + total + "元");System.out.println("----------------------------------------------");}
}
package com.fg.thread7;import java.util.ArrayList;
import java.util.List;public class ThreadDemo7 {public static final List<Integer> prizePool = new ArrayList<>();static {int[] prizes = {10, 5, 20, 50, 100, 200, 500, 800, 2, 80, 300, 700};for (int prize : prizes) {prizePool.add(prize);}}public static void main(String[] args) throws InterruptedException {MyThread t1 = new MyThread("抽奖箱1");MyThread t2 = new MyThread("抽奖箱2");t1.start();t2.start();t1.join();t2.join();t1.printSummary();t2.printSummary();int max1 = t1.getMaxPrize();int max2 = t2.getMaxPrize();if (max1 > max2) {System.out.println("在此次抽奖过程中," + t1.getName() + " 中产生了最大奖项,金额为 " + max1 + " 元");} else if (max2 > max1) {System.out.println("在此次抽奖过程中," + t2.getName() + " 中产生了最大奖项,金额为 " + max2 + " 元");} else {System.out.println("在此次抽奖过程中,两个抽奖箱都抽中了相同的最大奖项,金额为 " + max1 + " 元");}}
}
10. 线程池
线程池就是提前创建好多个线程,放在一个池子(集合)中。程序需要用线程时就从池中取出,执行完任务再归还,而不是每次都重新 new 一个线程。
创建方法 | 描述 |
---|---|
Executors.newFixedThreadPool(int n) | 固定大小线程池 |
Executors.newCachedThreadPool() | 缓存线程池(线程数量无限制) |
Executors.newSingleThreadExecutor() | 单线程线程池(顺序执行任务) |
Executors.newScheduledThreadPool(int n) | 可执行定时任务或周期性任务的线程池 |
自定义线程池:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, // 核心线程数,可以小于06, // 最大线程数,不能小于0,最大线程数60, // 空闲线程最大存活时间TimeUnit.SECONDS, // 时间单位new ArrayBlockingQueue<>(3), // 阻塞(任务)队列Executors.defaultThreadFactory(), // 创建线程工厂new ThreadPoolExecutor.AbortPolicy() // 任务拒绝策略
);