多线程(初阶)

文章目录

  • 一、认识线程(Thread)
    • 1.1 概念
      • 1.1.1 什么是线程
      • 1.1.2 为什么要有线程
      • 1.1.3 进程和线程的区别(重要)
      • 1.1.4 Java的线程和操作系统线程的关系
    • 1.2 第一个多线程 程序
    • 1.3 创建线程(重要)
      • 1.3.1 继承 Tread 类
      • 1.3.2 实现 Runnable 接口
      • 1.3.3 匿名内部类 创建Thread 子类对象
      • 1.3.4 匿名内部类 创建实现 Runnable 接口的Thread子类对象
      • 1.3.5 lambda 表达式创建实现 Runnable 接口的Thread 的⼦类对象
  • 二、Thread 类及常用方法
    • 2.1 Thread 常见的构造方法
    • 2.2 Thread 的几个常见属性
    • 2.3 启动线程 - start() (面试题)
    • 2.4 中断一个线程
    • 2.5 等待一个线程- join()
    • 2.6 获取当前线程引用
    • 2.7 休眠当前线程
  • 三、线程的状态
    • 3.1 观察线程的所有状态
  • 四、多线程带来的风险-线程安全(重点)
    • 4.1 观察线程不安全
    • 4.2 什么是线程安全
    • 4.3 线程不安全的原因
  • 4.4 解决上述的线程不安全问题
  • 五 synchronized 关键字(监视器锁 monitor lock)
    • 5.1 synchronized 的特性
    • 5.2 synchronized 的使用
      • 5.2.1 修饰代码块 :明确指明锁的哪个对象
      • 5.2.2 修饰方法
    • 5.3 Java 标准库中的线程安全类
  • 六、volatile 关键字
    • 6.1 volatile 保证内存可见性
    • 6.2 volatile 不保证原子性
  • 七、wait 和 notify
    • 7.1 wait()方法
    • 7.2 notify()方法
    • 7.3 notifyAll()方法
    • 7.4 wait 和 sleep 的对比(重要)
  • 八、多线程案例
    • 8.1 单例模式
      • 8.1.1 饿汉模式
      • 8.1.2 懒汉模式
    • 8.2 阻塞队列
      • 8.2.1 阻塞队列的定义
      • 8.2.2 消费者模型
      • 8.2.3 标准库中的阻塞队列
      • 8.2.4 阻塞队列的模拟实现
    • 8.3 定时器
      • 8.3.1 什么是定时器
      • 8.3.2 标准库中的定时器
      • 8.3.3 模拟实现定时器
    • 8.4 线程池
      • 8.4.1 什么是线程
      • 8.4.2 标准库中的线程(重要)
      • 8.4.3 模拟实现线程池
  • 九、对比线程和进程
    • 9.1 线程的优点
    • 9.2 线程和进程的区别

一、认识线程(Thread)

1.1 概念

1.1.1 什么是线程

⼀个线程就是⼀个"执⾏流",每个线程之间都可以按照顺序执⾏⾃⼰的代码,多个线程之间"同时"执⾏着多份代码。

1.1.2 为什么要有线程

  1. 并发编程成为“刚需”
    • 单核 CPU 的发展遇到了瓶颈,要想提⾼算⼒,就需要多核 CPU,⽽并发编程能更充分利⽤多核 CPU 资源。
    • 有些任务场景需要 “等待 IO”,为了让等待 IO 的时间能够去做⼀些其他的⼯作,也需要⽤到并发编程。
  2. 虽然多进程也能实现 并发编程,但是线程⽐进程更轻量
    在这里插入图片描述
    • 创建线程比创建进程更块
    • 销毁线程比销毁进程更快
    • 调度线程比调度进程更快
  3. 线程虽然⽐进程轻量,但还不满⾜,于是⼜有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)

1.1.3 进程和线程的区别(重要)

  • 进程包含线程(线程不能独立存在,要依附于进程),每个进程⾄少有⼀个线程存在,即主线程
  • 进程和线程 都是用来实现并发编程场景的,但线程比进程更轻量,更高效
  • 进程和进程之间不共享资源,同⼀个进程的线程之间共享资源(内存和硬盘)
  • 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位
  • 进程之间是独立的,⼀个进程挂了⼀般不会影响到其他进程,但⼀个线程挂了,很大可能影响同进程内的其他线程(整个进程崩溃)

在这里插入图片描述

1.1.4 Java的线程和操作系统线程的关系

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对⽤户层提供了⼀些 API 供⽤⼾使⽤。
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装。

1.2 第一个多线程 程序

感受多线程程序和普通程序的区别:

  • 每个线程都是⼀个独⽴的执⾏流
  • 多个线程之间 “并发” 执⾏
