多线程操作

一.多线程

1.线程的创建

1.继承Thread类,重写run()方法创建线程

在这里插入图片描述
2.实现Runnable接口,重写run()方法

在这里插入图片描述
3.匿名内部类创建线程
在这里插入图片描述
4.匿名内部类实现Runnable接口创建线程
在这里插入图片描述
5.[常用]lambda表达式创建线程
在这里插入图片描述

2.启动线程

Thread类使用start方法,启动一个线程,对于同一个Thread对象只能调用一次.

经典面试题:start()和run()的区别:
start 方法:在 Java 多线程中,start()方法是定义在Thread类中的一个方法。它的主要作用是启动一个新的线程。当调用start()方法时,Java 虚拟机(JVM)会为这个线程分配必要的系统资源,如内存空间等,然后自动调用该线程的run()方法来执行线程体中的代码.
run()方法:同样是定义在Thread类中的方法,它包含了线程要执行的具体代码逻辑,也就是线程体。不过,如果直接调用run()方法(像调用普通方法一样),它不会开启新的线程,而是在当前线程中同步执行run()方法中的代码。

public class MyThread extends Thread {public void run() {System.out.println("线程执行体");}public static void main(String[] args) {MyThread myThread = new MyThread();myThread.run(); }
}

在这个例子中,myThread.run()就像是普通的方法调用,代码会在main线程中执行,而不是开启一个新的线程来执行。

3.终止线程

Thread.currentThread()

这个代码是Thread中内置的,用于获取当前线程实例,哪个线程调用就是哪个线程的实例(类似于this)

Thread.currentThread().isInterrupted()

这个代码是Thread内部自带的标志位.

  t1.interrupt();

修改上述的标志位可以调用interrupt()方法,

接下来我们看一段代码:

package thread;public class ThreadDemo24 {public static void main(String[] args) {Thread t1=new Thread(()->{while (Thread.currentThread().isInterrupted()){System.out.println("t1线程");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t1.interrupt();}
}

代码运行后会抛出异常
在这里插入图片描述
为什么会抛出这个异常是因为sleep,在代码执行到sleep的时候,调用interrupt()的时候,休眠并没有结束,被提前唤醒了,sleep被提前唤醒,会做两件事:
1.抛出InterruptedException异常(紧接着会被catch捕捉到)
2.清除Thread对象的.isInterrupted()的标志位
我们通过t1.interrupt();已经将标志位设置为false,但是sleep的提前唤醒又将标志位置为true,此时线程还是会继续循环进行.

那么我们该如何解决上面的问题?
我们只需要在catch中加入break即可

package thread;public class ThreadDemo24 {public static void main(String[] args) {Thread t1=new Thread(()->{while (Thread.currentThread().isInterrupted()){System.out.println("t1线程");try {Thread.sleep(1000);} catch (InterruptedException e) {break;}}});t1.start();t1.interrupt();}
}

sleep清空标志的操作,原则上是给了我们自己很多的操作空间.

4.变量捕获

1.变量捕获的概念
在 Lambda 表达式中,变量捕获是指 Lambda 表达式能够访问并使用其所在作用域(通常是定义 Lambda 表达式的方法或代码块)中的外部变量。这些外部变量可以是局部变量、实例变量或者静态变量。例如,在一个方法内部定义了一个 Lambda 表达式,这个 Lambda 表达式可以捕获该方法中的局部变量来在自己的代码块中使用。
2.局部变量的捕获
有效捕获条件:Lambda 表达式可以捕获方法中的局部变量,但这些局部变量必须是事实上的final变量。这意味着变量一旦被初始化后,就不能再重新赋值。例如:

public class LambdaVariableCapture {public static void main(String[] args) {int num = 10;Runnable lambda = () -> {System.out.println("捕获的变量值为: " + num);};lambda.run();}
}

在这个例子中,num是一个局部变量,它在被 Lambda 表达式捕获后,不能再被修改。如果尝试修改num的值,例如在定义 Lambda 表达式之后添加num = 20;这样的语句,编译器会报错。

在这个例子中,num是一个局部变量,它在被 Lambda 表达式捕获后,不能再被修改。如果尝试修改num的值,例如在定义 Lambda 表达式之后添加num = 20;这样的语句,编译器会报错。

5.等待线程结束

多个线程执行的顺序是不确定的(随机调度,抢占式执行),虽然线程的执行是无序的,但是可以在应用程序中,通过一些api,来影响线程的执行顺序.join()就是其中的一种.

package thread;public class ThreadDemo16 {public static int count=0;public static void main(String[] args) throws InterruptedException {Object locker=new Object();Thread t1=new Thread(()->{for (int i=0;i<50000;i++){synchronized (locker){count++;}}});Thread t2=new Thread(()->{for (int i=0;i<50000;i++){synchronized (locker){count++;}}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);}}

像上面代码展示,让两个线程t1,t2对count变量进行++操作,首先为了防止多线程引起的线程安全问题我们要对count进行加锁操作(后面会讲),当t1,t2运行完毕之后main线程进行汇总,于是我们就需要让main线程最后再执行,
此时就需要调用join()方法,join()方法可以让线程进入等待.在哪个线程中调用了join()方法(上面代码就是在main线程中调用了join()方法),哪个线程就要等待.例如:在main线程中t1线程调用join(),就是main线程等待t1线程运行结束.

6.为什么引入多线程

如果当前的cpu是一个多核的cpu,如果只是一个单线程的程序,只是把一个cpu吃满,其他的cpu核心就浪费了,为了提高cpu的利用率,就可以引入多线程,每个线程只完成一部分工作.
比如一款游戏,有的线程负责画面的渲染,有的负责游戏背后的逻辑运算,有的负责网络通信,如果只有一个线程,即使把一个cpu吃满了也不会比多个线程一起执行来的快.

7.线程的状态

Java中,线程有下面几个状态
NEW状态:Thread状态创建好了之后,但是还没有在start方法中创建线程
TERMINATED状态:Thread线程仍然存在,但是线程内部已经执行完成
RUNNABLE状态:就绪状态,表示这个线程正在cpu上执行,或者随时可以去cpu上执行
WATITING状态:不带时间的阻塞(死等),必须满足一定的条件,才会解除阻塞
TIMED_WAITING状态:指的是带时间的阻塞,到达一定时间后自觉解除阻塞
BLOCKED状态:由于锁竞争,引起的阻塞.
在这里插入图片描述
一个Thread对象只能start一次.

二.线程安全[最重要]

引入多线程的目的是为了"并发编程",虽然多线程的好处很多,但是线程安全的问题也是最让人苦恼的.

1.什么是线程安全

一个代码在单线程或者多线程下执行都不会产生bug.这个情况就是"线程安全"的.
但是代码如果在单线程下运行正确但是在多线程下很可能产生bug.这个情况就被称为"线程不安全"或者是"存在线程安全问题"

接下来我们看一个例子:

package thread;public class ThreadDemo29 {public static int count=0;public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i=0;i<50000;i++){count++;}});Thread t2=new Thread(()->{for (int i=0;i<50000;i++){count++;}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);}
}

上述的这个循环自增的代码就存在线程安全问题,如果是在单线程的情况下可能不存在问题,但是在多线程的情况下就会产生问题.
在这里插入图片描述
在这里插入图片描述
我们会发现,每次运行的结果都是不一样的,而且都并不正确.这个就是线程安全问题

接下来我们深度分析下为什么会出现这样的问题,以上述代码为例:

这个count++其实是由3个指令组成的
1)load 从内存中读取数据到cpu的寄存器
2)add 把寄存器的值加1
3)save 将寄存器的值写回到内存中
如果是单个线程的情况下执行这三个指令是不会有问题的,但是在多线程下,并发执行上述的操作可能就会出现问题(线程之间的调度顺序是不一样的)
如图:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
我们可以看到在两个线程t1和t2中,由于抢占式执行,t1的load之后,紧接着t2的load,所以两个线程自增一次后都只加了1,并没有按照我们所想的两个线程增加了2.这就是由于线程的随机调度,抢占式执行造成的线程安全问题.

