【多线程-从零开始-肆】线程安全、加锁和死锁

news2024/11/25 14:22:06

进程状态

进程状态:

  • 就绪:正在 CPU 上执行,或者随时可以去 CPU 上执行
  • 阻塞:暂时不能参与 CPU 的执行

Java 的线程,对应状态做了更详细的区分,不仅仅是就绪和阻塞了

六种状态:

  1. NEW
    • 当前 Thread 对象虽然有了,但是内核的线程还没有(还没调用过 start)
  2. TERMINATE
    • 当前 Thread 对象虽然还在,但是内核的线程已经销毁(线程已经结束)
  3. RUNNABLE
    • 就绪状态,正在 CPU 上运行 + 随时可以去 CPU 上运行
  4. BLOCKED
    • 阻塞状态
    • 锁竞争引起的阻塞
  5. TIMED_WAITING
    • 阻塞状态
    • 有超时时间的等待,比如 sleep 或者 join 带参数版本
  6. WAITING
    • 阻塞状态
    • 没有超时时间
  • 通过 jconsole 可以直接看到线程的状态
  • 学习线程状态主要是为了调试,比如,遇到某个代码功能没有执行,就可以通过观察对应线程的状态,看是否是因为一些原因阻塞了

线程安全

是什么

罪魁祸首是:线程的调度是随机的


在多个线程同时执行某个代码的时候,可能会引起一些奇怪的 bug。理解了线程安全,才能避免/解决上述的 bug

因为多个线程并发执行引起的 bug,称为“线程安全问题”或者“线程不安全”

public class Demo8 {  
    private static int count = 0;  
  
    public static void main(String[] args) throws InterruptedException {  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 5000; i++) {  
                count++;  
            }        
        });        
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 5000; i++) {  
                count++;  
            }        
        });        
        t1.start();  
        t2.start();  
  
        t1.join();  
        t2.join();  
        System.out.println(count);  
    }
}
  • 此时打印出来的结果不等于 10000,并且每一次都不一样
  • 这个写法是 t1 和 t2 并发执行,产生了 bug

若将 join 执行的时机改一下,就不会产生这种 bug

public class Demo8 {  
    private static int count = 0;  
  
    public static void main(String[] args) throws InterruptedException {  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 5000; i++) {  
                count++;  
            }        
        });        
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 5000; i++) {  
                count++;  
            }        
        });        
        t1.start();  
        t1.join();  
        
        t2.start();  
        t2.join();  
        System.out.println(count);  
    }
}
  • 此时打印出来的结果就一直为 10000
  • 这个写法本质上相当于 t1 先执行,执行完之后 t2 再执行,t1 和 t2 是串行执行的

为什么

  • 上面代码中的 count++ 操作,在CPU 看来,是三个指令

    1. 把内存中的数据读取到 CPU 寄存器—— load
    2. 把 CPU 寄存器里的数据+1—— add
    3. 把寄存器的值,写回内存—— save
      由于不同架构的 CPU,有不同的指令集,不同的指令集里都有不同的指令,针对这三个操作,不同 CPU 里的对应指令名称肯定是不同的
      image.png
  • CPU 在调度执行线程的时候,说不上啥时候就会把线程给切换走(抢占式执行,随机调度),指令是 CPU 最基本的单位,要调度,至少把当前线程执行完,不会执行一般调度走

  • 但是 count++是三个指令,可能会出现 CPU 执行了其中的一个或者两个或者三个指令调度走的情况

  • 基于上面的情况,两个线程同时对 count 进行++,就容易出现 bug
    image.png

  • 只要 t1 和 t2 的三个指令执行不是连在一起的,就都会出现 bug,只有两种情况能正常执行
  • 出现 bug 之后,count 的大小永远小于 10000,但也有可能小于 5000
  • 因为可能出现 t1++一次的过程中,t2++两次,这样得到的结果,正常应该++3 次,而实际只++1 次

综上,原因为:

  1. 线程在操作系统中,随机调度,抢占式执行
  2. 多个线程,同时修改同一个变量
  3. 修改操作不是“原子“的(比如++就是三个指令)
  4. 内存可见性
  5. 指令重排序

解决方案

  • 上面的第一个原因无法干预,操作系统内核负责的工作,应用层的程序员无法干预
  • 第二个原因如果禁止变量修改,也是第一种解决线程安全的思路,但普适性不高,因为是否可行要看实际需求
  • 解决线程安全问题,最主要的办法就是把“非原子”的修改,变成“原子”的

