线程学习(2)

 💕"i need your breath"💕

作者:Mylvzi 

 文章主要内容:线程学习(2) 

前情回顾:

  在上一篇博客中介绍到了进程与线程的区别,以及初步了解如何在Java实现多线程编程,通过内置的Thread类来实现多线程,充分利用多核cpu资源,要充分认识到每一个线程都是一个独立的"执行流",本篇文章继续讲解和Thread有关的一些操作

一.Thread类的创建方式

1.继承Thread  重写run

//创建一个类  继承于Thread类
class MyThread extends Thread {@Overridepublic void run() {// 线程的入口  告诉线程要执行哪些逻辑System.out.println("hello thread");try {// 休眠1sThread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}
}
public class Test {public static void main(String[] args) throws InterruptedException {// 首先要实例化出一个Thread类Thread thread = new MyThread();
//         start和run都是Thread类的成员
//         run只是告诉线程要去执行那些逻辑
//         start是真正的调用系统的api,创建出一个线程,再让线程去执行runthread.start();
//        thread.run();while (true) {System.out.println("hello main");// 休眠1sThread.sleep(1000);}}
}

2.实现Runnable  重写run

  创建自定义类时让其实现Runnable接口,这样写的原因本质在于Thread类也实现了Runnable接口

class MyThread implements Runnable {@Overridepublic void run() {while(true) {System.out.println("==");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}
}
public class Demo3 {public static void main(String[] args) throws InterruptedException {// 使用向上转型 是Java生态中的常见方式// 先实现一个Runnable接口Runnable runnable = new MyThread();Thread thread = new Thread(runnable);thread.start();while (true) {System.out.println("==");Thread.sleep(1000);}}
}

说明:

Runnable表示的是一个"可以运行的任务",这个任务是交给线程执行还是交给其他是体执行,Runnable本身并不关心~

Runnable接口用来表示一个可以在线程中单独执行的任务,一个类只要实现了Runnable接口并且实现他的run方法,那么这个类的实例就能够单独在线程中执行,Runnable接口就像是一个点石成金的"魔法师",只要被他修饰过,就具有了"可被执行"的属性,这个任务不仅仅可以通过线程来执行,也可以通过线程池和执行器来执行

使用Runnable接口有哪些好处呢?直接继承Thread类不是更简单么?使用Runnable接口最大的好处就是可以"解耦合",降低代码之间的联系性,代码之间的联系性越高,耦合度就越高;反之亦然,耦合度过高不利于我们之后对代码进行修改~就像你和你最好的哥们一起创业,分钱肯定是不好分的~

上述两种创建Thread类的方式有所不同,第一种是直接通过MyThread类来实例化一个Thread类,第二种是先通过MyThread类先实例化一个Runnable接口,再通过这个接口去实例化一个Thread类。为什么第二种方式耦合度更低呢?原因在于第二种方式自定义类和Thread类之间的联系性降低了,他们之间是通过Runnable接口来联系起来的,以后使用更多线程的时候就都可以通过Runnable这个接口来实现,请看第二种方式创建线程的图解

第一种方式的图解

很明显第二种方式代码之间的耦合性更低

3.继承Thread,重写run,使用匿名内部类

public class Demo4 {public static void main(String[] args) throws InterruptedException {// 继承Thread  使用匿名内部类Thread t = new Thread() {@Overridepublic void run() {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};t.start();while (true) {System.out.println("hello main");Thread.sleep(1000);}}
}

4.实现Runnable  重写run,使用匿名内部类

public class Demo11 {public static void main(String[] args) {// 实现Runnable  重写run  使用匿名内部类Runnable runnable = new Runnable() {@Overridepublic void run() {while(true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}};Thread t = new Thread(runnable);t.start();}
}

5.使用lambda表达式+Runnable接口(推荐方式)

Runnable接口是一个函数式接口,只有一个抽象方法run,所以可以使用lambda表达式来实现

public class Demo12 {public static void main(String[] args) throws InterruptedException {// 使用lambda表达式Runnable runnable = () -> {while (true) {System.out.println("Mythread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}};Thread thread = new Thread(runnable);thread.start();while (true) {System.out.println("main");Thread.sleep(1000);}}
}

使用这种方式创建线程,代码既简洁,又优雅,耦合性也低,推荐大家使用这种方式创建线程

Thread类的其他构造方法

Thread(String name) 创建线程对象,并命名

这个构造方法主要用于给线程命名,方便后续进行调试