2.造成线程不安全的原因

(1)根本原因:线程的随机调度,抢占式执行.
(2)代码结构:代码中多个线程同时修改同一个变量
(3)[直接原因]上述多线程的修改操作,不是原子的
(4)内存可见性问题
(5)指令重排序

3.如何解决线程安全问题

我们根据上面造成线程不安全的原因解决线程安全问题.

针对原因1,我们无法干预,因为系统内部已经实现了抢占式执行
针对原因2.我们要分情况,有的时候可以调整,有的时候调整不了
针对原因3.对于修改操作不是原子的,我们可以通过加锁来干预

4.锁

1.锁的概念:
在 Java 中,锁是一种用于控制多个线程对共享资源访问的机制。当多个线程并发访问共享数据时,如果不加以控制,可能会导致数据不一致等问题。
锁就像我们人去上厕所,进入厕所门之后我们就会把门锁上,这样就是加锁.别人进不来,只能等我们出去之后他们才能进来,等我们出来的时候就是释放锁
在这里插入图片描述

5.如何加锁

Java中我们通常时候synchronized来进行加锁操作.

我们该如何进行加锁?
1.首先准备好一个锁对象,加锁解锁操作都是由锁对象展开的.在Java中,任何一个对象都可以作为锁对象.

Object locker=new Object();

上面的Object实例就可以作为一个锁对象.

加锁后的作用:如果一个线程,针对一个对象加上锁之后,其他线程也尝试对这个对象进行加锁,就会产生阻塞(BLOCKED),一直阻塞到加锁线程释放锁为止.

package thread;public class ThreadDemo31 {public static void main(String[] args) {Object locker=new Object();Thread t1=new Thread(()->{synchronized (locker){System.out.println("t1线程");}});Thread t2=new Thread(()->{synchronized (locker){System.out.println("t1线程");}});t1.start();t2.start();}
}

就像上述代码,t1,t2对locker这个对象进行加锁操作,如果t1拿到了锁,t2就要进行等待,等待t1释放锁之后t2才可以拿到锁.
在这里插入图片描述
在Java中我们可以通过类对象进行加锁(因为一个类的类对象只有一个)
在这里插入图片描述
如果synchronized是加到static方法上,就等价于给类对象加锁

6.Java中的可重入锁

package thread;public class ThreadDemo31 {public static void main(String[] args) {Object locker=new Object();Thread t1=new Thread(()->{synchronized (locker){synchronized (locker){System.out.println("t1线程");}}});}
}

如上述代码,针对locker对象再次加锁,会不会加锁成功?
答案是会的
当前由于是同一个线程,此时的锁对象知道第二次加锁的线程就是持有锁的线程,第二次操作就可以直接放行,不会出现阻塞问题.这个特性就是"可重入"
上述同样逻辑的代码,在C++中,std::mutex这个锁就是不可重入的,一旦出现上面一样逻辑的代码,这个时候无法自动恢复的线程就卡死了,出现卡死的情况称为"死锁"

可重入锁是如何加锁解锁的?
在这里插入图片描述
通过一个计数器我们可以识别解锁时机的关键要点.

7.死锁的三种场景

1.一个线程一把锁

如果是不可重入锁,并且一个线程对这把锁加锁两次,就会出现死锁.

2.两个线程两把锁
线程1获取锁A
线程2获取锁B
接下来,在线程1持有锁A的同时想要获取锁B,线程2在持有锁B的情况下想要获取锁A,这样就出现了死锁.

package thread;public class ThreadDemo31 {public static void main(String[] args) {Object A=new Object();Object B=new Object();Thread t1=new Thread(()->{synchronized (A){System.out.println("获取锁A");}synchronized (B){System.out.println("线程t1拿到两把锁");}});Thread t2=new Thread(()->{synchronized (B){System.out.println("获取锁B");}synchronized (A){System.out.println("线程t1拿到两把锁");}});}
}

3.N个线程M把锁(哲学家就餐问题)
在这里插入图片描述
我们通过引入加入锁顺序的规则,就可以避免死锁.

8.内存可见性引起的线程安全问题

假设一个线程写,一个线程读.这个时候是否会有线程安全问题呢?

我们看下面一段代码

