14-1.Java 多线程(创建线程的方式、Thread 常用方法、线程安全、线程同步、线程通信、线程池使用、并发与并行、线程的生命周期、乐观锁与悲观锁)

一、线程概述

  • 线程是一个程序内部的一条执行流程
  1. 程序中如果只有一条执行流程,那这个程序就是单线程的程序

  2. 多线程是指从软硬件上实现的多条执行流程的技术,多条线程由 CPU 负责调度执行

  • Java 通过 java.lang.Thread 类的对象来代表线程的

二、创建线程的方式

1、继承 Thread 类
(1)实现步骤
  1. 定义一个子类 MyThread 继承线程类 java.lang.Thread,重写 run 方法

  2. 创建 MyThread 类的对象

  3. 调用线程对象的 start 方法启动线程(启动后还是执行 run 方法的)

(2)注意事项
  1. 启动线程必须是调用 start 方法,不是调用 run 方法,直接调用 run 方法会当成普通方法执行,此时相当于单线程执行,只有调用 start 方法才是启动一个新的线程执行

  2. 不要把主线程任务放在启动子线程之前,否则主线程一直是先跑完的,相当于是一个单线程的效果

(3)具体实现
  1. MyThread.java
package com.my.threadcreate;// 1、继承 Thread 类
public class MyThread extends Thread{// 2、重写 Thread 类的 run 方法@Overridepublic void run() {// 线程执行任务for (int i = 0; i < 5; i++) {System.out.println("子线程 MyThread 输出:" + i);}}
}
  1. ThreadTest1.java
package com.my.threadcreate;public class ThreadTest1 {// main 方法是由一条默认的主线程执行public static void main(String[] args) {// 3、创建 MyThread 线程类代表一个线程MyThread myThread = new MyThread();// 4、启动线程myThread.start();for (int i = 0; i < 5; i++) {System.out.println("主线程 main 输出:" + i);}}
}
  • 输出结果
子线程 MyThread 输出:0
主线程 main 输出:0
子线程 MyThread 输出:1
主线程 main 输出:1
子线程 MyThread 输出:2
主线程 main 输出:2
子线程 MyThread 输出:3
主线程 main 输出:3
子线程 MyThread 输出:4
主线程 main 输出:4
(4)优缺点
  1. 优点:编码简单

  2. 缺点:线程类已经继承 Thread,无法继承其他类,不利于功能的扩展

2、实现 Runnable 接口
(1)实现步骤
  1. 定义一个线程任务类 MyRunnable 实现 Runnable 接口,重写 run 方法

  2. 创建 MyRunnable 任务对象

  3. 把 MyRunnable 任务对象交给 Thread 处理

  4. 调用线程对象的 start 方法,启动线程

(2)具体实现
  1. MyRunnable.java
package com.my.threadcreate;// 1、继承 Runnable 接口
public class MyRunnable implements Runnable {// 2、重写 Runnable 接口的 run 方法@Overridepublic void run() {// 线程执行任务for (int i = 0; i < 5; i++) {System.out.println("子线程 MyRunnable 输出:" + i);}}
}
  1. ThreadTest2.java
package com.my.threadcreate;public class ThreadTest2 {public static void main(String[] args) {// 3、创建任务对象MyRunnable myRunnable = new MyRunnable();// 4、把任务对象交给一个线程对象处理new Thread(myRunnable).start();for (int i = 0; i < 5; i++) {System.out.println("主线程 main 输出:" + i);}}
}
  • 输出结果
主线程 main 输出:0
子线程 MyRunnable 输出:0
主线程 main 输出:1
子线程 MyRunnable 输出:1
主线程 main 输出:2
子线程 MyRunnable 输出:2
主线程 main 输出:3
子线程 MyRunnable 输出:3
主线程 main 输出:4
子线程 MyRunnable 输出:4
(3)优缺点
  1. 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强

  2. 缺点:需要多一个 Runnable 对象

3、匿名内部类实现 Runnable 接口
(1)实现步骤
  1. 创建 Runnable 的匿名内部类对象

  2. 交给 Thread 线程对象

  3. 调用线程对象的 start 方法,启动线程

(2)具体实现
  • ThreadTest3.java
package com.my.threadcreate;public class ThreadTest3 {public static void main(String[] args) {Runnable runnable = new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("子线程输出:" + i);}}};new Thread(runnable).start();for (int i = 0; i < 5; i++) {System.out.println("主线程 main 输出:" + i);}}
}
  • 输出结果
子线程输出:0
主线程 main 输出:0
子线程输出:1
主线程 main 输出:1
子线程输出:2
主线程 main 输出:2
子线程输出:3
主线程 main 输出:3
子线程输出:4
主线程 main 输出:4
(3)简写形式
new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("子线程输出:" + i);}}
}).start();
4、实现 Callable 接口
(1)问题引入
  • 以前的线程创建方式都存在的一个问题:假如线程执行完毕后有一些数据需要返回,它们重写的 run 方法均不能直接返回结果

  • 解决策略:JDK5.0 提供了 Callable 接口和 FutureTask 类来实现,这种方式最大的优点就是可以返回线程执行完毕后的结果

(2)实现步骤
  1. 定义一个类实现 Callable 接口,重写 call 方法,指定任务和要返回的数据

  2. 把 Callable 类型的对象装成 FutureTask 未来任务对象

  3. 把 FutureTask 未来任务对象交给 Thread 处理

  4. 调用线程对象的 start 方法,启动线程

  5. 线程执行完毕后、通过 FutureTask 未来任务对象的 get 方法获取线程任务执行的结果

