当前位置: 首页 > news >正文

深入理解 Java 多线程:锁策略与线程安全

文章目录

  • 一、常见的锁策略
    • 1. 乐观锁&&悲观锁
    • 2. 读写锁
    • 3. 重量级锁&&轻量级锁
    • 4. 自旋锁
    • 5. 公平锁&&不公平锁
    • 6. 可重入锁 && 不可重入锁
  • 二、CAS
    • 1. 什么是 CAS
    • 2. CAS 是怎么实现的
    • 3.CAS 有哪些应用
      • 1) 实现原子类
      • 2) 实现自旋锁
    • 4. CAS 的 ABA 问题
      • 1)、什么是 ABA 问题
      • 2)、ABA 问题引来的 BUG
      • 3).解决方案
  • 三、Synchronized 原理
    • 1. 加锁工作过程
      • 1)偏向锁
      • 2)轻量级锁
      • 3)重量级锁
      • 4)其他的优化操作
  • 四、死锁
    • 1.死锁是什么
    • 2.那么如何避免死锁呢?
  • 结语


在这里插入图片描述

一、常见的锁策略

1. 乐观锁&&悲观锁

悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
乐观锁:
假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。

举个例子: 同学 A 和 同学 B 想请教老师一个问题.
同学 A 认为 “老师是比较忙的, 我来问问题, 老师不一定有空解答”. 因此同学 A 会先给老师发消息: “老师你忙嘛? 我下午两点能来找你问个问题嘛?” (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题. 如果得到了否定的答复, 那就等一段时间, 下次再来和老师确定时间. 这个是悲观锁.
同学 B 认为 “老师是比较闲的, 我来问问题, 老师大概率是有空解答的”. 因此同学 B 直接就来找老师.(没加锁, 直接访问资源) 如果老师确实比较闲, 那么直接问题就解决了. 如果老师这会确实很忙, 那么同学 B 也不会打扰老师, 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁.

这两种思路不能说谁优谁劣, 而是看当前的场景是否合适.
如果当前老师确实比较忙, 那么使用悲观锁的策略更合适, 使用乐观锁会导致 “白跑很多趟”, 耗费额外的资源.
如果当前老师确实比较闲, 那么使用乐观锁的策略更合适, 使用悲观锁会让效率比较低

Synchronized 初始使用乐观锁策略. 当发现锁竞争比较频繁的时候, 就会自动切换成悲观锁策略.

就好比同学 C 开始认为 “老师比较闲的”, 问问题都会直接去找老师.
但是直接来找两次老师之后, 发现老师都挺忙的, 于是下次再来问问题, 就先发个消息问问老师忙不忙, 再决定是否来问问题.

乐观锁的一个重要功能就是要检测出数据是否发生访问冲突. 我们可以引入一个 “版本号” 来解决.

举个例子:

假设我们需要多线程修改 “用户账户余额”
设当前余额为 100. 引入一个版本号 version, 初始值为 1. 并且我们规定 “提交版本必须大于记录当前版本才能执行更新余额”

  1. 线程 A 此时准备将其读出( version=1, balance=100 ),线程 B 也读入此信息( version=1, balance=100)
    在这里插入图片描述
  2. 线程 A 操作的过程中并从其帐户余额中扣除 50( 100-50 ),线程 B 从其帐户余额中扣除 20( 100-20 );
  3. 线程 A 完成修改工作,将数据版本号加1( version=2 ),连同帐户扣除后余额(balance=50),写回到内存中;
    在这里插入图片描述
  4. 线程 B 完成了操作,也将版本号加1( version=2 )试图向内存中提交数据( balance=80 ),但此时比对版本发现,操作员 B 提交的数据版本号为 2 ,数据库记录的当前版本也为 2 ,不满足 “提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略。就认为这次操作失败.
    在这里插入图片描述

2. 读写锁

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。

一个线程对于数据的访问, 主要存在两种操作: 读数据 和写数据.

两个线程都只是读一个数据, 此时并没有线程安全问题. 直接并发的读取即可.
两个线程都要写一个数据, 有线程安全问题.
一个线程读另外一个线程写, 也有线程安全问题.

读写锁就是把读操作和写操作区分对待. Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁

ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进行加锁解锁

其中,
读加锁和读加锁之间, 不互斥.
写加锁和写加锁之间, 互斥.
读加锁和写加锁之间, 互斥

注意, 只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了.
因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径

读写锁特别适合于 “频繁读, 不频繁写” 的场景中.

3. 重量级锁&&轻量级锁

锁的核心特性 “原子性”, 这样的机制追根溯源是 CPU 这样的硬件设备提供的.

  1. CPU 提供了 “原子操作指令”.
  2. 操作系统基于 CPU 的原子指令, 实现了 mutex 互斥锁.
  3. JVM 基于操作系统提供的互斥锁, 实现了 synchronized 和 ReentrantLock 等关键字和类.
    在这里插入图片描述

重量级锁: 加锁机制重度依赖了 操作系统 提供了 mutex;

大量的内核态用户态切换
很容易引发线程的调度

轻量级锁: 加锁机制尽可能不使用 mutex, 而是尽量在用户态代码完成. 实在搞不定了, 再使用 mutex.

少量的内核态用户态切换.
不太容易引发线程调度

如何理解用户态与内核态

想象去银行办业务. 在窗口外, 自己做, 这是用户态. 用户态的时间成本是比较可控的. 在窗口内, 工作人员做, 这是内核态. 内核态的时间成本是不太可控的. 如果办业务的时候反复和工作人员沟通, 还需要重新排队, 这时效率是很低的.

synchronized 开始是一个轻量级锁. 如果锁冲突比较严重, 就会变成重量级锁.

4. 自旋锁

按之前的方式,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度. 但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.
一旦锁被其他线程释放, 就能第一时间获取到锁.

理解自旋锁 vs 挂起等待锁
想象一下, 去追求一个女神. 当男生向女神表白后, 女神说: 你是个好人, 但是我有男朋友了~~
挂起等待锁: 陷入沉沦不能自拔… 过了很久很久之后, 突然女神发来消息, “咱俩要不试试?” (注意, 这个很长的时间间隔里, 女神可能已经换了好几个男票了).
自旋锁: 死皮赖脸坚韧不拔. 仍然每天持续的和女神说早安晚安. 一旦女神和上一任分手, 那么就能立刻抓住机会上位.

自旋锁是一种典型的 轻量级锁 的实现方式.

优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.
缺点: 如果锁被其他线程持有的时间比较久, 那么就会持续的消耗 CPU 资源. (而挂起等待的时候是不消耗 CPU 的)

synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的

5. 公平锁&&不公平锁

假设三个线程 A, B, C. A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待.
当线程 A 释放锁的时候, 会发生啥呢?

公平锁: 遵守 “先来后到”. B 比 C 先来的. 当 A 释放锁的之后, B 就能先于 C 获取到锁.
非公平锁: 不遵守 “先来后到”. B 和 C 都有可能获取到锁.

注意:
操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序. 公平锁和非公平锁没有好坏之分, 关键还是看适用场景.

6. 可重入锁 && 不可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。
Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
而 Linux 系统提供的 mutex 是不可重入锁.

理解 “把自己锁死”
一个线程没有释放锁, 然后又尝试再次加锁.
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会死锁
这样的锁称为不可重入锁.

synchronized 是可重入锁

二、CAS

1. 什么是 CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
比较 A 与 V 是否相等。(比较)
如果比较相等,将 B 写入 V。(交换)
返回操作是否成功。

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

CAS 可以视为是一种乐观锁. (或者可以理解成 CAS 是乐观锁的一种实现方式)

2. CAS 是怎么实现的

针对不同的操作系统,JVM 用到了不同的 CAS 实现原理,简单来讲:
java 的 CAS 利用的的是 unsafe 这个类提供的 CAS 操作;
unsafe 的 CAS 依赖了的是 jvm 针对不同的操作系统实现的 Atomic::cmpxchg;
Atomic::cmpxchg 的实现使用了汇编的 CAS 操作,并使用 cpu 硬件提供的 lock 机制保证其原子性。
简而言之,是因为硬件予以了支持,软件层面才能做到。

3.CAS 有哪些应用

1) 实现原子类

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的. 典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.