import java.util.Scanner;public class Test {public static int flag=0;public static void main(String[] args) {Thread t1=new Thread(()->{while (flag==0){//什么都不做}System.out.println("t1线程结束");});Thread t2=new Thread(()->{System.out.println("请输入flag的值");Scanner scanner=new Scanner(System.in);flag=scanner.nextInt();});t2.start();t1.start();}
}

t1线程进行读操作,t2线程进行写操作.t2线程等待用户输入,无论是t1先启动还是t2先启动,等待用户输入的过程中,t1必然已经执行了很多次.
在这里插入图片描述
我们可以看到即使输入0,可执行文件也不会结束.

 while (flag==0){//什么都不做}

这个代码核心指令有两条
1.load 读取内存中的flag到寄存器中
2.拿着寄存器的值和0进行比较.

这个执行过程中,有两个关键要点.
1.load操作执行的结果每次都是一样的
2.load操作的开销,远远超过条件跳转.访问寄存器的速度远远超过访问内存

频繁的执行load和条件跳转的操作,load的开销大,并且load的结果也不会有什么变化(注意:真正变化是在用户输入之后,但是此时已经load了不知道多少次),此时JVM就怀疑这里的load操作是否有存在的必要.此时JVM可能就会做出代码优化.把上述load操作给优化掉,优化掉之后,就相当于不再重复的读取内存的值了,而是直接读取寄存器中之前"缓存"的值.从而提高运行速度.
上述的这种情况,在单线程下可能不会有问题,但是在多线程下可能就会存在问题.
在这里插入图片描述
所以,为了确保我们当前的代码无论怎么写,都不会出现内存可见性问题
Java提供了volatile关键字就可以使上述的优化被强制关闭,可以确保每次循环都是在内存中重新读取数据了

 /*** flag变量用作全局标志,其值的变化代表不同的状态或行为* 使用volatile关键字确保多线程环境下的可见性,即当一个线程修改了flag的值,* 其他线程可以立即看到修改后的值*/public volatile static int flag=0;

volatile还有一个作用就是关闭指令重排序,后续我们会再介绍

三.wait和notify(等待通知机制)

wait():wait()是Object类中的方法,它会使当前线程进入等待状态,直到另一个线程调用该对象的notify()或notifyAll()方法将其唤醒,或者经过了指定的超时时间(如果使用了带超时参数的wait(long timeout)或wait(long timeout, int nanos))。

当线程调用wait()方法时,它会释放它持有的该对象的锁,允许其他线程获取该锁并继续执行。

notify():otify()也是Object类中的方法,它会唤醒一个等待在该对象上的线程。如果有多个线程在等待,它只会随机唤醒其中一个。

调用notify()方法的线程必须持有该对象的锁,否则会抛出IllegalMonitorStateException。

1.线程饿死

线程饿死(Thread Starvation)是指一个线程由于无法获取所需的资源(如 CPU 时间片、锁等)而长时间无法执行的情况。这通常发生在某些线程总是优先获得资源,而其他线程一直处于等待状态,最终导致这些等待的线程无法正常运行的情况。

在这里插入图片描述
在这里插入图片描述
此时就可以用到wait()和notify()了,让1号滑稽看看当前条件是不是满足,如果不满足就wait(),其他线程让条件满足之后再notify().唤醒一号滑稽.

在这里插入图片描述
在这里插入图片描述
wait()必须要放到synchronized里面(wait要释放锁,前提是你的先拿到锁).

public static void main(String[] args) throws InterruptedException {Object locker=new Object();synchronized (locker){locker.wait();}}

调用wait的对象必须和synchronized是一致的,因此wait解锁必然解的是locker的锁
后续被唤醒之后,重新获取锁,还是获取到locker的锁.

调用notify的线程也需要使用同样的对象.

在这里插入图片描述

// 程序的入口点
public static void main(String[] args) throws InterruptedException {// 创建一个对象实例作为线程之间的同步锁Object locker=new Object();// 创建第一个线程,该线程将尝试获取锁并等待通知Thread t1=new Thread(()->{try {// 同步块,使用locker对象作为锁synchronized (locker){// 在等待前打印消息System.out.println("wait之前");// 调用wait方法,线程将释放锁并等待,直到其他线程调用notify或notifyAll方法locker.wait();// 在等待后打印消息System.out.println("wait之后");}}catch (InterruptedException e){// 捕获中断异常并打印堆栈跟踪e.printStackTrace();}});// 创建第二个线程,该线程将尝试获取锁并发出通知Thread t2=new Thread(()->{try {// 同步块,使用locker对象作为锁synchronized (locker){// 在发送通知前打印消息System.out.println("notify之前");// 使当前线程睡眠2秒,模拟某些操作的延迟Thread.sleep(2000);// 调用notify方法,唤醒一个在该锁上等待的线程locker.notify();// 在发送通知后打印消息System.out.println("notify之后");}}catch (InterruptedException e){// 捕获中断异常并打印堆栈跟踪e.printStackTrace();}});// 启动第一个线程t1.start();// 启动第二个线程t2.start();
}

t1线程启动就会调用wait,进入线程等待,t2线程启动会先sleep一会,sleep时间到了之后进行notify()唤醒t1.

注意:
(1).wait()提供了两个版本,
在这里插入图片描述

其实是三个,但是 第三个我们并不常用.

(2)wait()和notify()彼此之间是通过相同的对象联系起来的.

(3).notifyAll是唤醒这个对象上所有等待的线程.

2.wait和sleep的区别

wait提供了一个超时时间的版本
sleep也能指定时间
都是时间到,解除阻塞,继续执行.

使用wait,最主要的目标就,一定是要不知道等多少时间的前提下使用的,所谓的超时时间,其实是"兜底的".

使用sleep,一定是知道要等多少时间的前提下使用的.虽然能提前唤醒,但是通过异常唤醒,这个操作不应该.

四.单例模式

1.什么是单例模式

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。

2.饿汉模式和懒汉模式

实现单例模式的方式有很多种,这里我们介绍两种最基础的实现方式.

1.饿汉模式
饿汉模式是单例模式的一种实现方式,其核心特点是在类加载时就创建单例对象,确保在程序运行期间该类的单例对象始终存在。
饿汉模式在多线程中天然就是线程安全的

/*** Singleton类提供了一种单例模式的实现方式,确保该类只有一个实例存在*/
class Singleton{// 在类加载时就创建Singleton实例,实现线程安全的懒汉式单例模式private static Singleton instance=new Singleton();/*** 提供全局访问点来获取Singleton类的唯一实例** @return Singleton类的唯一实例*/public static Singleton getInstance() {return instance;}/*** 私有化构造方法,防止外部实例化Singleton对象*/private Singleton(){ }
}
/*** Test类目前为空类,用于未来可能的测试代码编写*/
public class Test {
}
  private static Singleton instance=new Singleton();

这个引用就是我们希望创建出唯一实例的引用.
static 静态的,指的是类属性, instance就是Singleton类对象里面持有的属性
每个类的类对象只有一个,类对象的static属性自然也就只有这一个.

  public static Singleton getInstance() {return instance;}

这段代码的作用就是想要使用这个类的实例,就需要通过这个方法来获取实例.不应该在其他的代码中重新new这个对象,而是直接调用这个方法来获取到现成的对象.

private Singleton(){ }

单例模式的点睛之笔,为了防止其他类new一个对象,这里直接将构造方法私有化,其他类调用不了构造方法自然也就创建不了这个实例.

饿汉模式是单例模式中一种比较简单的写法,所谓"饿"形容的非常迫切.
实例在类加载的时候就已经创建好了,创建的时机非常早,相当于可执行文件一运行,实例就创建了,所以称为"饿汉模式"

2.懒汉模式
懒汉模式是单例模式的一种实现方式,其核心特点是在第一次需要使用单例对象时才进行实例化,而不是像饿汉模式那样在类加载时就创建实例,这样可以节省资源

懒汉模式在多线程下存在线程安全问题

    class SingletonLazy{private volatile SingletonLazy instance=null;private Object locker=new Object();public SingletonLazy getInstance() {if (instance==null){synchronized (locker){if (instance==null){instance=new SingletonLazy();}}}return instance;}}public class Test2 {}

在这里插入图片描述

3.多线程下的饿汉和懒汉

对于饿汉模式来说,在getinstance()方法下直接返回一个实例,本质上是"读操作"
多个线程读取同一个变量,本身就是线程安全的.

public Singleton getinstance(){return instance;}

对于懒汉模式

public SingletonLazy getInstance() {if (instance==null){synchronized (locker){if (instance==null){instance=new SingletonLazy();}}}return instance;}

我们可以看到,在getInstance()方法中instance=new SingletonLazy();属于"写操作"
return instance;属于读操作,这样的代码可能就会存在线程安全问题.
在这里插入图片描述
如果是多线程按照上述执行逻辑的情况下就会出现线程安全问题,因为实例被new了两次,这样就不是单例模式了
为了能保证线程安全我们就需要对其进行加锁.

 synchronized (locker){if (instance==null){instance=new SingletonLazy();}}}

此时就可以确保在多线程的情况下,一个线程执行完new操作,修改了instance,再回到另一个线程,if就不会成立了,就会直接返回一个实例.

但是还会存在一个问题:
在这里插入图片描述
此时我们就要在锁的外面再嵌套一层if的判断

 if (instance==null){synchronized (locker){if (instance==null){instance=new SingletonLazy();}}}

在这里插入图片描述
解决了线程安全问题和提高性能的问题,这个代码还存在一点小问题

指令重排序
1.定义:
指令重排序是指在不改变程序执行结果的前提下,编译器或处理器为了优化性能,对程序中的指令执行顺序进行重新排序的一种手段。在单线程环境下,这种重排序不会影响程序的正确性,但在多线程环境中,可能会导致一些意想不到的结果。

上述懒汉模式代码就可能存在指令重排序.

 instance=new SingletonLazy();

在这里插入图片描述
在这里插入图片描述
上述代码中,由于t1线程执行完1和3步骤后调度走了,此时instance指向的是一个非null的,但是未初始化的对象.此时t2线程判断if(instance==null)不成立.就会直接return.如果t2直接使用instance的方法或者属性,就会出现问题.

那我们该如何解决呢?
核心思路就是加volatile;
在这里插入图片描述

private volatile SingletonLazy instance=null;

五.阻塞队列


public class MyBlockQueue {//创建一个顺序表队列private String[] elems;//提供一个构造方法,初始化elems数组public MyBlockQueue(int capcity){this.elems=new String[capcity];}private Object locker=new Object();private int head=0;private int tail=0;private int size=0;public void put(String s) throws InterruptedException {synchronized (locker){if (size==elems.length){//如果当前队列元素满了,我们就要停下来等待,其他线程调用take方法取走元素locker.wait();}//将s存放到elems数组tail的位置elems[tail]=s;//tail++tail++;if (tail>=elems.length){tail=0;}size++;locker.notify();}}public String take() throws InterruptedException {String elem=null;synchronized (locker){while (size==0){locker.wait();}elem=elems[head];head++;if (head==elems.length){head=0;}size--;locker.notify();return elem;}}
}

1.什么是阻塞队列
队列:先进先出.

阻塞队列:基于普通队列做出的扩展
(1).他是线程安全的
(2).具有阻塞特性.
在这里插入图片描述
2.阻塞队列的作用:阻塞队列最常用的就是基于阻塞队列实现生产者-消费者模型.

什么是生产者消费者模型
就像我们包饺子:
在这里插入图片描述
生产者消费者模型在实际开发中非常有意义.

作用1:解耦合
在这里插入图片描述
在这里插入图片描述
引入生产者消费者模型后
在这里插入图片描述
在这里插入图片描述

作用2:削峰填谷
在这里插入图片描述
如果突然之间传来了很多请求,B和C本身承受不住.就会导致系统崩了,于是我们引入阻塞队列来针对这一情况来处理.
在这里插入图片描述
还有一点,有人听到阻塞队列不由得想起消息队列,而消息队列就是基于阻塞队列实现的服务器程序.

六.定时器

定时器相当于闹钟,网络通信中,经常需要设定超时时间,我们可以通过定时器可以设定超时时间。

Java标准库中提供了定时器的实现,具体用法如下

public class demo30 {public static void main(String[] args) {Timer timer = new Timer();//参数1表示要完成什么事,第二个参数表示完成这件事所用时间timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello3");}}, 3000);//timer不仅能安排一个任务timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello2");}}, 2000);timer.schedule(new TimerTask() {@Overridepublic void run() {System.out.println("Hello1");}}, 1000);System.out.println("程序开始运行");}
}

接下来我们自己实现一个定时器

import java.util.PriorityQueue;/*** 自定义定时任务类,实现Comparable接口以比较任务执行时间* 因为PriorityQueue中存储的元素都必须是可比较的*/
class MyTImertask implements Comparable<MyTImertask>{private long time; // 任务执行时间private  Runnable runnable; // 任务执行的Runnable对象/*** 构造方法,初始化任务执行的Runnable对象和执行时间* @param runnable 任务执行的Runnable对象* @param delay 延迟执行的时间*/public MyTImertask(Runnable runnable,long dealy){this.runnable=runnable;this.time=System.currentTimeMillis()+dealy;}/*** 获取任务的执行时间* @return 任务的执行时间*/public long getTime() {return time;}/*** 执行任务*/public void run(){runnable.run();}/*** 比较两个任务的执行时间* @param o 被比较的任务* @return 执行时间的差值*/@Overridepublic int compareTo(MyTImertask o) {return (int) (this.time-o.time);}
}/*** 自定义定时器类,用于调度任务*/
class MyTimer{private Thread t=null; // 定时器线程private PriorityQueue<MyTImertask> queue=new PriorityQueue(); // 优先队列存储任务private Object locker=new Object(); // 同步块对象,用于线程安全/*** 调度任务,添加任务到优先队列* @param runnable 任务执行的Runnable对象* @param delay 延迟执行的时间*/public void schedule(Runnable runnable,long delay){synchronized (locker){MyTImertask task=new MyTImertask(runnable,delay);queue.offer(task);locker.notify();}}/*** 构造方法,初始化定时器线程*/public MyTimer(){t=new Thread(()->{try {synchronized (locker){while (true){while (queue.isEmpty()){locker.wait();}//获取当前任务MyTImertask curtask=queue.peek();//看当前任务是不是到了执行的时间if (System.currentTimeMillis()>=curtask.getTime()){//到时间就执行,执行完并且删除任务queue.poll();curtask.run();}else {locker.wait(curtask.getTime()-System.currentTimeMillis());}}}}catch (InterruptedException e){e.printStackTrace();}});}
}

七.线程池(ThreadPoolExecutor)

1.基本概念

线程池是一种线程使用模式,它预先创建一定数量的线程,当有任务提交时,从线程池中获取一个空闲线程来执行该任务。任务执行完毕后,线程不会销毁,而是返回线程池等待下一个任务,这样可以避免频繁创建和销毁线程带来的开销.

2.参数

如何理解好线程池,就要理解他的每个参数.
在这里插入图片描述
(1).核心线程数(int corePoolSize)
核心线程数就像是一个公司的正式员工人数

(2).最大线程数(int maximumPoolSize)
最大线程数就像是一个公司正式的员工加上实习生.

线程池可以支持线程扩容,某个线程池初始情况可能有n个线程,实际情况中如果n个不够用,则会自动扩容增加线程的个数。在Java标准库的线程池中,线程分为核心线程(线程池中最少有多少个线程)和非核心线程(线程扩容的过程中新增的线程)核心线程数+非核心线程数的最大值就是最大的线程数。核心线程会始终存在线程池中,非核心线程在系统繁忙的时候创建,非繁忙的时候被销毁

(3).非核心线程允许空闲时间(long keepAliveTime)
类似于实习生上班摸鱼时间,在任务不紧张的情况下我们不能立即销毁线程,而是先将他保留一会,看是否还有需要.

(4).keepAliveTime和unit表示非核心线程允许休息(摸鱼)的最大时间的数值和单位,keepAliveTime是数值,unit是单位,包含的单位如下
在这里插入图片描述
(5).工作队列( BlockingQueue workQueue)
线程池的工作过程是典型的生产者消费者模型,程序员使用的时候,通过形如submit这样的方法,把要执行的任务设定到线程池中,线程池内部的工资线程负责执行这些任务,此处有一个阻塞队列,submit把任务塞到阻塞队列中,工作线程从阻塞队列取任务。队列可以自行指定队列的容量和队列的类型。

(6).线程工厂(ThreadFactory threadFactory)
工厂设计模式是一种在创建类的实例时使用的设计模式,工厂设计模式通过普通的静态方法来创建对象和初始化,而不是通过构造方法,这些static修饰的静态方法也称为工厂方法。有的工厂方法也会单独放在类当中,这样的类也称工厂类。threadFactory就是Thread类的工厂类通过这个类可以完成Thread的实例创建和初始化。这个参数一般使用ThreadFactory的默认值Executors.defaultThreadFactory() 即可

(7).拒绝策略( RejectedExecutionHandler handler))

拒绝策略是整个线程池中最重要的东西

在这里插入图片描述

在这里插入图片描述
ThreadPoolExecutor使用起来比较麻烦,于是标准库又对这个类进一步封装成Executors。

Executors提供了一些工厂方法,可以更方便的构造线程池

例如,

ExecutorService service=Executors.CachedThreadPool();
ExecutorService service=Executors.newFixedThreadPool();
ExecutorService service=Executors.newScheduledThreadPool();
ExecutorService service=Executors.newWorkStealingPool();
ExecutorService service=Executors.newSingleThreadExecutor();

3.自己实现线程池

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;/*** 自定义线程池执行器* 该执行器用于管理和调度线程,以执行提交的任务*/
class MyThreadPoolExecutor{// 存储线程的列表private List<Thread> threadList=new ArrayList<>();// 任务队列,使用阻塞队列来存储待执行的任务private BlockingQueue<Runnable> queue=new ArrayBlockingQueue<>(1000);/*** 构造函数,初始化线程池* @param n 线程池中线程的数量*/public MyThreadPoolExecutor(int n){// 创建并启动指定数量的线程for (int i=0;i<n;i++){// 使用Lambda表达式定义线程的执行逻辑Thread t=new Thread(()->{while (true){try {// 从队列中获取任务并执行Runnable runnable=queue.take();runnable.run();} catch (InterruptedException e) {// 如果线程被中断,则抛出运行时异常throw new RuntimeException(e);}}});t.start();threadList.add(t);}}/*** 提交任务到线程池执行* @param runnable 要执行的任务,必须实现Runnable接口* @throws InterruptedException 如果在将任务放入队列时被中断*/public void submit(Runnable runnable) throws InterruptedException {// 将任务放入队列,由线程池中的线程执行queue.put(runnable);}
}public class Test {
}

八.锁策略

乐观锁与悲观锁

乐观锁:在加锁之前,预估当前出现锁冲突的概率不大,所以加锁的时候就不会做太多的工作,加锁的速度更快,但是可能会引入一些其他的问题

悲观锁:在加锁之前,预估当前出现锁冲突的概率比较大,所以加锁的时候就会做很多工作,加锁的速度就变慢,但是不容易出现一些问题.

举个列子:就像是疫情期间,可能会动不动封小区,但是我和我妈都有不同的判断.

我妈认为事态比较紧急,认为完全会封小区,所以会提前准备很多物资,做很多很多准备

而我认为没那么邪乎,接着每天吃喝玩乐,认为没必要那么担心.

轻量级锁和重量级锁

轻量级锁:加锁的速度快,开销比较小—>一般都是乐观锁
重量级锁:枷锁的速度慢,开销比较大—>一般就是悲观锁

有人就会问,轻量级,重量级锁和乐观,悲观锁有什么区别,看起来都一样啊?

其实是一样的,但是是站在两种不同的角度:
乐观和悲观是未加锁之前,对未发生的事情进行评估.
轻量重量是对加锁之后,对结果进行的评价

自旋锁和挂起等待锁

自旋锁就是轻量级锁的一种典型实现.
加锁的时候,搭配上循环,如果成功拿到锁,循环结束,如果拿不到锁,不会阻塞放弃cpu,而是再次循环尝试加锁.

挂起等待锁就是重量级锁的一种实现.
在加锁不成功的时候先主动放弃,进入阻塞等待,让出cpu去做一些别的事情.等到有机会再去参与锁竞争,但是阻塞等待的时间可能是未知数.好处就是可以在阻塞的过程中把cpu资源让出来做别的事情.

再举个例子:
突然有一天我和喜欢的女生表白.但是被发了张好人卡.拒绝了我.
那下面我应该怎么做?
1.继续当舔狗,如此循环往复.她如果分手了我就能第一时间掌握情报,发动恋爱攻势,每天暧昧不清,但是这样我就没有心思做别的事情,一门心思的扑在女神上面,耗时耗力.
2.先暂时放弃追女神.专注学习多线程,争取拿个好offer.过了一段时间我听说女神又分手了,这时候我心里渴望爱情的火苗又熊熊燃烧,再次尝试对女神发动恋爱攻势

其实,上面第一种就是自旋锁,刚一释放锁,逮住机会进行加锁,很大概率可以成功,但是会过多消耗cpu,第二种就是挂起等待锁,先放弃锁竞争,进入阻塞.但是一旦进入阻塞,什么时候去cpu上调度就是个未知数了,可能中间已经有好多线程进行了加锁.但是好处就是可以让出cpu去做一些其他的事情.

读写锁

读写锁就是把加锁分成两种情况:读加锁和写加锁.

如果一个线程读,另一个线程也只是读,就不会存在线程安全问题.
如果一个线程写,另一个线程无论是读还是写都会可能存在线程安全问题.

如果两个或者多个线程都是加读锁,此时不会产生锁冲突;两个或者多个线程加写锁,此时会产生锁冲突;如果两个线程一个线程是写锁,一个线程是读锁,此时会产生锁冲突。读写锁也是操作系统内置的锁,在Java中对此进行了封装,类名叫ReentrantReadWriteLock,这个类里面包含两个内部类

ReentrantReadWriteLock.ReadLock
ReentrantReadWriteLock.WriteLock

这两个类都提供了lock和unlock方法进行加锁和解锁,使用方法如下

public class demo32 {public static void main(String[] args) {ReentrantReadWriteLock outerReadLock = new ReentrantReadWriteLock();//先创建外部类//读锁ReentrantReadWriteLock.ReadLock readLock = outerReadLock.readLock();//通过外部类来创建内部类readLock.lock();readLock.unlock();//写锁ReentrantReadWriteLock.WriteLock writeLock = outerReadLock.writeLock();//通过外部类来创建内部类writeLock.lock();writeLock.unlock();}
}

公平锁和非公平锁

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

当女神和现任男友分手之后,谁来上位?有两种方案,1:按照先来后到的顺序,追求女神时间更久的老铁先上位;2:所有老铁上位的概率都一样,各凭本事竞争。对于计算机来说,约定好方案1是公平的~

按照先来后到的方式进行加锁,就是公平锁.而对于机会均等的方式进行加锁,就是非公平锁

synchronized属于非公平锁,n个线程竞争同一个锁,其中一个线程拿到锁,等到这个线程释放锁之后,剩下的n-1个线程需要重新竞争各凭本事拿到锁,另外操作系统的内核针对锁的处理也是如此。如果需要使用非公平锁,直接使用系统原生的锁即可(synchronized),如果需要使用公平锁,可以利用优先级队列,记录等待时间,让等待久的线程先拿到锁

可重入锁和不可重入锁

可重入锁在死锁问题中介绍过,如果一个线程对一把锁连续加锁两次,可能会出现死锁问题,如果把这把锁设为可重入就可以避免死锁问题。

可重入锁的特点:

1、记录当前是哪个线程持有这把锁

2、加锁的时候会判断申请加锁的线程是不是记录好的持有这把锁的线程

3、会有一个计数器,记录加锁的次数,从而确定何时释放锁

synchronized原理

乐观锁/悲观锁—>自适应
轻量级锁/重量级锁—>自适应
自旋锁/挂起等待锁—>自适应
不是读写锁
不是公平锁
是可重入锁

1.锁升级

synchronized加锁过程:开始使用synchronized加锁的时候会处于"偏向锁"状态,在遇到锁竞争的时候,“偏向锁"会升级为轻量级锁,此处的轻量级锁就是通过自旋锁实现的.如果线程一旦增多,大量的线程都在自旋,cpu的消耗就会很大了.于是就会升级为重量级锁,此处的重量级锁就是通过挂起等待锁实现的.此时拿不到锁的线程就不会自旋了,而是进入"阻塞等待”.就会让出cpu了

1.无锁->偏向锁->轻量级锁->重量级锁

关于偏向锁:就相当于男女之间搞暧昧,如果没有男生来竞争,那么就一直保持着朋友之上,恋人未满的关系,如果有人来竞争,就立即官宣.如果没人竞争,你又喜欢上别人,那么也不会向情侣分手那么复杂,毕竟我们只是朋友
在这里插入图片描述

在这里插入图片描述

2.锁消除

锁消除是编译器的优化策略,编译器会判断程序员写的synchronized代码是否真的需要加锁,如果没必要加锁,编译器会自动把synchronized给去掉。

3.锁粗化

锁粗化也是编译器的优化策略,在synchronized大括号中,代码越多(执行次数),粒度越粗,代码越少粒度越细。锁粗化就是把很多个细粒度的锁,优化合并成一个粗粒度的锁。这样可以缩短等待时间。
在这里插入图片描述

九.JUC(java.util.cuncurrent)

JUC就是放了一些多线程编程时有用的类~

Callable接口

Callable和Runnable类似,只不过Callable的call方法是有返回值的,而Runnable的run方法没有返回值。Runnable关注的是执行的过程,不关注执行结果,Callable关注执行结果

案列:计算1+2+3+…+1000\

Runnable实现:

  //用Runnable接口创建线程//Runnable注重过程,没有返回值//需要我们自己定义static变量将结果存储到变量中然后打印private static int sum=0;public static void main(String[] args) throws InterruptedException {Thread t=new Thread(new Runnable() {int result=0;@Overridepublic void run() {for (int i=1;i<1000;i++){result+=i;}sum=result;}});t.start();t.join();System.out.println(sum);

Callable实现:

 public static void main(String[] args) throws InterruptedException, ExecutionException {Callable<Integer> callable=new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result=0;for (int i=1;i<=1000;i++){result+=i;}return result;}};//因为Thread没有定义callable的构造方法//所以我们要使用FutureTask,相当于一个"粘合剂"//先将callable存储到FutureTask,然后将FutureTask存储到Thread中//FutureTask futureTask=new FutureTask(callable);Thread t=new Thread(futureTask);t.start();//futureTask的字面意识是未来要执行的任务//就像是去吃麻辣烫,点完菜之后会给你一个手牌,而futureTask就类似手牌一样的凭据,后面可以根据手牌取餐,futureTask也就可以执行任务System.out.println(futureTask.get());}

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

ReentrantLock(可重入锁)

ReentrantLock的锁风格就是传统的锁风格,需要lock和unlock来加锁解锁.
这种加锁解锁的弊端就是可能在遇到return或者抛出异常的时候忘记解锁.

为什么有synchronized还要用ReentrantLock呢?

因为ReentrantLock中的有些操作是比synchronized更有用
1.ReentrantLock提供了trylock操作
lock在直接加锁过程中,没加上锁就要阻塞
trylock在加锁过程中,即使加锁不成,也不会阻塞,直接返回false
2.ReentrantLock提供了公平锁(通过设置参数就可以设计公平锁)
3.搭配的等待通知机制不同
对于synchronize使用的是wait()和notify()
对于ReentrantLock搭配Condtion类,性能更好一点.

信号量(Semaphore)

信号量就是用来表示"可用资源的个数".

就像是停车场门口的电子牌,上面会显示还有多少个车位,开出一个车,数字就+1,进去一个车数字就-1,信号量也是同样的原理.如果"可用资源个数"为0,那么释放资源(V操作)就会进入阻塞

PV操作:
信号量就是用来表示"可用资源的个数,
如果申请一个资源,就会使数字-1,这种操作叫P操作
如果释放一个资源,就会使数字+1,这种操作叫V操作

我们所谓的锁,本身就是一种特殊的信号量,可以认为就是计数值为1的信号量,
当调用Semaphore中的acquire()方法的时候申请一个资源,此时"可用资源的个数"为0,那么就进入阻塞,当调用Semaphore中的release()方法时释放一个资源,此时"可用资源的个数"为1,通过阻塞达到加锁解锁的效果

public static void main(String[] args) throws InterruptedException {Semaphore semaphore=new Semaphore(1);Thread t1=new Thread(()->{for (int i = 0; i < 50000; i++) {try {semaphore.acquire();} catch (InterruptedException e) {throw new RuntimeException(e);}count++;semaphore.release();}});Thread t2=new Thread(()->{for (int i = 0; i < 50000; i++) {try {semaphore.acquire();} catch (InterruptedException e) {throw new RuntimeException(e);}count++;semaphore.release();}});t1.start();t2.start();t1.join();t2.join();System.out.println("count="+count);}

释放状态 就是 1
加锁状态 就是 0
对于这种非0即1的信号量我们称为"二元信号量"

在这里插入图片描述
我们可以看到,当我们定义可用资源数为1的时候,调用acquire();方法申请资源
当资源申请没被释放的情况下,就会阻塞.从而也达到了加锁的效果.

CountDownLatch

把一个大任务拆分成小任务,由每个线程分别执行.
就像多线程下载,把一个很大的下载的文件拆分成多个部分,每个线程负责下载一个部分,最后整合到一起.
像这样的场景,必须要都全部下载完成之后再整合
所以使用CountDownLatch就可以很好感知到是否已经全部下载完毕.

CountDownLatch 维护了一个计数器,初始值为一个正整数,当调用 countDown() 方法时,计数器值会递减 1。如果调用 await() 方法的线程发现计数器的值大于 0,调用方法的线程会一直阻塞,直到计数器的值变为 0。

public static void main(String[] args) throws InterruptedException {//CountDownLatch 维护了一个计数器,初始值为一个正整数。// 当调用 countDown() 方法时,计数器值会递减 1。// 如果调用 await() 方法的线程发现计数器的值大于 0,它们会一直阻塞,直到计数器的值变为 0。CountDownLatch latch=new CountDownLatch(10);for (int i = 0; i < 10; i++) {int id=i;Random random=new Random();int time=(random.nextInt(5)+1)*1000;Thread t=new Thread(()->{System.out.println("线程"+id+"正在下载");try {Thread.sleep(time);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程"+id+"结束下载");// 当调用 countDown() 方法时,计数器值会递减 1。latch.countDown();});t.start();}//调用 await() 方法的线程发现计数器的值大于 0,调用方法的线程会一直阻塞,直到计数器的值变为 0。latch.await();System.out.println("所有下载完毕");}

线程安全的集合类

Java中的集合类如ArrayList、Queue、HashMap等都是线程不安全的,而Vector、Stack、Hashtable虽然是线程安全的,它们内置了synchronized但是在多线程环境下也是不推荐使用的。那么怎么如果想在多线程环境下使用集合类,如何避免线程安全问题?

1.自己加锁

2.如果想在多线程环境下使用ArrayList、LinkedList等List这类的集合类,可以使用带锁的List:

List<Integer> list= Collections.synchronizedList(new ArrayList<>());
List<Integer> list = Collections.synchronizedList(new LinkedList<>());

在这里插入图片描述
3.CopyOnWrite(写时拷贝)
例如一个顺序表,多个线程读取表中的数据的时候肯定是没有线程安全问题的,但是一旦有线程修改里面的值,就可能会有线程安全问题.
CopyOnWrite的处理方式就是,如果要修改数据,就会先将表复制一份,然后修改数据,将原来表中的引用指向新的表
在这里插入图片描述

ConcurrentHashMap

1、优化锁了的粒度

Hashtable的加锁是直接给put、get方法加锁(也就是给this加锁),整个的Hashtable对象就是一把锁,任何一个对该对象的操作都会发生锁竞争。

而我们的ConcurrentHashMap不一样,ConcurrentHashMap是给每个哈希表中的链表进行加锁,也就是说它不是一把锁,而是很多把锁。

在这里插入图片描述
如果是只有一把锁,多个线程同时修改不同的链表是不会产生线程安全问题的,但是仍然会产生阻塞。

2、ConcurrentHashMap引入了CAS原子操作,修改size(哈希表中的元素个数)这样的操作是不会加锁的,而是借助CAS完成。

3、对读操作做了处理,1和2是针对写操作进行处理,针对读操作通过volatile等确保读取的数据不是“半成品”

4、针对哈希表扩容做了优化

普通的哈希表扩容是重新创建一个哈希表,把原来的数据都搬过去,而且是一次性搬过去。ConcurrentHashMap是创建新的哈希表,每次操作都只搬运一部分数据,减少了开销。

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

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

相关文章

根文件系统 Debian10【1】移植

1.开发背景 一般根文件系统使用 Busybox 或者是 Buildroot 构建&#xff0c;这样构建出来的文件系统比较小&#xff0c;但是不具备上网功能&#xff0c;扩展性比较差。随着 ARM 的日益强大&#xff0c;ARM 可以搭载更庞大复杂的系统&#xff0c;可以是 Ubuntu 或者 Debian 等发…

OpenSIPS-Dispatcher模块详解:优化SIP流量分发的利器

在 OpenSIPS 中&#xff0c;dispatcher 模块用于实现负载均衡和故障转移。通过 dispatcher 模块&#xff0c;你可以将 SIP 请求分发到一组后端服务器&#xff08;如媒体服务器、代理服务器等&#xff09;&#xff0c;并根据配置的算法和策略动态调整分发逻辑。 模块功能使用样…

09vue3实战-----引入element-plus组件库中的图标

09vue3实战-----引入element-plus组件库中的图标 1.安装2.引入3.优化 element-plus中的icon图标组件的使用和其他平台组件(如el-button按钮)是不一样的。 1.安装 npm install element-plus/icons-vue2.引入 在这我们只讲述最方便的一种引入方法------完整引入。这需要从elem…

Docker 部署 GitLab

一、下载镜像 docker pull gitlab/gitlab-ce 二、运行容器 docker run -d --name gitlab-20080 \n -p 20443:443 -p 20080:80 -p 20022:22 \n -v /wwwroot/opt/docker/gitlab-20080/etc:/etc/gitlab \n -v /wwwroot/opt/docker/gitlab-20080/log:/var/log/gitlab \n -v /www…

优惠券平台(十七):实现用户查询/取消优惠券预约提醒功能

业务背景 当用户预约了一个或多个优惠券抢购提醒后&#xff0c;如果不再需要提醒&#xff0c;可以取消预约通知。不过&#xff0c;虽然用户可以取消提醒&#xff0c;但已经发送到 MQ 的消息不会被撤回&#xff0c;消费者在时间点到达时依然会收到消息。此时&#xff0c;我们不…

【个人开发】macbook m1 Lora微调qwen大模型

本项目参考网上各类教程整理而成&#xff0c;为个人学习记录。 项目github源码地址&#xff1a;Lora微调大模型 项目中微调模型为&#xff1a;qwen/Qwen1.5-4B-Chat。 去年新发布的Qwen/Qwen2.5-3B-Instruct同样也适用。 微调步骤 step0: 环境准备 conda create --name fin…

深入理解进程优先级

目录 引言 一、进程优先级基础 1.1 什么是进程优先级&#xff1f; 1.2 优先级与系统性能 二、查看进程信息 2.1 使用ps -l命令 2.2 PRI与NI的数学关系 三、深入理解Nice值 3.1 Nice值的特点 3.2 调整优先级实践 四、进程特性全景图 五、优化实践建议 结语 引言 在操…

大数据学习之SparkSql

95.SPARKSQL_简介 网址&#xff1a; https://spark.apache.org/sql/ Spark SQL 是 Spark 的一个模块&#xff0c;用于处理 结构化的数据 。 SparkSQL 特点 1 易整合 无缝的整合了 SQL 查询和 Spark 编程&#xff0c;随时用 SQL 或 DataFrame API 处理结构化数据。并且支…

k8s的操作指令和yaml文件

一、项目的生命周期 创建----》发布----》更新----》回滚----》删除 1.创建 kubectl create deployment nginx1 --imagenginx:1.22 --replicas3 #基于deployment控制器创建pod&#xff0c;控制器的名称是nginx1,pod使用的镜像是nginx:1.22&#xff0c;pod的数量有3个 2.发布 ku…

解锁 DeepSeek 模型高效部署密码:蓝耘平台全解析

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

k8s部署rabbitmq

1. 创建provisioner制备器&#xff08;如果已存在&#xff0c;则不需要&#xff09; 1.1 编写nfs-provisioner-rbac.yaml配置文件 apiVersion: v1 kind: ServiceAccount metadata:name: nfs-client-provisionernamespace: wms --- kind: ClusterRole apiVersion: rbac.author…

评估大模型(LLM)摘要生成能力:方法、挑战与策略

大语言模型&#xff08;LLMs&#xff09;有着强大的摘要生成能力&#xff0c;为信息快速提取和处理提供了便利。从新闻文章的快速概览到学术文献的要点提炼&#xff0c;LLMs 生成的摘要广泛应用于各个场景。然而&#xff0c;准确评估这些摘要的质量却颇具挑战。如何确定一个摘要…

dmd-50

dmd-50 一、查壳 无壳&#xff0c;64位 二、IDA分析 main 下面的内容中数据经过R键转换&#xff0c;你就会知道v41的内容&#xff0c;以及是当v41成立时key是有效的。 v41870438d5b6e29db0898bc4f0225935c0 结合上面的函数知道&#xff1a;v41经过MD5解密后是key 注意是…

关于图像锐化的一份介绍

在这篇文章中&#xff0c;我将介绍有关图像锐化有关的知识&#xff0c;具体包括锐化的简单介绍、一阶锐化与二阶锐化等方面内容。 一、锐化 1.1 概念 锐化&#xff08;sharpening&#xff09;就是指将图象中灰度差增大的方法&#xff0c;一次来增强物体的轮廓与边缘。因为发…

全程Kali linux---CTFshow misc入门(38-50)

第三十八题&#xff1a; ctfshow{48b722b570c603ef58cc0b83bbf7680d} 第三十九题&#xff1a; 37换成1&#xff0c;36换成0&#xff0c;就得到长度为287的二进制字符串&#xff0c;因为不能被8整除所以&#xff0c;考虑每7位转换一个字符&#xff0c;得到flag。 ctfshow{5281…

vue3学习四

七 标签ref属性 设置标签ref属性&#xff0c;类似于设置标签id。 普通标签 <template name"test4"> <p ref"title" id"title" click"showinfo">VIEW4</p> <View3/><script lang"ts" setup>…

STM32 软件SPI读写W25Q64

接线图 功能函数 //写SS函数 void My_W_SS(uint8_t BitValue) {GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); }//写SCK函数 void My_W_SCK(uint8_t BitValue) {GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); }//写MOSI函数 void My_W_MOSI(uint8_t Bit…

pytest-xdist 进行多进程并发测试

在自动化测试中&#xff0c;运行时间过长往往是令人头疼的问题。你是否遇到过执行 Pytest 测试用例时&#xff0c;整个测试流程缓慢得让人抓狂&#xff1f;别担心&#xff0c;pytest-xdist 正是解决这一问题的利器&#xff01;它支持多进程并发执行&#xff0c;能够显著加快测试…

CLion2024.3.2版中引入vector头文件报错

报错如下&#xff1a; 在MacBook端的CLion中引入#include <vector>报 vector file not found&#xff08;引入map、set等也看参考此方案&#xff09;&#xff0c;首先可以在Settings -> Build,Execution,Deployment -> Toolchains中修改C compiler和C compiler的路…

【RocketMQ 存储】- 同步刷盘和异步刷盘

文章目录 1. 前言2. 概述3. submitFlushRequest 提交刷盘请求4. FlushDiskWatcher 同步刷盘监视器5. 同步刷盘但是不需要等待刷盘结果6. 小结 本文章基于 RocketMQ 4.9.3 1. 前言 RocketMQ 存储部分系列文章&#xff1a; 【RocketMQ 存储】- RocketMQ存储类 MappedFile【Rock…