可重入锁和不可重入锁

概念

Reentrant = Re + entrant,Re是重复、又、再的意思,entrant是enter的名词或者形容词形式,翻译为进入者或者可进入的,所以Reentrant翻译为可重复进入的、可再次进入的,因此ReentrantLock翻译为重入锁或者再入锁。

可重入锁又名递归锁,指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

可重入锁,当同一个线程在外层方法获取对象锁之后,再进入该线程的内层方法会自动获取锁(前提,锁对象是同一个对象),不会因为之前已经获取过还没释放而阻塞。

重入到哪里:进入同步域(即同步代码块/方法或显式锁锁定的代码)

通俗理解

  • 可重入锁就是一证通/一卡通,只需一张卡就可以通过所有相同关卡。
  • 不可重入锁就是:即使每个关卡相同,你也得再拿一个一摸一样的卡来。

如果把证件/卡看作是同步锁,把关卡看作是同步域(即同步代码块/方法或显式锁锁定的代码),那么可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。

在Java中,除了ReentrantLock(显式的可重入锁)以外,synchronized也是重入锁(隐式的可重入锁)。

不可重入锁别名:不可重入锁也叫自旋锁。

可重入锁的工作原理

可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。

ReentrantLock实现了Lock接口,Lock接口里面定义了java中锁应该实现的几个方法:

// 获取锁
void lock();
// 获取锁(可中断)
void lockInterruptibly() throws InterruptedException;
// 尝试获取锁,如果没获取到锁,就返回false
boolean tryLock();
// 尝试获取锁,如果没获取到锁,就等待一段时间,这段时间内还没获取到锁就返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 释放锁
void unlock();
// 条件锁
Condition newCondition();

总结

  • 可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁,这样的锁就叫做可重入锁。与可重入锁相反,不可重入锁不可递归调用,递归调用就发生死锁。
  • 隐式锁(即synchronized关键字使用的锁)默认是可重入锁,显式锁(即Lock)也有ReentrantLock这样的可重入锁。
  • 可重入锁的工作原理很简单,就是用一个计数器来记录锁被获取的次数,获取锁一次计数器+1,释放锁一次计数器-1,当计数器为0时,表示锁可用。
  • 不可重入锁也叫自旋锁。

ReenTrantLock可重入锁和synchronized的区别

可重入性:

从名字上理解,ReenTrantLock的字面意思就是再进入的锁,其实synchronized关键字所使用的锁也是可重入的,两者关于这个的区别不大。两者都是同一个线程每进入一次,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

锁的实现:

Synchronized是依赖于JVM实现的,而ReenTrantLock是JDK实现的,有什么区别,说白了就类似于操作系统来控制实现和用户自己敲代码实现的区别。前者的实现是比较难见到的,后者有直接的源码可供阅读。

性能的区别:

在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

功能区别:

便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。

锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

ReenTrantLock独有的能力:

  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
  • ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。

ReenTrantLock实现的原理:

在网上看到相关的源码分析,本来这块应该是本文的核心,但是感觉比较复杂就不一一详解了,简单来说,ReenTrantLock的实现是一种自旋锁,通过循环调用CAS操作来实现加锁。它的性能比较好也是因为避免了使线程进入内核态的阻塞状态。想尽办法避免线程进入内核的阻塞状态是我们去分析和理解锁设计的关键钥匙。

什么情况下使用ReenTrantLock:

答案是,如果你需要实现ReenTrantLock的三个独有功能时。

ReentrantLock源码分析:
主要内部类:
ReentrantLock中主要定义了三个内部类:Sync、NonfairSync、FairSync。

abstract static class Sync extends AbstractQueuedSynchronizer {}static final class NonfairSync extends Sync {}static final class FairSync extends Sync {}

(1)抽象类Sync实现了AQS的部分方法;

(2)NonfairSync实现了Sync,主要用于非公平锁的获取;

(3)FairSync实现了Sync,主要用于公平锁的获取。

主要属性:
private final Sync sync;
主要属性就一个sync,它在构造方法中初始化,决定使用公平锁还是非公平锁的方式获取锁。

主要构造方法:

// 默认构造方法
public ReentrantLock() {sync = new NonfairSync();//默认创建非公平锁
}
// 自己可选择使用公平锁还是非公平锁
public ReentrantLock(boolean fair) {
//参数是true,则创建公平锁,false创建非公平锁sync = fair ? new FairSync() : new NonfairSync();//自己决定使用公平锁还是非公平锁
}