class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}

假设两个线程同时调用 getAndIncrement
1)1) 两个线程都读取 value 的值到 oldValue 中. (oldValue 是一个局部变量, 在栈上. 每个线程有自己的栈)

在这里插入图片描述
2) 线程1 先执行 CAS 操作. 由于 oldValue 和 value 的值相同, 直接进行对 value 赋值.

注意:
CAS 是直接读写内存的, 而不是操作寄存器.
CAS 的读内存, 比较, 写内存操作是一条硬件指令, 是原子的.

在这里插入图片描述
3) 线程2 再执行 CAS 操作, 第一次 CAS 的时候发现 oldValue 和 value 不相等, 不能进行赋值. 因此需要进入循环. 在循环里重新读取 value 的值赋给 oldValue

在这里插入图片描述
4) 线程2 接下来第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作.

在这里插入图片描述
5) 线程1 和 线程2 返回各自的 oldValue 的值即可.

通过形如上述代码就可以实现一个原子类. 不需要使用重量级锁, 就可以高效的完成多线程的自增操作. 本来 check and set 这样的操作在代码角度不是原子的. 但是在硬件层面上可以让一条指令完成这个操作, 也就变成原子的了.

2) 实现自旋锁

基于 CAS 实现更灵活的锁, 获取到更多的控制权.

