一 多线程基础知识
- 相关概念
-
进程 (Process): 进程是程序的基本执行实体。
- 进程是操作系统分配资源的基本单位。
- 每个进程都有自己的内存空间、代码段、数据段等。
- 进程之间相互独立,一个进程的崩溃不会影响其他进程。
- 进程是程序的基本执行实体。
-
线程 (Thread): 应用软件中相互独立,可以同时运行的功能
- 线程是进程中最小的执行单元。
- 一个进程可以包含一个或多个线程。
- 线程共享同一进程的内存空间和其他资源。
- 线程之间的切换开销比进程小,因此多线程更高效。
- 线程是操作系统能够进行运算调度的最小单位。他被包含在进程之中,是进程中的实际运作单位。
-
并发 (Concurrency): 在同一时刻,有多个指令在单个CPU上交替运行。
- 并发是指多个线程在一段时间内交替执行。
- 在单核CPU上,线程轮流使用CPU时间片。
- 在多核CPU上,多个线程可以真正并行执行。
-
并行 (Parallelism): 在同一时刻,有多个指令在多个CPU上同时执行
- 并行是指多个线程同时执行。
- 需要多核CPU才能实现真正的并行执行。
1 多线程的三种实现方式对比
多线程的第一种启动方式:继承Thread类
- 1自己定义一个类去继承Thread
- 2重写run方法
- 3创建子类对象,并启动线程
代码实现:
自定义类:
public class shu16_2 extends Thread {
@Override
public void run() {
//书写线程要执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("安贤好帅"+getName());
}
}
}
实例化对象:
public class shu16_1 {
public static void main(String[] args) {
//实例化对象
shu16_2 p1 = new shu16_2();
shu16_2 p2 = new shu16_2();
shu16_2 p3 = new shu16_2();
//起名字
p1.setName("线程一");
p2.setName("线程二");
p3.setName("线程三");
//启动线程
p1.start();
p2.start();
p3.start();
}
}
执行结果:
多线程的第二种启动方式:实现Runnable接口
- 1 自己定义一个类实现Runable接口
- 2 重写里面的run方法
- 3 创建自己的类和对象
- 4 创建一个Thread类的对象,并开启线程
代码实现:
自定义类
public class shu16_3 implements Runnable {
@Override
public void run() {
//书写需要执行的代码
for (int i = 0; i < 100; i++) {
Thread thread = Thread.currentThread();
System.out.println("安贤好帅啊!" + thread.getName());
}
}
}
实例化对象
public class shu16_4 {
public static void main(String[] args) {
//实例化对象
shu16_3 p1 = new shu16_3();
//创建线程对象
Thread thread1 = new Thread(p1);
Thread thread2 = new Thread(p1);
//给线程设置名字
thread1.setName("thread1");
thread2.setName("thread2");
//开启线程
thread1.start();
thread2.start();
}
}
运行结果:
多线程的第三种启动方式:利用Callable和Future接口
- 1 创建一个自定义类实现Callable接口
- 2 重写call(有返回值,表示多线程要运行的结果)
- 3 实例化对象(表示多线程要执行的任务)
- 4 创建FutureTask的对象(作用管理多线程运行的结果)
- 5 创建Thread对象开启线程
代码实现:
自定义类
import java.util.concurrent.Callable;
public class shu16_5 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
实例化对象
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class shu16_6 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//实例化自定义对象
shu16_5 p1 = new shu16_5();
//实例化FutureTask对象用于管理多线程运行的结果
FutureTask<Integer> ft = new FutureTask<>(p1);
//创建线程对象
Thread thread1 = new Thread(ft);
//开启线程
thread1.start();
//获取多线程运行的结果
Integer num = ft.get();
System.out.println(num);
}
}
运行结果:
2 多线程的常见成员方法
1 线程优先级
自定义类
public class shu16_10 implements Runnable{
@Override
public void run() {
Thread thread = Thread.currentThread();
for(int i=1;i<=100;i++){
System.out.println("hello world"+thread.getName()+i);
}
}
}
相关方法的调用
public class shu16_11 {
public static void main(String[] args) {
shu16_10 p1 = new shu16_10();
Thread t1 = new Thread(p1, "线程一");
Thread t2 = new Thread(p1, "线程二");
//优先级 getPriority()--默认5 等级1-10越大越高
int num1 = t1.getPriority();
int num2 = t2.getPriority();
System.out.println(num1 + " " + num2);
//获取main主线程的优先级(概率性)
System.out.println(Thread.currentThread().getPriority());
System.out.println(Thread.currentThread().getName());
//给线程设置优先级
t1.setPriority(1);
t2.setPriority(10);
//启动线程
t1.start();
t2.start();
}
}
2 守护线程
代码实现
自定义类
public class shu16_12 extends Thread{
public shu16_12() {
}
public shu16_12(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(getName()+"@"+i);
}
}
}
public class shu16_13 extends Thread{
public shu16_13() {
}
public shu16_13(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(getName() + "@" + i);
}
}
}
方法的调用
public class shu16_14 {
public static void main(String[] args) {
//创建对应的线程对象
shu16_12 t1 = new shu16_12("女神");
shu16_13 t2 = new shu16_13("备胎");
//设置守护线程(备胎线程)
//当其他非守护线程执行结束,非守护线程没有执行下去的必要了,会在短时间内迅速停止(不是立刻)
t2.setDaemon(true);
//开启线程
t1.start();
t2.start();
}
}
运行结果:
3 出让/礼让线程
代码实现:
自定义类
public class shu16_15 extends Thread {
public shu16_15() {
}
public shu16_15(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
System.out.println(getName() + " @ " + i);
//出让当前CPU的执行权-减少连续长时间执行同一个线程的概率(但也不是绝对的)
Thread.yield();
}
}
}
方法调用:
public class shu16_14 {
public static void main(String[] args) {
//创建对应的线程对象
shu16_15 t1 = new shu16_15("坦克");
shu16_15 t2 = new shu16_15("飞机");
//开启线程
t1.start();
t2.start();
}
}
运行结果:
4 插入/插队线程
代码实现:
自定义类:
public class shu16_16 extends Thread {
public shu16_16() {
}
public shu16_16(String name) {
super(name);
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(i + " " + getName());
}
}
}
方法调用:
public class shu16_17 {
public static void main(String[] args) throws InterruptedException {
//创建线程
shu16_16 t1 = new shu16_16("土豆");
// //开启土豆线程
// t1.start();
//
// //main线程
// for(int i =1; i<=100; i++){
// System.out.println(Thread.currentThread().getName()+" "+i);
// }
//开启土豆线程
t1.start();
//线程t1插到当前线程之前(当前线程main)
t1.join();
//main线程
for(int i =1; i<=10; i++){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
运行结果:
3 线程的生命周期
Question:sleep方法会让线程睡眠,线程睡眠时间到了之后,会立马执行下面的代码吗?
- 当睡眠时间到期后,线程会从阻塞状态变为可运行状态(Runnable),但并不保证它会立即执行。
- 即使睡眠时间到期,线程也需要与其他可运行状态的线程竞争CPU资源。
- 因此,睡眠时间到期后,线程可能会立即执行,也可能需要等待一段时间才能得到CPU时间片。
- 操作系统的线程调度器决定哪个线程获得CPU时间片。
二 多线程的安全问题
1 同步代码块
- 同步代码块:把操作共享数据的代码锁起来。
- 特点:锁默认打开,有一个线程进去了,锁自动关闭。
- 特点:里面的代码全部执行完毕,线程出来,锁自动打开。
一个具体实现:
代码实现:
实现过程中如果使用sleep睡眠代码,无法抛出只能使用try_catch,原因是父类方法中的Run方法没有写throw抛出,子类就不能直接跑,需要try,catch。
为了解决卖票重复问题,需要将进入线程的线程锁起来,等执行完毕下一个线程再进入。
锁对象需要唯一(也可以使用当前类的字节码对象,类名.class())
public class shu16_18 implements Runnable {
static final Object lock = new Object();
static int ticket = 0;
@Override
public void run() {
while (true) {
synchronized (lock) {
if (ticket < 100) {
ticket++;
System.out.println(Thread.currentThread().getName() + "窗口卖出第" + ticket + "张票");
} else
break;
}
}
}
}
方法调用:
public class shu16_19 {
public static void main(String[] args) {
//创建对象
shu16_18 t1 = new shu16_18();
//创建线程
Thread thread1 = new Thread(t1,"线程一");
Thread thread2 = new Thread(t1,"线程二");
Thread thread3 = new Thread(t1, "线程三");
//开启线程
thread1.start();
thread2.start();
thread3.start();
}
}
2 同步方法
同步方法:就是把synchronized关键字加到方法上
- 1 同步方法是锁住方法里面的所有代码。
- 2 锁对象不能自己指定。
- 3 锁对象如果是非静态的,使用this。
- 4 锁对象如果是静态的,使用当前类的字节码文件,类名.class。
代码实现:(不需要使用static,对象作为参数传递指挥创建一次他的对象,都是共有的)
public class shu16_20 implements Runnable {
int ticket = 0;
@Override
public void run() {
//1 循环
while (true) {
//2 同步代码块(同步方法)
if (method()) break;
}
}
private synchronized boolean method() {
//3 判断共享数据是否到了末尾
if (ticket == 1000) {
return true;
}//4 如果没有到末尾,执行相应的代码
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "卖出第" + ticket + "张票");
return false;
}
}
方法调用:
public class shu16_21 {
public static void main(String[] args) {
//实例化对象
shu16_20 r1 = new shu16_20();
//创建对象的三个线程
Thread thread1 = new Thread(r1,"窗口一");
Thread thread2 = new Thread(r1,"窗口二");
Thread thread3 = new Thread(r1,"窗口三");
//开启线程
thread1.start();
thread2.start();
thread3.start();
}
}
补充:StringBuffer就是加了线程安全锁(考虑了数据安全问题),StringBuilder没有安全锁(没有考虑数据安全问题)
3 Lock/Unlock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法
void lock(): 获得锁
void unlock(): 释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
代码实现:
错误示范:程序没有正常停止(还有线程在进程中,但是第一个执行完的线程直接跳出循环导致,但是其他线程被锁住无法执行判断语句跳出)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class shu16_20 implements Runnable {
int ticket = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
if (ticket == 100) {
break;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket++;
System.out.println(Thread.currentThread().getName() + "卖出第" + ticket + "张票");
}
lock.unlock();
}
}
改进方法:(使用try-catch-finally)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class shu16_20 implements Runnable {
int ticket = 0;
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
try {
if (ticket == 1000) {
break;
}
Thread.sleep(10);
ticket++;
System.out.println(Thread.currentThread().getName() + "卖出第" + ticket + "张票");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
}
}
4 死锁
死锁是指多个进程在争夺资源时互相等待,导致所有相关进程都无法继续执行的现象。
我们需要避免这种情况的发生,减少锁之间的嵌套可能性。
课外资料:银行家算法
银行家算法是一种用于避免死锁的资源分配算法,最初是由艾德斯格·迪科斯彻(Edsger Dijkstra)为解决操作系统中的资源分配问题而提出的。这个算法的名字来源于一个类比:银行家在发放贷款时会评估客户的偿还能力,确保即使所有客户都要求最大额度的贷款,银行仍然有足够的资金满足所有客户的偿还需求,从而不会陷入财务危机。同样地,在操作系统中,银行家算法用于判断在分配资源给进程之前,系统是否处于安全状态,以防止进入不安全状态导致死锁。
5 生产者和消费者/ 等待唤醒机制
- 生产者消费者模式是一个十分经典的多线程协作模式。
- 核心思想:利用桌子来控制线程的执行。
常见成员方法:
代码实现 一 :(基础方式)
1 定义一个吃货类
//消费者(吃货)
public class AXFoodie extends Thread {
@Override
public void run() {
while (true) {
synchronized (AXDesk.lock) {
if (AXDesk.count == 0) {
break;
} else {
//没有的时候等待并唤醒厨师
if (AXDesk.foodFlag == 0) {
try {
AXDesk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
AXDesk.lock.notifyAll();
//有的时候吃完了再继续唤醒厨师
} else {
//吃的数量减一
AXDesk.count--;
//统计状态,实时更新
System.out.println("吃货吃面条,还能吃" + AXDesk.count+"碗");
System.out.println();
//吃完之后唤醒厨师继续操作
AXDesk.lock.notifyAll();
//修改状态
AXDesk.foodFlag = 0;
}
}
}
}
}
}
2 定义一个厨师类
//生产者(厨师)
public class AXCook extends Thread {
@Override
public void run() {
while (true) {
synchronized (AXDesk.lock) {
if (AXDesk.count == 0) {
break;
} else {
if (AXDesk.foodFlag == 1) {
try {
AXDesk.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
//如果没有就制作食物
System.out.println("厨师做了一碗面条");
System.out.println();
//修改桌子上的食物状态
AXDesk.foodFlag = 1;
//叫醒等待的消费者开吃
AXDesk.lock.notifyAll();
}
}
}
}
}
}
3 定义一个媒介桌子
//桌子(中间者)
public class AXDesk {
//是否有(面条)
public static int foodFlag = 0;
//总个数
public static int count = 10;
//锁对象
public final static Object lock = new Object();
}
4 测试类:
public class AX1 {
public static void main(String[] args) {
AXCook cook = new AXCook();
AXFoodie eat = new AXFoodie();
cook.setName("厨师");
eat.setName("吃货");
cook.start();
eat.start();
}
}
代码执行结果:
代码实现 二:(阻塞队列方式实现)
生产者和消费者必须使用同一个阻塞队列
代码实现:
输出语句写在锁的外面会导致重复输出语句
import java.util.concurrent.ArrayBlockingQueue;
public class Cook extends Thread {
ArrayBlockingQueue<String> queue;
public Cook(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
queue.put("面条");
System.out.println("厨师放了一碗面条");
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
2
import java.util.concurrent.ArrayBlockingQueue;
public class Foodie extends Thread {
ArrayBlockingQueue<String> queue;
public Foodie(ArrayBlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
try {
String food = queue.take();
System.out.println(food);
} catch (InterruptedException e) {
throw new IndexOutOfBoundsException();
}
}
}
}
方法调用:
import java.util.concurrent.ArrayBlockingQueue;
public class shu17_1 {
public static void main(String[] args) {
//创建队列 - 创建对象时将队列作为参数传递
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);
//创建线程的对象
Cook cook = new Cook(queue);
Foodie foodie = new Foodie(queue);
cook.start();
foodie.start();
}
}
6 补充(线程的状态种类)
7一个具体实现(红包)
import java.util.Random;
public class ax1 extends Thread {
//定义抢红包的最小值
static final double MIN = 0.01;
//红包金额
static double money = ax2.money;
//红包的数量
static int counter = ax2.counter;
@Override
public void run() {
synchronized (ax1.class) {
if (counter == 0) {
System.out.println(getName() + "没有抢到红包!");
} else {
double prize;
if (counter == 1) {
prize = money;
} else {
Random r = new Random();
double v = money - (counter - 1) * MIN;
prize = Math.round(r.nextDouble(v) * 100) / 100.0;
if (prize < MIN) {
prize = MIN;
}
}
money = money - prize;
counter--;
System.out.printf(getName() + "抢到了 %.2f 元", prize);
System.out.println();
}
}
}
}
main
import java.util.Scanner;
public class ax2 {
static double money;
static int counter;
static final double MIN = 0.01;
static final double MAX_MONEY = 20000; // 假设最大金额为2万元
static final int MAX_COUNTER = 5; // 假设最多可以发5个红包
public static void main(String[] args) throws InterruptedException {
Scanner input = new Scanner(System.in);
// 输入红包金额并验证
while (true) {
System.out.print("输入你需要发送红包的数额:");
money = input.nextDouble();
if (money >= MIN && money <= MAX_MONEY) {
break;
} else {
System.out.println("请输入有效的红包金额(" + MIN + " 到 " + MAX_MONEY + " 元之间)");
}
}
// 输入红包数量并验证
while (true) {
System.out.print("输入你要发送红包的数量:");
counter = input.nextInt();
if (counter > 0 && counter <= MAX_COUNTER && money >= counter * MIN) {
break;
} else {
System.out.println("请输入有效的红包数量(1 到 " + MAX_COUNTER + " 个之间,且总金额足够分配给每个红包)");
}
}
System.out.println("红包总额为 " + money + " 元,共有 " + counter + " 个红包。");
//创建线程
ax1 ax1 = new ax1();
ax1 ax2 = new ax1();
ax1 ax3 = new ax1();
ax1 ax4 = new ax1();
ax1 ax5 = new ax1();
//设置名字
ax1.setName("超哥");
ax2.setName("贤哥");
ax3.setName("李四");
ax4.setName("张三");
ax5.setName("王五");
//开启线程
ax1.start();
ax2.start();
ax3.start();
ax4.start();
ax5.start();
input.close();
}
}
三 线程池
1:创建一个池子,池子中是空的。
2:提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子,下回再次提交任务时,不需要创建新的线程,直接服用已有的线程即可。
3:如果提交任务时,池子中没有空闲线程也无法创建新的线程,任务就会排队等待。
代码步骤:
1 创建线程池
2 提交任务
3 所有的任务完成关闭线程池。
1 将main休眠等线程从线程池中出来再调用,使用的全是线程一
public class MyRunable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
方法调用:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
//获取线程池对象 - 没有上限的线程池
ExecutorService P1 = Executors.newCachedThreadPool();
//提交任务-让main线程睡眠,目的-等线程1回到线程池
P1.submit(new MyRunable());
Thread.sleep(1000);
P1.submit(new MyRunable());
Thread.sleep(1000);
P1.submit(new MyRunable());
Thread.sleep(1000);
//销毁线程池
P1.shutdown();
}
}
2 分配多个任务,开启多个线程执行
public class MyRunable implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
方法调用:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
//获取线程池对象 - 没有上限的线程池
ExecutorService P1 = Executors.newCachedThreadPool();
//提交任务- 使用多个线程
P1.submit(new MyRunable());
P1.submit(new MyRunable());
P1.submit(new MyRunable());
P1.submit(new MyRunable());
//销毁线程池
P1.shutdown();
}
}
3 设置有上限的线程池,分配多个任务
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
//获取线程池对象 - 设置有上限的线程池
ExecutorService P1 = Executors.newFixedThreadPool(2);
//提交任务- 使用多个线程,但是最多只会开启两个线程,多余的在外面排队
P1.submit(new MyRunable());
P1.submit(new MyRunable());
P1.submit(new MyRunable());
P1.submit(new MyRunable());
//销毁线程池
P1.shutdown();
}
}
4 自定义线程池
具体分析:
五个任务
八个任务
十个任务
任务拒绝策略:
舍弃情况:
代码实现:
1
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ax1 {
public static void main(String[] args) {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量
5,//最大线程数量
60,//空闲线程最大存活时间
TimeUnit.SECONDS,//时间单位
new ArrayBlockingQueue<Runnable>(3),//任务队列长度
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务拒绝策略
);
}
}
5 最大并行数
在 Java 等编程语言中,线程池的最大并行数指的是线程池中可以同时运行的线程的最大数量。
代码实现:
public class ax2 {
public static void main(String[] args) {
int i = Runtime.getRuntime().availableProcessors();
System.out.println(i);
}
}