公平锁

这里我们假设ReentrantLock的实例是通过以下方式获得的:

ReentrantLock reentrantLock = new ReentrantLock(true);//公平锁

下面的是加锁的主要逻辑:

// ReentrantLock.lock()
public void lock() {// 调用的sync属性的lock()方法// 这里的sync是公平锁,所以是FairSync的实例sync.lock();
}
// ReentrantLock.FairSync.lock()
final void lock() {// 调用AQS的acquire()方法获取锁// 注意,这里传的值为1acquire(1);
}
// AbstractQueuedSynchronizer.acquire()
public final void acquire(int arg) {// 尝试获取锁// 如果失败了,就排队if (!tryAcquire(arg) &&// 注意addWaiter()这里传入的节点模式为独占模式acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}
// ReentrantLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {// 当前线程final Thread current = Thread.currentThread();// 查看当前状态变量的值int c = getState();// 如果状态变量的值为0,说明暂时还没有人占有锁if (c == 0) {// 如果没有其它线程在排队,那么当前线程尝试更新state的值为1// 如果成功了,则说明当前线程获取了锁if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {// 当前线程获取了锁,把自己设置到exclusiveOwnerThread变量中// exclusiveOwnerThread是AQS的父类AbstractOwnableSynchronizer中提供的变量setExclusiveOwnerThread(current);// 返回true说明成功获取了锁return true;}}// 如果当前线程本身就占有着锁,现在又尝试获取锁// 那么,直接让它获取锁并返回trueelse if (current == getExclusiveOwnerThread()) {// 状态变量state的值加1int nextc = c + acquires;// 如果溢出了,则报错if (nextc < 0)throw new Error("Maximum lock count exceeded");// 设置到state中// 这里不需要CAS更新state// 因为当前线程占有着锁,其它线程只会CAS把state从0更新成1,是不会成功的// 所以不存在竞争,自然不需要使用CAS来更新setState(nextc);// 当线程获取锁成功return true;}// 当前线程尝试获取锁失败return false;
}
// AbstractQueuedSynchronizer.addWaiter()
// 调用这个方法,说明上面尝试获取锁失败了
private Node addWaiter(Node mode) {// 新建一个节点Node node = new Node(Thread.currentThread(), mode);// 这里先尝试把新节点加到尾节点后面// 如果成功了就返回新节点// 如果没成功再调用enq()方法不断尝试Node pred = tail;// 如果尾节点不为空if (pred != null) {// 设置新节点的前置节点为现在的尾节点node.prev = pred;// CAS更新尾节点为新节点if (compareAndSetTail(pred, node)) {// 如果成功了,把旧尾节点的下一个节点指向新节点pred.next = node;// 并返回新节点return node;}}// 如果上面尝试入队新节点没成功,调用enq()处理enq(node);return node;
}
// AbstractQueuedSynchronizer.enq()
private Node enq(final Node node) {// 自旋,不断尝试for (;;) {Node t = tail;// 如果尾节点为空,说明还未初始化if (t == null) { // Must initialize// 初始化头节点和尾节点if (compareAndSetHead(new Node()))tail = head;} else {// 如果尾节点不为空// 设置新节点的前一个节点为现在的尾节点node.prev = t;// CAS更新尾节点为新节点if (compareAndSetTail(t, node)) {// 成功了,则设置旧尾节点的下一个节点为新节点t.next = node;// 并返回旧尾节点return t;}}}
}
// AbstractQueuedSynchronizer.acquireQueued()
// 调用上面的addWaiter()方法使得新节点已经成功入队了
// 这个方法是尝试让当前节点来获取锁的
final boolean acquireQueued(final Node node, int arg) {// 失败标记boolean failed = true;try {// 中断标记boolean interrupted = false;// 自旋for (;;) {// 当前节点的前一个节点final Node p = node.predecessor();// 如果当前节点的前一个节点为head节点,则说明轮到自己获取锁了// 调用ReentrantLock.FairSync.tryAcquire()方法再次尝试获取锁if (p == head && tryAcquire(arg)) {// 尝试获取锁成功// 这里同时只会有一个线程在执行,所以不需要用CAS更新// 把当前节点设置为新的头节点setHead(node);// 并把上一个节点从链表中删除p.next = null; // help GC// 未失败failed = false;return interrupted;}// 是否需要阻塞if (shouldParkAfterFailedAcquire(p, node) &&// 真正阻塞的方法parkAndCheckInterrupt())// 如果中断了interrupted = true;}} finally {// 如果失败了if (failed)// 取消获取锁cancelAcquire(node);}
}
// AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire()
// 这个方法是在上面的for()循环里面调用的
// 第一次调用会把前一个节点的等待状态设置为SIGNAL,并返回false
// 第二次调用才会返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 上一个节点的等待状态// 注意Node的waitStatus字段我们在上面创建Node的时候并没有指定// 也就是说使用的是默认值0// 这里把各种等待状态再贴出来//static final int CANCELLED =  1;//static final int SIGNAL    = -1;//static final int CONDITION = -2;//static final int PROPAGATE = -3;int ws = pred.waitStatus;// 如果等待状态为SIGNAL(等待唤醒),直接返回trueif (ws == Node.SIGNAL)return true;// 如果前一个节点的状态大于0,也就是已取消状态if (ws > 0) {// 把前面所有取消状态的节点都从链表中删除do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 如果前一个节点的状态小于等于0,则把其状态设置为等待唤醒// 这里可以简单地理解为把初始状态0设置为SIGNAL// CONDITION是条件锁的时候使用的// PROPAGATE是共享锁使用的compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
// AbstractQueuedSynchronizer.parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {// 阻塞当前线程// 底层调用的是Unsafe的park()方法LockSupport.park(this);// 返回是否已中断return Thread.interrupted();
}