(3)具体实现
  1. MyCallable.java
package com.my.threadcreate;import java.util.concurrent.Callable;// 1、实现 Callable 接口
public class MyCallable implements Callable<String> {private int n;public MyCallable(int n) {this.n = n;}// 2、重写 Callable 接口的 run 方法@Overridepublic String call() throws Exception {// 线程执行任务并返回结果int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return "子线程 MyCallable 求出 1 - " + n + " 的和是:" + sum;}
}
  1. ThreadTest4.java
package com.my.threadcreate;import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class ThreadTest4 {public static void main(String[] args) {// 3、创建一个 MyCallable 对象MyCallable myCallable = new MyCallable(100);// 4、将 MyCallable 对象封装成一个 FutureTask 未来任务对象// FutureTask 未来任务对象的作用//  (1)是一个任务对象,实现了 Runnable 接口//  (2)可以在线程执行完毕之后调用 get 方法获取线程执行完毕后的结果FutureTask<String> futureTask = new FutureTask<>(myCallable);// 5、把 FutureTask 未来任务对象交给 Thread 对象new Thread(futureTask).start();try {// 6、获取线程执行完毕后返回的结果System.out.println(futureTask.get());} catch (InterruptedException e) {throw new RuntimeException(e);} catch (ExecutionException e) {throw new RuntimeException(e);}}
}
  • 输出结果
子线程 MyCallable 求出 1 - 100 的和是:5050
(4)补充
  • ThreadTest4_.java,FutureTask 未来任务对象 get 方法的执行策略
package com.my.threadcreate;import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class ThreadTest4_ {public static void main(String[] args) {MyCallable myCallable1 = new MyCallable(100);MyCallable myCallable2 = new MyCallable(10);FutureTask<String> futureTask1 = new FutureTask<>(myCallable1);FutureTask<String> futureTask2 = new FutureTask<>(myCallable2);new Thread(futureTask2).start();new Thread(futureTask1).start();try {// 获取线程执行完毕后返回的结果// 如果执行到这,假如上面的线程还没有执行完毕,这里会暂停,等待上面的线程执行完毕System.out.println(futureTask1.get());System.out.println(futureTask2.get());} catch (InterruptedException e) {throw new RuntimeException(e);} catch (ExecutionException e) {throw new RuntimeException(e);}}
}
  • 输出结果
子线程 MyCallable 求出 1 - 10000 的和是:50005000
子线程 MyCallable 求出 1 - 10 的和是:55
(5)优缺点
  1. 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强,且可以在线程执行完毕后去获取线程执行的结果

  2. 缺点:编码复杂一点


三、Thread 常用方法

1、基本介绍
2、演示
  • MyThread.java
package com.my.threadmethod;public class MyThread extends Thread{public MyThread(String name) {setName(name);}@Overridepublic void run() {Thread thread = Thread.currentThread();for (int i = 0; i < 5; i++) {System.out.println("子线程 " + thread.getName() + " 输出:" + i);}}
}
  1. ThreadMethodTest1.java
package com.my.threadmethod;public class ThreadMethodTest1 {public static void main(String[] args) {MyThread myThread1 = new MyThread("thread001");
//        myThread1.setName("thread1");myThread1.start();// 子线程名字System.out.println(myThread1.getName());MyThread myThread2 = new MyThread("thread002");
//        myThread1.setName("thread2");myThread2.start();System.out.println(myThread2.getName());Thread thread = Thread.currentThread();// 主线程的名字System.out.println(thread.getName());for (int i = 0; i < 5; i++) {System.out.println("主线程 main 输出:" + i);}}
}
  • 输出结果
thread001
thread002
main
主线程 main 输出:0
主线程 main 输出:1
主线程 main 输出:2
主线程 main 输出:3
主线程 main 输出:4
子线程 thread002 输出:0
子线程 thread001 输出:0
子线程 thread002 输出:1
子线程 thread001 输出:1
子线程 thread002 输出:2
子线程 thread001 输出:2
子线程 thread002 输出:3
子线程 thread001 输出:3
子线程 thread002 输出:4
子线程 thread001 输出:4
  1. ThreadMethodTest2.java
package com.my.threadmethod;public class ThreadMethodTest2 {public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 5; i++) {if (i == 3) {// 让当前执行线程暂停 5 秒Thread.sleep(5 * 1000);}System.out.println("主线程 main 输出:" + i);}MyThread myThread1 = new MyThread("thread001");myThread1.start();myThread1.join();MyThread myThread2 = new MyThread("thread002");myThread2.start();myThread2.join();}
}
  • 输出结果
主线程 main 输出:0
主线程 main 输出:1
主线程 main 输出:2
主线程 main 输出:3
主线程 main 输出:4
子线程 thread001 输出:0
子线程 thread001 输出:1
子线程 thread001 输出:2
子线程 thread001 输出:3
子线程 thread001 输出:4
子线程 thread002 输出:0
子线程 thread002 输出:1
子线程 thread002 输出:2
子线程 thread002 输出:3
子线程 thread002 输出:4

四、线程安全

1、基本介绍
  • 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题
2、举例说明
  • 小明和小红是一对夫妻,他们有一个共同的账户,余额是 10 万元,小明和小红同时来取钱,并且两人各自都取钱 10 万元,他们取钱行为如下
  1. 判断余额是否足够

  2. 吐出 10 万元

  3. 更新账户余额

  • 这样的行为存在的线程安全问题
小明线程小红线程
判断余额是否足够,足够-
-判断余额是否足够,足够
吐出 10 万元-
更新账户余额,余额 0-
-吐出 10 万元
-更新账户余额,余额 -10 * 1000
3、模拟演示
(1)实现思路
  1. 提供一个账户类,创建一个账户对象代表两人的共享账户

  2. 定义一个线程类,用于创建两个线程,分别代表小明和小红

  3. 创建两个线程,传入同一个账户对象给这两个线程处理

  4. 启动这两个线程,同时去同一个账户对象中取钱 10 万

(2)具体实现
  1. Account.java
package com.my.threadsafe;public class Account {private String cardId; // 卡号private double money; // 余额public Account() {}public Account(String cardId, double money) {this.cardId = cardId;this.money = money;}public String getCardId() {return cardId;}public void setCardId(String cardId) {this.cardId = cardId;}public double getMoney() {return money;}public void setMoney(double money) {this.money = money;}// 取钱public void drawMoney(int m) {String name = Thread.currentThread().getName();// 判余额是否足够if (this.money >= m) {System.out.println(name + " 来取钱,取 " + m);this.money -= m;System.out.println(name + " 取钱完成,余额 " + money);} else {System.out.println(name + " 来取钱,余额不足");}}
}
  1. DrawThread.java
package com.my.threadsafe;public class DrawThread extends Thread{private Account account;public DrawThread(Account account, String name) {super(name);this.account = account;}@Overridepublic void run() {super.run();account.drawMoney(10 * 10000);}
}
  1. ThreadTest.java
package com.my.threadsafe;public class ThreadTest {public static void main(String[] args) {// 1、创建一个账户对象,代表两个人的共享账户Account account = new Account("abcd", 10 * 10000);// 2、创建两个线程,分别代表小明和小红,去同一个账户取钱 10 万new DrawThread(account, "小明").start();new DrawThread(account, "小红").start();}
}
  • 输出结果
小红 来取钱,取 100000
小明 来取钱,取 100000
小红 取钱完成,余额 0.0
小明 取钱完成,余额 -100000.0

四、线程同步

1、基本介绍
  • 线程同步:解决线程安全问题的方案

  • 线程同步思想:让多个线程实现先后依次访问共享资源

  • 线程同步的常见方案:加锁,每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来

2、同步代码块
(1)基本介绍
synchronized (【同步锁】) {【访问共享资源的核心代码】
}
  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全

  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

  • 注:对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出 Bug

(2)具体实现
public void drawMoney(int m) {String name = Thread.currentThread().getName();synchronized ("Hello World") {if (this.money >= m) {System.out.println(name + " 来取钱,取 " + m);this.money -= m;System.out.println(name + " 取钱完成,余额 " + money);} else {System.out.println(name + " 来取钱,余额不足");}}
}
  • 测试输出结果
小明 来取钱,取 100000
小明 取钱完成,余额 0.0
小红 来取钱,余额不足
(3)修改实现
  • 上例中的锁对象“Hello World”不光可以锁住小明或小红线程,也会锁住其他线程
Account newAccount = new Account("abcd", 10 * 10000);
new DrawThread(newAccount, "jack").start();
new DrawThread(newAccount, "tom").start();
  • 所以该锁对象的选择有问题,锁的范围太大了,需要修改
public void drawMoney(int m) {String name = Thread.currentThread().getName();synchronized (this) {if (this.money >= m) {System.out.println(name + " 来取钱,取 " + m);this.money -= m;System.out.println(name + " 取钱完成,余额 " + money);} else {System.out.println(name + " 来取钱,余额不足");}}
}
(4)补充
  • 锁对象随便选择一个唯一的对象可能会影响其他无关线程的执行
  1. 建议使用共享资源作为锁对象,对于实例方法建议使用 this 作为锁对象
public void test() {synchronized (this) {}
}
  1. 对于静态方法建议使用字节码(【类名】.class)对象作为锁对象
public static void test() {synchronized (Account.class) {}
}
3、同步方法
(1)基本介绍
【修饰符】 synchronized 【返回值类型】 【方法名称】(【形参列表】) {【访问共享资源的核心代码】
}
  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全

  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

(2)具体实现
public synchronized void drawMoney(int m) {String name = Thread.currentThread().getName();if (this.money >= m) {System.out.println(name + " 来取钱,取 " + m);this.money -= m;System.out.println(name + " 取钱完成,余额 " + money);} else {System.out.println(name + " 来取钱,余额不足");}
}
  • 测试输出结果
小明 来取钱,取 100000
小明 取钱完成,余额 0.0
小红 来取钱,余额不足
(3)底层原理
  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
  1. 如果方法是实例方法,同步方法默认用 this 作为的锁对象

  2. 如果方法是静态方法:同步方法默认用字节码(【类名】.class)作为的锁对象

(4)同步代码块对比同步方法
  1. 范围上:同步代码块锁的范围更小,同步方法锁的范围更大

  2. 可读性:同步方法更好

4、Lock
(1)基本介绍
  • Lock 锁是 JDK5 开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大

  • Lock 是接口,不能直接实例化,可以采用它的实现类 ReentrantLock 来构建 Lock 锁对象

构造器说明
public ReentrantLock()获取 Lock 锁实现类对象
方法说明
void lock()获取锁
void unlock()释放锁
(2)具体实现
// 这里建议使用 final 关键字修饰,因为一个账户对象中的锁对象是唯一的,且不能进行替换
private final Lock lock = new ReentrantLock();public void drawMoney(int m) {String name = Thread.currentThread().getName();// 加锁lock.lock();if (this.money >= m) {System.out.println(name + " 来取钱,取 " + m);this.money -= m;System.out.println(name + " 取钱完成,余额 " + money);} else {System.out.println(name + " 来取钱,余额不足");}// 解锁lock.unlock();
}
  • 测试输出结果
小明 来取钱,取 100000
小明 取钱完成,余额 0.0
小红 来取钱,余额不足
(3)存在问题
  • 一旦加锁后的程序产生异常且没有进行异常处理,就会导致没有机会解锁
public void drawMoney(int m) {String name = Thread.currentThread().getName();// 加锁lock.lock();if (this.money >= m) {System.out.println(name + " 来取钱,取 " + m);this.money -= m;System.out.println(name + " 取钱完成,余额 " + money);// 这行代码会导致异常int num = 10 / 0;} else {System.out.println(name + " 来取钱,余额不足");}// 解锁lock.unlock();
}
  • 测试输出结果
小明 来取钱,取 100000
小明 取钱完成,余额 0.0
Exception in thread "小明" java.lang.ArithmeticException: / by zero
(4)修改实现
public void drawMoney(int m) {String name = Thread.currentThread().getName();// 加锁lock.lock();try {if (this.money >= m) {System.out.println(name + " 来取钱,取 " + m);this.money -= m;System.out.println(name + " 取钱完成,余额 " + money);// 这行代码会导致异常int num = 10 / 0;} else {System.out.println(name + " 来取钱,余额不足");}} catch (Exception e) {e.printStackTrace();} finally {// 解锁lock.unlock();}
}
  • 测试输出结果
小明 来取钱,取 100000
小明 取钱完成,余额 0.0
小红 来取钱,余额不足
java.lang.ArithmeticException: / by zero

五、线程通信

1、基本介绍
  • 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺
2、线程通信的常见模型
  • 线程通信的常见模型有生产者与消费者模型
  1. 生产者线程负责生产数据

  2. 消费者线程负责消费生产者生产的数据

  • 生产者生产完数据应该等待自己,通知消费者消费,消费者消费完数据也应该等待自己,通知生产者生产
3、具体实现
(1)需求
(2)实现代码
  • Desk
package com.my.threadcommunication;import java.util.ArrayList;
import java.util.List;public class Desk {private List<String> baoZiList = new ArrayList<>();// 这两个方法都是用 this 作为锁,所以可以锁住五个线程// 放一个包子// 三个生产者共享public synchronized void put() {try {String name = Thread.currentThread().getName();if (baoZiList.size() == 0) {baoZiList.add(name + " 做的包子");System.out.println(name + " 做了一个包子");Thread.sleep(2000);}// 唤醒别人,等待自己this.notifyAll();this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 拿一个包子// 两个消费者共享public synchronized void get() {try {String name = Thread.currentThread().getName();if (baoZiList.size() == 1) {System.out.println(name + " 吃了 " + baoZiList.get(0));baoZiList.clear();Thread.sleep(2000);}// 唤醒别人,等待自己this.notifyAll();this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}
}
  • ThreadCommunicationTest.java
package com.my.threadcommunication;public class ThreadCommunicationTest {public static void main(String[] args) {Desk desk = new Desk();// 创建三个生产者new Thread(() -> {while (true) {desk.put();}}, "厨师 1").start();new Thread(() -> {while (true) {desk.put();}}, "厨师 2").start();new Thread(() -> {while (true) {desk.put();}}, "厨师 3").start();// 创建两个消费者new Thread(() -> {while (true) {desk.get();}}, "吃货 1").start();new Thread(() -> {while (true) {desk.get();}}, "吃货 2").start();}
}
4、补充
  • Object 类的等待和唤醒方法
方法说明
void wait()让当前线程等待并释放所占锁,直到另一个线程调用 notify 方法或 notifyAll 方法
void notify()唤醒正在等待的单个线程
void notifyAll()唤醒正在等待的所有线程
  • 注:上述方法应该使用当前同步锁对象进行调用

六、线程池引入

1、概述
  • 线程池就是一个可以复用线程的技术,例如,在系统中创建出三个线程,这三个线程处理完任务后可以继续复用这三个线程去处理其他任务,即这三个线程会长久的存活重复的利用

  • 不使用线程池的问题:用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的,而创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能

2、工作原理
  • 以用户发起请求为例,并不是用户发起请求,即来一个任务就创建一个线程,线程池中有一块区域用于一些固定数量的线程,线程池中还有一块区域用于存放任务
  1. 假如我们的固定数量的线程有 3 个,此时有一个任务过来了,就会出现一个线程去处理这个任务

  2. 如果达到了线程处理任务的上线,即 3 个线程此时都在处理任务,那么新的任务就会进行排队

  3. 假如一个线程处理完任务了,它就可以去处理下一个任务

  • 注:线程池既可以控制线程的数量,也能控制任务的数量,这样就能避免资源耗尽的风险
3、概念理解
  1. 线程池中的线程称之为工作线程或核心线程(Work Thread),它们是固定数量,可以重复利用的

  2. 线程池中的任务存放在任务队列(Work Queue),这些任务其实就是对象,它必须是实现 Runnable 或 Callable 接口的


七、线程池使用

1、引入
  • JDK5.0 起提供了代表线程池的接口 ExecutorService
  1. 使用 ExecutorService 的实现类 ThreadPoolExecutor 自己创建一个线程池对象

  2. 使用线程池的工具类 Executors 调用方法返回不同特点的线程池对象

2、线程池创建
(1)基本介绍
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
参数说明
corePoolSize指定线程池的核心线程的数量
maximumPoolSize指定线程池的最大线程数量
keepAliveTime指定临时线程的存活时间
unit指定临时线程存活的时间单位(秒、分、时、天)
workQueue指定线程池的任务队列
threadFactory指定线程池的线程工厂
handler指定线程池的任务拒绝策略,即当线程都在忙且任务队列也满了时,新任务来了该如何处理
  • 类比现实场景
参数说明
corePoolSize正式工有 3 人
maximumPoolSize最大员工数有 5 人,临时工有 2 人
keepAliveTime临时工空闲多久被开除
unit-
workQueue客人排队的地方
threadFactory负责招聘员工的人事
handler忙不过来咋办
(2)具体实现
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
(3)注意事项
  1. 临时线程什么时候创建?新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程

  2. 什么时候会开始拒绝新任务?核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务

3、处理 Runnable 任务
(1)基本介绍
(2)具体实现
  1. RunnableTask.java
package com.my.threadpool;public class RunnableTask implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " Hello World");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}
}
  1. 测试代码
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());RunnableTask runnableTask = new RunnableTask();
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 复用前面的核心线程
pool.execute(runnableTask); // 复用前面的核心线程
  • 输出结果
pool-1-thread-1 Hello World
pool-1-thread-2 Hello World
pool-1-thread-3 Hello World
pool-1-thread-2 Hello World
pool-1-thread-1 Hello World
(3)关闭线程池
  • 注:线程池默认不会关闭,里面的线程会长久存活为系统不断提供服务的,在实际开发中,一般不会关闭线程池
  1. 等待全部任务执行完毕后关闭线程池
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());RunnableTask runnableTask = new RunnableTask();
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 复用前面的核心线程
pool.execute(runnableTask); // 复用前面的核心线程pool.shutdown(); // 等待线程池的任务全部执行完毕后,再关闭线程池
  • 输出结果
pool-1-thread-1 Hello World
pool-1-thread-3 Hello World
pool-1-thread-2 Hello World
pool-1-thread-1 Hello World
pool-1-thread-3 Hello World
  1. 立即关闭线程池
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());RunnableTask runnableTask = new RunnableTask();
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 复用前面的核心线程
pool.execute(runnableTask); // 复用前面的核心线程pool.shutdownNow(); // 立即关闭线程池
  • 输出结果
pool-1-thread-1 Hello World
pool-1-thread-2 Hello World
pool-1-thread-3 Hello World
Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-3" java.lang.RuntimeException: java.lang.InterruptedException: sleep interruptedat com.my.threadpool.RunnableTask.run(RunnableTask.java:11)at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.InterruptedException: sleep interruptedat java.base/java.lang.Thread.sleep(Native Method)at com.my.threadpool.RunnableTask.run(RunnableTask.java:9)... 3 more
java.lang.RuntimeException: java.lang.InterruptedException: sleep interruptedat com.my.threadpool.RunnableTask.run(RunnableTask.java:11)at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.InterruptedException: sleep interruptedat java.base/java.lang.Thread.sleep(Native Method)at com.my.threadpool.RunnableTask.run(RunnableTask.java:9)... 3 more
java.lang.RuntimeException: java.lang.InterruptedException: sleep interruptedat com.my.threadpool.RunnableTask.run(RunnableTask.java:11)at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.InterruptedException: sleep interruptedat java.base/java.lang.Thread.sleep(Native Method)at com.my.threadpool.RunnableTask.run(RunnableTask.java:9)... 3 more
(4)临时线程的创建时机
  1. RunnableTask.java,延长休眠时间以模拟该任务需要较长时间处理
package com.my.threadpool;public class RunnableTask implements Runnable{@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " Hello World");try {Thread.sleep(Integer.MAX_VALUE);} catch (InterruptedException e) {throw new RuntimeException(e);}}
}
  1. 测试代码,此时 3 个核心线程在忙,任务队列占满了,再出现新任务的时候就会创建临时线程
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());RunnableTask runnableTask = new RunnableTask();
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务pool.execute(runnableTask);
pool.execute(runnableTask);
pool.execute(runnableTask);
pool.execute(runnableTask);// 到达临时线程创建的时机
pool.execute(runnableTask);
pool.execute(runnableTask);
  • 输出结果