加锁

  • 此处的加锁并不是真的让 count++变成原子的,也没有干预线程的调度,只不是通过这种加锁的方式,使一个线程在执行 count++的过程中,其他的线程的 count++不能插队进来

  • 把非原子的修改操作,打包成一个整体,变成原子的操作

  • 在 Java 中,提供了一个 synchornized 关键字,来完成加锁操作

    • 这是一个关键字,不是函数,后面的() 并非参数,而是需要指定一个“锁对象”,然后通过“锁对象”来进行后续的判定
    • () 里的对象可以指定任何对象
    • {}里面的代码,就是要打包到一起的,() 里面还可以放任意的其他代码,包括调用别的方法啥的,只要是合法的 Java 代码,都是可以放的 image.png|220
public class Demo8 {  
    private static int count = 0;  
    private static Object locker = new Object();  
  
    public static void main(String[] args) throws InterruptedException {  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 5000; i++) {  
                synchronized(locker){  
                    count++;  
                };            
            }        
        });        
        Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 5000; i++) {  
                synchronized (locker){  
                    count++;  
                }            
            }        
        });        
        t1.start();  
        t2.start();  
  
        t1.join();  
        t2.join();  
        System.out.println(count);  
    }
}
  • 由于 t1 和 t2 都是针对 locker 对象加锁,t1 先加锁,就加锁成功了,于是 t1 继续执行 () 里面的代码(进厕所,执行上厕所的操作)
  • t2 后加锁的,发现 locker 对象已经被别人先锁了,就只能等(说明现在厕所有人,于是只能排队等待)
  • 又因为 t1 的 unlock 一定是在 save 之后,确保了 t2 执行 load 的时候,t1 已经 save
  • 这两者的++操作不会穿插执行了,也就不会相互覆盖掉对方的结果了
  • 这里本质上是将随机的并发执行过程变成了串行执行

  • 锁对象肯定是个对象,不能拿 int、double 这种内置类型写到() 里面,但是其他类型,只要是 Object 或者其子类,都可以。例如:一个字符串 s = “hello”,一个链表 list… 都行

  • 锁对象作用,就是用来区分多个线程是否针对“同一个对象”加锁

    • 是针对同一个对象的话就会出现“阻塞”(锁竞争/锁冲突)
    • 不是针对一个对象加锁,就不会出现“阻塞”,两个线程仍然是随机调度的并发执行
    • 锁对象,填哪个对象不重要,多个线程是否是同一个锁对象才重要

注意:

  • 加锁后的代码,本质上比 join 的串行执行效率是要高很多的
  • 加锁只是把线程中的一小部分逻辑变成“串行执行”,剩下的其他部分,仍然是可以并发执行的
Thread t1 = new Thread(() -> {  
    for (int i = 0; i < 5000; i++) {  
        synchronized(locker){  
            count++;  
        };    
    }
});  

Thread t2 = new Thread(() -> {  
    for (int i = 0; i < 5000; i++) {  
        synchronized (locker){  
            count++;  
        }    
    }
});
  • 这里面只是 count++是串行执行的,而 for 循环、比较、i++都是并发执行的
  • 只有锁里面的是串行的,外面的仍然能并发执行
  • 引入多线程并发,就是为了提高效率,引入锁之后,相当于一个线程中,小部分工作是不得不串行,但仍有大部分工作是可以并发的。虽然不如整体都并发效率高,但是肯定比整体都串行效率高

如果是三个线程针对同一个对象加锁,也是类似的情况

  • 其中某个线程先加上锁,另外两个线程阻塞等待,(哪个线程拿到锁,这个过程是不可预期的)
  • 拿到锁的线程释放了锁之后,剩下两个线程谁先拿到锁呢?也是不确定的
  • 此处 synchronizedJVM 提供的功能,synchronized 底层实现就是 JVM 中,通过 C++ 来实现的。进一步的,也是依靠操作系统提供的 API 实现的加锁,操作系统的 API 则是来自于 CPU 上支持的特殊指令来实现的
  • 系统原生的加锁 API 和很多编程语言的加锁操作的封装方式是两个函数:lock()unlock()
  • 像 Java 这种通过 synchronized 关键字,来同时完成加锁/解锁的,比较少见
    • 原生的这种做法,最大的问题在于 unlock 可能执行不到,后面排队想用这个锁的就用不了(占着坑不拉屎)
    • Java 中的 synchronized 是进入代码块就加锁,出了代码块就解锁,无论是 return 还是抛出异常,不管以哪种方式出了代码块都会自动解锁,有效避免了没有执行解锁操作的情况