/*** 通过创建一个 继承 thread类 的类 的方式创建线程,重写run方法*/
class MyThread extends Thread{@Overridepublic void run() {//这个方法是线程的入口方法while(true){System.out.println("hello thread");//重写父类的 run方法 并没有声明异常,子类重写这个方法也不能声明异常,只能采用捕获异常的方式try {//设置当前线程暂停执行指定的时间间隔(1秒),然后再恢复执行Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class demo1 {public static void main(String[] args) throws InterruptedException {Thread thread = new MyThread();//start 和 run 都是Thread 的成员// run 只描述线程的入口(线程要做什么)//start 是真正调用了系统API,在系统中创建线程,让线程再调用 runthread.start();while (true){System.out.println("hello main");// sleep方法可能抛出异常(受查异常---显示处理---声明或捕获异常)//设置当前线程暂停执行指定的时间间隔(1秒),然后再恢复执行Thread.sleep(1000);}}
}

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

1.3 创建线程(重要)

1.3.1 继承 Tread 类

继承 Thread 来创建⼀个线程类,重写run方法
具体实现参考上述 1.2.

1.3.2 实现 Runnable 接口

/*** 实现 Runnable接口,重写run*/
class MyRunnable implements Runnable{@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class demo2 {public static void main(String[] args) {Runnable runnable = new MyRunnable();Thread thread = new Thread(runnable);thread.start();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

在这里插入图片描述

1.3.3 匿名内部类 创建Thread 子类对象

在这里插入图片描述

/*** 使用匿名内部类创建 Thread 子类对象*/
public static void main(String[] args) {Thread thread = new Thread(){@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};thread.start();while(true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
}

在这里插入图片描述

1.3.4 匿名内部类 创建实现 Runnable 接口的Thread子类对象

/*** 使用匿名内部类创建 Runnable 的子类对象*/
public static void main(String[] args) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});thread.start();while (true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}

在这里插入图片描述

1.3.5 lambda 表达式创建实现 Runnable 接口的Thread 的⼦类对象

/*** 使用 lambda 表达式创建 Runnable子类对象*/
public static void main(String[] args) {Thread thread = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});thread.start();while (true){System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}
}

在这里插入图片描述

二、Thread 类及常用方法

Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。Thread 类的对象就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。

2.1 Thread 常见的构造方法

方法说明
Thread()创建线程对象
Thread(Runnable target)使用 Runnable 对象创建线程对象
Thread(String name)创建线程对象,并命名
Thread(Runnable target,String name)使用Runnable 对象创建线程对象,并命名
Thread(ThreadGroup group,Runnable target(了解))线程可以被用来分组管理,分好的组为线程组
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是新线程的名字");
Thread t4 = new Thread(new MyRunnable(), "这是新线程的名字");
/***  给线程起名字 这是新线程*/
public static void main(String[] args) {Thread thread = new Thread(() -> {while (true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}},"这是新的线程");// 创建线程thread.start();}

在这里插入图片描述

2.2 Thread 的几个常见属性

在这里插入图片描述

  • ID 是线程的身份标识,不同的线程不会重复。(id 是 Java 给这个线程分配的,不是系统API提供的线程 id,也不是PCB中的 id)
  • 名称是线程的名字,明确知道是哪一个线程
  • 状态,描述线程当前所处状态,是就绪状态,还是运行状态,又或者是阻塞状态等
  • 优先级,影响系统在微观上进行的调度 ,图中的方法提供API可以设置/获取优先级,但在应用程序的角度,很难察觉出优先级带来的差异
  • 后台线程(守护线程),不结束,并不影响整个进程的结束;前台线程,一个Java进程中,如果前台线程没有结束,整个进程一定不会结束。默认情况下一个线程是前台线程。
    在这里插入图片描述
  • 是否存活,Thread 对象的生命周期比系统内核中的线程更长一些,就会导致Thread 对象还存在,内核中的线程已经销毁了的情况,使用 isAliva 判定内核线程是否已经销毁
    在这里插入图片描述
  • 线程中断,参考下文

2.3 启动线程 - start() (面试题)

之前我们已经看到了 通过重写 run ⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运⾏。
重写 run ⽅法是描述线程要做的事情,调⽤ start() ⽅法,线程才真的在操作系统的底层创建出⼀个线程
start 和 run 的区别

  1. strat 方法内部,会调用系统API,在系统内核中创建线程
  2. run 方法,只是单纯的描述该线程要执行的内容(会在start 创建好线程后自动被调用)

start 和 run 方法的本质区别就是 start 会在系统内部创建出新线程,而 run 不会

2.4 中断一个线程

中断一个线程,其实就是终止或打断线程,意思就是让一个线程停止运行(销毁)。在Java中,要销毁或者说终止线程,做法比较唯一,就是让 run 方法尽快执行结束
常见方式:

  1. 通过共享标记进行沟通
public class demo8 {//自定义变量作为标志位private static boolean isQuit = false;public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {//while 尽快结束就意味着 run 方法尽快结束while (!isQuit){//线程的实际工作内容System.out.println("线程工作中");try {//新线程休眠(暂停)时间(毫秒)Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("线程工作结束");});//创建线程thread.start();//主线程休眠(暂停)时间(毫秒)Thread.sleep(5000);//设置线程要结束了isQuit = true;System.out.println("设置标志位 isQuit 为 true");}
}

在这里插入图片描述
在这里插入图片描述
2. 调用 Thread 内部提供的 interrupt 或 isInterrupted 方法
使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位,Thread 内部包含了⼀个 boolean 类型的变量作为线程是否被中断的标记。

方法说明
public void interrupt()终止线程,将线程的终止标志设置为 true。如果线程正在阻塞(sleep、wait、join等),调用 interrupt 终止将抛异常,否则只是设置终止标志,不会终止线程执行
public static boolean interrupted()静态方法,判断当前线程是否已被终止,并清除终止状态(多次调用只有第一次返回 true),如果线程终止,返回 true,否则返回 false
public boolean isInterrupted()判断线程是否已被终止,但不清除终止状态,如果线程终止,返回 true,否则返回 false

在这里插入图片描述

public class demo9 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() ->{//判断线程是否是终止状态while(!Thread.currentThread().isInterrupted()){System.out.println("线程工作中");try {//新线程休眠时间Thread.sleep(1000);} catch (InterruptedException e) {//抛出异常,循环继续进行(假装没听到)e.printStackTrace();// 1.可以在结束前,做一些其他工作,完成后再结束// 将其他工作的代码放在这里System.out.println("做一些其他工作");// 2. 使用 break 手动结束循环(即结束线程)break;}}});//创建线程thread.start();//主线程休眠时间Thread.sleep(5000);System.out.println("线程 thread 该终止了");thread.interrupt();}
}

在这里插入图片描述
注意:

  1. 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通知,清除终止标志
    • 当出现 InterruptedException 的时候,要不要结束线程取决于 catch 中代码的写法,可以选择忽略这个异常,也可以跳出循环结束线程
  2. 否则,只是内部的⼀个中断标志被设置,thread 可以通过
    • Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志这种⽅式通知收到的更及时,即使线程正在 sleep 也可以⻢上收到

2.5 等待一个线程- join()

有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作,也即是说,让一个线程等待另一个线程执行结束再继续执行,本质上就是在控制线程结束的顺序

方法说明
public void join()等待线程结束
public void join(long millis)等待线程结束, 最多等待 millis 毫秒
public void join(long millis, int nanos)同理,但可以更高精度
public class demo10 {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程在工作");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});//创建线程thread.start();System.out.println("等待开始");thread.join();System.out.println("等待结束");}
}

在这里插入图片描述

2.6 获取当前线程引用

方法说明
public static Thread currentThread()返回当前线程对象引用
public class demo11 {public static void main(String[] args) {Thread thread = Thread.currentThread();System.out.println(thread.getName());}
}

在这里插入图片描述

2.7 休眠当前线程

线程的调度是不可控的,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。

方法说明
public static void sleep(long millis) throws InterruptedException休眠当前线程 millis 毫秒
public static void sleep(long millis,int nanos) throws InterruptedException更高精度休眠当前线程 millis 毫秒
public static void main2(String[] args) throws InterruptedException {long start = System.currentTimeMillis();Thread.sleep(3000);long end = System.currentTimeMillis();// 每次进程休眠时间不确定,但是一个大于等于3000的数,例如 3014System.out.println("start - end = "+(end-start));
}

三、线程的状态

3.1 观察线程的所有状态

线程的状态是一个枚举类型Thread.State

public static void main(String[] args) {for (Thread.State state : Thread.State.values()) {System.out.println(state);}
}

在这里插入图片描述

  • NEW:安排了工作(确定了线程工作的内容/已经重写了run 方法),还没有开始执行
  • RUNNABLE:线程是可以执行的,换句话说,线程是正在执行或已经准备就绪,时刻可以开始执行
  • TERMINATED(终结的意思):Thread 对象还在,但内核中的线程已经销毁了或者说线程已经执行完了
  • TIMED_WAITING:阻塞,由于 sleep 固定时间(设置休眠时间)的方式产生的阻塞
  • WAITING:阻塞,由于 weit 不固定时间(不确定到底要等待多长时间)的方式产生的阻塞
  • BLOCKED:阻塞,由于锁竞争而产生的阻塞
public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(()->{});// 在调用 start 创建线程前 获取状态--此时就是 NEW 状态System.out.println(thread.getState());thread.start();// 主线程等待 thread 线程结束后再执行thread.join();// 获取 thread 线程 结束后的状态--TERMINATEDSystem.out.println(thread.getState());
}

在这里插入图片描述

public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(()->{while (true){}});// 在调用 start 创建线程前 获取状态--此时就是 NEW 状态System.out.println(thread.getState());thread.start();for (int i = 0; i < 5; i++) {// 获取 创建线程后的状态--RUNNABLESystem.out.println(thread.getState());Thread.sleep(1000);}// 主线程等待 thread 线程结束后再执行thread.join();// 获取 thread 线程 结束后的状态--TERMINATEDSystem.out.println(thread.getState());
}