pool-1-thread-3 Hello World
pool-1-thread-2 Hello World
pool-1-thread-4 Hello World
pool-1-thread-5 Hello World
pool-1-thread-1 Hello World
4、任务的拒绝策略
(1)基本介绍
  • 当核心线程在忙,任务队列占满了,临时线程也在忙,再出现新任务的时候就会启用拒绝策略
(2)演示
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());RunnableTask runnableTask = new RunnableTask();
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务
pool.execute(runnableTask); // 线程池会自动创建一个新线程,自动处理这个任务pool.execute(runnableTask);
pool.execute(runnableTask);
pool.execute(runnableTask);
pool.execute(runnableTask);// 到达临时线程创建的时机
pool.execute(runnableTask);
pool.execute(runnableTask);// 启用拒绝策略
pool.execute(runnableTask);
  • 输出结果
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.my.threadpool.RunnableTask@2d98a335 rejected from java.util.concurrent.ThreadPoolExecutor@16b98e56[Running, pool size = 5, active threads = 5, queued tasks = 4, completed tasks = 0]at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2065)at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:833)at java.base/java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1365)at com.my.threadpool.ThreadPoolTest.test4(ThreadPoolTest.java:82)at com.my.threadpool.ThreadPoolTest.main(ThreadPoolTest.java:10)
pool-1-thread-2 Hello World
pool-1-thread-1 Hello World
pool-1-thread-5 Hello World
pool-1-thread-4 Hello World
pool-1-thread-3 Hello World
5、处理 Callable 任务
(1)基本介绍
(2)具体实现
  1. CallableTask.java