自旋锁伪代码

public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}

4. CAS 的 ABA 问题

1)、什么是 ABA 问题

ABA 的问题:
假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A. 接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要
先读取 num 的值, 记录到 oldNum 变量中. 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z. 但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这个时候 t1 究竟是否要更新 num 的值为 Z 呢

到这一步, t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程.

在这里插入图片描述

2)、ABA 问题引来的 BUG

大部分的情况下, t2 线程这样的一个反复横跳改动, 对于 t1 是否修改 num 是没有影响的. 但是不排除一些特殊情况

下面我们举个例子:

假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50 操作.
我们期望一个线程执行 -50 成功, 另一个线程 -50 失败.
如果使用 CAS 的方式来完成这个扣款过程就可能出现问题.
正常的过程
1 . 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
2 . 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
3 . 轮到线程2 执行了, 发现当前存款为 50, 和之前读到的 100 不相同, 执行失败.
异常的过程
1 . 存款 100. 线程1 获取到当前存款值为 100, 期望更新为 50; 线程2 获取到当前存款值为 100, 期望更新为 50.
2 . 线程1 执行扣款成功, 存款被改成 50. 线程2 阻塞等待中.
3 . 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100 !!
4 . 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 再次执行扣款操作这个时候, 扣款操作被执行了两次!!! 都是 ABA 问题搞的鬼!!

3).解决方案

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期
CAS 操作在读取旧值的同时, 也要读取版本号.
真正修改的时候,
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了)
对比理解上面的转账例子

假设 滑稽老哥 有 100 存款. 滑稽想从 ATM 取 50 块钱. 取款机创建了两个线程, 并发的来执行 -50
操作.
我们期望一个线程执行 -50 成功, 另一个线程 -50 失败. 为了解决 ABA 问题, 给余额搭配一个版本号, 初始设为 1.
1 存款 100. 线程1 获取到 存款值为 100, 版本号为 1, 期望更新为 50; 线程2 获取到存款值为 100, 版本号为 1, 期望更新为 50.
2 线程1 执行扣款成功, 存款被改成 50, 版本号改为2. 线程2 阻塞等待中.
3 在线程2 执行之前, 滑稽的朋友正好给滑稽转账 50, 账户余额变成 100, 版本号变成3.
4 轮到线程2 执行了, 发现当前存款为 100, 和之前读到的 100 相同, 但是当前版本号为 3, 之前读到的版本号为 1, 版本小于当前版本, 认为操作失败.

三、Synchronized 原理

基本特点
结合上面的锁策略, 我们就可以总结出, Synchronized 具有以下特性:

  1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
  2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
  3. 实现轻量级锁的时候大概率用到的自旋锁策略
  4. 是一种不公平锁
  5. 是一种可重入锁
  6. 不是读写锁

1. 加锁工作过程

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。

在这里插入图片描述

1)偏向锁

第一个尝试加锁的线程, 优先进入偏向锁状态.

偏向锁不是真的 “加锁”, 只是给对象头中做一个 “偏向锁的标记”, 记录这个锁属于哪个线程. 如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态. 偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销. 但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.

2)轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁). 此处的轻量级锁就是通过 CAS 来实现.

通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
如果更新成功, 则认为加锁成功 。如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU)

自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.
因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了. 也就是所谓的 "自适应

3)重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁此处的重量级锁就是指用到内核提供的 mutex .

执行加锁操作, 先进入内核态.
在内核态判定当前锁是否已经被占用
如果该锁没有占用, 则加锁成功, 并切换回用户态.
如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒
这个线程, 尝试重新获取锁.