在这里插入图片描述

public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(()->{while (true){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});// 在调用 start 创建线程前 获取状态--此时就是 NEW 状态System.out.println(thread.getState());thread.start();for (int i = 0; i < 5; i++) {// 获取 创建线程后的状态--RUNNABLESystem.out.println(thread.getState());Thread.sleep(1000);}// 主线程等待 thread 线程结束后再执行thread.join();// 获取 thread 线程 结束后的状态--TERMINATEDSystem.out.println(thread.getState());
}

在这里插入图片描述

四、多线程带来的风险-线程安全(重点)

4.1 观察线程不安全

private  static int count = 0;
// 使用两个线程实现 count 在每个线程自增 5w ---多个线程修改同一个变量
public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(()->{// count 自增 5w 次for (int i = 0; i < 50000; i++) {count++;}});Thread thread2 = new Thread(()->{// count 自增 5w 次for (int i = 0; i < 50000; i++) {count++;}});//两个线程同时执行thread1.start();thread2.start();//等待两个线程都结束,再打印 count 的值thread1.join();thread2.join();//预期的 count 是10wSystem.out.println("count: "+count);
}

在这里插入图片描述

4.2 什么是线程安全

在多线程环境下代码的运行结果和在单线程环境下运行的结果相同,就说这个(多)线程(程序)是安全的

4.3 线程不安全的原因

  1. 操作系统中,线程的调度是随机的(是在系统内核中实现的),我们无法改变,但是我们必须要保证,在任何执行顺序下,代码都能正常工作

  2. 两个线程对同一个变量 进行修改。一个线程修改一个变量、两个线程修改不同变量 或者 两个线程对同一个变量读取,都不会有(安全)问题。

  3. 修改操作不是原子的
    在这里插入图片描述

  4. 内存可见性问题

  5. 指令重排序问题

4.4 解决上述的线程不安全问题

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

public class demo14 {private  static int count = 0;public static void main(String[] args) throws InterruptedException {Object lock = new Object();Thread thread1 = new Thread(()->{// count 自增 5w 次for (int i = 0; i < 50000; i++) {//加锁synchronized (lock){count++;}}});Thread thread2 = new Thread(()->{// count 自增 5w 次for (int i = 0; i < 50000; i++) {//加锁synchronized (lock){count++;}}});thread1.start();thread2.start();//等待两个线程都结束,再打印 count 的值thread1.join();thread2.join();//预期的 count 是10wSystem.out.println("count: "+count);}
}

在这里插入图片描述
在这里插入图片描述

五 synchronized 关键字(监视器锁 monitor lock)

5.1 synchronized 的特性

  1. 互斥
    synchronized 会起到互斥效果,某个线程执行到某个对象的 synchronized 中时,其他线程 执行了同一个对象的synchronized 就会阻塞等待
    • 进入 synchronized 修饰的代码块,就是加锁
    • 出 synchronized 修饰的代码块,就是 解锁
      在这里插入图片描述
      synchronised 底层是用操作系统的 mutex lock 来实现
  2. 可重入
    一个线程,连续对 一把锁 / 同一个锁对象 加锁两次,不会出现死锁的情况,就是可重入锁
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

5.2 synchronized 的使用

5.2.1 修饰代码块 :明确指明锁的哪个对象

  1. 锁任意对象
public static void main(String[] args) {Object lock = new Object();Thread thread1 = new Thread(()->{synchronized (lock){}});
}
  1. 锁当前对象
class SynchronizedDemo{public void  method(){synchronized (this){}}
}

在这里插入图片描述

5.2.2 修饰方法

  1. 修饰普通方法(实例方法)
class SynchronizedDemo{int count;public void  method(){synchronized (this){count++;}}synchronized public void method2(){count++;}
}
  1. 修饰静态方法(相当于对类对象加锁)
class SynchronizedDemo{int count;public void  method(){synchronized (this){count++;}}synchronized public void method2(){count++;}synchronized public static void method3(){}
}

使用实例

public static void main(String[] args) throws InterruptedException {SynchronizedDemo synchronizedDemo = new SynchronizedDemo();Thread thread1 = new Thread(()->{for (int i = 0; i < 50000; i++) {//synchronizedDemo.method();synchronizedDemo.method2();}});Thread thread2 = new Thread(()->{for (int i = 0; i < 50000; i++) {//synchronizedDemo.method();synchronizedDemo.method2();}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println(synchronizedDemo.count);//100000
}

5.3 Java 标准库中的线程安全类

Java 标准库中有很多都是线程不安全的,这些类可能会涉及多线程修改共享数据,又没有加锁措施

  • ArrayList
  • LinkedList
  • HashMap
  • TreeMap
  • HashSet
  • TreeSet
  • String Builder

也有一些线程安全的类,使用一些锁机制来控制

  • Vector(不推荐使用)
  • HashTable
  • ConcurrentHashMap
  • StringBuffer

还有的虽然没有加锁,但不涉及修改,也是线程安全的

  • String

六、volatile 关键字

6.1 volatile 保证内存可见性

写代码实现用户输入线程结束条件(isQuit > 0),线程可以立刻执行结束

public static int isQuit = 0;
public static void main(String[] args) {Thread thread = new Thread(()->{while (isQuit == 0){//循环体里什么都没干,一秒会执行很多次}System.out.println("线程 thread 结束");});thread.start();Thread thread1 = new Thread(()->{System.out.println("输入 isQuit:");Scanner scanner = new Scanner(System.in);//一旦用户输入值不是0,这时,线程thread 执行结束isQuit = scanner.nextInt();});thread1.start();
}

运行后发现
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
修改后的代码:

public class demo17 {public static volatile int isQuit = 0;public static void main(String[] args) {Thread thread = new Thread(()->{while (isQuit == 0){//循环体里什么都没干,一秒会执行很多次}System.out.println("线程 thread 结束");});thread.start();Thread thread1 = new Thread(()->{System.out.println("输入 isQuit:");Scanner scanner = new Scanner(System.in);//一旦用户输入值不是0,这时,线程thread 执行结束isQuit = scanner.nextInt();});thread1.start();}
}

6.2 volatile 不保证原子性

volatile 和 synchronized 有着本质的区别。synchronized 保证原⼦性, volatile 保证内存可⻅性
示例:
多线程实现计数器 count

class Count{private static int count = 0;//自增成为原子性操作synchronized void increase(){count++;}public int getCount(){return count;}
}
public class demo18 {public static void main(String[] args) throws InterruptedException {Count count = new Count();Object lock = new Object();Thread thread1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count.increase();}});Thread thread2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count.increase();}});thread1.start();thread2.start();//两个线程都结束在继续执行主线程thread1.join();thread2.join();//预期结果 10wSystem.out.println(count.getCount());//10w}
}

现在去掉修饰 increase 方法的 synchronized(加锁),给 count 加 volatile 关键字进行修饰

class Count{private static volatile int count = 0;void increase(){count++;}public int getCount(){return count;}
}
public class demo18 {public static void main(String[] args) throws InterruptedException {Count count = new Count();Object lock = new Object();Thread thread1 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count.increase();}});Thread thread2 = new Thread(() ->{for (int i = 0; i < 50000; i++) {count.increase();}});thread1.start();thread2.start();//两个线程都结束在继续执行主线程thread1.join();thread2.join();//预期结果 10wSystem.out.println(count.getCount());//结果却是一个不大于10w 的数}
}

上述代码运行结果证明 volatile 关键字并不能保证原子性

七、wait 和 notify

在这里插入图片描述
线程之间是抢占式执行,所以线程之间的执行先后顺序我们并不知道,但实际开发中,有时候希望合理的协调多个线程之间的执行先后顺序,就像 打一场篮球比赛
在这里插入图片描述
球场上的每个运动员都是一个独立的线程,而要完成进攻得分,需要多个运动员相互配合,按照一定的顺序执行一定的动作,即可认为有的线程要传球,有的线程要进球这样的动作
而要完成协调工作,就会涉及三个方法:

  1. wait() / wait(long timeout):使当前线程进入等待状态
  2. notify / notifyAll():唤醒当前对象上等待的线程

需要注意的是:wait、notify、notifyAll 都是 Object 类的方法

7.1 wait()方法

在这里插入图片描述
wait方法执行时做的事:

  1. 释放当前的锁
  2. 线程进入阻塞状态
  3. 当线程被唤醒时,重新获取这个锁

使用 wait 要搭配 synchronized , 确保在 wait 前获取到锁,脱离 synchronized 使用 wait 会抛出异常。
在这里插入图片描述

public static void main(String[] args) throws InterruptedException {Object object = new Object();synchronized (object){System.out.println("wait 等待前");// wait 放在 synchronized 来保证获取到锁object.wait();System.out.println("wait 等待后");}}

调用 wait 不一定就只有一个线程调用,N个线程都可以调用 wait ,这N线程都调用后,都处于阻塞状态。
wait 结束等待的条件:

  1. 其他线程调用该对象的 notify方法(唤醒时,会有一个重新获取锁的过程)
  2. wait等待时间超时(在调用wait 方法时,就指定等待时间)
  3. 其他线程调用该等待线程的interrupted 方法,使wait抛出 InterruptedException 异常

7.2 notify()方法

notify()方法是唤醒等待的线程

  • notify()方法也要和 synchronized 搭配使用,因为在唤醒等待的线程的时候,要重新获取这个锁,否则也会抛出异常
  • 如果有多个线程等待,线程程调度器就会随机挑选一个等待的线程
  • notify()方法后,当前线程不会马上释放这个锁,要等到执行notify()方法的线程执行完后才会释放这个锁

代码实现创建两个线程,都会等待第三个线程创建后(手动)确定唤醒哪一个

public class demo21 {public static void main(String[] args) {Object object2 = new Object();Thread thread1 = new Thread(() ->{synchronized (object2){System.out.println("线程 thread1 等待前");try {object2.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程 thread1 等待后");}});Thread thread2 = new Thread(() ->{synchronized (object2){System.out.println("线程 thread2 等待前");try {object2.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程 thread2 等待后");}});Thread thread3 = new Thread(() ->{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (object2){System.out.println("线程唤醒");object2.notify();System.out.println("线程已唤醒");}System.out.println("调用notify 的线程执行完");});thread1.start();thread2.start();thread3.start();}
}

在这里插入图片描述
在这里插入图片描述

7.3 notifyAll()方法

notify⽅法只是唤醒某⼀个等待线程,使⽤notifyAll⽅法可以⼀次唤醒所有等待的线程。

public class demo22 {public static void main(String[] args) {Object object1 = new Object();Thread thread1 = new Thread(() ->{synchronized (object1){System.out.println("线程 thread1 等待前");try {object1.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程 thread1 等待后");}});Thread thread2 = new Thread(() ->{synchronized (object1){System.out.println("线程 thread2 等待前");try {object1.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程 thread2 等待后");}});Thread thread3 = new Thread(() ->{try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (object1){System.out.println("线程唤醒");object1.notifyAll();System.out.println("线程唤醒后");}System.out.println("调用 notifyAll 的线程执行完");});thread1.start();thread2.start();thread3.start();}
}

在这里插入图片描述
注意:虽然是同时唤醒使用同一个锁的 所有的线程,但唤醒的所有线程需要竞争锁,并不是同时执行,仍有先后执行。

7.4 wait 和 sleep 的对比(重要)

  1. 起源上,wait 是 Object 的普通方法,sleep 是 Thread 的静态方法
  2. 应用场景上,wait 用于实现线程间的协调,需要搭配监视器(synchronized)使用,而 sleep 用于让线程休眠一段时间,不需要搭配监视器
  3. 锁的释放上,在调用 wait 时,会释放对象锁,其他线程可以获取该锁,而调用 sleep 时不会释放对象锁,其他线程不能获取该锁
  4. 唤醒方式上,wait 通过其他线程调用相同锁对象的 notify 或 notifyAll 来唤醒,而 sleep 在指定休眠时间过后自动唤醒,或者通过其他线程中断它来提前唤醒

八、多线程案例

8.1 单例模式

单例模式是校招最常考的设计模式之一(另一个是工厂模式)。

设计模式:就好比象棋中的棋谱,红⽅当头炮,⿊⽅⻢来跳,针对红⽅的⼀些⾛法,⿊⽅应招的时候有⼀些固定的套路。按照套路来⾛,局势就不会吃亏。
开发过程中,针对特定的问题场景,大佬总结出固定的套路,按固定套路来实现代码,不会吃亏

单例模式:一些场景中要求某个类只有一个实例(对象),不会再创建出多个实例。
单例模式实现方式有很多,最常见有 “饿汉” 和 “懒汉” 两种

8.1.1 饿汉模式

类加载时就创建实例。

class Singleton{//类加载时就创建实例private static Singleton instance = new Singleton();//保证没有其他的构造方法再创建实例private Singleton(){ };//只获取实例public static Singleton getInstance(){return instance;}
}

8.1.2 懒汉模式

类加载时不创建实例,第⼀次使⽤时才创建实例。

  1. 单线程版
class SingletonLazy{private static SingletonLazy instance = null;private SingletonLazy(){ };//在第一次使用时创建实例public static SingletonLazy getInstance(){if(instance == null){instance = new SingletonLazy();}return instance;}
}
  1. 多线程版
    多线程版的就不安全了,线程安全问题发生在首次创建实例的时候,如果多个线程中同时调用 getInstance 方法,就可能创建出多个实例。(多线程,可能既会获取又会修改 Instance)
    使用 synronized 对 创建实例的方法加锁。
class SingletonLazy{private static SingletonLazy instance = null;public synchronized static SingletonLazy getInstance(){if(instance == null){instance = new SingletonLazy();}return instance;}
}

也可以写成下面的代码,是同样效果

class SingletonLazy{private static SingletonLazy instance = null;public static SingletonLazy getInstance(){synchronized (SingletonLazy.class){if(instance == null){instance = new SingletonLazy();}}return instance;}
}
  1. 多线程版改进
    一旦以上述代码形式执行多线程,每一次调用 getInstance 都会先加锁(加锁开销很大,一旦加锁,就很可能会引发锁冲突进而会引起阻塞),锁竞争的频率就会很高,但是实际上,发生线程安全问题,只是在最开始(对象还没有new )的时候,对象被 new 过后就不需要再修改,只有读操作
    那么是否有办法让代码既线程安全又不会对执行效率有太多影响呢?
    在加锁的外层进行判断是否需要加锁,如果已经有对象了,线程就安全了,不需要加锁,如果没有对象,就会有线程安全问题,需要加锁
class SingletonLazy{private static volatile SingletonLazy instance = null;public static SingletonLazy getInstance(){//判断是否需要加锁if(instance == null){synchronized(SingletonLazy.class){//判断是否需要new 对象if(instance == null){instance = new SingletonLazy();}}}return instance;}
}

在这里插入图片描述
指令重排序 (编译器进行的优化—>在不改变逻辑的前提下调整代码执行顺序来提高执行效率)可能会对上述代码产生影响。
在这里插入图片描述
对于指令重排序问题,解决办法是 使用 volatile 关键字修饰 instance ,保证编译器不进行优化,也就不会出现指令重排序的问题。

class SingletonLazy{private static volatile SingletonLazy instance = null;public static SingletonLazy getInstance(){//判断是否需要加锁if(instance == null){synchronized(SingletonLazy.class){//判断是否需要new 对象if(instance == null){instance = new SingletonLazy();}}}return instance;}
}

8.2 阻塞队列

8.2.1 阻塞队列的定义

阻塞队列是一种特殊的队列,也遵守先进先出的原则
阻塞队列是一种线程安全的数据结构,有下面两个特性:

  • 当队列元素满的时候,继续入队列就会阻塞,一直到其他线程从队列中取走元素
  • 当队列为空的时候,继续出列也会阻塞,一直到其他线程向队列中插入元素

8.2.2 消费者模型

阻塞队列的经典应用场景就是 “生产者消费者模型”(一种典型的开发模式)。
在这里插入图片描述
在这里插入图片描述

8.2.3 标准库中的阻塞队列

在 Java 标准库中内置了阻塞队列,如果需要使⽤阻塞队列,直接使⽤标准库中的即可。

  • BlockingQueue 是⼀个接口,继承自 Queue,实现的方法有两种:基于数组和基于链表,实现的类是 ArrayBlockingQueue 和 LinkedBlockingQueue
  • put ⽅法⽤于阻塞式的⼊队列,take 方法⽤于阻塞式的出队列
  • BlockingQueue 也有 offer,poll,peek 等⽅法,但这些⽅法不具有阻塞特性,不建议使用
//基于数组的实现  需要指定容量否则会报错
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1000);
//基于链表的实现
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// ⼊队列,如果队列满就会阻塞,直到不再满
queue.put("abc");
// 出队列, 如果队列为空就会阻塞,直到不再为空.
String elem = queue.take();

8.2.4 阻塞队列的模拟实现

  • 通过 “循环队列” 的⽅式来实现
  • 使⽤ synchronized 进⾏加锁控制
  • 使用 volatile 防止内存可见性问题(代码中涉及共享数据的修改时,编译器可能会优化)
  • put 插⼊元素的时候,判定如果队列满了,就进⾏ wait。
    注意:要在循环中进⾏ wait,被唤醒(有可能是因为使用 intrruput 终止线程时唤醒 wait,抛出异常,线程正常结束,但如果是捕获了异常,代码会向后走,但是不知道此时队列是否已满,还要进行判断)时可能队列也是满了使用 wait 往往使用 while 作为条件判断方式,目的在于 让 wait 被唤醒后还能再确认一次是否仍满足条件
  • take 取出元素的时候,判定如果队列为空,就进⾏ wait (也是循环 wait) 。
class MyBlockQueue{//队列存储的数据,最大长度可以直接指定,也可以使用构造方法自定义指定private String[] elem = new String[1000];//队列的首位置private volatile int head;//队列的结束位置的下一位private volatile int rear;//记录队列元素个数private volatile int size;// 锁对象private Object locker = new Object();//入队public void push(String s) throws InterruptedException {// 由于方法中有很多数据可能会修改(可能会引起内存可见性问题),// 而又要尽量减少锁的使用(加锁,开销会很大),所以对整体加一个锁synchronized (locker){while (size == elem.length){//队列已满//进入阻塞状态locker.wait();//再次唤醒 wait 的时候还要判断队列是否满}elem[rear] = s;rear++;if(rear == elem.length){rear = 0;}size++;//唤醒的是 take方法中的 wait(由于空队引起的阻塞)locker.notify();}}//出队public String take() throws InterruptedException {// 由于方法中有很多数据可能会修改(可能会引起内存可见性问题),// 而又要尽量减少锁的使用(加锁,开销会很大),所以对整体加一个锁synchronized (locker){while (size == 0){//空队列//进入阻塞等待locker.wait();//再次唤醒 wait 的时候还要判断队列是否满}String ret = elem[head];head++;if(head == elem.length){head = 0;}size--;//唤醒的是 push 方法 中的 wait(由于队满而引起的阻塞)locker.notify();return ret;}}
}

生产者消费者模型

public static void main(String[] args) {BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1000);Thread threadProduct = new Thread(() ->{int num = 1;while (true){try {blockingQueue.put(num+"");System.out.println("生产元素:"+num);num++;//生产元素慢 0.5 秒Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}},"生产者");Thread threadCustomer = new Thread(() ->{while (true){try {String date = blockingQueue.take();System.out.println("消费元素:"+date);} catch (InterruptedException e) {e.printStackTrace();}}},"消费者");threadCustomer.start();threadProduct.start();
}

8.3 定时器

8.3.1 什么是定时器

定时器是软件开发的一个重要组件,类似于闹钟,作用是设定一个时间,当达到这个时间后,就执行一个指定好的代码
在这里插入图片描述
定时器作为实际开发中常用的组件,比如在网络通信中,如果对方在500毫秒内没有返回数据,就会断开连接尝试重新连接。
在这里插入图片描述

8.3.2 标准库中的定时器

Java标准库中提供一个 Timer 类就是定时器的实现,Timer 类的核心方法是 schedule ,翻译成中文 有安排的意思。

schedule 包含两个参数,第一个参数是将要执行的任务代码,第二个参数是指定等待多长时间才执行(单位:毫秒)。

public static void main(String[] args) {Timer timer = new Timer();//给定时器安排一个任务---》预定在一个3秒后执行(起始时间是从schedule开始计算)timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("3000");}},3000);//时间是毫秒级别timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("1000");}},1000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("2000");}},2000);System.out.println("程序开始");
}

在这里插入图片描述
在这里插入图片描述

8.3.3 模拟实现定时器

定时器构成

  • 有一个类,用来描述任务(任务内容和执行时间)
  • 有一个优先级队列,存放所有的任务(队首元素就是最先要执行的任务)
  • 有一个扫描线程,判断任务是否到了要执行的时间
//定义一个类用来描述任务,包含任务内容和执行时间
//任务要放入优先级队列,必须是可比较的,要实现比较的接口重写方法
class MyTimeTask implements Comparable<MyTimeTask>{private Runnable runnable;private long time;@Overridepublic int compareTo(MyTimeTask o) {//创建的优先级队列中,时间最小的放队首--先执行return (int) (this.time - o.time);}public MyTimeTask(Runnable runnable, long time) {this.runnable = runnable;//保存绝对时间(记录到什么时间才开始执行任务)this.time = System.currentTimeMillis() + time;}public long getTime() {return time;}public Runnable getRunnable() {return runnable;}
}
class MyTimer{//存储要执行的任务private PriorityQueue<MyTimeTask> priorityQueue = new PriorityQueue<>();//锁对象private Object locker = new Object();//安排任务public void schedule( Runnable runnable,long time){synchronized (locker){priorityQueue.offer(new MyTimeTask(runnable, time));//唤醒等待的线程locker.notify();}}//创一个扫描线程public MyTimer(){Thread thread = new Thread(() ->{//一直扫描队首的任务,查看是否达到执行的时间while (true){try {synchronized (locker){while (priorityQueue.isEmpty()){//空的任务队列==》等待,直到队列不为空才被唤醒locker.wait();}MyTimeTask myTimeTask = priorityQueue.peek();//获取当前时间long curTime = System.currentTimeMillis();if (curTime >= myTimeTask.getTime()){//任务时间已经达到-->执行任务myTimeTask.getRunnable().run();//从任务队列中删除priorityQueue.poll();}else {// 没有达到任务时间,不执行任务,等到任务要开始执行// wait 方法使线程阻塞,线程不会在cpu上调度,不占cpu资源// 避免忙等(什么都不干,也没有休息,一直占用cpu资源)locker.wait(myTimeTask.getTime() - curTime);}}}catch (InterruptedException e) {e.printStackTrace();}}});thread.start();}
}
public class demo26 {public static void main(String[] args) {MyTimer myTimer = new MyTimer();myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("3000");}},3000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("1000");}},1000);myTimer.schedule(new Runnable() {@Overridepublic void run() {System.out.println("2000");}},2000);System.out.println("程序开始");System.out.println("计数器开始启动");}
}

在这里插入图片描述

8.4 线程池

8.4.1 什么是线程

线程诞生是因为进程的创建和销毁,太重量(开销比较大,效率就比较慢),而当线程的创建和销毁也频繁的时候,那么线程的开销也不能忽视,为了提高效率,Java 中有了线程池这个概念,用来减少创建和销毁线程的开销当在创建第一个线程的时候,就把要使用的其他线程也提前创建好,放在池子里,后续使用的时候,直接从池子里取出来
在这里插入图片描述

8.4.2 标准库中的线程(重要)

  • 使用 Executors.newFixedThreadPool(10); 创建出固定线程数量(这里是10个)的线程池
  • 返回值是 ExecutorService 类型
  • 通过 ExecutorService.submit 方法,将一个任务提交到线程池中
ExecutorService service = Executors.newFixedThreadPool(10);
service.submit(new Runnable() {@Overridepublic void run() {System.out.println("工程模式创建线程池");}
});

在这里插入图片描述
在这里插入图片描述
Executors 创建线程池的几种方式:

  1. newFixedThreadPool:创建固定数量的线程池
  2. newCachedThreadPool:创建线程数目动态增长(线程根据需要,自动被动的被创建出来)的线程池
  3. newSingleThreadExecutor:创建单个线程的线程池
  4. newScheduledThreadPool:设置多长时间后执行命令相当于定时器的进阶版,不是一个线程负责执行任务,而是有多个线程执行到时间的任务

Executors 本质上是 ThreadPoolExecutor 类的封装,ThreadPoolExecutor 类核心方法只有两个:构造和添加任务(submit)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

8.4.3 模拟实现线程池

  • 方法 submit ,将任务加入线程池中
  • 使用 一个阻塞队列(BlockingQueue)组织所有的任务
  • 指定线程池中线程的最大数目,当线程超过这个最大数目,不再创建线程
class MyThreadPool{//  阻塞队列---》 组织/存放 任务private BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(10);// 通过这个方法,将任务加入到队列中public void submit(Runnable runnable) throws InterruptedException {// 任务满了 ,就会阻塞等待blockingQueue.put(runnable);}//创建线程池时,创建好线程并执行任务public MyThreadPool(int n){// 创建 n 个线程for (int i = 0; i < n; i++) {// 描述 线程执行的任务Thread thread = new Thread(() ->{try {//   获取并执行 任务Runnable runnable = blockingQueue.take();runnable.run();} catch (InterruptedException e) {e.printStackTrace();}});thread.start();}}
}
public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool(3);for (int i = 0; i < 10; i++) {int count = i;myThreadPool.submit(new Runnable() {@Overridepublic void run() {// System.out.println("任务:"+i);System.out.println("人任务:"+count);}});}
}

在这里插入图片描述
在这里插入图片描述

九、对比线程和进程

9.1 线程的优点

  1. 线程比进程更轻量,创建一个线程的开销比创建一个进程的开销小
  2. 操作系统调度线程比调度进程的效率更高
  3. 线程占用的资源比进程更少
  4. 充分利用多处理器(cpu)可并行的数量

9.2 线程和进程的区别

  1. 进程包含线程(线程不能独立存在,要依附于进程),每个进程⾄少有⼀个线程存在,即主线程
  2. 进程和线程 都是用来实现并发编程场景的,但线程比进程更轻量,更高效
  3. 进程和进程之间不共享资源,同⼀个进程的线程之间共享资源(内存和硬盘)
  4. 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位
  5. 进程之间是独立的,⼀个进程挂了⼀般不会影响到其他进程,但⼀个线程挂了,很大可能影响同进程内的其他线程(整个进程崩溃)

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

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

相关文章

iframe渲染后端接口文件和实现下载功能

一&#xff1a;什么是iframe&#xff1f; 1、介绍 iframe 是HTML 中的一种标签&#xff0c;全称为 Inline Frame&#xff0c;即内联框架。它可以在网页中嵌入其他页面或文档&#xff0c;将其他页面的内容以框架的形式展示在当前页面中。iframe的使用方式是通过在HTML文档中插入…

Linux_安装docker

安装包管理工具yum-utils&#xff0c;并设置docker储存库&#xff08;如果已有&#xff0c;不用安装&#xff09; # 安装包管理工具 sudo yum install -y yum-utils # 安装docker储存库 sudo yum-config-manager \--add-repo \http://mirrors.aliyun.com/docker-ce/linux/cen…

react之基于@reduxjs/toolkit使用react-redux

react之基于reduxjs/toolkit使用react-redux 一、配置基础环境二、使用React Toolkit 创建 counterStore三、为React注入store四、React组件使用store中的数据五、实现效果六、提交action传递参数七、异步状态操作 一、配置基础环境 1.使用cra快速创建一个react项目 npx crea…

python图

有向图&#xff1a;图中的每条边都有方向的图叫有向图。此时&#xff0c;边的两个顶点有次序关系&#xff0c;有向边 < u,v>成为从顶点u到顶点v的一条弧&#xff0c;u成为弧尾&#xff08;始点&#xff09;&#xff0c;v成为弧头&#xff08;终点&#xff09;&#xff0c…

我叫:选择排序【JAVA】

1.我是个啥子&#xff1f;&#xff1f; 选择式排序&#xff1a;属于内部排序法,从欲排序的数据中,按指定的规则选出某一元素&#xff0c;再依规定交换位置后达到排序的目的。 2.我的思想 基本思想:第一次从arr[0]~arr[n-1]中选取最小值&#xff0c;与arr[0]交换&#xff0c;第…

【C++】类和对象(7)--友元, static成员

目录 一 友元 1 友元概念 2 友元函数 3 友元类 二 static成员 1 概念 2 用法 3 static成员特性 4 例题 一 友元 1 友元概念 友元提供了一种突破封装的方式&#xff0c;有时提供了便利。但是友元会增加耦合度&#xff0c;破坏了封装&#xff0c;所以 友元不宜多用。 …

2023年高压电工证考试题库及高压电工试题解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2023年高压电工证考试题库及高压电工试题解析是安全生产模拟考试一点通结合&#xff08;安监局&#xff09;特种作业人员操作证考试大纲和&#xff08;质检局&#xff09;特种设备作业人员上岗证考试大纲随机出的高压…

【碰碰球】弹珠游戏-微信小程序项目开发流程详解

还记得小时候玩过的弹珠撞击游戏不&#xff0c;这里把它的实现原理通俗易懂地讲一下&#xff0c;看看怎样实现一个碰碰球(弹珠)小游戏&#xff0c;除了个人玩法&#xff0c;也可以双人玩哦&#xff0c;与打乒乓球一样的&#xff0c;可练习临场反应。 创建项目 打开微信开发者…

前端面试:如何实现并发请求数量控制?

题目&#xff1a;实现一个并发请求函数concurrencyRequest(urls, maxNum) 要求如下&#xff1a; 要求最大并发数 maxNum;每当有一个请求返回&#xff0c;就留下一个空位&#xff0c;可以增加新的请求;所有请求完成后&#xff0c;结果按照 urls 里面的顺序依次打出&#xff1b;…

DE算法简介

文章目录 前言一、DE是什么&#xff1f;二、DE流程2.1 初始化种群2.2 变异&#xff08;差分操作&#xff09;2.3 交叉2.4 选择2.5 重复迭代 三、DE运行结果 前言 这两天看了DE算法&#xff0c;简单说下自己的认识 一、DE是什么&#xff1f; 百科定义&#xff1a;差分进化算…

Vue+ElementUI技巧分享:自定义表单项label的文字提示

文章目录 概要在表单项label后添加文字提示1. 使用 Slot 自定义 Label2. 添加问号图标与提示信息 slot的作用详解1. 基本用法2. 具名插槽 显示多行文字提示的方法1. 问题背景2. 实现多行内容显示3. 样式优化 结语 概要 在Vue和ElementUI的丰富组件库中&#xff0c;定制化表单是…

Linux进程间通信之匿名管道

文章目录 为什么要有进程间通信pipe函数共享管道原理管道特点管道的四种情况 管道的应用场景&#xff08;进程池&#xff09;ProcessPool.ccTask.hpp 为什么要有进程间通信 数据传输&#xff1a;一个进程需要将它的数据发送给另一个进程 资源共享&#xff1a;多个进程之间共享…

Vue3-自定义hook函数

Vue3-自定义hook函数 功能&#xff1a;可以将组合式API封装成一个函数&#xff0c;用于解决代码复用的问题。注意&#xff1a;需要在src文件夹下创建一个文件夹hooks&#xff0c;在里面放js文件&#xff0c;命名随意&#xff0c;主要是将setup函数中的代码放入js文件中。 // s…

Windows10下Maven3.9.5安装教程

文章目录 1.下载maven2.安装3.配置系统变量3.1.新建系统变量 MAVEN_HOME3.2.编辑系统变量Path 4.CMD命令测试是否安装成功5.配置maven本地仓库6.配置国内镜像仓库 1.下载maven 官网 https://maven.apache.org/download.cgi 点击下载。 2.安装 解压到指定目录 D:\installSoft…

计算机硬件的基本组成

一、冯诺依曼结构 存储程序&#xff1a; “存储程序”的概念是指将指令以二进制代码的形式事先输入计算机的主存储器&#xff0c;然后按其在存储器中的首地址执行程序的第一条指令&#xff0c;以后就按该程序的规定顺序执行其他指令&#xff0c;直至程序执行结束。 冯诺依曼计…

io多路复用:select、poll和epoll

1、为什么使用多路复用&#xff1a; 1.1单线程BIO监听socket 多路复用一般用于网络io当中&#xff0c;提到网络io我们肯定能想到socket。如果我们想要一个线程单纯的用向下文的方式监听很多个socket看他是否有事件发生&#xff0c;那这样是不可行。 但上一个socket1没有可读事…

Codewhisperer 使用评价

最近亚⻢逊推出了一款基于机器学习的 AI 编程助手 Amazon CodeWhisperer&#xff0c;可以实时提供代码建议。在编写代码时&#xff0c;它会自动根据现有的代码和注释给出建议。Amazon CodeWhisperer 与GitHub Copilot类似&#xff0c;主要的功能有: 代码补全注释和文档补全代码…

Banana Pi BPI-W3之RK3588安装Qt+opencv+采集摄像头画面.

场景&#xff1a;在Banana Pi BPI-W3 RK3588上做qt开发工作RK3588安装Qtopencv采集摄像头画面 2. 环境介绍 硬件环境&#xff1a; Banana Pi BPI-W3RK3588开发板、MIPI-CSI摄像头( ArmSoM官方配件 )软件版本&#xff1a; OS&#xff1a;ArmSoM-W3 Debian11 QT&#xff1a;QT5…

MySQL/Oracle用逗号分割的id怎么实现in (逗号分割的id字符串)。find_in_set(`id`, ‘1,2,3‘) 函数,

1.MySQL 1.1.正确写法 select * from student where find_in_set(s_id, 1,2,3); 1.2.错误示范 select * from student where find_in_set(s_id, 1,2 ,3); -- 注意&#xff0c;中间不能有空格。1、3 select * from student where find_in_set(s_id, 1,2, 3); -- 注意…

在Windows系统中查找GitBash安装位置

使用注册表可以轻松获取&#xff1a; reg query HKEY_LOCAL_MACHINE\SOFTWARE\GitForWindows /v InstallPath | findStr InstallPath此时输出一串字符&#xff0c; 通过字符串切割即可获取&#xff1a;