package com.my.threadpool;import java.util.concurrent.Callable;public class CallableTask implements Callable<String> {private int n;public CallableTask(int n) {this.n = n;}@Overridepublic String call() throws Exception {// 线程执行任务并返回结果int sum = 0;for (int i = 1; i <= n; i++) {sum += i;}return Thread.currentThread().getName() + " 求出 1 - " + n + " 的和是:" + sum;}
}
  1. 测试代码
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());CallableTask callableTask = new CallableTask(100);
Future<String> f1 = pool.submit(callableTask);
Future<String> f2 = pool.submit(callableTask);
Future<String> f3 = pool.submit(callableTask);
Future<String> f4 = pool.submit(callableTask);try {System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());
} catch (InterruptedException e) {throw new RuntimeException(e);
} catch (ExecutionException e) {throw new RuntimeException(e);
}
  • 输出结果
pool-1-thread-1求出 1 - 100 的和是:5050
pool-1-thread-2求出 1 - 100 的和是:5050
pool-1-thread-3求出 1 - 100 的和是:5050
pool-1-thread-2求出 1 - 100 的和是:5050
6、Executors 创建线程池
(1)基本介绍
  • 注:这些方法的底层都是通过线程池的实现类 ThreadPoolExecutor 来创建线程池对象的
public static ExecutorService newFixedThreadPool(int nThreads) {return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
(2)具体实现
  1. 创建固定数量的线程池
ExecutorService pool = Executors.newFixedThreadPool(3);CallableTask callableTask = new CallableTask(100);
Future<String> f1 = pool.submit(callableTask);
Future<String> f2 = pool.submit(callableTask);
Future<String> f3 = pool.submit(callableTask);
Future<String> f4 = pool.submit(callableTask);try {System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());
} catch (InterruptedException e) {throw new RuntimeException(e);
} catch (ExecutionException e) {throw new RuntimeException(e);
}
  • 输出结果