类对象:

  • 一个 Java 进程中,一个类的对象是只有唯一一个的
  • 类对象也是对象,也能写到 synchronized() 里面
  • 写类对象和写其他对象没有任何本质区别
  • 换句话说,写成类对象,就是偷懒的做法,不想创建单独的锁对象了,就可以拿类对象来客串一下
Thread t1 = new Thread(() -> {  
    for (int i = 0; i < 5000; i++) {  
        synchronized(Demo8.class){  
            count++;  
        }    
    }
});

synchronized() 还可以修饰一个方法

class  Counter {  
    public  int count = 0;  
    synchronized public void add() {  
        count++;  
    }
    
    //等价于
    public void add() {
    	synchronized (this) {
    		count++;
    	}
    }
}  

  • 针对这个写法,锁对象就是 this,谁调用 add 谁就是锁对象
  • 在这里调用 add 的都是 counter,所以他们是同一个锁对象
synchronized public static void func() {
	//错
}

//对
public static void func() {
	synchronized (Counter.class)
}
  • static 方法没有 this
  • static 方法也叫类方法,和具体的实例无关,只是和类相关
  • 此时 static 方法和类对象相关,此时的写法就是给类对象加锁
  • 并非是写了 synchronized 就一定线程安全,还是得看代码咋写
  • 锁是需要的时候才使用,不需要的时候不要使用,用锁是会付出代价的(性能代价)
  • 使用锁,就可能触发阻塞,一旦某个线程阻塞,啥时候能恢复阻塞,继续执行,是不可预期的(可能需要非常多的时间)

synchronized 的几种使用方式:

  1. synchronized(){},() 里面指定锁对象
  2. synchronized 修饰一个普通的方法,相当于针对 this 加锁
  3. synchronized 修饰一个静态方法,相当于针对对应的类对象加锁

理解锁对象的作用,可以把任意的 Object/Object 子类的对象作为锁对象。锁对象是啥不重要,重要的是两个线程的锁对象是否是同一个。是同一个才会出现阻塞/锁竞争,不是同一个是不会出现的

死锁

使用锁的过程中,一种典型的、严重的 bug

一、一个线程针对一把锁,连续加锁两次

class  Counter {  
    public  int count = 0;  
    public void add() {  
        //随后调用add方法,又尝试对counter加锁  
        //但counter已经被加锁了,如果再次尝试对counter加锁,就会出现阻塞等待  
        synchronized (this){  
            count++;  
        }    
    }
}  
  
public class Demo9 {  
    public static void main(String[] args) throws InterruptedException {  
        Counter counter = new Counter();  
        Thread t1 = new Thread(() -> {  
            for (int i = 0; i < 5000; i++) {  
                //首先执行到这里,对counter加锁成功  
                synchronized (counter){  
                    counter.add();  
                }            
            }        
        });        
    Thread t2 = new Thread(() -> {  
            for (int i = 0; i < 5000; i++) {  
                synchronized (counter){  
                    counter.add();  
                }            
            }        
        });  
        t1.start();  
        t2.start();  
        t1.join();  
        t2.join();  
        System.out.println(counter.count);  
    }
}
  • 里面的 synchronized 想要拿到锁,就需要外面的 synchronized 释放锁
  • 外面的 synchronized 要释放锁,就需要执行到 }
  • 要想执行到 } ,就需要执行完这里的 add
  • 但是 add 阻塞着呢~

  • 但是上面的运行结果霉运造成死锁,是因为 Java 为了减少程序员写出死锁的概率,引入了特殊机制,解决上述的死锁问题——“可重入锁