        // 可以为线程起一个名字作为标识  对线程的执行没有影响  就是单纯的一个"标识"  方便之后调试进行检查区分Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}},"这是一个线程名字");

Thread(Runnable target, String name) 使用 Runnable 对象创建线程对象,并命名 【了解】Thread(ThreadGroup group, Runnable target) 线程可以被用来分组管理,分好的组即为线程组

二.Thread类的一些属性

1.ID

线程的唯一标识,是Java为每个线程分配的"身份标识"

获取方法

getId() 

        Thread t = new Thread();long tid = t.getId();// 返回值是一个长整型System.out.println("线程ID:" + tid);// 输出线程ID:20

2.名称name

就是线程的名字,便于后序进行调试

获取方法

getName()

        Thread t = new Thread("我是线程");String tName = t.getName();System.out.println(tName);// 输出我是线程

注意:此方法在源码中是被final修饰的,意味着子类无法重写方法

3.状态 state

进程最常见的两种状态是就绪状态和阻塞状态,线程也有自己的一些属性

        // 获取线程的所有状态for (Thread.State state : Thread.State.values()) {System.out.print(state+" ");}

  • NEW  Thread  对象已经存在 但是还没有通过start方法调用
  • RUNNABLE  就绪状态  线程已经在cpu上执行/等在在cpu上执行
  • TERMINATED  Thread对象还在  但系统内核中的线程不存在
  • TIMED_WAITING  阻塞 由于sleep这种固定时间的方式产生的阻塞
  • WAITING  阻塞 由于wait这种不固定时间的方式产生的阻塞
  • BLOCKED  阻塞  由于锁竞争导致的阻塞
        Thread t = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}});System.out.println(t.getState());// Thread类存在,但是还没有调用start方法,状态为NEWt.start();System.out.println(t.getState());// RUNNABLEThread.sleep(3000);System.out.println(t.getState());// TERMINATED

4.优先级priority

获取线程的优先级

获取方法

getPriority

        Thread t = new Thread("我是线程");int tPriority = t.getPriority();System.out.println(tPriority);

说明:其实此方法很"鸡肋",因为线程的优先级是由cpu的调度器决定的,在我们写代码的过程中很少去关注优先级,一是我们根本就观察不到,二是根本也没这个必要

5.是否是后台线程

线程可以分为两类,前台线程和后台线程,默认情况下是前台线程。后台线程又叫做"守护线程",就像一场表演的后台工作人员一样,对于后台线程来说,后台线程不结束,不影响整个进程的结束(表演完了,可后台人员还需要处理后事,他们的工作还没结束),而对于前台线程来说,一个Java程序中,如果还有前台进程没有结束,则整个进程是一定不会结束的

获取方法

isDaemon

        // 源码规定   默认是前台线程/* Whether or not the thread is a daemon thread. */private boolean daemon = false;Thread t = new Thread("我是线程");boolean isDaemon = t.isDaemon();System.out.println(isDaemon);// 输出false

代码验证

    public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();// 默认是前台线程  持续打印hello thread}

对于这个代码来说,主线程中没有要执行的语句,也就是说他的主线程是在一瞬间就执行完了,但是由于t是前台线程,前台线程不结束整个进程就不会结束,如果将t设置为后台线程呢

    public static void main(String[] args) {Thread t = new Thread(() -> {while (true) {System.out.println("hello thread");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});// 在线程开启前将其设置为后台线程  t.setDaemon(true);t.start();}

 

可以看到什么也没有打印。因为主线程是前台线程,飞快执行完毕之后没有其他的前台线程,整个进程终止,也就是说t线程没来得及执行,整个进程就结束了。也就是说只要一个进程中的所有前台线程结束,就代表整个进程的结束

验证,先让主线程休眠3s,3s之后主线程会立即结束,尽管t线程内部还有语句没有执行,由于前台线程的结束,导致整个进程结束

    public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (true) {try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("hello thread");}});// 在线程开启前将其设置为后台线程t.setDaemon(true);t.start();System.out.println("主线程开启");Thread.sleep(3000);}

 6.是否存活

  判断内核线程是否还存活

在Java中我们通过Thread类来创建出一个线程,但实际上Thread类的生命周期要比内核中的线程要长一些,也就是说线程已经不存在了,但是你创建的Thread类仍然存在,使用isAlive判定内核线程是否已经结束

isAlive()