pool-1-thread-1 求出 1 - 100 的和是:5050
pool-1-thread-2 求出 1 - 100 的和是:5050
pool-1-thread-3 求出 1 - 100 的和是:5050
pool-1-thread-3 求出 1 - 100 的和是:5050
  1. 创建一个线程的线程池
ExecutorService pool = Executors.newSingleThreadExecutor();CallableTask callableTask = new CallableTask(100);
Future<String> f1 = pool.submit(callableTask);
Future<String> f2 = pool.submit(callableTask);
Future<String> f3 = pool.submit(callableTask);
Future<String> f4 = pool.submit(callableTask);try {System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());
} catch (InterruptedException e) {throw new RuntimeException(e);
} catch (ExecutionException e) {throw new RuntimeException(e);
}
  • 输出结果
pool-1-thread-1 求出 1 - 100 的和是:5050
pool-1-thread-1 求出 1 - 100 的和是:5050
pool-1-thread-1 求出 1 - 100 的和是:5050
pool-1-thread-1 求出 1 - 100 的和是:5050
(3)注意事项
  • 大型并发系统环境中使用 Executors 如果不注意可能会出现系统风险
7、任务执行顺序
(1)基本介绍
  • 线程池中的任务执行顺序并不是按照提交任务的顺序进行的,而是由线程池的调度算法来决定

  • 如果想要使他们顺序执行,可以使用 newSingleThreadExecutor 或设置 ThreadPoolExecutor 核心线程数为 1