可重入锁:
synchronized 是“可重入锁”,针对上述一个线程连续加锁两次的情况做了特殊处理。C++/Python 中的锁就没有这样的功能

  • 加锁的时候,需要判定当前这个锁是否是已经被占用的状态
  • 可重入锁就是在所中额外记录一下,当前是哪个线程对这个锁加锁了
  • 对于可重入锁来说,发现加锁的线程就是当前锁的持有线程,并不会真正进行任何加锁操作,也不会进行任何“阻塞操作”,而是直接放行,往下执行代码
  • 比如你向一个女生表白了,她同意了,那你就对她加锁了,她的持有人就是你
  • 再有人对她加锁的时候,她就会进行判定,看当前这个要加锁的人,是不是她的持有人
  • 若是隔壁老王找到她说“美女,我好喜欢你”,那她就会告诉他“你是个好人”
  • 若是你找到她说“我好喜欢你”,那她就会说“我也好喜欢你”
//真加锁
synchronized (this) {
	//直接放行,不会真加锁
	synchronized (this){
	}
}

二、两个线程两把锁

  • 若有两个线程 1 和 2,两把锁 A 和 B
  • 线程 1 先针对 A 加锁,线程 2 针对 B 加锁
  • 线程 1 不释放锁 A 的情况下,再针对 2 加锁;同时,线程 2 在不释放 B 的情况下针对 A 加锁

  • 比如你和你女朋友出去吃饺子,她拿的醋,你拿的酱油
  • 你说:你把醋给我,我用完了给你
  • 她说:凭什么,你把酱油给我,我用完了给你
  • 结果是你俩互不相让,僵持住了
  • 死锁往往是出现了“循环依赖
  • 这种死锁情况,可重入锁机制也无能为力
public class Demo1 {  
    private static Object locker1 = new Object();  
    private static Object locker2 = new Object();  
  
    public static void main(String[] args) {  
        Thread t1 = new Thread(() -> {  
            synchronized (locker1){  
                System.out.println("t1 加锁 locker1 完成");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                
                synchronized (locker2){  
                    System.out.println("t1 加锁 locker2 完成");  
                }            
            }        
        });        
        Thread t2 = new Thread(() -> {  
            synchronized (locker2) {  
                System.out.println("t2 加锁 locker2 完成");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }  
                synchronized (locker1){  
                    System.out.println("t2 加锁 locker1 完成");  
                }            
            }       
        });        
        t1.start();  
        t2.start();  
    }
}
打印结果:
t1 加锁 locker1 完成
t2 加锁 locker2 完成
  • 代码中的 sleep 是为了确保 t1 和 t2 都先分别拿到了 locker1locker2,然后再分别拿对方的锁。如果没有 sleep,执行顺序就不可控,可能出现某个线程一口气拿到两把锁,另一个线程还没开始执行的情况,无法构造出死锁
  • 两个线程的第二次交叉加锁都没执行到,说明这两个线程都在第二次 synchronized 的时候阻塞住了,如果不人为干预,就会永远堵在这image.png|659

三、有 N 个线程,M 个锁

经典模型:哲学家就餐问题:image.png|313


死锁的四个必要条件

死锁的四个必要条件:(缺一不可)

  1. 锁是互斥的[锁的基本特性]
    • 基本特性,无法干预
  2. 锁是不可抢占的。线程 1 拿到了锁 A,如果线程 1 不主动释放 A,线程 2 就不能把 A 抢过来[锁的基本特性]
    • 基本特性,无法干预
  3. 请求和保持。线程 1 拿到锁 A 之后,在不释放 A 的前提下,去拿锁 B[代码结构]
    • 如何避免:
    • 先释放 A,再拿 B
    • 避免锁嵌套锁
  4. 循环等待/环路等待/循环依赖
    • 如何避免:
    • 一个简单有效的方法:给锁编号,1、2、3、4、…、N
      约定所有的线程在加锁的时候,都不许按照一定的顺序来加锁(比如,必须针对编号小的锁进行加锁,后针对编号大的锁进行加锁)
    • 约定加锁顺序
public class Demo1 {  
    private static Object locker1 = new Object();  
    private static Object locker2 = new Object();  
  
    public static void main(String[] args) {  
        Thread t1 = new Thread(() -> {  
            synchronized (locker1){  
                System.out.println("t1 加锁 locker1 完成");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }                
                synchronized (locker2){  
                    System.out.println("t1 加锁 locker2 完成");  
                }            
            }        
        });        
        Thread t2 = new Thread(() -> {  
            synchronized (locker1) {  
                System.out.println("t2 加锁 locker1 完成");  
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    throw new RuntimeException(e);  
                }  
                synchronized (locker2){  
                    System.out.println("t2 加锁 locker2 完成");  
                }            
            }        
        });       
        t1.start();  
        t2.start();  
    }
}
输出结果:
t1 加锁 locker1 完成
t1 加锁 locker2 完成
t2 加锁 locker1 完成
t2 加锁 locker2 完成
  • 将上锁的顺序都改为先上 locker1,再上 locker2 就能解决这里的死锁问题了

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

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

