多线程进阶
进阶的内容,就关于线程的面试题为主了,涉及到的内容在工作中使用较少,但面试会考!!!
锁的策略
加锁的过程中,在处理冲突的过程中,涉及到的一些不同的处理方法,此处的锁策略,并非是 Java 独有的,需要重点理解一些相关的概念。
1. 乐观锁 和 悲观锁
这是两种不同的锁的实现方式
乐观锁: 在加锁之前,预估当前出现锁冲突的概率不大,因此在进行加锁的时候,就不会做太多的工作。加锁过程中做的事情比较少,加锁的速度可能就更快,但是更容易引入一些其他的问题。(可能会消耗更多的 CPU 资源)
悲观锁:在加锁之前,预估当前出现锁冲突的概率比较大,因此在进行加锁的时候,做的事情就会更多,加锁的速度可能更慢,但是整个过程中不容易出现其他的问题。
2. 轻量级锁 和 重量级锁
轻量级锁:加锁的开销小,加锁的速度更快 ==》 轻量级锁,一般就是乐观锁
重量级锁:加锁的开销大,加锁的速度更慢 ==》 重量级锁,一般即是悲观锁
轻量级和重量级锁:是加锁之后,对结果的一种评价
乐观锁和悲观锁:是在加锁之前,对未发生的事情进行的一种评估
但整体来说,这两种角度,描述的是同一件事情
3. 自旋锁 和 挂起等待锁
自旋锁:是轻量级锁的一种典型实现。进行加锁的时候,搭配一个 while 循环,如果加锁成功,自然循环结束。如果加锁不成功,不是阻塞放弃 CPU,而是进行下一次循环,再次尝试获取到锁。
这个反复快速执行的过程,就称为“自旋”,一旦其他线程释放了锁,就能第一时间拿到锁,同时,这样的自旋锁,也是乐观锁,使用自旋锁的前提:就是预期锁冲突的概率不大,其他线程释放了锁,就能第一时间拿到锁。万一当前加锁的线程特别多,自旋的意义就不大了,白白浪费 CPU 了
挂起等待锁:就是重量级锁的一种典型实现,同时也是一个悲观锁,在进行挂起等待的时候,就需要内核调度器介入了,这一块要完成的操作就很多了,真正获取到锁要花费的时间也就多了。但这个锁是可以适应于锁冲突激烈的情况。
举个栗子:我是一个资深舔狗,每天都会向女神问候:早安午安晚安。有一天,我向女神表白,“女神女神,你能不能做我的女朋友”(尝试加锁),女神给了我一个字:“滚”。
被女神拒绝之后,我有两种处理方式:
1. 放弃了~~ 从此再也不联系女神了 ==》 我不想联系女神了,进入了阻塞等待,我就把 CPU 让出来了,可以安心学习了(但嘴上说,再也不联系了,身体上还是很诚实。)某一天,我通过其他途径,听说女神分手了,我的心思又活泛起来了,情不自禁又来找女神了,又尝试对女神加锁~~ (如果女神确实分手了,我是有可能上位的)
但注意:这种策略中,我们获知女神分手了之后这个消息,一般是会在发生这个事情之后几个月才听说,我再尝试加锁,就不会像自旋锁那么快~~
线程一旦进入阻塞,就需要重新参与系统的调度,什么时候能够再调度上 CPU 就是未知数了~~
但是这种策略的好处是,在阻塞的过程中,把 CPU 的资源让出来了,可以趁机做一点其他的事情~~(即使有很多人都是女神的备胎,也没关系,反正我是阻塞等待,我能趁机去学习,当备胎 2 3 4 5 号他们和女神相处,我依然学我的,他们全分手了,我再去~~)
2. 坚信一个道理:只要锄头挥的好,没有墙角挖不倒。仍然每天向女神问候早安午安晚安,时不时的再表白一次。这种方式,就是自旋锁。
当然这种情况,一旦女神分手了,我的机会就来了,就有很大的可能性,乘虚而入,一举加上锁~~~
加锁消耗的时间就比较短,这边一释放,我立即就加上锁。但是缺点就是比较消耗 CPU,每天都得花时间和女神交流(导致我这边就没有心思干别的事情)
自旋锁也是乐观锁,预估了锁竞争不太激烈才能使用,试想一下,如果女神的备胎不止我一个,有十几个兄弟都是备胎,也和我一样每天早安午安晚安一样问候,此时,女神就算分手了,也不一定轮得到我~~
Java 中的 synchronized 呢
Java 中的 synchronized 算那种情况呢? ==》 synchronized 具有自适应的能力!!!
synchronized 在某些情况下,乐观锁 / 轻量级锁 / 自旋锁,在某些情况下,悲观锁 / 重量级锁 / 挂起等待锁
synchronized 的内部会自动的评估当前锁冲突的激烈程度。
如果当前锁冲突的激烈程度不大,就处于 乐观锁 / 轻量级锁 / 自旋锁
如果当前锁冲突的激烈程度很大,就处于 悲观锁 / 重量级锁 / 挂起等待锁
上面 synchronized 会自适应的本质就是 JVM 的大佬们(他暖,我哭),为了让我们这些菜鸟程序员轻松一些,引入的一些优化方式,我们其实并不需要知道这几个锁策略具体是啥意思,就无脑用 synchronized 一般就不会有什么问题,并且还很高效~~~
4. 普通互斥锁 和 读写锁
普通读写锁:就类似于 synchronized 操作会涉及到 加锁 和 解锁
读写锁: 这里的读写锁,就把加锁分为两种情况了
1. 加“读”锁
2. 加“写”锁“
读锁和读锁之间,不会出现锁冲突(不会阻塞);写锁和写锁之间,会出现锁冲突(会阻塞);读锁和写锁之间,会出现锁冲突(会阻塞)
一个线程加 读锁 的时候,另一个线程,只能读,不能写
一个线程加 写锁 的时候,另一个线程,不能写,也不能读
为什么要引入读写锁呢?
如果两个线程读,本身就是线程安全的!!!不需要进行互斥!!!
如果使用 synchronized 这种方式加锁,两个线程读,也会产生互斥,产生阻塞...这样的话又没必要,又会对性能产生一定的损失~~
完全给读操作不加锁,也不行,就怕一个线程读操作,一个线程写操作,可能会读到写了一般的数据...
读写锁,就可以很好的解决上述问题~~~ 读写锁就能把这些并发读之间的锁冲突的开销给省下,对于性能的提升十分明显~~~
在标准库中,也提供了专门的类,实现读写锁(本质上还是系统提供的读写锁,提供 API,JVM 中封装了 API 给 Java 程序员使用~~~),这里暂不介绍~~~
5. 公平锁 和 非公平锁
和前面提过的”线程饿死“有一点关系
公平锁:遵守”先来后到“,谁先来的,谁就在锁被释放后先获得
非公平锁:不遵守”先来后到“
举个栗子:
当女神和男票恋爱中,兄弟们都在当备胎等待,A 兄弟已经追女神 1 年,B 兄弟追女神 1 个月,C 兄弟昨晚上才开始追女神
当女神分手后:公平锁的情况下,A 号大兄弟是最开始舔的,他就嗖嗖的上位追女神了,剩下两位老哥就继续等着
非公平锁:三位大兄弟不管谁先开始舔的,对着女神就是一拥而上~~~
注意:
操作系统内部的线程调度就可以视为是随机的,如果不做任何额外的限制,锁就是非公平的。如果要想实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序
公平锁和非公平锁并没有好坏之分,关键还是看使用场景
6. 可重入锁 和 不可重入锁
可重入锁:”可以重新进入的锁“,即允许同一个线程多次获取到同一把锁,且不会死锁。比如一个递归函数中里面又加锁操作,递归的过程中,这个锁如果不会阻塞自己,那么这个锁就是可重入锁(因此,可重入锁也叫做递归锁)。可重入锁中需要记录持有锁的线程是谁,加锁的次数的计数
不可重入锁:一个线程针对一把锁,连续加锁两次,会产生死锁。
Java 里只要一 Reentrant 开头命名的都是可重入锁,而且 JDK 提供的所有线程的 Lock 实现类,包括 synchronized 都是可重入的锁
理解“把自己锁死”:
一个线程没有释放锁,然后又尝试加锁
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待,直到第一次的锁被释放,才能获取到第二个锁,但是释放第一个锁也是由该线程来完成,结果这个线程已经躺平了,啥也不相干了,也就无法进行解锁操作,这样就会死锁
上面描述的锁,即为不可重入锁
上面的“锁策略”就是一大堆的名词解释,我们需要对这些词,有一个概念上的认识即可。
例如,对于 synchronized 来说
1. 乐观锁 / 悲观锁 自适应
2. 轻量级锁 / 重量级锁 自适应
3. 自旋锁 / 挂起等待锁 自适应
4. 不是读写锁
5. 非公平锁
6. 可重入锁
对于系统原生的锁(Linux 提供的 mutex 这个锁)
1. 悲观锁
2. 重量级锁
3. 挂起等待锁
4. 不是读写锁
5. 非公平锁
6. 不可重入锁
synchronized 内部的工作原理
synchronized 的内部优化是非常好的,大部分情况下,使用 synchronized 不会有什么问题
但 synchronized 的加锁过程,尤其是“自适应” 的过程,到底是怎么回事呢???
当线程执行到 synchronized 的时候,如果当前这个对象处于未加锁的状态,就会经历以下过程:
1. 偏向锁阶段
核心思想是:懒汉模式,能不加锁,就不加锁,能晚加锁,就晚加锁。所谓偏向锁,并非是真的加锁了,而是做了一个非常轻量的标记。
换句话说,就是搞暧昧,就是偏向锁,只是做一个标记,并没有真的加锁(也不会有互斥),但如果发现有其他线程,来和我竞争这个锁,就会在另一个线程之前,先把锁获取到。然后就会从偏向锁升级为轻量级锁(升级为轻量级锁就是真加锁了,存在互斥了~)
如果在搞暧昧的过程中,没有人来竞争,就把加锁这样的操作就完全省略了~~~
即非必要 不加锁 在遇到竞争的时候,偏向锁并没有提高效率,但是,如果在没有竞争的情况下,偏向锁也就大幅度的提高了效率~~
举个栗子:
假设我是一个美女(好看 & 有才华 & 琴棋书画样样俱全 & 有心计 & 时间管理大师)
我谈了一个男票,谈了一段时间之后,如果我厌烦了,想换一个男朋友,效率是比较低的,要做两个事情:
1. 想办法和现在的男朋友分手~~~(各种吵架,没事找事,冷暴力...)
2. 和下一个小哥哥培养感情
我们前面引入的池的概念就是对第二件事的优化 --> “备胎池”
那怎么对第一阶段进行优化呢?? ==》 搞暧昧~~~
当我最开始和这个小哥哥谈恋爱的时候,我就不和他确认关系~~~
有情侣之实,但是无情侣之名,我们俩每天在一起干的事情,都是情侣之间的事情,但每当小哥哥提到说,我们是什么关系的时候,我就笑而不答,或者扯开话题。
如果未来有一天,我厌倦了,我就直接可以把他拉黑,踹开一边即可,如果他再来纠缠我,我就补充一句,“我们只是朋友~~~”
当前我在和这个小哥哥搞暧昧的时候,如果我突然发现,有其他的妹子,也在试图接近我家小哥哥,这个时候,我就立即和小哥哥确定关系,并且让其他妹子,离我家哥哥远点~~~(一切尽在掌握之中,哥哥当然不会拒绝我~)
2. 轻量级锁阶段
(假设有竞争 但不多)
此处是通过自旋锁的方式来实现的。
优势: 另外的线程把锁释放到了,就会第一时间拿到锁
劣势: 比较消耗 CPU 资源
与此同时, synchronized 内部也会统计,当前这个锁对象上,有多少个线程在参与竞争,这里当发现参与竞争的线程比较多了,就会进一步的升级到重量级锁(对于自旋锁来说,如果同一个锁竞争者很多,大量的线程都在自旋,整体 CPU 的消耗就很大了)
补充:偏向锁标记,是锁对象里面的一个属性,每个锁对象都有自己的标记,当这个锁首次被加锁的时候,先进入偏向锁,如果这个过程中,没有涉及到锁竞争,下次加锁还是先进入偏向锁,一旦这个过程中升级成为轻量级锁了,后续再针对这个对象加锁,都是轻量级锁了(跳过了偏向锁~~~)
3. 重量级锁阶段
此时拿不到锁的线程就不会再继续自旋了,而是进入“阻塞等待”,让出 CPU(不会让 CPU 的占用率太高)当线程释放锁的时候,就会由系统内核随机唤醒一个线程来获取锁了
到底多少个线程算多呢???这是 JVM 源码里面的,我们要重点关注的是,会有这种”策略“,参数是可以随时调整的,策略是通用的!
锁消除
也是 synchronized 中内置的优化策略,是编译器优化的一种方式,编译器在编译代码的时候,如果发现这个代码,不需要加锁,就会自动的把锁给干掉~~~
但这里的优化是比较保守的,比如,就只有一个线程,在这一个线程里加锁了,或者说,加锁代码中,并没有涉及到”成员变量的修改“,只是一些局部变量的修改(如果加锁代码块中只涉及局部变量的修改,而没有对成员变量(类的属性)进行修改,也不需要加锁。这是由于局部变量是线程私有的,每个线程都有自己独立的副本,不会出现多个线程同时访问同一个局部变量的情况,也就不会有数据竞争问题。),是不需要加锁的。
其他模棱两可的情况,编译器也不确定的时候,是不会去消除的。
锁消除 ==》 针对 一眼看上去就完全不会涉及到线程安全问题的代码,就能够把锁消除掉
锁粗化
会把多个细粒度的锁,合并成一个粗粒度的锁
(synchronized { } 大括号里面包含的代码越少,就会认为锁的粒度越细,包含的代码越多,就会认为锁的粒度越粗)
通常情况下,让锁的粒度细一些, 是有利于多个线程并发执行的,但也有点时候,希望锁的粒度粗一些~~~
如上图,在代码执行的过程中,涉及很多加锁和解锁,即锁的粒度较细,每次加锁都是有可能涉及到阻塞的
如下图,编译器就会把三次细锁粒度的锁合并成一个粗粒度的锁了 ==》 粗化也是为了提高效率~~~
小结:
synchronized 背后是涉及了很多很多的“优化手段”
1. 锁升级 ==》 偏向锁 -> 轻量级锁 -> 重量级锁
2. 锁消除 ==》 自动干掉不必要的锁
3. 锁粗化 ==》 把多个细粒度的锁合并成一个粗粒度的锁,减少锁竞争的开销
这些机制都是在内部默默发挥作用的,是 JVM 的大佬为我们默默奉献的(他暖,我哭~~~)
CAS
什么是 CAS
CAS:Compare and swap,字面意思:“比较并交换”,是一个特殊的 CPU 指令(严格的说,和 Java 无关)(JVM 中 关于 CAS 的 API 都是放在 unsafe 包里的,unsafe 即不安全的)
一个 CAS 就会涉及到一下操作:我们假设内存中的原数据为 V,寄存器中的值是 A,需要修改的是新值 B,会有三个操作:
1. 比较原数据 V 和寄存器中的值 A 是否相等 (比较)
2. 如果比较相等,把 B 写入 V。(交换)
3. 返回操作是否成功
CAS 伪代码
其中,address 是内存地址中的值 expectValue 是寄存器中的旧值,swapValue 是寄存器中的新值。 if 语句中判断条件是,比较 address 内存地址中的值,是否和 expected 寄存器中的值相同,如果相同,就把 swap 寄存器的值和 address 内存中的值,进行交换,返回 ture;如果不相同,则啥都不敢,返回 false。(说是交换,也可以理解为“赋值”,我们往往只关注内存里最终的值,寄存器用完就不需要了~~)
CAS 一条 CPU 指令 就可以完成我们上述的功能 ==》单个 CPU 指令,本身就是原子的
CAS 的线程安全问题
基于 CAS 指令,就给线程安全问题的代码,打开了一个新世界的大门!!!我们之前为了实现线程安全,往往都是依靠加锁来保证的,但一旦有了加锁,就会导致阻塞,从而就会引起性能降低。
使用 CAS,不涉及加锁,就不会导致阻塞,合理使用也是可以保证线程安全的 ==》 无所编程(是多线程编程中的一个特殊技巧)
CAS 本身的 CPU 指令,操作系统又对指令进行了封装,JVM 又对操作系统提供的 API 封装了一层,有的 CPU 可能会不支持 CAS (但我们 x86 这种主流 CPU 都是没问题的)
Java 中的 CAS 的 API 放到了 unsafe 包里面(这里面的操作, 涉及到一些系统底层的内容,使用不当的话可能会带来一些风险,一般不建议直接使用 CAS)
Java 的标准库,对于 CAS 又进行了进一步的封装,提供了一些工具类,供程序员们使用。
最主要的一个工具,叫做 “原子类”
在这个类中,就进行了一些封装,比如对 Integer 和 Long进行了封装,针对这样的对象进行多线程修改,就是线程安全的了。
示例代码:
这个代码就是我们之前典型的多线程可能不安全的代码,如果定义 count 的时候,是使用 private static int count = 0,然后在线程中均使用 count++的话,就是线程不安全的!!!
但是,如果是用 AtomicInteger 定义 count(此时是一个对象了),初始值传入参数为 0,然后再线程中,使用 getAndIncrement 方法,代替了 后置++,此时这个方法,就是通过 CAS 的方式实现的,这里的代码,就没有加锁,但也能保证线程的安全!!(并且这个代码要更为高效,没有锁,也就没有阻塞,也就不会损耗效率)
之前的 count++ 是三个指令(多线程的三个指令,会相互的穿插执行,引起线程不安全,之前加锁,就是为了能让三个指令称为原子的)此处,这里的 getAndIncrement 对变量的修改,是 CAS 指令,CAS 指令本身就只是一条 CPU 指令,天然就是原子的
原子类自增的源代码:
标准库中的代码,看起来有点复杂,我们可以用一段伪代码来理解:
再这段伪代码中,oldValue 期望是一个放在寄存器里面的值,这个值就是初始化成 AtomicInteger 里面保存的整数值 value,如果内存地址的值 value 和 寄存器里面的值 oldValue 比较相同,则可以交换,oldValue + 1 和 value 交换,然后循环结束,此时 value 已经更新成 value + 1了,如果没成功,就再来一次,直到成功为止
画图讲解:
如下图为多线程情况下:
最开始我们初始化 value 为 0
多线程执行 ==》
t1 线程,将 value 赋值给 oldValue
然后调度到 t2 线程执行,t2 线程也赋值 oldValue 为 0
然后 t2 线程进入 while 循环,比较 value 和 oldValue 此时均为 0,此时还有一个寄存器三,为 oldValue + 1(即此时为 1)
会将 oldValue + 1 寄存器中的值 1 和 内存中的 0 进行交换
这样就通过线程 2,将 value 从 0 -> 1,将 value 重新赋给 oldValue 返回 oldValue。
然后 t1 线程又被调度上来了,再执行 t1 线程
注意,这个时候,t1 线程中再执行,value 的值已经由 0 变为 1了,但此时寄存器 1 的 oldValue 记录的仍然是 0,这里就会发现 value 和 oldValue 不同,意味着在 CAS 之前,另一个线程修改了 value(通过这个方式,能识别出是否有人修改)所以就不会进行交换,进入while 循环,将 value 的值,重新赋给 oldValue
然后再进入 while 循环,这时候 value 和 oldValue 的值就相同了,然后还有另一个寄存器存储 oldValue + 1
再进行交换,将 value 从 1 -> 2,然后将 value 再赋值给 oldValue 返回 oldValue
之前的线程不安全,是内存变了,但是寄存器中的值没有跟着变,接下来的修改操作就会出错了,但使用 CAS 这种方式,通过一次内存和寄存器值的比较,就能确保识别出内存的值是不是变了,不会,才会进行修改,如果变了,就会重新读取内存的值,确保是基于内存中的最新的值进行修改。非常巧妙的把之前的线程安全问题就解决了~~~
实现自旋锁
基于 CAS 实现更灵活的锁,获得到更多的控制权
自旋锁伪代码:
当 owner 不为 null 的时候,意味着锁已经被其他线程持有。此时,当前尝试获取锁的线程并不会进入阻塞状态(不会像传统锁机制下调用 wait 方法一样阻塞)而是在这个 while 循环中不停的执行(“忙等”)。持续的尝试 CAS 操作区获取锁,只要获取不成功就一直循环,不放弃 CPU 资源,但也不参与 CPU 调度中的线程上下文切换等调度流程,避免了调度开销~~~但是这种方式的缺点就是自旋的锁会一直占用 CPU,需要消耗更多的 CPU 资源。
CAS 的 ABA 问题
举个栗子:“翻新机”,我们以为买到的是一个新的极其,但实际上买到的是一个“二手的机器“,外表看起来是崭新的,但是内部已经是别人的形状了...
CAS 在使用的时候,关键要点是:判定当前内存的值是否和寄存器中的值是一样的 ==》 如果是一样的,就进行修改,不一样,就什么也不做。(本质上是判定,但是如果当前代码执行过程中,有其他线程穿插进来了...可能存在这样的情况,比如数值本来是 0,执行 CAS 之前,另一个线程把这个值从 0 -> 100,又从 100 -> 0,虽然最终的结果仍然是 0,但并不是没有别的线程穿插,而是其他线程穿插过程中,把值修改了,又改回去了)。一般来说,即使出现上述的情况,问题也不大,不会产生什么 bug,但是怕是一些极端的场景!!!
假设,去银行取钱~~~
初始情况下,账户余额 1000,要取 500。取钱的时候,ATM 卡了一下,按了一下没有反应(t1线程),又按了一下(t2线程)。此时产生了两个线程,去尝试进行扣款操作了。
如果是按照上述的方法来执行,是可以正常执行的,没有问题。
但如果,就在此时此刻,t3 线程,又给我们的账户,存了 500。此时,唉哟我嘞个豆
t1 线程执行到这里,就不知道,当前的 balance 中的 1000 是个什么情况了,是始终没有变化呢? 还是变了又变回来了...那 t1 线程,如果认为是没有变化,继续减 500,那岂不是我亏大了~~~
上面的 ATM 栗子,充满了假设和巧合,是一个非常极端的栗子
对于 ABA 问题,解决方案:
1. 约定数据变化是单向的(只能增加或者减少),不能是双向的(既能增加,又能减少)
2. 对于本身就必须双向变化的数据,可以给它引入一个版本号,版本号这个数字就是只能增加,不能减少的
补充: CAS 的操作,本是上还是 JVM 帮我们封装好的,上面所述的细节我们是没办法直接感知到的~~~
JUC(java.util.concurrent)的常见类
JUC 这个包里面,存放了一些进行多线程编程的时候的一些比较有用的类
Callable 接口
先回忆一下,我们之前创建线程的方法:
1. 继承 Thread(包含匿名内部类的方式)
2. 实现 Runnable(包含了匿名内部类的方式)
3. 基于 lambda 表达式
4. 就是基础 Callable(interface)
5. 基于线程池
Runnable 关注的是过程,不关注执行结果,Runnable 提供的 run 方法,返回值类型是 void,Callable 要关注执行结果,Callable 提供的 call 方法,返回值是线程执行任务得到的结果
如果我们要编写多线程代码,希望关注线程中代码的返回值的时候,创建一个新线程,用新的线程实现 1 + 2 + 3 +.. + 1000
代码实现如下:
虽然上面的代码可以解决问题,但并不“优雅”(要在主线程中获取到线程中的计算结果,还要再倒腾一个成员变量来获取)。
使用 Callable 可以更好的解决问题
这个泛型<V>,代表的是,期望线程的入口方法中,返回值的类型
这里我们希望返回值是一个整数 --> Integer
这样使用 Callable,就不需要引入额外的成员变量了,直接借助这里的返回值即可~~
但是,当我们传入的时候发现,Thread 并没有提供构造函数来传入 callable
这里我们可以引入一个 FutureTask类,来作为 Thread 和 callable 的”粘合剂“。
futureTask ==》 未来的任务(任务可能还没执行完呢),既然这个任务是在未来执行完毕,最终去取结果的时候,就需要有一个凭据,这个凭借就是 futureTask(举个栗子:吃麻辣烫,选好菜之后,服务员会给我们一个小牌子,小牌子上有号码,到时候拿小牌子取餐~~~这个小牌子,就是 futureTask)。此时代码也不需要 t.join() 了~~~
注意,futureTask.get() ,这个操作也是具有阻塞功能的,如果线程还没执行完毕,get 就会阻塞,等到线程执行完毕了,return 的结果,就会被 get 给返回回来!
Callable 其实是一个 ”锦上添花“ 的东西,它能干的事情,其实 Runnable 也能干,不过,对于这种带有返回值的任务,在多线程中使用 Callable 的确会更好一些,代码更直观,更简单~~~
不过还是需要重点理解 FutureTask 的作用!!!
完整代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class ThreadDemo46 {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result = 0;for (int i = 0; i < 1000; i++) {result += i;}return result;}};FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();System.out.println(futureTask.get());}
}
ReentrantLock
ReentrantLock:是一种可重入锁,与 synchronized 定位类似,都是用来实现互斥效果,保证线程安全(ReentrantLock 也是可重入锁,“Reentrant”的单词的原意就是“可重入”)
synchronized 也是可重入锁呀。上古时期的 Java 中,synchronized 不够强壮,功能也不够强大,也并没有我们上面所述的各种优化,ReentrantLock 就是用来实现可重入锁的选择(历史遗留问题)后来 synchronized 被各种优化的变的厉害了之后,ReentrantLock 就用的少了,但仍然有一席之地~~
ReentrantLock 是传统锁的风格,这个对象提供了两个方法:lock 和 unlock
这种写法,就容易引起,我们加了锁之后,忘记 unlock 解锁了。或者是,在 unlock 之前,触发了 return 或者 异常,就可能 unlock 执行不到了。==》 正确使用 ReentrantLock 就需要把 unlock 的操作放到 finally 里面。
ReentrantLock 与 synchronized 的区别
既然有了 synchronized(优化也非常好)那为什么还要有 ReentrantLock 呢???
1. ReentrantLock 提供了 tryLock 操作。lock 是直接进行加锁,如果加锁不成功,就会阻塞。但 trylock,是尝试进行加锁,如果加锁不成功,不会阻塞,会直接返回一个 false。(提供了更多的“可操作空间”)
2. ReentrantLock 提供了公平锁的实现(通过队列记录加锁线程的先后顺序)。synchronized 是非公平锁。在 ReentrantLock 构造方法中填写参数,就可以将其设置为公平锁
3. 搭配的等待通知机制不相同。对于 synchronized,搭配 wait / notify。 对于 ReentrantLock,搭配 Cindition 类,功能比 wait / notify 略强一点点,可以更精确控制唤醒某个指定的线程....
但是,在实际上绝大部分的开发中,使用 synchronized 就足够了!!!
信号量 Semaphore
信号量,用来表示“可用资源的个数”,本质上就是一个计数器。
举个栗子来理解:
可以把信号量想象成是停车场的展示牌:当前有车位 100 个,表示有 100 个可用资源。当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1(这个称为信号量的 P 操作),当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1(这个称为信号量的 V 操作)。如果计数器的值已经为 0 了,该尝试申请资源,就会阻塞等待,知道其他线程释放资源。
Semaphore 的 PV 操作中的加减计数器操作都是原子的,可用在多线程环境下直接使用。
信号量也是操作系统内部给我们提供的一个机制,操作系统对应的 API 被 JVM 封装了一下,就可以通过 Java 代码来调用这里的相关操作了~~~
信号量是更广义的锁!!!
所谓的锁,本之上也是一种特殊的信号量。锁,可用认为就是计数值为 1 的信号量。释放状态,就是计数值为 1 的信号量,加锁状态,就是计数值为 0 的信号量。对于这种非 0 即 1 的信号量,称为 “二元信号量”。
代码示例:
作为锁使用:
CountDwonLatch
CountDownLatch 是针对特定场景来解决问题的小工具
比如,多线程执行一个任务,把大的任务,拆分成几个部分,由每个线程分别执行。
举个栗子:“多线程下载”,例如 IDM 这样的软件。下载一个文件,这个文件可能很大,但是可用拆成多个部分,每个线程负责下载一部分,下载完成之后,最终把下载的结果都拼接到一起。在多线程下载的场景,最终执行完成之后,要把所有内容拼到一起,这个拼接必须要等到所有的线程执行完毕。
使用 CountDownLatch 就可以很方便的感知到上面的这个事情(所有的线程执行完毕)(比我们调用多次 join 要简单方便一些~~~)
如果使用 join 方式,就只能使每个线程只执行一个任务,借助 CountDownLatch 就可用让一个线程能执行多个任务~~~
示例代码:
线程安全的集合类
原来的集合类,大部分都不是线程安全的。但 Vector,Stack,Hashtable 是线程安全的,这三个类,在关键方法上加上了 synchronized,因此,这几个兄弟,无论如何都得加锁,哪怕单线程的时候,也需要加锁,这样的做法是不科学的,这几个好兄弟,现在官方已经不建议使用了,可能在未来的某个版本就删掉了...
多线程环境使用 ArrayList
1. 程序员自己按照情况使用同步机制(synchronized 或者 ReentrantLock)
前面有讲解,此处不做重复说明~
2. Collections.synchronizedList(new ArrayList)
这个包里面的方法,相当于是给 ArrayList 套了一个壳,ArrayList 本身的各种操作是不带锁的,但是通过上面的套壳操作之后,得到了新的对象,新的对象里面的方法就是都带有锁的,这样更方便我们灵活使用~~~
3. 使用 CopyOnWriteArrayList
这玩意是叫 写时拷贝
线程安全问题,本质上就是多个线程修改同一个数据的时候可能出现问题。
例如有一个顺序表如下:
如果多个线程,读这个程序表,是没有任何线程安全问题的。
但一旦有线程要修改里面的值,就可能引发线程安全问题
但如果使用CopyOnWriteArrayList,它如果发现有线程修改了里面的值,它就会把顺序表复制一份, 修改新的顺序表内容,并且修改引用的指向(这个操作是原子的,不需要加锁)
总结来讲就是:
当我们往一个容器添加元素的时候,不直接往容器里面添加,而是线将当前容器进行 Copy,复制出一个新的容器,然后在新的容器里面添加元素。添加完元素之和,再将原容器的引用指向新的容器。
这样做的好处是,可用对 CopyOnWrite 容器进行并发的读,而且不需要加锁,因为当前容器不会添加任何的元素。所以 CopyOnwrite 容器其实也是一种读和写分离的思想,读和写是不同的容器。
优点: 在读操作多,写操作少的场景下,性能不是很高,不需要加锁竞争
缺点:占用内存较多,并且新写的数据不能被第一时间读到
多线程环境使用哈希表
HashMap 本身不是安全的
在多线程环境下使用哈希表可用使用:Hashtable(在关键方法上添加了 synchronized) 和 ConcurrentHashMap
Hashtable
1. 只是简单的在关键方法加上了 synchronized 关键字
这相当于直接针对 Hashtable 对象本身加锁
此时,尝试修改两个不同链表的元素,都会触发锁冲突!!!(仔细观察,就会发现,如果修改两个不同链表上的元素,并不会涉及线程安全问题。如果修改的是同一个链表上的元素,才会可能涉及到线程安全问题~~~此时,针对同一个链表,是需要加锁的,如果针对的是不同链表进行操作,是不需要加锁的!!!)
2. size 属性也是通过 synchronized 来控制线程同步是,也会比较满
3. 一旦触发扩容,就由该线程完成整个扩容过程,这个过程就会涉及到大量的元素拷贝,效率非常低 ==》 不稳定~~~
ConcurrentHashMap
相比于 Hashtable 进行了一系列的改进和优化
(在 Java 1.7 及其之前,ConcurrentHashMap 是通过“分段锁”来实现的。给若干个链表分配一把锁,这样设定,不太合适,实现也复杂)
Java 1.8 中:
1. 读操作没有加锁了(但是使用了 volatile 保证从内存读取结果),只对写操作进行加锁,加锁的方式仍然是 synchronized,但不是锁整个对象,而是“锁桶”(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率。
ConcurrentHashMap 就是把锁变小了,给每一个链表都发了一个锁
此时,操作不同链表的时候,就不会产生锁冲突。而且上述设定,不会产生更多的空间代价。因为 Java 中任何一个对象都可用直接作为锁对象。本身哈希表中,就得有数组,数组的元素都是已经存在的,此时,只需要使用数组元素(链表头结点)作为加锁的对象即可。
2. 充分利用 CAS 特性:比如 size 属性通过 CAS 来及逆行更新,避免出现重量级锁的情况。
synchronized 虽然刚开始是偏向锁 / 轻量级锁,但是有可能升级为重量级锁,且过程是不可控的
3. 针对扩容操作的优化 --> 化整为零
扩容是一个重量操作,这里有一个概念是负载因子,即描述了每个桶上平均有多少个元素,当同上的链表的元素个数不是太多,就能达到 O(1) 时间复杂度
(负载因子不是 0.75!!!0.75 是负载因子默认的扩容阈值,不是负载因子本体。负载因子是我们算出来的数,用实际的元素个数 / 数组的长度,那我们算出来的值和扩容阈值进行比较,来看是否需要扩容)
如果桶上的链表的元素个数太多 ==》 1. 变成树 2. 扩容
扩容,即创建一个更大的数组,把就的 hash 表的元素都给搬运到新的数组上,如果 hash 表本身元素非常多,这里的扩容操作就会消耗很长的时间!!!(hash 表平时都很快,O(1),突然间某个操作非常慢,然后过一会就又快了,这样的表现是不稳定的,无法控制什么时候触发扩容)
ConcurrentHashMap 就优化为了化整为零,蚂蚁搬家~~~
1. 发现需要扩容的线程,会创建一个新的数组,同时只搬运几个元素过去
2. 扩容期间,新老数组同时存在。
3. 后续每个来操作 ConcurrentHashMap 的线程,都会参与搬运的过程,每个操作负责搬运一小部分元素~~~
4. 搬完最后一个元素,再把老的数组删掉
5. 这个期间,插入只往新数组中添加
6. 这个期间,查找需要同时查新数组和老数组~~~
HashMap 的扩容操作是一把梭哈,在某一次插入元素的操作中,整体完成扩容了
ConcurrentHashMap 则是每次操作都只搬运一部分元素
假设这里有 1kw 个元素,此时扩容的时候,每次插入 / 查找 / 删除,都会搬运一部分元素,一共会用多次搬运完成(花的时间会长一些,虽然总体时间变长了,但是每次操作的时间都不会很长,就避免出现很卡的情况了~~~)
完!!!
相关文章:
多线程进阶
进阶的内容,就关于线程的面试题为主了,涉及到的内容在工作中使用较少,但面试会考!!! 锁的策略 加锁的过程中,在处理冲突的过程中,涉及到的一些不同的处理方法,此处的锁…...
聊一聊接口测试时遇到第三方服务时怎么办
目录 一、使用 Mock 或 Stub 模拟第三方服务 二、利用第三方服务的沙箱(Sandbox)环境 三、测试隔离与数据清理 四、处理异步回调 五、容错与异常测试 六、契约测试 在我们进行接口测试时,有的时候会遇到要调用第三方服务即外部的API&am…...
《Python星球日记》第22天:NumPy 基础
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、NumPy 简介1. 什么是 NumPy?为什么使用 NumPy?2. 安…...
Spring Boot 中 Bean 的生命周期详解
Spring Boot 中 Bean 的生命周期详解 一、引言 在 Spring Boot 应用中,Bean 是构成应用程序的基础组件。理解 Bean 的生命周期对于开发高效、稳定的 Spring Boot 应用至关重要。本文将深入探讨 Spring Boot 中 Bean 的完整生命周期过程。 二、Bean 生命周期的基本…...
结构化需求分析:功能、数据与行为的全景建模
目录 前言1 功能模型:数据流图(DFD)的结构与应用1.1 数据流图的基本构成要素1.2 数据流图的层次化设计1.3 数据流图的建模价值 2 数据模型:ER图揭示数据结构与关系2.1 ER图的基本组成2.2 建模过程与注意事项2.3 数据模型的价值体现…...
OpenCompass模型评估
OpenCompass面向大模型的开源方和使用者, 提供开源、高效、全面的大模型评测开放平台。 一、OpenCompass文档 1.基础安装 使用Conda准备 OpenCompass 运行环境: conda create --name opencompass python3.10 -y conda activate opencompass2. 安装 Op…...
基于51单片机语音实时采集系统
基于51单片机语音实时采集 (程序+原理图+PCB+设计报告) 功能介绍 具体功能: 系统由STC89C52单片机ISD4004录音芯片LM386功放模块小喇叭LCD1602按键指示灯电源构成 1.可通过按键随时选择相应的录音进行播…...
NeuroImage:膝关节炎如何影响大脑?静态与动态功能网络变化全解析
膝骨关节炎(KOA)是导致老年人活动受限和残疾的主要原因之一。这种疾病不仅引起关节疼痛,还会显著影响患者的生活质量。然而,目前对于KOA患者大脑功能网络的异常变化及其与临床症状之间的关系尚不清楚。 2024年4月10日,…...
高级java每日一道面试题-2025年4月01日-微服务篇[Nacos篇]-Nacos集群的数据一致性是如何保证的?
如果有遗漏,评论区告诉我进行补充 面试官: Nacos集群的数据一致性是如何保证的? 我回答: Nacos 集群数据一致性保障机制详解 在 Java 高级面试中,Nacos 集群的数据一致性保障是考察分布式系统核心能力的关键点。以下是 Nacos 通过多种机制和技术确保…...
阿里云 OSS 在 ZKmall开源商城的应用实践
ZKmall开源商城通过深度整合阿里云OSS(对象存储服务),构建了高效、安全的文件存储与管理体系,支撑商品图片、用户上传内容等非结构化数据的存储与分发。结合阿里云OSS的技术特性与ZKmall的微服务架构,其实践方案可总结…...
【Linux】线程池与封装线程
目录 一、线程池: 1、池化技术: 2、线程池优点: 3、线程池应用场景: 4、线程池实现: 二、封装线程: 三、单例模式: 四、其他锁: 五、读者写者问题 一、线程池: …...
protobuf的应用
1.版本和引用 syntax "proto3"; // proto2 package tutorial; // package类似C命名空间 // 可以引用本地的,也可以引用include里面的 import "google/protobuf/timestamp.proto"; // 已经写好的proto文件是可以引用 我们版本选择pr…...
linux shell编程之条件语句(二)
目录 一. 条件测试操作 1. 文件测试 2. 整数值比较 3. 字符串比较 4. 逻辑测试 二. if 条件语句 1. if 语句的结构 (1) 单分支 if 语句 (2) 双分支 if 语句 (3) 多分支 if 语句 2. if 语句应用示例 (1) 单分支 if 语句应用 (2) 双分支 if 语句应用 (3) 多分支 …...
图论整理复习
回溯: 模板: void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {处理节点;backtracking(路径,选择列表); // 递归回溯ÿ…...
企业指标设计方法指南
该文档聚焦企业指标设计方法,适用于企业中负责战略规划、业务运营、数据分析、指标管理等相关工作的人员,如企业高管、部门经理、数据分析师等。 主要内容围绕指标设计展开:首先指出指标设计面临的困境,包括权责不清、口径不统一、缺乏标准规范、报表体系混乱、指标…...
AIP-217 不可达资源
编号217原文链接AIP-217: Unreachable resources状态批准创建日期2019-08-26更新日期2019-08-26 有时,用户可能会请求一系列资源,而其中某些资源暂时不可用。最典型的场景是跨集合读。例如用户可能请求返回多个上级位置的资源,但其中某个位置…...
SAP系统控制检验批
问题:同一批物料多检验批问题 现象:同一物料多采购订单同一天到货时,对其采购订单分别收货,导致系统产生多个检验批,需分别请检单、检验报告等,使质量部工作复杂化。 原因:物料主数据质量试图设…...
JavaScript 代码混淆与反混淆技术详解
一、代码混淆:让别人看不懂你的代码 混淆技术就是一种“代码伪装术”,目的是让别人很难看懂你的代码逻辑,从而保护你的核心算法或敏感信息。 1. 变量名压缩 原理:把变量名改成乱码,比如把calculatePrice改成a&#…...
Android studio | From Zero To One ——手机弹幕
===================================================== github:https://github.com/MichaelBeechan CSDN:https://blog.csdn.net/u011344545 ===================================================== 滚动显示 代码activity_main.xmlactivity_fullscreen.xmlAndroidManife…...
面向对象的需求分析与UML构造块详解
目录 前言1 面向对象的需求分析概述2 UML构造块概述3 UML事物详解3.1 结构事物(Structural Things)3.2 行为事物(Behavioral Things)3.3 分组事物(Grouping Things)3.4 解释事物(Annotational T…...
LeetCode 2843.统计对称整数的数目:字符串数字转换
【LetMeFly】2843.统计对称整数的数目:字符串数字转换 力扣题目链接:https://leetcode.cn/problems/count-symmetric-integers/ 给你两个正整数 low 和 high 。 对于一个由 2 * n 位数字组成的整数 x ,如果其前 n 位数字之和与后 n 位数字…...
RocketMQ深度百科全书式解析
一、核心架构与设计哲学 1. 设计目标 海量消息堆积:单机支持百万级消息堆积,适合大数据场景(如日志采集)。严格顺序性:通过队列分区(Queue)和消费锁机制保证局部顺序。事务…...
A2A与MCP Server:AI智能体协作与工具交互的核心协议对比
A2A与MCP Server:AI智能体协作与工具交互的核心协议对比 摘要 在AI智能体技术爆发式增长的今天,谷歌的A2A协议与Anthropic的MCP协议正在重塑AI系统架构。本文通过协议栈分层模型、企业级架构设计案例及开发者实践指南三大维度,揭示二者在AI生…...
如何将网页保存为pdf
要将网页保存为PDF,可以按照以下几种方法操作: 1. 使用浏览器的打印功能 大多数现代浏览器(如Chrome、Firefox、Edge等)都支持将网页保存为PDF文件。步骤如下: 在 Google Chrome 中: 打开你想保存为PDF…...
位运算与实战场景分析-Java代码版
一、为什么每个程序员都要掌握位运算? 在电商秒杀系统中,位运算可以快速判断库存状态;在权限管理系统里,位运算能用极小的空间存储复杂权限配置;在算法竞赛中,位运算更是高频出现的性能优化利器。这项看似…...
【“星睿O6”AI PC开发套件评测】+ Debian 系统安装及 sysbench 跑分对比
很荣幸这次可以得到机会评测 “星睿O6”AI PC开发套件。第一篇文章,我将分为两个部分: 官方 Debian 系统安装到 NVMEsysbench 跑分以及对比 RK3568 和 I712700KF 正文开始之前,忍不住还是想放几张开箱照片,板子实在是太精致了。…...
java——继承
继承是面向对象的三大特征之一,可以使得子类具有父类的属性和方法,还可以在子类中重新定义,追加属性和方法。继承是指在原有类的基础上,进行功能扩展,创建新的类型。 概念与作用 代码复用:继承能够避免重…...
STM32嵌入式开发从入门到实战:全面指南与项目实践
STM32嵌入式开发从入门到实战:全面指南与项目实践 一、STM32开发基础概述 1.STM32微控制器核心特性 STM32微控制器基于ARM Cortex - M内核,具备显著的架构优势。其32位处理能力,能够高效处理复杂的计算任务,相较于传…...
企业数据孤岛如何破
企业数据孤岛如何破 背景信息传统方式Flink CDC如何用技术之力 背景信息 在数字化转型的浪潮中,企业数据的价值正从“事后分析”向“实时驱动”快速迁移。企业需要快速、高效地将分散在不同系统中的数据整合起来,以支持实时分析和业务决策。诚然&#x…...
源码编译安装Nginx
源码编译安装Nginx 源码编译安装Nginx创建nginx服务用户安装编译环境依赖包下载Nginx源码构建编译选项,创建makefile文件编译安装nginx为Nginx创建服务单元设置Nginx开机自启服务 yum安装Nginxyum安装openresty 源码编译安装Nginx 如果需要最新版本及定制化模块可以通过源码安…...
查看容器内的eth0网卡对应宿主机上的哪块网卡
查看容器内的eth0网卡对应宿主机上的哪块网卡 问题描述解决办法1. 进入容器,查看网卡的iflink(接口链路索引)值方法1:方法2: 2. 从宿主机过滤查询到的iflink(接口链路索引)值3. 确定veth接口连接的网桥方法2: brctl查看连接到网桥的接口--推荐 4. 查看网桥连接的物理网卡 问题描…...
虚拟偶像“C位出道”:数字浪潮下的崛起与财富密码(3/10)
摘要:虚拟偶像作为数字时代的新宠,凭借数字技术与文化创意的深度融合,在全球范围内迅速崛起。从早期的简单2D形象到如今高度逼真、智能交互的3D虚拟偶像,其发展得益于计算机图形学、动作捕捉、AI等技术的进步。虚拟偶像不仅在娱乐…...
swift菜鸟教程13(函数)
一个朴实无华的目录 今日学习内容:1.Swift 函数1.1函数定义:使用关键字 func。1.2函数参数:以逗号分隔。1.3不带参数函数1.4元组作为函数返回值1.5没有返回值函数1.6函数参数名称1.6.1局部参数名1.6.2外部参数名 1.7可变参数1.8常量ÿ…...
MacOS红队常用攻击命令
MacOS红队常用攻击命令 1.自动化武器2.系统信息3.服务 & 内核信息4.快捷命令5.网络相关6.brew相关 / 软件包相关7.高权限命令8.创建一个管理员权限的后门用户 1.自动化武器 1、linPEAS LinPEAS 是一个脚本,用于在 Linux/Unix/MacOS 主机上搜索提权路径 2、me…...
无人机的振动与噪声控制技术!
一、振动控制技术要点 1. 振动源分析 气动振动:旋翼桨叶涡脱落(如叶尖涡干涉)、动态失速(Dynamic Stall)引发的周期性气动激振力(频率与转速相关)。 机械振动:电机偏心、传动轴不…...
如何使用 Spring Boot 实现分页和排序?
全文目录: 开篇语1. 创建 Spring Boot 项目2. 配置数据库连接3. 创建实体类4. 创建 Repository 接口5. 创建分页和排序服务6. 创建控制器7. 测试分页和排序请求示例:返回结果: 8. 总结 文末 开篇语 哈喽,各位小伙伴们,…...
浅谈编译型语言的运用
如大家所熟悉的,程序在执行之前需要一个专门的编译过程,把程序编译成机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了,程序执行效率高,依赖编译器,如 C/C、Golang 等,…...
知识了解02——了解pnpm+vite+turbo+monorepo的完整构建步骤(react子项目)
(1)初始化monorepo 1)创建项目目录并进入当前目录 2)初始化 pnpm 工作区,生成一个package.json文件 3)在项目根目录下创建 pnpm-workspace.yaml 文件,并定义工作区目录 (2)安装 Turborepo 1)安…...
MySQL 半同步复制,给数据找靠谱 “分身”
目录 一背景 二、MySQL 复制基础概念 为何需要 MySQL 复制 传统异步复制 半同步复制的诞生 三、MySQL 半同步复制原理详解 主要组件及作用 工作流程 半同步复制流程图 四、MySQL 半同步复制配置与代码示例 环境准备 主服务器配置 从服务器配置 示例说明 五、MyS…...
uniapp离线打包提示未添加videoplayer模块
uniapp中使用到video标签,但是离线打包放到安卓工程中,运行到真机中时提示如下: 解决方案: 1、把media-release.aar、weex_videoplayer-release.aar放到工程的libs目录下; 文档:https://nativesupport.dcloud.net.cn/…...
机器人零位标定修正流程介绍
如果想看运动学标定可以看看 机器人运动学参数标定, 一次性把运动学参数和零位标定等一起标定求解. 1. 零位标定 零位标定是机器人运动学标定中的一个重要步骤,其目的是校正机器人关节的初始位置误差。以下是需要进行零位标定的主要原因: 制造误差 在机…...
应用层通信报文设计
/* --------------------------------------------------------------- | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | --------------------------------------------------------------- | 状态 1byte | 保留字段 4byte | 数据长…...
一周学会Pandas2 Python数据处理与分析-Pandas2读取Excel
锋哥原创的Pandas2 Python数据处理与分析 视频教程: 2025版 Pandas2 Python数据处理与分析 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili Excel格式文件是办公使用和处理最多的文件格式之一,相比CSV文件,Excel是有样式的。Pandas2提…...
技术分享|iTOP-RK3588开发板Ubuntu20系统旋转屏幕方案
iTOP-3588开发板采用瑞芯微RK3588处理器,是全新一代AloT高端应用芯片,采用8nmLP制程,搭载八核64位CPU,四核Cortex-A76和四核Cortex-A55架构,主频高达2.4GHz。是一款可用于互联网设备和其它数字多媒体的高性能产品。 在…...
ubuntu 20.04 安装源码编译 ros humble过程
公司要兼容ros1还需要ros2 这个时候不得不使用ubuntu20.04 安装 humble 但实际上在20.04上安装humble是需要在源码编译的。 根据这个帖子 https://blog.csdn.net/m0_62353836/article/details/129730981 重写一份,以应对无法下载的问题 系统配置 #检查是否为UTF-8编码,是则跳…...
Ubuntu18.04.06安装window虚拟机,安装VirtualBox
VirtualBox官网没有支持Ubuntu18的版本,最低是ubuntu20; 但是现在用的系统是UBuntu18.04.06,又不能升级,查阅了很多办法,最终终于安装VirtualBox可用版本; 1,在Ubuntu18自带的软件应用市场,搜VirtualBox;…...
Matlab 四分之一车体被动悬架、pid、模糊控制和模糊pid控制
1、内容简介 Matlab 198-四分之一车体被动悬架、pid、模糊控制和模糊pid控制 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...
Linux-----驱动
一、内核驱动与启动流程 1. Linux内核驱动 Nor Flash: 可线性访问,有专门的数据及地址总线(与内存访问方式相同)。 Nand Flash: 不可线性访问,访问需要控制逻辑(软件)。 2. Linux启动流程 ARM架构: IRAM…...
用HTML和CSS绘制佩奇:我不是佩奇
在这篇博客中,我将解析一个完全使用HTML和CSS绘制的佩奇(Pig)形象。这个项目展示了CSS的强大能力,仅用样式就能创造出复杂的图形,而不需要任何图片或JavaScript。 项目概述 这个名为"我不是佩奇"的项目是一个纯CSS绘制的卡通猪形象…...
Qwen2.5-7B-Instruct FastApi 部署调用教程
1 环境准备 基础环境最低要求说明: 环境名称版本信息1Ubuntu22.04.4 LTSCudaV12.1.105Python3.12.4NVIDIA CorporationRTX 3090 首先 pip 换源加速下载并安装依赖包 # 升级pip python -m pip install --upgrade pip # 更换 pypi 源加速库的安装 pip config set g…...