(2)演示
  • MyOrderRunnable.java
package com.my.threadpool;public class MyOrderRunnable implements Runnable {private int num;public MyOrderRunnable(int num) {this.num = num;}@Overridepublic void run() {System.out.println(num);}
}
  1. 测试线程池执行任务的无序性
ExecutorService pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());for (int i = 1; i <= 10; i++) {pool.execute(new MyOrderRunnable(i));
}
  • 输出结果
1
3
2
5
7
8
10
9
4
6
  1. 使用 newSingleThreadExecutor,有序执行任务
ExecutorService executorService = Executors.newSingleThreadExecutor();for (int i = 1; i <= 10; i++) {executorService.execute(new MyOrderRunnable(i));
}
  • 输出结果
1
2
3
4
5
6
7
8
9
10
  1. 设置 ThreadPoolExecutor 核心线程数为 1, 有序执行任务
ExecutorService pool = new ThreadPoolExecutor(1, 5, 8, TimeUnit.SECONDS,new ArrayBlockingQueue<>(1000), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());for (int i = 1; i <= 10; i++) {pool.execute(new MyOrderRunnable(i));
}
  • 输出结果
1
2
3
4
5
6
7
8
9
10
8、核心线程的数量配置
  1. 计算密集型的任务:核心线程数量 = CPU 的核数 + 1

  2. IO 密集型的任务(读文件、通信):核心线数量 = CPU 核数 * 2


八、并发与并行