下面我们看一下主要方法的调用关系:

ReentrantLock#lock()
->ReentrantLock.FairSync#lock() // 公平模式获取锁->AbstractQueuedSynchronizer#acquire() // AQS的获取锁方法->ReentrantLock.FairSync#tryAcquire() // 尝试获取锁->AbstractQueuedSynchronizer#addWaiter()  // 添加到队列->AbstractQueuedSynchronizer#enq()  // 入队->AbstractQueuedSynchronizer#acquireQueued() // 里面有个for()循环,唤醒后再次尝试获取锁->AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire() // 检查是否要阻塞->AbstractQueuedSynchronizer#parkAndCheckInterrupt()  // 真正阻塞的地方


获取锁的主要过程大致如下:

(1)尝试获取锁,如果获取到了就直接返回了;

(2)尝试获取锁失败,再调用addWaiter()构建新节点并把新节点入队;

(3)然后调用acquireQueued()再次尝试获取锁,如果成功了,直接返回;

(4)如果再次失败,再调用shouldParkAfterFailedAcquire()将节点的等待状态置为等待唤醒(SIGNAL);

(5)调用parkAndCheckInterrupt()阻塞当前线程;

(6)如果被唤醒了,会继续在acquireQueued()的for()循环再次尝试获取锁,如果成功了就返回;

(7)如果不成功,再次阻塞,重复(3)(4)(5)直到成功获取到锁。

以上就是整个公平锁获取锁的过程,下面我们看看非公平锁是怎么获取锁的。

相对于公平锁,非公平锁加锁的过程主要有两点不同:

(1)一开始就尝试CAS更新状态变量state的值,如果成功了就获取到锁了;

(2)在tryAcquire()的时候没有检查是否前面有排队的线程,直接上去获取锁才不管别人有没有排队;

总的来说,相对于公平锁,非公平锁在一开始就多了两次直接尝试获取锁的过程


可重入锁代码演示:

代码演示synchronized可重入锁:

package com.fan.sync;
public class SyncLockDemo {public static void main(String[] args) {//synchronizedObject o = new Object();new Thread(()->{//要同步的任务,synchronized是隐式可重入锁synchronized (o){//第一层关卡,用的是同一把锁System.out.println(Thread.currentThread().getName()+"外层");synchronized (o){//第二层关卡,用的是同一把锁System.out.println(Thread.currentThread().getName()+"中层");synchronized (o){//第三层关卡,用的是同一把锁System.out.println(Thread.currentThread().getName()+"内层");}}}},"t1").start();}
}

输出:
t1外层
t1中层
t1内层

演示synchronized 可以递归调用,即可重入锁的案例:

package com.fan.lock;public class SyncLockDemo2 {public synchronized void add(){add();//递归调用add()}public static void main(String[] args) {new SyncLockDemo2().add();//对象调用递归方法}
}

可以进行循环递归调用,因为可以重新进去调用。

ReentrantLock演示可重入锁代码:

package com.fan.lock;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ReenterLockDemo {public static void main(String[] args) {//Lock演示可重入锁Lock lock = new ReentrantLock();//创建线程new Thread(()->{//要子线程单独执行的任务,此任务要上锁/加锁//加锁/上锁lock.lock();try {System.out.println(Thread.currentThread().getName()+"外层");//内层try {lock.lock();//内层上锁System.out.println(Thread.currentThread().getName()+"内层");}finally {lock.unlock();//内层释放锁}}finally {lock.unlock();//解锁/释放锁}},"t1").start();}
}

输出:
t1外层
t1内层

证明可以重入进入。

不释放内层锁的代码:
造成其他线程等待,程序不能结束,不能输出sss字符串:

package com.fan.lock;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ReenterLockDemo {public static void main(String[] args) {//Lock演示可重入锁Lock lock = new ReentrantLock();//创建线程new Thread(()->{//要子线程单独执行的任务,此任务要上锁/加锁//加锁/上锁lock.lock();try {System.out.println(Thread.currentThread().getName()+"外层");//内层try {lock.lock();//内层上锁System.out.println(Thread.currentThread().getName()+"内层");}finally {}}finally {lock.unlock();//解锁/释放锁}},"t1").start();//另外创建新线程new Thread(()->{lock.lock();System.out.println("sss");lock.unlock();},"t2").start();}
}

输出结果如下:

由于内层锁未被释放,不能输出“sss”。


可重入锁(ReentrantLock):

  • 可重入锁是一种支持重进入的锁机制。重进入是指一个线程在持有锁的情况下,可以再次获取相同的锁而不会被阻塞。
  • 可重入锁实现了Lock接口,提供了比内置锁(synchronized关键字)更多的灵活性和功能。
  • 可重入锁允许一个线程反复获得该锁,避免了死锁的发生,同时也提高了代码的简洁性和可读性。
  • 可重入锁支持公平性设置,使得等待时间最长的线程优先获取锁。

不可重入锁(NonReentrantLock):

  • 不可重入锁是一种不支持重进入的锁机制。也就是说,当一个线程获得了不可重入锁之后,如果再次尝试获取锁,就会被阻塞,直到当前持有锁的线程释放锁。
  • 不可重入锁在Java中没有内置的实现,需要通过自定义实现或基于AQS(AbstractQueuedSynchronizer)等基础类来构建。
  • 不可重入锁可能会导致死锁问题,因为如果一个线程在持有锁的情况下又尝试获取同一个锁,就会导致自己无限等待。

总结

可重入锁允许同一线程多次获得锁,而不可重入锁则不支持同一个线程多次获得锁。在大多数情况下,可重入锁是更常用和推荐的选择,因为它提供了更多的功能、灵活性和安全性,同时避免了死锁问题。但在某些特殊情况下,不可重入锁也可能有其应用场景,例如需要强制确保某段代码只能被一个线程执行。


synchronized具体采用了哪些锁策略呢?

1.既是悲观锁,又是乐观锁

2.既是重量级锁,又是轻量级锁

3.重量级锁部分是基于多系统互斥锁实现的,轻量级锁部分是基于自旋锁实现的

4.synchronized是非公平锁(不会遵守先来后到,锁释放之后,哪个线程拿到锁个凭本事

5.synchronized是可重入锁(内部会记录哪个线程拿到了锁,记录引用计数)

6.synchronized不是读写锁

synchronized-内部实现策略(自适应)

讲解一下自适应:代码中写了一个synchhronized之后,可能产生一系列自适应的过程,锁升级(锁膨胀)

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

偏向锁,不是真的加锁,而只是做了一个标记,如果有别的线程来竞争锁,才会真的加锁,如果没有别的线程竞争,就自始至终都不加锁了(渣女心态,没人来追你,我就钓鱼,你要是被追了,我先给你个身份,让别人别靠近你。)——当然加锁本身也有一定消耗

偏向锁在没人竞争的时候就是一个简单的(轻量的)标记,如果有别的线程来尝试加锁,就立即把偏向锁升级成真正加锁,让别人阻塞等待(能不加锁就不加锁)

轻量级锁-synchronized通过自旋锁的方式实现轻量级锁——这边把锁占据了,另一个线程按照自旋的方式(这个锁操作比较耗cpu,如果能够快速拿到锁,多耗点也不亏),来反复查询当前的锁状态是不是被释放,但是后续,如果竞争这把锁的线程越来越多了(锁冲突更加激烈了),从轻量锁,升级到重量级锁~随着竞争激烈,即使前一个线程释放锁,也不一定能够拿到锁,何时能拿到,时间可能比较久了会

 💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖

锁清除:编译器,会智能的判断,当前这个代码,是否有必要加锁,如果你写了加锁,但实际没必要加锁,就会自动清除锁

如:单个线程使用StringBuffer编译器进行优化,是保证优化之后的逻辑和之前的逻辑是一致的,这样就会让代码优化变的保守起来~~咱们猿们也不能指望编译器优化,来提升代码效率,自己也要有作用,判断何时加锁,也是咱们非常重要的工作。

锁粗化:

关于锁的粒度,锁中操作包含代码多:锁粒就大

//1号       全写的是伪代码
和2号比较明显是2号的粒度更大
for(
synchronized(this){count++}
}//2号
synchronized(this){
for{
count++}
}

锁粒大,锁粒小各有好处:

锁粒小,并发程度会更高,效率也会更快

锁粒大,是因为加锁本身就有开销。(如同打电话,打一次就行,老打电话也不好)

上述的都是基本面试题
 


CAS全称(Compare and swap) 

字面意思:比较并且交换

能够比较和交换,某个寄存器中的值和内存中的值,看是否相等,如果相等就把另一个寄存器中的值和内存进行交换

boolean CAS(address,expectValue,swapValue){if(&address==expectValue){         //这个&相当于C语言中的*,看他两个是否相等&address=swapValue;             //相等就换值return true;                 
}return false;

此处严格的说是,adress内存的值和swapValue寄存器里的值,进行交换,但是一般我们重点关注的是内存中的值,寄存器往往作为保存临时数据的方式,这里的值是啥,很多时候我们选择是忽略的。

这一段逻辑是通过一条cpu指令完成的(原子的,或者说确保原子性)给我们编写线程安全代码,打开了新的世界。

CAS的使用

1.实现原子类:多线程针对一个count++,在java库中,已经提供了一组原子类

java.util.concurrent(并发的意思).atomic

AtomicInteger,AtomicLong,提供了自增/自减/自增任意值,自减任意值··,这些操作可以基于CAS按照无锁编程的方式来实现。

如:

for(int i=0;i<5000;i++){count.getAndIncrement();                         //count++count.incrementAndGet();                        //++countcount.getAndDecrement();                      //count--count.decrementAndGet()                      //--count}
import java.util.concurrent.atomic.AtomicInteger;public class Demo6 {public  static AtomicInteger count=new AtomicInteger(0);    //这个类的初值呗public static void main(String[] args) throws InterruptedException {Thread t1=new Thread(()->{for (int i=0;i<500;i++){count.getAndIncrement();}});Thread t2=new Thread(()->{for (int i=0;i<500;i++){count.getAndIncrement();}});t1.start();t2.start();t1.join();            //注意要等待两个线程都结束再开始调用t2.join();System.out.println(count);}
}

上述原子类就是基于CAS完成的

当两个线程并发的线程执行++的时候,如果加限制,意味着这两个++是串行的,能计算正确的,有时候者两个++操作是穿插的,这个时候是会出现问题的

加锁保证线程安全:通过锁,强制避免出现穿插~~

原子类/CAS保证线程安全,借助CAS来识别当前是否出现穿插的情况,如果没有穿插,此时直接修改就是安全的,如果出现了穿插,就会重新读取内存中最新的值,再次尝试修改。

部分源码合起来的意思就是 

public int getAndIncrement(){int oldValue=value;       //先储存值,防止别的线程偷摸修改之后,无法恢复到之前的值while(CAS(value,oldValue,OldValue+1)!=true){  //检查是否线程被别的偷摸修改了//上面的代码是Value是否等于oldValue,假如等于就把Value赋值OldValue+1oldValue=value;                        //假如修改了就恢复了原来的样子}return oldValue;}

假如这种情况,刚开始设置value=0, 

 

CAS是一个指令,这个指令本身是不能够拆分的。

是否可能会出现,两个线程,同时在两个cpu上?微观上并行的方式来执行,CAS本身是一个单个的指令,这里其实包含了访问操作,当多个cpu尝试访问内存的时候,本质也是会存在先后顺序的。

就算同时执行到CAS指令,也一定有一个线程的CAS先访问到内存,另一个后访问到内存

为啥CAS访问内存会有先后呢?

多个CPU在操作同一个资源,也会涉及到锁竞争(指令级别的锁),是比我们平时说的synchronized代码级别的锁要轻量很多(cpu内部实现的机制) 
 

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

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

相关文章

SpringCloud微服务 【实用篇】| RabbitMQ快速入门、SpringAMQP

目录 一&#xff1a;初始RabbitMQ 1. 同步和异步通讯 1.1 同步调用 1.2 异步调用 2. MQ常见框架 二&#xff1a;RabbitMQ快速入门 1. RabbitMQ概述和安装 2. 常见消息队列模型 3. 快速入门案例 三&#xff1a;SpringAMQP 1. Basic Queue 简单队列模型 2. Work Queu…

尝试OmniverseFarm的最基础操作

目标 尝试OmniverseFarm的最基础操作。本地机器作为Queue和Agent&#xff0c;同时在本地提交任务。 主要参考了官方文档&#xff1a; Farm Queue — Omniverse Farm latest documentation Farm Agent — Omniverse Farm latest documentation Farm Examples — Omniverse Far…

遥感单通道图像保存为彩色图像

系列文章目录 第一章PIL单通道图像处理 文章目录 系列文章目录前言一、代码实现二、问题记录在这里插入图片描述 总结 前言 将单通道图像以彩色图像的形式进行保存主要使用了PIL库 一、代码实现 palette_data [***]&#xff1a;可以进行自定义设置 代码如下&#xff1a; fr…

docker微服务案例

文章目录 建立简单的springboot项目(boot3)boot2建立通过dockerfile发布微服务部署到docker容器编写Dockerfile打包成镜像运行镜像微服务 建立简单的springboot项目(boot3) 1.建立module 2. 改pom <?xml version"1.0" encoding"UTF-8"?> <…

Apache JMeter 5.5: 新手指南

如何获取并运行 JMeter 首先&#xff0c;要使用 JMeter&#xff0c;你需要从官网获取软件包。前往 Apache JMeter 的官方页面&#xff0c;然后下载所 需的压缩文件。 配置和启动 JMeter 获取了 JMeter 后&#xff0c;由于它是无需安装即可使用的工具&#xff0c;直接解压下载…

构建自己的私人GPT-支持中文

上一篇已经讲解了如何构建自己的私人GPT&#xff0c;这一篇主要讲如何让GPT支持中文。 privateGPT 本地部署目前只支持基于llama.cpp 的 gguf格式模型&#xff0c;GGUF 是 llama.cpp 团队于 2023 年 8 月 21 日推出的一种新格式。它是 GGML 的替代品&#xff0c;llama.cpp 不再…

PPT插件-大珩助手-选择同类

选择同类-颜色 对于选中的形状&#xff0c;一键选中当前页中的所有相同颜色的形状 选择同类-文本 一键选择当前页中的所有文本对象 选择同类-非文本 一键选择当前页中的所有非文本对象 选择同类-反选 一键选择当前页未选择的对象 软件介绍 PPT大珩助手是一款全新设计的…

TinyLog iOS v3.0接入文档

1.背景 为在线教育部提供高效、安全、易用的日志组件。 2.功能介绍 2.1 日志格式化 目前输出的日志格式如下&#xff1a; 日志级别/[YYYY-MM-DD HH:MM:SS MS] TinyLog-Tag: |线程| 代码文件名:行数|函数名|日志输出内容触发flush到文件的时机&#xff1a; 每15分钟定时触发…

groovy XmlParser 递归遍历 xml 文件,修改并保存

使用 groovy.util.XmlParser 解析 xml 文件&#xff0c;对文件进行修改&#xff08;新增标签&#xff09;&#xff0c;然后保存。 是不是 XmlParser 没有提供方法遍历每个节点&#xff0c;难道要自己写&#xff1f; 什么是递归&#xff1f; 不用说&#xff0c;想必都懂得~ …

如何通过兴趣爱好选职业?

一个错误的选择&#xff0c;可能造成终身的遗憾&#xff0c;一个正确的选择&#xff0c;可以让我们少奋斗几十年。所以无论现在付出多少代价&#xff0c;多花一些时间&#xff0c;去研究以下未来的职业方向&#xff0c;这是值得的。 职业定位&#xff08;专业定位&#xff09;…

红队打靶练习:TOMMY BOY: 1

目录 信息收集 1、arp 2、nmap 3、nikto 4、whatweb WEB robots.txt get flag1 get flag2 FTP登录 文件下载 更改代理 ffuf爆破 get flag3 crunch密码生成 wpscan 1、密码爆破 2、登录wordpress ssh登录 get flag4 信息收集 get flag5 信息收集 1、arp …

阿里云c8i服务器CPU性能、架构及费用测评

阿里云第八代云服务器ECS计算型c8i实例&#xff0c;CPU采用Intel Xeon Emerald Rapids或者Intel Xeon Sapphire Rapids&#xff0c;主频不低于2.7 GHz&#xff0c;全核睿频3.2&#xff0c;阿里云百科aliyunbaike.com分享阿里云c8i服务器CPU处理器型号、存储、网络、安全、使用场…

【uniapp】APP打包上架应用商-注意事项

初雪云-uniapp启动图自定义生成&#xff08;支持一键生成storyboard&#xff09; HBuilderX需要的自定义storyboard文件格式为 " zip压缩包 " 一、“Android” — 设置targetSdkVersion 小米、OPPO、vivo、华为等主流应用商店&#xff0c;将于2023年12月采用 targetS…

多链混沌:Layer2 格局演变与跨链流动性的新探索

点击查看原文&#xff1a;多链混沌&#xff1a;Layer2 格局演变与跨链流动性的新探索 如今的 Crypto 是一个由多链构成的混沌世界。曾经&#xff0c;以太坊聚集了加密世界绝大多数的流动性与 DeFi 应用&#xff0c;但现在其 TVL 占比已经降到 60% 以下&#xff0c;并仍处于下降…

golang实现加密解密文档

golang实现加密解密文档 package mainimport ("bytes""crypto/aes""crypto/cipher""crypto/rand""encoding/base64""flag""fmt""io""io/ioutil" )func main() {encodePtr : flag.…

【开发小程序多少钱?智创开发】

开发一个小程序费用主要看做什么和怎么做&#xff1f; 第一部分&#xff1a;做什么&#xff1f; 做什么是指功能部分&#xff0c;开发的功能不一样&#xff0c;耗时也就不一样&#xff0c;价格自然也就不一样了。就好比买房&#xff0c;套二的公寓和别墅价格自然差距很大。所…

【python基础教程】print输出函数和range()函数的正确使用方式

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 print()有多个参数&#xff0c;参数个数不固定。 有四个关键字参数&#xff08;sep end file flush&#xff09;&#xff0c;这四个关键字参数都有默认值。 print作用是将objects的内容输出到file中&#xff0c;objects中的…

uni-app的学习【第二节】

四 路由配置及页面跳转 (1)路由配置 uni-app页面路由全部交给框架统一管理,需要在pages.json里配置每个路由页面的路径以及页面样式(类似小程序在app.json中配置页面路由) (2)路由跳转 uni-app有两种页面路由跳转方式:使用navigator组件跳转(标签式导航)、调用API跳…

AI人工智能虚拟现实行业发展分析

AI人工智能和虚拟现实是当今科技领域最受关注和研究的两个领域。这两项技术的迅速发展给各行各业带来了巨大的变革和机遇。在过去的几年里&#xff0c;AI和虚拟现实已经取得了显著的进展&#xff0c;并且有着广阔的发展前景。 AI人工智能作为一种模拟人类智能的技术&#xff0…

LangChain v0.1.0:大模型应用技术革新的里程碑

LangChain v0.1.0 最新版本发布, 大模型应用技术革新的里程碑 LangChain官方宣布发布了langchain 0.1.0,这是Langchain第一个稳定版本。支持 Python 和 JavaScript。 Langchain简介 LangChain已经存在了一年多,随着LangChain成长为构建LLM应用程序的默认框架,LangChain已…