    public static void main(String[] args) throws InterruptedException {// 创建一个线程Thread t = new Thread(() -> {System.out.println("线程开始");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("线程结束");});// 开启线程t.start();System.out.println(t.isAlive());// 输出trueThread.sleep(3000);System.out.println(t.isAlive());// 输出false}

 线程t内部的方法我们称之为回调方法,当回调方法执行完毕之后,就代表t这个线程的终止,但是Thread类对象的生命周期并未结束

        System.out.println(t.isAlive());// 输出false// 开启线程t.start();

如果在线程开启之前打印,输出false,因为此时t线程还没有被创建

 三.线程的中断

终止/打断  interrupt

在Java中要想销毁/中断一个线程的方法是比较唯一的,

就是想办法让run方法尽快执行完毕

那么如何实现呢?这里提供两种方法

1.手动设置标志位,来作为run方法结束的条件

很多线程之所以会持续很久,是因为run方法内部存在循环,结束run方法就是终止循环

    // 将标志位设置为类变量private static boolean isQuit = false;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {while (!isQuit) {System.out.println("Thread is working");}});t.start();// 五秒后改变标志位Thread.sleep(2000);isQuit = true;}

通过设置标志位,并在主线程中修改标志位,这样就实现了在5秒之后中断此线程的效果

注意:

1.isQuit不能设置为main方法中的局部变量,因为在lambda表达式中使用的变量必须是被final修饰的常量,如果设置为局部变量,就无法再次更改isQuit,导致无法结束循环。

2.将isQuit设置为"类变量",lambda表达式此时访问这个成员就不再是变量捕获了,而是内部类访问外部类这个语法了。此时就没有final的限制

上述方法虽然能够结束run方法,但是过于繁琐,且不优雅,需要人为的手动设置标志位,同时,如果在主线程中我们改变了标志位的值,但是此时线程却在sleep,那就只能等到线程再次苏醒才能终止该线程,所以说通过设置标志位的方法来终止线程还有反应不及时的问题

2.使用Thread内部自带的"标志位"

  其实在Thread类中,有自带的标志位isInterrupted,默认是false

    public static void main(String[] args) throws InterruptedException {Thread t= new Thread(() -> {// 先获取当前Thread的实例  在判断其自带的标志位isInterruptedwhile (!Thread.currentThread().isInterrupted()) {while (true) {System.out.println("Thread is working");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});t.start();Thread.sleep(3000);// 此方法就是将自带的标志位isInterrupted设置为truet.interrupt();}

通过t.interrupt()方法将标志位设置为true来终止线程,这种方法的一个优点是即使线程内部处于"阻塞"状态(sleep),也能够强制将其唤醒,终止run方法,反应更加及时 

总结:两种中断线程的方法逻辑都是一样的,即设置合适的标志位,并修改该标志位来终止run方法,从而终止整个线程,但是更加推荐第二种方法

但是上述代码的运行结果是什么呢?请看

异常被抛出且被捕获,但是t线程仍在工作,并没有发生中断,这是为什么呢?通过interrupt方法唤醒线程之后,此时sleep方法会抛出异常,同时自动清除刚才设置的标志位,相当于白白设置标志位了,为什么要这么做呢?是为了让我们有更多的操作空间,在捕获到异常之后,我们可以自由采用以下三种处理方式

                    try {Thread.sleep(1000);} catch (InterruptedException e) {// 1.方式1  不管不顾  让t线程继续运行e.printStackTrace();// 2.方式2 使用break直接中断进程// 3.方式3  捕获到线程之后处理其他工作的代码// 此处就存放需要解决的其他工作的代码}

四.线程的等待

  一个线程等待另一个线程执行结束,再继续执行。线程等待的本质是控制线程结束的顺序

在Java中使用join来实现线程等待效果

主线程中使用join,就是主线程等待另一个线程结束再继续执行主线程的其余代码

    public static void main(String[] args) throws InterruptedException {Thread t= new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("线程工作中!");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.start();System.out.println("线程开启");t.join();// 让主线程等待t线程System.out.println("线程结束");}

t.join()的工作过程

  1. 如果t线程还没有结束,就让主线程等待t线程执行结束,再去执行主线程中剩余的代码,此时主线程就是一个"阻塞"状态
  2. 如果t线程已经结束了,直接返回,不存在"阻塞状态"

在哪个线程中调用join方法就是让哪个线程等待另一个线程

说明:join方法默认是"死等",即如果被等待的线程没结束,就不会执行其余代码,但这种方式存在一个问题,如果被等待的线程是死循环,那其余代码就永远无法执行,在实际的开发中,我么更推荐"有时间"的等待

此处表示主线程只等待1s,1s之后就会去执行主线程中剩余代码 

补充:关于调度开销

当我们使用Thread.sleep方法时,我们通过设置一定的时间让线程处于阻塞状态,结束之后再恢复为就绪状态,由阻塞到就绪其时间一定等于sleep的时间么

        long beg = System.currentTimeMillis();Thread.sleep(1000);// 休眠1slong end = System.currentTimeMillis();System.out.println("时间:" + (end - beg) + " ms");// 输出1003

 

可见由阻塞到就绪这部分的时间并不等于sleep的时间,原因在于休眠结束之后,线程并不是立马就变为就绪状态,而是需要通过调度器进行调度,而这种调度是需要时间的,这部分由于调度器调度所产生的时间就叫做调度开销

五.线程安全问题

有些代码如果只是一个线程单独去执行,执行结果是完全正确的

但是,同样的代码,如果使用多个线程同时去执行,执行结果就可能产生问题,这种就是"线程安全问题"/"线程不安全"

比如我们要对一个数使其自增1w,如果只使用一个线程来解决,其结果一定正确

    public static void main(String[] args) {// 在主线程中单独执行int cnt = 0;for (int i = 0; i < 10000; i++) {cnt++;}System.out.println(cnt);// 输出10000}

如果使用两个线程实现这个目标,则应该是一个线程自增5000次,加起来一共自增1w次

    private static int cnt = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {cnt++;}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {cnt++;}});// 线程开启t1.start();t2.start();// 让主线程等待两个线程结束t1.join();t2.join();// 输出打印System.out.println(cnt);// 输出7351
}

最后的打印结果是一个莫名其妙的数,不是我们想的1w,如果继续重复尝试,发现每次打印的结果还都不相同 ,程序出现bug了,这种问题就是在并发编程中常遇到的线程安全问题

为什么会出现这种问题呢,此时就要深入底层去看下cnt++这个操作是如何实现的

cnt++的实现在底层中分为三步

  1. load 把数据从内存中 读取到cpu寄存器中
  2. add 把寄存器中的数据+1
  3. save  把寄存器中的数据,保存到内存之中 

站在cpu的角度,cnt++这个操作分别对应着三条cpu指令,是由这三条指令实现的~

如果使用多线程来执行上述代码,由于线程之间的调度顺序是随机的,就会导致在一些调度顺序下发生错误,下面来看都有哪些可能的调度顺序

可以看出,调度顺序的种类其实是无数种!!!一是调度操作的逻辑顺序,二是每个线程执行多少次我们并不知道,在图中,只有前两种的调度顺序才能达到我们想要的结果,下面以一个反例来验证其他顺序的错误

由于线程调度的随机性,也就说上述调度顺序也是随机的,所以最终产生的结果也是随机的(但是最终的结果一定比1w小,因为只有前两种调度顺序才能实现数字的正确增加) 

那一定比5000大么,这也是不一定的,如果在t1自增一次的过程中,t2自增了两次,一共消耗了三次自增,但实际上只自增了一次,如果这种逻辑顺序占多数,就有可能出现<5000的情况

产生线程安全问题的原因

  1.  操作系统中,线程的调度顺序是随机的(抢占式执行) 罪魁祸首
  2. 多个线程,针对同一个变量进行修改(上述例子就是)
  3. 修改操作不是原子的,cnt++这个操作是分三步执行的,不是原子的。什么是原子的呢》比如存在一个cpu指令能同时完成cnt++的三个操作
  4. 内存可见性问题
  5. 指令重排序问题

说明:

对于第二种原因,改变一些描述就不是线程安全问题了

  • 一个线程,针对同一个变量进行修改  ok
  • 多个线程,针对不同的变量进行修改  ok
  • 多个线程,针对不同的变量进行读取  ok 

通过加锁就能解决上述问题

六.锁 synchronized

  如何给Java的代码进行加锁呢?其中最常用的方法是通过synchronized关键字(最好还是掌握下他的发音和含义)

synchronnized在使用的时候需要搭配{}来使用,进了{}就相当于"加锁",出了{}就是"解锁",在已经加锁的状态下,如果另一个线程也尝试同样加这个锁,就会发生"锁竞争"/"锁冲突",后一个线程就会阻塞等待

加锁,我们要明确是给谁加锁,也就是要对具体的对象进行加锁,只有当两个线程针对同一个对象进行加锁,才会发生冲突,针对不同的对象加锁,就不会发生冲突(可以把加锁理解为确立男女朋友关系,一旦确立(加锁),就不允许其他人再进入了,除非原先的关系破裂(解锁),不能脚踏两只船~~~)

代码实现:

        // 锁竞争的对象Object locker = new Object();Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {// 使用synchronized关键字进行加锁synchronized(locker) {cnt++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {// 使用synchronized关键字进行加锁synchronized(locker) {cnt++;}}});

在这个代码中,我们先是创建了一个用于"加锁"的对象locker,接着进行加锁,如何加锁呢?根据上述引发线程安全的2"多个线程,针对同一个变量进行修改",我们要限制的是两个线程不能同时对同一个变量进行修改,所以应该加锁的操作是"cnt++",使用synchronized(locker){}对其进行加锁

  

这种情况是我们上述所说的会引发线程安全问题的一种调度顺序,下面看看加锁是如何解决这个问题的

今天线程的学习就到这里,敬请期待后续章节 

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

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

相关文章

Python发送邮件

Python发送邮件 一、概念二、邮件服务器设置三、发送邮件流程3.1 登录邮箱3.2 准备数据3.3 发送邮件 四、实现4.1 发送文本4.2 发送html4.3 发送图片4.4 发送文件 一、概念 SMTP(Simple Mail Transfer Protocol)&#xff0c;即简单邮件传输协议,它是一组用于由源地址到目的地址…

自定义 spring-boot组件自动注入starter

1&#xff1a;创建maven项目 2&#xff1a;pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocati…

Jenkins + gitlab 持续集成和持续部署的学习笔记

1. Jenkins 介绍 软件开发生命周期(SLDC, Software Development Life Cycle)&#xff1a;它集合了计划、开发、测试、部署的集合。 软件开发瀑布模型 软件的敏捷开发 1.1 持续集成 持续集成 (Continuous integration 简称 CI): 指的是频繁的将代码集成到主干。 持续集成的流…

全光谱护眼灯哪个牌子好?全光谱备考护眼台灯推荐

什么是全光谱&#xff1f;全光谱指的是光谱中包含紫外光、可见光、红外光的光谱曲线&#xff0c;并且在可见光部分中红绿蓝的比例与阳光近似&#xff0c;显色指数接近于100的光谱。太阳光的光谱可以称作全光谱&#xff0c;太阳光的色温是随着四季和早晚时间变化而变化&#xff…

SD 一次性客户地址如何打开

一次性客户 写入后在哪里看具体数据呢 在转到->抬头->合作伙伴 双击Y0

基于EasyDarwin、ffmpeg实现rtsp推流

目录 1 安装EasyDarwin 2 编译安装ffmpeg 3 启动EasyDarwin 4 ffmepg推流 5 百度网盘备份 某项目中测试时需要用到推流&#xff0c;于是用EasyDarwin、ffmpeg实现了RTSP推流&#xff0c;简单记录下过程&#xff0c; 1 安装EasyDarwin 这个可以去官网下载&#xff1a;Eas…

大模型互相“薅羊毛”背后,行业基本操作,规范化势在必行

最近&#xff0c;字节跳动被曝调用 OpenAI API 接口训练大模型的争议&#xff0c;以及谷歌大模型 Gemini 被曝使用百度文心一言进行中文语料训练等事件&#xff0c;在行业里引发了不小的关注和讨论。 不明真相的网友们一边热情吃瓜&#xff0c;一边也在感叹 AI 大厂之间互相“…

【论文解读】Efficient SAO Coding Algorithm for x265 Encoder

时间&#xff1a;2015年 级别&#xff1a;IEEE 机构&#xff1a;上海交通大学 摘要 x265是一款开源的HEVC编码器&#xff0c;采用了多种优化技术&#xff0c;具有较快的编码速度和优良的编码性能。作为HEVC的一项关键技术&#xff0c;x265还采用了样本自适应偏移(sample adap…

工作流引擎的架构设计与对比

所谓工作流引擎是指 workflow 作为应用系统的一部分&#xff0c;并为之提供对各应用系统有决定作用的&#xff0c;根据角色、分工和条件的不同决定信息传递路由、内容等级 等核心解决方案。工作流引擎可以灵活地配置工作流程&#xff0c;并且可以自动化的根据配置进行状态变更和…

WebRPC开发基础流程

一、WebRTC 使用入门 WebRTC&#xff08;全称 Web Real-Time Communication&#xff09;&#xff0c;即网页即时通信。 是一个支持网页浏览器进行实时语音对话或视频对话的技术方案。从前端技术开发的视角来看&#xff0c;是一组可调用的API标准。 WebRTC API 官网&#xff1…

Observability:捕获 Elastic Agent 和 Elasticsearch 之间的延迟

在现代 IT 基础设施的动态环境中&#xff0c;高效的数据收集和分析至关重要。 Elastic Agent 是 Elastic Stack 的关键组件&#xff0c;通过促进将数据无缝摄取到 Elasticsearch 中&#xff0c;在此过程中发挥着至关重要的作用。 然而&#xff0c;显着影响此过程整体有效性的关…

金融CRM有用吗?金融行业CRM有哪些功能

市场形式波诡云谲&#xff0c;金融行业也面临着资源体系分散、竞争力后继不足、未知风险无法规避等问题。金融企业该如何解决这些问题&#xff0c;或许可以了解一下CRM管理系统&#xff0c;和其提供的金融行业CRM解决方案。 金融行业是银行业、保险业、信托业、证券业和租赁业…

Windows 终端与 PowerShell:它们有何不同?

如果您使用的是 Windows 11 或 Windows 10&#xff0c;你会发现预安装了 PowerShell 和 Windows 终端。两者都是命令行界面&#xff0c;允许您输入文本命令来执行各种任务&#xff0c;但它们有何不同&#xff1f; PowerShell 是对命令提示符的改进 Windows 11 中的 Windows Po…

基于SSM的双减后初小教育课外学习生活活动平台的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

odoo17核心1——概述

odoo17发布了&#xff0c;如果说odoo16是一个承前启后的版本&#xff0c;那么odoo17则完全抛弃了历史包袱&#xff0c;全面简化了前端代码&#xff0c;是一个里程碑式的版本。 在学习odoo的过程中&#xff0c;结合对源码的阅读&#xff0c;对odoo的设计哲学有了一些自己的感悟…

分享一些实用工具和学习网站

1 前言 虽然已经工作过几年的时间了&#xff0c;但是学习的脚步是不能停止的&#xff0c;对于学习&#xff0c;特别是自学&#xff0c;善于搜索网上的一些资源来辅助&#xff0c;还是非常有必要的&#xff0c;下面我就把这几年私藏的各种资源&#xff0c;网站贡献出来给你们。…

BSWM 模式管理(二)ESH

BSWM 模式管理 ESH 1 ECU State Handling (ESH)2 BSWM ESH 五大模式与六大通用状态机3 状态机对应的切换条件 conditions or rules4 默认主要的 ACTION 或者 ACTION LIST1 ECU State Handling (ESH) 与 ECUM 相关,整个 ECU 状态管理的状态机制 2 BSWM ESH 五大模式与六大通…

时间序列预测模型全家桶-最全教程

时序预测&#xff1a;LSTM、ARIMA、Holt-Winters、SARIMA模型的分析与比较-CSDN博客 风速预测&#xff08;二&#xff09;基于Pytorch的EMD-LSTM模型-CSDN博客 风速预测&#xff08;三&#xff09;EMD-LSTM-Attention模型-CSDN博客 风速预测&#xff08;四&#xff09;基于P…

Spring Boot学习随笔- 文件上传和下载(在线打卡、附件下载、MultipartFile)

学习视频&#xff1a;【编程不良人】2021年SpringBoot最新最全教程 第十二章、文件上传、下载 文件上传 文件上传是指将文件从客户端计算机传输到服务器的过程。 上传思路 前端的上传页面&#xff1a;提交方式必须为post&#xff0c;enctype属性必须为multipart/form-data开发…

Maven将Jar包打入本地仓库

Maven将Jar包打入本地仓库 Maven将Jar包打入本地仓库嘚吧嘚下载Maven配置Maven新建MAVEN_HOME编辑Path验证Maven配置 Jar包打入Maven仓库 Maven将Jar包打入本地仓库 嘚吧嘚 最近项目用到一个Jar包&#xff0c;不能从远程仓库拉取&#xff0c;只有一个Jar包&#xff0c;所以需…