4)其他的优化操作

锁消除:编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
什么是 "锁消除“

有些应用程序的代码中, 用到了
StringBuffer sb = new StringBuffer();
sb.append(“a”);
sb.append(“b”);
sb.append(“c”);
sb.append(“d”);
此时每个 append 的调用都会涉及加锁和解锁. 但如果只是在单线程中执行这个代码, 那么这些加
锁解锁操作是没有必要的, 白白浪费了一些资源开销

锁粗化: 一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.
实际开发过程中, 使用细粒度锁, 是期望释放锁的时候其他线程能使用锁.
但是实际上可能并没有其他线程来抢占这个锁. 这种情况 JVM 就会自动把锁粗化, 避免频繁申请释放锁
下面举例来理解锁粗话

老哥当了领导, 给下属交代工作任务:
方式一:
打电话, 交代任务1, 挂电话.
打电话, 交代任务2, 挂电话.
打电话, 交代任务3, 挂电话
方式二:
打电话, 交代任务1, 任务2, 任务3, 挂电话.
显然, 方式二是更高效的方案.

可以看到, synchronized 的策略是比价复杂的, 在背后做了很多事情, 目的为了让程序猿哪怕啥都不懂, 也不至于写出特别慢的程序

四、死锁

1.死锁是什么

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

这里例子是很多资料谈到的 哲学家就餐问题,本文不详细解释

注意:
死锁是一种严重的 BUG!! 导致一个程序的线程 “卡死”, 无法正常工作!

2.那么如何避免死锁呢?

死锁产生的四个必要条件:

  1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路

当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

破坏循环等待
最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号(1, 2, 3…M).
N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.