1、进程
  • 正在运行的程序(软件)就是一个独立的进程

  • 线程是属于进程的,一个进程中可以同时运行很多个线程

  • 进程中的多个线程其实是并发和并行执行的

2、并发
  • 进程中的线程是由 CPU 负责调度执行的,但 CPU 能同时处理线程的数量有限,为了保证全部线程都能往前执行 CPU 会轮询为系统的每个线程服务,由于 CPU 切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发
3、并行
  • 在同一个时刻上,同时有多个线程在被 CPU 调度执行

九、线程的生命周期

1、生命周期引入
  • 线程的生命周期也就是线程从生到死的过程中,经历的各种状态及状态转换

  • Java 总共定义了 6 种状态,它们都定义在 Thread 类的内部枚举类中

public class Thread implements Runnable {...public enum State {NEW,RUNNABLE,BLOCKED,WAITING,TIMED_WAITING,TERMINATED;}...
}
2、6 种状态的互相转换

十、乐观锁与悲观锁

1、基本介绍
  • 悲观锁:一上来就加锁,没有安全感,每次只能一个线程进入访问完毕后再解锁, 线程安全,性能较差

  • 乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制,线程安全,性能较好

2、案例引入
(1)线程安全问题回顾
  • MyRunnable.java
package com.my.happybluelock;public class MyRunnable implements Runnable{private int count;@Overridepublic void run() {for (int i = 0; i < 1000; i++) {count++;System.out.println("count:" + count);}}
}
  • ThreadSafeTest.java
package com.my.happybluelock;public class ThreadSafeTest {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();for (int i = 0; i < 100; i++) {new Thread(myRunnable).start();}}
}
  • 输出结果
...
count:99995
count:99996
count:99997
count:99998
count:99999
(2)案例分析
线程 1线程 2
取出 count 的值,此时 count 为 10-
-取出 count 的值,此时 count 为 10
进行自增操作进行自增操作
将结果 11 给 count将结果 11 给 count
3、处理策略
(1)基本介绍
  1. 悲观锁处理:使用 synchronized 关键字或 synchronized 同步代码块

  2. 乐观锁处理:

(2)乐观锁原理简述
线程 1线程 2
取出 count 的值,此时 count 为 10
同时记录 count 的原始值为 10
-
-取出 count 的值,此时 count 为 10
同时记录 count 的原始值为 10
进行自增操作进行自增操作
检查现在的 count 是否为原始值 10
是,将结果 11 给 count
-
-检查现在的 count 是否为原始值 10
不是,则放弃操作
(2)具体实现
  1. 悲观锁处理
package com.my.happybluelock;public class MyRunnable implements Runnable{private int count;@Overridepublic void run() {for (int i = 0; i < 1000; i++) {synchronized (this) {count++;System.out.println("count:" + count);}}}
}
  1. 乐观锁处理