相关文章

vulnhub靶机实战_DC-8

一、靶机下载 靶机下载链接汇总&#xff1a;https://download.vulnhub.com/使用搜索功能&#xff0c;搜索dc类型的靶机即可。本次实战使用的靶机是&#xff1a;DC-8系统&#xff1a;Debian下载链接&#xff1a;https://download.vulnhub.com/dc/DC-8.zip 二、靶机启动 下载完…

C++编程基础的学习

Qt跨平台特性 在深入探讨C编程的基础知识之前&#xff0c;我们首先需要了解Qt框架的跨平台特性。Qt是一个功能强大的跨平台应用程序框架&#xff0c;它允许开发者编写一次代码&#xff0c;然后在多个平台上运行&#xff0c;包括Windows、macOS、Linux、iOS、Android等。这种跨…

Linux网络编程3

并发服务器 1.TCP多进程并发服务器 服务器端&#xff1a; 客户端&#xff1a; 2.TCP多线程服务器 服务器端&#xff1a; 客户机端&#xff1a; 需要学习的函数还有 1. send() 函数 send() 函数用于在套接字上发送数据。它是网络编程中发送数据到对端的主要函数之一。 函数…

人像修图-高低频磨皮

原理 将图像分成高频图层&#xff08;处理纹理细节&#xff09;和低频图层&#xff08;处理光影和光影&#xff09;&#xff0c;以达到修饰时互不干扰 步骤 复制两个图层 在低频图层建立高斯模糊&#xff1a;滤镜->模糊->高斯模糊。注意半径一般根据皮肤占比&#xf…

day 20进程

一、程序和进程的区别 程序&#xff1a;保存在磁盘空间中的的一段代码的集合&#xff0c;死的 进程&#xff1a;是一个程序动态执行的过程&#xff0c;包括进程的创建、调度和消亡的过程 二、进程相关的命令 PID:进程的标识符(进程的ID) PPID:父进程的ID号 三、进程的创建…

redis的数据结构与对象

简单动态字符串 文章目录 简单动态字符串SDS的定义SDS的结构图示结构SDS字段解析SDS的特点 SDS和字符串的区别常数复杂度获取字符串的长度杜绝缓冲区的溢出减少修改字符串时的内存分配次数二进制安全兼容部分c字符串函数总结 链表链表和链表节点的实现链表节点&#xff08;list…

全球手机基站位置数据,包含(2G-5G)基站

OpenCellID 是一个由社区维护的项目&#xff0c;它提供了一个开放的数据集&#xff0c;包含全球各地的移动通信基站信息。这个项目对于需要获取蜂窝网络基础设施详细信息的研究人员、开发者以及组织来说非常有用。这些信息可以被用来进行各种分析和应用开发&#xff0c;例如改进…

Python 在开发中的设计模式有哪些?怎样使用?

大家好&#xff01;我是爱摸鱼的小鸿&#xff0c;关注我&#xff0c;收看每期的编程干货。 今天我们要聊点硬核的——设计模式。不过&#xff0c;不用担心&#xff0c;我会带着热情来跟你分享这些看似枯燥的知识点。让我们一起从“代码搬砖工”蜕变成“代码艺术家”吧&#xff…

Redis面试题大全

文章目录 Redis有哪几种基本类型Redis为什么快&#xff1f;为什么Redis6.0后改用多线程?什么是热key吗&#xff1f;热key问题怎么解决&#xff1f;什么是热Key&#xff1f;解决热Key问题的方法 什么是缓存击穿、缓存穿透、缓存雪崩&#xff1f;缓存击穿缓存穿透缓存雪崩 Redis…

python爬虫预备知识三-多进程

python实现多进程的方法&#xff1a;fork、multiprocessing模块创建多进程。 os.fork方法 os.fork方法只适合于unix/linux系统&#xff0c;不支持windows系统。 fork方法调用一次会返回两次&#xff0c;原因在于操作系统将当前进程&#xff08;父进程&#xff09;复制出一份…