可能产生环路等待的代码:

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {// do something...}}}
};
t1.start();
Thread t2 = new Thread() {@Overridepublic void run() {synchronized (lock2) {synchronized (lock1) {// do something...}}}
};
t2.start();

不会产生环路等待的代码

约定好先获取 lock1, 再获取 lock2 , 就不会环路等待

Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {// do something...}}}
};
t1.start();
Thread t2 = new Thread() {@Overridepublic void run() {synchronized (lock1) {synchronized (lock2) {// do something...}}}
};
t2.start();

结语

多线程编程是 Java 开发中极为关键的领域,这篇文章深入探讨了多线程进阶知识,涵盖常见锁策略、CAS 机制、Synchronized 原理、死锁 等丰富内容。通过学习这些知识,我们对多线程编程的复杂性和强大功能有了更深刻的理解。在实际开发中,根据不同场景合理选择锁策略、运用线程安全集合类、构建高效线程池等技术,能够显著提升程序性能和稳定性。

以上就是本文全部内容,感谢各位能够看到最后,如有问题,欢迎各位大佬在评论区指正,希望大家可以有所收获!创作不易,希望大家多多支持!

最后,大家再见!祝好!我们下期见!

相关文章:

深入理解 Java 多线程:锁策略与线程安全

文章目录 一、常见的锁策略1. 乐观锁&&悲观锁2. 读写锁3. 重量级锁&&轻量级锁4. 自旋锁5. 公平锁&&不公平锁6. 可重入锁 && 不可重入锁 二、CAS1. 什么是 CAS2. CAS 是怎么实现的3.CAS 有哪些应用1) 实现原子类2) 实现自旋锁 4. CAS 的 ABA 问…...

Java从入门到“放弃”(精通)之旅——数组的定义与使用⑥

Java从入门到“放弃”(精通)之旅🚀——数组⑥ 前言——什么是数组? 数组:可以看成是相同类型元素的一个集合,在内存中是一段连续的空间。比如现实中的车库,在java中,包含6个整形类…...

VsCode搭建

安全性问题的声明: VScode是一个由微软出品的开源软件编辑器 VScode下载 https://code.visualstudio.com/Download 官网直接下载即可,windows和linux的vscode使用命令是类似的。 VScode插件相关 为了方便安装,推荐设置并使用vscode插件的…...

【NLP 65、实践 ⑯ 基于Agent优化文章】

羁绊由我而起,痛苦也由我承担 —— 25.4.18 一、⭐【核心函数】定义大模型调用函数 call_large_model prompt:用户传入的提示词(如 “请分析这篇作文的主题”),指导模型执行任务 client:Zhipu…...

【Redis】从单机架构到分布式,回溯架构的成长设计美学

前言 🌟🌟本期讲解关于分布式架构的发展相关知识介绍~~~ 🌈感兴趣的小伙伴看一看小编主页:GGBondlctrl-CSDN博客 🔥 你的点赞就是小编不断更新的最大动力 🎆那么废…...

增量式PID基础解析与代码实例:温控系统

目录 1. 前言 2. 增量式PID控制的基本原理 2.1 PID控制的基本概念 2.2 增量式PID控制的特点 3. 增量式PID控制的Python实现:温控系统 3.1 构建增量式PID控制器 3.2 使用增量式PID控制器 3.3 运行模拟 3.4 完整代码 4. 参数调整与优化 4.1 参数选择 4.2…...

数据结构——栈以及相应的操作

栈(Stack) 在维基百科中是这样定义的: 堆栈(stack) 又称为栈或堆叠,是计算机科学中的一种抽象资料类型,只允许在有序的线性资料集合中的一端(称为堆栈顶端,top)进行加入数据(push)和…...

opencv 图像的旋转

图像的旋转 1 单点旋转2. 图片旋转(cv2.getRotationMatrix2D)3. 插值方法3.1 最近邻插值(cv2.INTER_NEAREST)3.2 双线性插值(cv2.INTER_LINEAR)3.3 像素区域插值(cv2.INTER_AREA)3.4 双三次插值(cv2.INTER_CUBIC&#…...

P3916 图的遍历

P3916 图的遍历 题目来源-洛谷 题意 有向图中,找出每个节点能访问到的最大的节点 思路 每个节点的最大节点,不是最长距离,如果是每个节点都用dfs去找最大值,显然1e6*1e6 超时了,只能60分从第一个节点开始遍历&…...

Vue3 + Three.js 场景编辑器开发实践

文章目录 前言项目背景与意义项目技术栈在线演示核心功能实现1. 智能化场景管理2. 专业级模型处理3. 可视化材质与照明4. 相机与渲染引擎5. 场景操作 项目优势开发目标1. 几何模型和模型导入,场景新建按钮增加2. 提供场景内容的可视化编辑功能3. 渲染器场景&#xf…...

Vue3 本地打包启动白屏解决思路!! !

“为什么我访问 http://127.0.0.1:5501/index.html 白屏,删了 index.html 再访问 / 就又活过来了?” —— 你的项目与 SPA 路由的“宫斗大戏” 一、问题复现 场景 本地通过 VSCode Live Server(或其他静态服务器)启动了打包后的 V…...

Ubuntu 安装 Docker 教程(官方推荐方式)

✅ 步骤 1:卸载旧版本(如果有) for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done---### ✅ 步骤 2:更新 APT 索引并安装依赖项bash sudo a…...

WPF 点击按钮,显示隐藏另一个控件

<Button Content"显示隐藏" Click"operationDetails_Click" /> private void operationDetails_Click(object sender, RoutedEventArgs e) { 另一个控件的Name.Visibility 另一个控件的Name.Visibility Visibility.Visible ? Visibility.Col…...

RBAC的使用

1、简述RBAC的作用及工作流程 Rbac基于角色访问控制&#xff0c;用于管理用户对集群资源的访问权限&#xff0c;通过定义角色和绑定规则&#xff0c;将用户与权限进行关联&#xff0c;作用&#xff1a;权限精细化管理&#xff0c;操作便捷与统一管理&#xff0c;动态调整权限。…...

QML中的3D功能--模型导入与修改

在Qt 3D中导入和修改3D模型是开发3D应用程序的基础。以下是详细的流程和技术方案: 一、模型导入基础 1. 支持的文件格式 Qt 3D支持多种3D模型格式: OBJ (Wavefront) FBX (Autodesk) DAE (Collada) GLTF/GLB (推荐格式) STL (3D打印格式) 2. 基本导入方法 qml import Qt3…...

树莓派超全系列教程文档--(33)树莓派启动选项

树莓派启动选项 启动选项start_file &#xff0c;fixup_filecmdlinekernelarm_64bitramfsfileramfsaddrinitramfsauto_initramfsdisable_poe_fandisable_splashenable_uartforce_eeprom_reados_prefixotg_mode &#xff08;仅限Raspberry Pi 4&#xff09;overlay_prefix配置属…...

dotnet core webapi 实现 异常处理中间件

目录 第一步&#xff1a;创建异常处理中间件类&#xff08;自定义响应格式&#xff09; 第二步&#xff1a;在 Program.cs 中使用中间件 三、效果 第一步&#xff1a;创建异常处理中间件类&#xff08;自定义响应格式&#xff09; public static class ExceptionMiddleware…...

实现Azure Function安全地请求企业内部API返回数据

需要编写一个Function在Azure云上运行&#xff0c;它需要访问企业内部的API获取JSON格式的数据&#xff0c;企业有网关和防火墙&#xff0c;API有公司的okta身份认证&#xff0c;通过公司的域账号来授权访问&#xff0c;现在需要创建一个专用的域账号&#xff0c;让Function访问…...

实现Azure Databricks安全地请求企业内部API返回数据

需要编写一个Databricks在Azure云上运行&#xff0c;它需要访问企业内部的API获取JSON格式的数据&#xff0c;企业有网关和防火墙&#xff0c;API有公司的okta身份认证&#xff0c;通过公司的域账号来授权访问&#xff0c;现在需要创建一个专用的域账号&#xff0c;让Databrick…...

结合建筑业务讲述TOGAF标准处理哪种架构

TOGAF标准处理哪种架构 内容介绍业务架构业务策略&#xff0c;治理&#xff0c;组织和关键业务流程数据架构组织的逻辑和物理数据资产以及数据管理资源的结构应用架构待部署的各个应用程序&#xff0c;它们之间的交互以及与组织核心业务流程的关系的蓝图技术架构支持业务&#…...

深度学习--卷积神经网络CNN原理

文章目录 一、CNN图像原理1、了解CNN如何处理图像 二、CNN图像识别1、画面不变性2、主要表现1&#xff09;平移不变性2&#xff09;尺度不变性3&#xff09;旋转不变性 3、传统神经网络1&#xff09;数据预处理2&#xff09;特征提取3&#xff09;搭建神经网络模型4&#xff09…...

PostgreSQL 常用客户端工具

PostgreSQL 常用客户端工具 PostgreSQL 拥有丰富的客户端工具生态系统&#xff0c;以下是各类常用工具的详细分类和介绍&#xff1a; 一 图形化客户端工具 1.1 跨平台工具 工具名称特点适用场景许可证pgAdmin官方出品&#xff0c;功能全面开发/运维PostgreSQLDBeaver支持多…...

PostgreSQL数据库yum方式安装详解

PostgreSQL数据库yum方式安装 1. 基础环境配置2. 前期安装准备3. 软件安装3.1 YUM方式安装&#xff08;PG 13&#xff09;3.1.1 安装rpm仓库3.1.2 安装软件包PG133.1.3 初始化数据库3.1.4 启动数据库3.1.5 开机自启动3.1.6 编辑环境变量 4. 安装后配置 1. 基础环境配置 2. 前期…...

浅析vue2和vue3的区别

以下是 Vue 2 和 Vue 3 的主要区别: 一、核心特性 1. 响应式机制 Vue 2: 基于 Object.defineProperty 实现响应式。无法检测对象属性的新增和删除,需要使用 Vue.set 或 $set。Vue 3: 使用 Proxy 替代 Object.defineProperty。能够直接检测对象属性的新增和删除,无需额外方…...

【Linux我做主】make和makefile自动化构建

make和makefile自动化构建 make和makefile自动化构建github地址前言背景介绍为什么需要make和makefile&#xff1f; make和makefile解析什么是make和makefile依赖关系和依赖方法核心语法结构简单演示编译清理 多阶段编译示例 make时执行的顺序场景1&#xff1a;clean目标在前(特…...

spring boot应用部署IIS

Windows IIS 部署 Spring Boot 应用详细指南 本文档提供了在 Windows Server 上使用 IIS 部署 Spring Boot 应用的完整步骤和最佳实践。 目录 概述前期准备Spring Boot 应用准备安装配置必要组件配置 IIS 站点配置反向代理配置 Windows 服务配置应用自启动HTTPS 配置日志配置…...

Linux系统之部署TestNet资产管理系统

Linux系统之部署TestNet资产管理系统 一、TestNet 介绍1.1 TestNet简介1.2 主要特点1.3 主要使用场景 二、本次实践规划2.1 本地环境规划2.2 本次实践介绍 三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本 四、部署TestNet系统4.1 下载T…...

@EnableAsync+@Async源码学习笔记之六

接上文&#xff0c;我们本文分析 AsyncExecutionAspectSupport 的源码&#xff1a; package org.springframework.aop.interceptor;import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFu…...

SQL系列:常用函数

1、【MySQL】合并字段函数&#xff08;列转行&#xff09; 它可以将两个字段中的数据合并到一个字段中。 1&#xff09;CONCAT函数 CONCAT函数可以将多个字段中的数据合并到一个字段中。它的语法格式如下&#xff1a; SELECT CONCAT(字段1,字段2,...字段N) FROM 表名;SELEC…...

Git 中修改某个特定的commit提交内容

在 Git 中修改某个特定的提交&#xff08;commit&#xff09;通常需要使用 交互式变基&#xff08;Interactive Rebase&#xff09; 或 修改提交&#xff08;Commit Amend&#xff09;。以下是不同场景下的具体操作步骤&#xff1a; 一、修改最近的提交&#xff08;最新提交&am…...

FHS --- linux目录结构(部分目录解释)

根目录&#xff08;/&#xff09; 的意义和内容 &#xff1a; 根目录是整个系统最重要的一个目录&#xff0c;因为不但所有的目录都是由根目录衍生出来的&#xff0c;同时根目录也与开机/还原/系统修复等动作有关 。 由于系统开机时需要特定的开机软件、核心档案、开机所需程序…...

不带无线网卡的Linux开发板上网方法

I.MX6ULL通过网线上网 设置WLAN共享修改开发板的IP 在使用I.MX6ULL-MINI开发板学习Linux的时候&#xff0c;有时需要更新或者下载一些资源包&#xff0c;但是开发板本身是不带无线网卡或者WIFI芯片的&#xff0c;尝试使用网口连接笔记本&#xff0c;笔记本通过无线网卡连接WIFI…...

图书管理系统C语言

图书管理系统C语言代码示例。 该系统可以实现图书信息&#xff08;包含图书编号、书名、作者、出版社、价格、库存数量&#xff09;的录入、显示、查询、修改、删除等功能&#xff0c;还具备一定的错误处理和输入验证。 #include <stdio.h> #include <stdlib.h> …...

关于大型语言模型的“生物学”

我知道我们已经聊过很多次&#xff0c;关于LLM是怎么运作的&#xff0c;它们的影响力&#xff0c;还有它们的使用场景。但尽管现在有那么多讲LLM的文章&#xff0c;它们本质上还是个黑箱。 但我们真正要问自己的问题是&#xff0c;为什么理解这些系统的内部结构很重要&#xf…...

图像预处理-图像边缘检测(流程)

一.高斯滤波 因为图像边缘检测就是把像素值有差异的地方提取出来&#xff0c;所以噪声会有很大影响&#xff0c;因此需要对图像进行平滑处理&#xff0c;高斯滤波是流程中常用的方法。 二.计算图像的梯度与方向 过程中通常使用sobel算子进行梯度计算&#xff0c;在OpenCV中&am…...

解锁思想道德修养的奥秘:用思维导图开启智慧之旅

在我们的成长过程中&#xff0c;思想道德修养如同基石&#xff0c;奠定了我们为人处世、面对生活挑战的基本态度和准则。而如何高效地梳理和掌握思想道德修养的丰富内容呢&#xff1f;思维导图这一强大工具为我们提供了独特视角和便捷途径。 思想道德修养的关键板块 道德理论…...

swagger的简介及使用方法

Swagger 是一个用于描述、生成、文档化和测试 RESTful API 的开源工具集。它可以自动生成 API 文档&#xff0c;帮助开发者理解和使用 API。Swagger 由 Swagger.io 提供&#xff0c;并已经发展成了一套广泛应用于 API 设计和文档的标准。 Swagger 项目的历史可以追溯到 2010 年…...

解决Ubuntu图形化界面操作适配问题

1 缘起 使用Ubuntu GNOME图形化系统作为开发机&#xff0c; 遇到与Windows操作不一致的地方&#xff0c;比如PyCharm、IntelliJ时无法正确代码跳转&#xff0c; 如CtrlAltLeft&#xff0c;CtrlAltRight无法正常在代码级别跳转&#xff0c;只能在文件级别跳转。 基于这个开端&a…...

End-to-End从混沌到秩序:基于LLM的Pipeline将非结构化数据转化为知识图谱

摘要:本文介绍了一种将非结构化数据转换为知识图谱的端到端方法。通过使用大型语言模型(LLM)和一系列数据处理技术,我们能够从原始文本中自动提取结构化的知识。这一过程包括文本分块、LLM 提示设计、三元组提取、归一化与去重,最终利用 NetworkX 和 ipycytoscape 构建并可…...

使用Ingress发布应用程序

使用Ingress发布应用程序 文章目录 使用Ingress发布应用程序[toc]一、什么是Ingress二、定义Ingress三、什么是Ingress控制器四、部署nginx Ingress控制器1.了解nginx Ingress控制器的部署方式2.安装nginx Ingress控制器3.本地实际测试 五、使用Ingress对外发布应用程序1.使用D…...

llama-factory微调报错:

报错信息 [INFO] [utils.py:789:see_memory_usage] CPU Virtual Memory: used 81.51 GB, percent 64.9% W0419 10:14:27.573000 108354 site-packages/torch/distributed/elastic/multiprocessing/api.py:897] Sending process 108373 closing signal SIGTERM W0419 10:14:27…...

【LLaMAFactory】LoRa + 魔搭 微调大模型实战

前言 环境准备 之前是在colab上玩&#xff0c;这次在国内的环境上玩玩。 魔搭&#xff1a;https://www.modelscope.cn/ 现在注册&#xff0c;有100小时的GPU算力使用。注册好了之后&#xff1a; 魔搭社区 这里使用qwen2.5-7B-Instruct模型&#xff0c;这里后缀Instruct是指…...

【愚公系列】《Python网络爬虫从入门到精通》054-Scrapy 文件下载

&#x1f31f;【技术大咖愚公搬代码&#xff1a;全栈专家的成长之路&#xff0c;你关注的宝藏博主在这里&#xff01;】&#x1f31f; &#x1f4e3;开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主&#xff01; &#x1f…...

db中查询关于null的sql该怎么写

正确示例 # 等于null select * from 表名 where 字段名 is NULL; # 不等于null select * from 表名 where 字段名 is not NULL;若需要同时判断字段不等于某个值且不为null select * from users where age ! 30 and age is not null; select * from users where age ! 30 or a…...

React 文章列表

自定义hook 在src/hooks文件夹下封装 useChannel.js // 获取频道列表的逻辑 import { useEffect , useState } from "react" import { getChannelAPI } from "/apis/article"function useChannel(){// 获取频道的逻辑 const [channelList,setChannelList…...

中间件--ClickHouse-12--案例-1-日志分析和监控

1、案例背景 一家互联网公司需要实时分析其服务器日志、应用日志和用户行为日志&#xff0c;以快速发现潜在问题并优化系统性能。 2、需求分析 目标&#xff1a;实时分析日志数据&#xff0c;快速发现问题并优化系统性能。数据来源&#xff1a; 服务器日志&#xff1a;如 Ng…...

QML中的3D功能--自定义着色器开发

在 Qt 3D 中使用自定义着色器可以实现高度定制化的渲染效果。以下是完整的自定义着色器开发方案。 一、基础着色器创建 1. 创建自定义材质 qml import Qt3D.Core 2.15 import Qt3D.Render 2.15 import Qt3D.Extras 2.15Entity {components: [Transform { translation: Qt.v…...

如何防止接口被刷

目录 &#x1f6e1;️ 一、常见的防刷策略分类 &#x1f527; 二、技术实现细节 ✅ 1. 基于 IP 限流 ✅ 2. 给接口加验证码 ✅ 3. 使用 Token 限制接口访问权限 ✅ 4. 给接口加冷却时间&#xff08;验证码类经典&#xff09; ✅ 5. 使用滑动窗口限流算法&#xff08;更精…...

18、TimeDiff论文笔记

TimeDiff **1. 背景与动机****2. 扩散模型基础****3. TimeDiff 模型****3.1 前向扩散过程****3.2 后向去噪过程** 4、TimeDiff&#xff08;架构&#xff09;原理训练推理其他关键点解释 DDPM&#xff08;相关数学&#xff09;1、正态分布2、条件概率1. **与多个条件相关**&…...

docker底层原理

一句话&#xff0c;dockerfile里面的一行指令&#xff0c;就是一个layer层 docker底层原理 在机器上安装docker服务器端的程序&#xff0c;就会在机器上自动创建以下目录&#xff0c;默认安装路径是/var/lib/ docker服务器端的工作目录的作用如下&#xff0c;镜像的每一层的元数…...