多线程(初阶)

文章目录

  • 一、认识线程(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.hqwc.cn/news/194861.html

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

相关文章

2023上海初中生古诗文大会复赛12月2日举行,关键事项为您划重点

今天中午12点&#xff0c;古诗文大会官微发布消息&#xff1a;2023上海中学生古诗文大会&#xff08;初中组&#xff09;复选将于12月2日举行。 具体安排和注意事项、常见问题&#xff0c;六分成长为您整理如下。 一、2023年初中生古诗文大会复赛日期和时间 12月2日&#xff…

OFDM通信系统仿真之交织技术

文章目录 前言一、交织1、概念2、图形举例3、交织的位置 二、MATLAB仿真1、MATLAB 程序2、仿真结果 前言 之前的博客&#xff1a;OFDM深入学习及MATLAB仿真 中有对交织的概念进行讲解&#xff0c;但讲解还是比较浅显&#xff0c;且仿真实现时并没有加入交织及解交织流程&#…

庖丁解牛:NIO核心概念与机制详解 01 _ 入门篇

文章目录 Pre输入/输出Why NIO流与块的比较通道和缓冲区概述什么是缓冲区&#xff1f;缓冲区类型什么是通道&#xff1f;通道类型 NIO 中的读和写概述Demo : 从文件中读取1. 从FileInputStream中获取Channel2. 创建ByteBuffer缓冲区3. 将数据从Channle读取到Buffer中 Demo : 写…

【评估分级方法】自然断点法(Python实现全代码)

自然断点法 自然断点法有两个称呼&#xff0c;一个就是直接英文名称&#xff0c;叫做“Natural Breaks”&#xff0c;这就不解释了&#xff0c;还有一个称呼就是ArcGIS里面用的&#xff0c;叫做“Jenks”&#xff0c;主要是来源于它的创造者&#xff1a;乔治弗雷德里克詹克斯&…

python 的 import 机制

引言 对于初学 python&#xff0c;或多或少在 import 一个 module 时遇到过 ImportError: attempted relative import with no known parent package 这样的错误信息。对于初学 python&#xff0c;遇到这样的问题是因为在执行 python xxx.py 程序时&#xff0c;xxx.py 程序中 …

ubuntu安装完qt后发现找不到图标

layout: post # 使用的布局&#xff08;不需要改&#xff09; title: Qt启动问题 # 标题 subtitle: ubuntu安装完Qt #副标题 date: 2023-11-18 # 时间 author: BY ThreeStones1029 # 作者 header-img: img/about_bg.jpg #这篇文章标题背景图片 catalog: true # 是否归档 tags: …

R语言:利用biomod2进行生态位建模

在这里主要是分享一个不错的代码&#xff0c;喜欢的可以慢慢研究。我看了一遍&#xff0c;觉得里面有很多有意思的东西&#xff0c;供大家学习和参考。 利用PCA轴总结的70个环境变量&#xff0c;利用biomod2进行生态位建模&#xff1a; #------------------------------------…

【半监督学习】CNN与Transformer的结合

本文介绍了几篇结合使用CNN和Transformer进行半监督学习的论文&#xff0c;CNN&Trans&#xff08;MIDL2022&#xff09;&#xff0c;Semi-ViT&#xff08;ECCV2022&#xff09;&#xff0c;Semiformer&#xff08;ECCV2022&#xff09;. Semi-Supervised Medical Image Seg…

【Promise12数据集】Promise12数据集介绍和预处理

【Segment Anything Model】做分割的专栏链接&#xff0c;欢迎来学习。 【博主微信】cvxiayixiao 本专栏为公开数据集的介绍和预处理&#xff0c;持续更新中。 要是只想把Promise12数据集的raw形式分割为png形式&#xff0c;快速导航&#xff0c;直接看2&#xff0c;4标题即可 …

交易机器人-规则部分

微信公众号&#xff1a;大数据高性能计算 背景 背景是基于人工去做交易本身无法做到24小时无时无刻的交易&#xff0c;主要是虚拟币本身它是24小时交易&#xff0c;人无法做到24小时盯盘&#xff0c;其次就是如果你希望通过配置更加复杂的规则甚至需要爬取最新的信息走模型进行…

个人博客添加访问人数以及访问时间-githubpage

layout: post # 使用的布局&#xff08;不需要改&#xff09; title: 个人博客添加访问人数以及访问时间 # 标题 subtitle: 个人博客优化 #副标题 date: 2023-11-18 # 时间 author: BY ThreeStones1029 # 作者 header-img: img/about_bg.jpg #这篇文章标题背景图片 catalog: tr…

MySQL 的执行原理(四)

5.5. MySQL 的查询重写规则 对于一些执行起来十分耗费性能的语句&#xff0c;MySQL 还是依据一些规则&#xff0c;竭尽全力的把这个很糟糕的语句转换成某种可以比较高效执行的形式&#xff0c;这个过程也可以 被称作查询重写。 5.5.1. 条件化简 我们编写的查询语句的搜索条件…