ESP8266使用舵机以及16路PWM舵机PCA 9685的使用方式

PWM全称 50Hz也就是一秒内变换50次 根据上面的公式 一个高电平一个低电平叫一个脉冲。 例如每个脉冲占20毫秒&#xff0c;那么他的频率是多少&#xff1f; 就是用1去除以他的周期&#xff0c;也就是我们上面说的20&#xff0c;那么就是除0.02,1秒等于1000毫秒&#xff0c;20…

网络安全 - 应急响应检查表

前言 本项目旨在为应急响应提供全方位辅助&#xff0c;以便快速解决问题。结合自身经验和网络资料&#xff0c;形成检查清单&#xff0c;期待大家提供更多技巧&#xff0c;共同完善本项目。愿大家在应急之路一帆风顺。 图片皆来源于网络&#xff0c;如有侵权请联系删除。 一…

南山智尚10亿元定增质疑声连连,与控股股东超70亿资金往来引瞩目

《港湾商业观察》施子夫 王璐 近期&#xff0c;南山智尚&#xff08;300918.SZ&#xff09;发布了《向特定对象发行A股股票募集说明书(修订稿)》。 据了解&#xff0c;公司此次拟募集资金总额不超过10亿元&#xff0c;扣除发行费用后的募集资金净额将全部用于年产8万吨高性能…

鸿蒙HarmonyOS实战:ArkUI组件添加内容背景模糊效果

动画效果可以丰富界面的细节&#xff0c;提升UI界面的真实感和品质感。例如&#xff0c;模糊和阴影效果可以让物体看起来更加立体&#xff0c;使得动画更加生动。ArkUI提供了丰富的效果接口&#xff0c;开发者可快速打造出精致、个性化的效果。本章中主要对常用的模糊、阴影和色…

速卖通、Lazada、虾皮卖家是如何自建买家账号测评的?

在跨境电商领域&#xff0c;速卖通、Lazada、Shopee等平台上的卖家为了提升店铺信誉、提高产品排名和销量&#xff0c;常常需要借助买家账号进行测评。然而&#xff0c;依赖外部服务商往往存在风险&#xff0c;如账号质量参差不齐、恶意差评等问题。因此&#xff0c;自己养国外…

Crowd-SAM:SAM作为拥挤场景中目标检测的智能标注器

摘要 目标检测是一项重要任务&#xff0c;广泛应用于各种场景。通常&#xff0c;它需要大量的标签进行训练&#xff0c;这相当耗时&#xff0c;尤其是在拥挤的场景中。最近&#xff0c;Segment Anything Model&#xff08;SAM&#xff09;作为一种强大的零样本分割器应运而生&…

数学中常用字母符号读法

文章目录 一、希腊字母二、其他字母1.字母上一横2.拉长的s&#xff08;‌∫&#xff09;‌3.数列中的e4. N:非负整数集合或自然数集合{0,1,2,3,…n} 一、希腊字母 Ω ω&#xff1a;欧米伽 Omega。 ∑ σ&#xff1a;西格玛 Sigma。 作用&#xff1a;是一个求和符号&#xf…

滑动窗口专题——找到所有字母的异位词

一、题目解析&#xff1a; 题意如图 二、算法分析 本题依旧是两种方法&#xff1a;暴力枚举、滑动窗口 暴力枚举&#xff1a; 枚举出所有的字串进行比较&#xff0c;符合则记录位置&#xff0c;最终返回结果数组。 滑动窗口哈希表&#xff1a; 思路&#xff1a;1、初始化左…

怎么区分Alpha因子和风险因子?

这是一个絮絮叨叨的专题系列&#xff0c;跟大伙儿唠一唠量化相关的小问题&#xff0c;有感而发写到哪算哪&#xff0c;这是第3期&#xff0c;来唠个14块钱的~ 不知大伙儿有木有这样的疑惑&#xff1f; 看到Barra里面有Size、Liquid等因子&#xff0c;这些因子同样出现在很多人的…

【Redis】String字符串

目录 String字符串 常见命令 SET GET MSET MGET SETNX 计数命令 INCR INCRBY DECY DECYBY INCRBYFLOAT 其他命令 APPEND GETRANGE SETRANGE STRLEN 内部编码 String类型的典型使用场景 缓存(Cache)功能 计数功能 共享会话(Session) String字符串 字符…