package com.my.happybluelock;import java.util.concurrent.atomic.AtomicInteger;public class MyRunnable implements Runnable{// 整数修改的乐观锁,使用原子类实现private AtomicInteger count = new AtomicInteger();@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("count:" + count.incrementAndGet());}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/487763.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

中介者模式的理解和实践

一、中介者模式概述 中介者模式&#xff08;Mediator Pattern&#xff09;&#xff0c;也称为调解者模式或调停者模式&#xff0c;是一种行为设计模式。它的核心思想是通过引入一个中介者对象来封装一系列对象之间的交互&#xff0c;使得这些对象不必直接相互作用&#xff0c;从…

MySQL-DQL之数据多表操作

文章目录 一. 多表操作1. 表与表之间的关系2. 外键约束3. 创建外键约束表(一对多操作) 二. 多表查询1. 多表查询① 交叉连接查询(基本不会使用-得到的是两个表的乘积) [了解]&#xff08;不要记住&#xff09;② 交集运算&#xff1a;内连接查询(join)③ 差集运算&#xff1a;外…

Qt之自定义动态调控是否显示日志

创作灵感 最近在芯驰x9hp上开发仪表应用。由于需要仪表警告音&#xff0c;所以在该平台上折腾并且调试仪表声音的时候&#xff0c;无意间发现使用&#xff1a; export QT_DEBUG_PLUGINS1 可以打印更详细的调试信息。于是想着自己开发的应用也可以这样搞&#xff0c;这样更方便…

Nanolog起步笔记-9-log解压过程(3)寻找meta续

Nanolog起步笔记-9-log解压过程-3-寻找meta续 当前的目标新的改变decompressNextLogStatementmetadata查看业务面的log语句注释掉 runBenchmark();改过之后&#xff0c;2条记录之后&#xff0c;这里就直接返回了 小结 当前的目标 没有办法&#xff0c;还要继续。 当前的目标&a…

最小二乘法拟合出二阶响应面近似模型

背景&#xff1a;根据样本试验数据拟合出二阶响应面近似模型&#xff08;正交二次型&#xff09;&#xff0c;并使用决定系数R和调整的决定系数R_adj来判断二阶响应面模型的拟合精度。 1、样本数据&#xff08;来源&#xff1a;硕士论文《航空发动机用W形金属密封环密封性能分析…

《操作系统 - 清华大学》6 -7:局部页面置换算法:Belady现象

文章目录 1. 定义2. LRU、FIFO和Clock的比较 1. 定义 局部页面置换算法的特点是针对一个正在运行的程序&#xff0c;它访问内存的情况&#xff0c;访问页的情况&#xff0c;来决定应该采取什么样策略&#xff0c;把相应的页替换出去&#xff0c;站在算法本身角度来考虑置换哪个…

【开源免费】基于SpringBoot+Vue.JS在线办公系统(JAVA毕业设计)

本文项目编号 T 001 &#xff0c;文末自助获取源码 \color{red}{T001&#xff0c;文末自助获取源码} T001&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 查…

05-标准库开发-STM32-IIC协议

七、STM32中IIC协议 概述 Inter-Integrated Circuit (IIC)&#xff0c;也常称为I2C&#xff08;I squared C&#xff09;&#xff0c;是一种同步、串行、半双工通信总线协议。它主要用于连接低速外围设备到处理器或微控制器上&#xff0c;如MPU6050姿态传感器、OLED显示屏、存…

【linux系统】基础开发工具(yum、Vim)

1. 软件包管理器 1.1 什么是软件包 在Linux下安装软件, ⼀个通常的办法是下载到程序的源代码, 并进⾏编译, 得到可执⾏程序. 但是这样太麻烦了, 于是有些⼈把⼀些常⽤的软件提前编译好, 做成软件包(可以理解成windows上的安装程序)放在⼀个服务器上, 通过包管理器可以很⽅便的…

UFUG2601_project_Fall2024 MiniDB Project

PS&#xff1a;如果读过题了可以跳过题目描述直接到题解部分 链接&#xff1a;UFUG2601_project_Fall2024 MiniDB Project 文章目录 题目题解声明可完成操作运行逻辑大致思路数据存储数据类型数据名称 命令输入文件读入命令读入 操作2.1 Create Database and Use Database2.2 C…

this version of the Java Runtime only recognizes class file versions up to 52.0

问题描述 Exception in thread "main" java.lang.UnsupportedClassVersionError: com/xxx/Main has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versi…

Tr0ll: 1 Vulnhub靶机渗透笔记

Tr0ll: 1 本博客提供的所有信息仅供学习和研究目的&#xff0c;旨在提高读者的网络安全意识和技术能力。请在合法合规的前提下使用本文中提供的任何技术、方法或工具。如果您选择使用本博客中的任何信息进行非法活动&#xff0c;您将独自承担全部法律责任。本博客明确表示不支…

CAP定理

2.1 CAP 定理的由来与证明 CAP 定理是计算机科学界的“铁律”&#xff0c;最早由 Eric Brewer 提出&#xff0c;后来被正式证明&#xff1a; 分布式系统里&#xff0c;一致性&#xff08;C&#xff09;、可用性&#xff08;A&#xff09;、分区容错性&#xff08;P&#xff09…

【flutter】webview下载文件方法集锦

说明&#xff1a;android的webview是不支持下载的&#xff01;&#xff01;&#xff01; 所以我们需要监听下载接口 然后手动执行下载操作&#xff0c;分为三种类型 直接打开浏览器下载&#xff08;最简单&#xff09;&#xff0c;但是一些下载接口需要cookie信息时不能满足 …

Java版-图论-最短路-Floyd算法

实现描述 网络延迟时间示例 根据上面提示&#xff0c;可以计算出&#xff0c;最大有100个点&#xff0c;最大耗时为100*wi,即最大的耗时为10000&#xff0c;任何耗时计算出来超过这个值可以理解为不可达了&#xff1b;从而得出实现代码里面的&#xff1a; int maxTime 10005…

SQL注入基础入门篇 注入思路及常见的SQL注入类型总结

目录 前言一、了解mysql数据库1、了解sql增删改查2、了解sql查询 二、sql注入基础三、学习sql注入漏洞1、union注入1、判断数字型注入还是字符型型注入&#xff1a;2、判断闭合方式&#xff08;字符型注入&#xff09;&#xff1a;3、判断回显位4、查询库名&#xff0c;表名&am…

基于Spring Boot库存管理系统

文末获取源码和万字论文&#xff0c;制作不易&#xff0c;感谢点赞支持。 基于Spring Boot库存管理系统 当下&#xff0c;如果还依然使用纸质文档来记录并且管理相关信息&#xff0c;可能会出现很多问题&#xff0c;比如原始文件的丢失&#xff0c;因为采用纸质文档&#xff0c…

JSSIP的使用及问题(webRTC,WebSockets)

简介 项目中有一个需要拨打电话的功能&#xff0c;要求实时的进行音频接听&#xff0c;并且可以在电话接听或者挂断等情况下做出相应的操作。jssip作为一个强大的实现实时通信的javascript库&#xff0c;这不门当户对了嘛。 jssip&#xff08;官网&#xff1a; JsSIP - the J…

【Cadence32】PCB多层板电源、地平面层创建心得➕CM约束管理器Analyze分析显示设置➕“DP”报错DRC

【转载】Cadence Design Entry HDL 使用教程 【Cadence01】Cadence PCB Edit相对延迟与绝对延迟的显示问题 【Cadence02】Allegro引脚焊盘Pin设置为透明 【Cadence03】cadence不小心删掉钢网层怎么办&#xff1f; 【Cadence04】一般情况下Allegro PCB设计时的约束规则设置&a…

Java阶段三06

第3章-第6节 一、知识点 理解MVC三层模型、理解什么是SpringMVC、理解SpringMVC的工作流程、了解springMVC和Struts2的区别、学会使用SpringMVC封装不同请求、接收参数 二、目标 理解MVC三层模型 理解什么是SpringMVC 理解SpringMVC的工作流程 学会使用SpringMVC封装请求…