【JavaEE】锁策略、CAS和synchronized的优化

news2024/10/5 20:23:51

目录

1、常见的锁策略

1.1、乐观锁 vs 悲观锁

1.2、轻量级锁 vs 重量级锁 

1.3、自旋锁 vs 挂起等待锁

 1.4、互斥锁 vs 读写锁

1.4.1、读写锁的使用场景(适用于"频繁 读,不频繁写"的场景) 

1.5、可重入锁 vs 不可重入锁 

1.5.1、死锁的多种情况

 1.5.2、死锁的破解方法

1.6、公平锁 vs 非公平锁

 2、CAS(Compare and swap)

2.1、CAS的应用 

2.1.1、实现原子类

2.1.2、实现自旋锁

2.2、 CAS的ABA问题

2.2.1、什么是ABA问题

 2.2.2、解决CAS中的ABA问题

3、synchroinzed原理

3.1、synchronized的基本特点

 3.2、synchronized加锁工作过程(锁膨胀/锁升级)

3.2.1、偏向锁

3.3、锁消除

3.4、 锁粗化


1、常见的锁策略

1.1、乐观锁 vs 悲观锁

锁的实现者,预测接下来所冲突的概率是比较大,还是比较小,根据这个冲突的概率,来决定接下来该咋做。

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

【举个🌰】:2022年国家疫情放开,有的人听到这个消息,是悲观的态度,想着社会上有大面积的人感染,为了和这些人不接触,所以这些比较悲观的人会屯大量的物资。所以悲观锁一般要做的工作更多一些,效率会更低一些。乐观的人,提到这个消息,无所谓,既然国家都放开了,这就说明病毒的已经没有太大的威胁了,也就没有屯物资。所以乐观锁做的工作就会更少一点,效率更高一点。但这并不绝对。


1.2、轻量级锁 vs 重量级锁 

  • 重量级锁加锁解锁过程更慢,更低效加锁机制重度依赖了OS提供了mutex,大量的内核态和用户态的切换,很容易引发线程的调度。
  • 轻量级锁加锁解锁工程更快,更高效加锁机制尽可能不适用mutex,而是尽量在用户态代码完成。是在搞不定了,在使用mutex。少量的内核态和用户态的切换,不太容易引发线程调度。

也可以认为一个乐观锁很可能是一个轻量级锁,一个悲观锁很可能也是一个重量级锁(但是这个结论不绝对)


1.3、自旋锁 vs 挂起等待锁

自旋锁轻量级锁的一种典型实现挂起等待锁重量级锁的一种典型实现

  • 自旋锁:如果获取锁失败了,立即再次尝试获取锁,无限次循环,直到获取到锁为止,第一次获取锁失败,第二次的尝试会在极短的时间内到来一旦锁被其他线程释放,就能第一时间获取到锁。(通常自旋锁是纯用户态的不需要经过内核态,获取锁的时间相对更短)
  • 挂起等待锁当某个线程在获取到锁的时候,其他那些没有获取到锁的线程只能挂起等待,此时这些线程会被CPU调度走,等到锁被释放,这些被CPU调度走的线程,在被CPU调度回来后,就会重新进行锁竞争。(通过内核的机制来实现挂起等待,获取锁的时间更长了)

【举个🌰】:比如我们在追求自己的女神的时候,被发好人卡,这个时候我们没有气馁,和之前一样每天给女神发,早安,晚安。有一天女神和自己的男朋友分手了,这个时候,我们就能第一时间抓住时机上位。这就相当于自旋锁,他会一直占用CPU的资源,进行忙等;而另一种情况,就是我们在被拒绝之后,潜心敲代码,将女神抛掷脑后,突然有一天女神说要不咱俩处对象试试。这种情况就相当于挂起等待锁,CPU将没有获得锁的线程调度去干别的事情,当“女神”这个锁被释放了,在将这些线程调度回来,进行锁竞争,然后某个线程获取锁。

✨ 自旋锁的优缺点:

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

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


 1.4、互斥锁 vs 读写锁

  • 互斥锁:是一种独占锁,之前的博客中使用synchronized加锁之后,线程A获得这个锁了,线程A在没有释放这个锁之前,那么线程B就会加锁失败,失败的线程B就会释放CPU让给其他线程,既然线程B释放掉了CPU,自然线程B加锁的代码就会被阻塞。对于互斥锁只有两个操作:加锁和解锁。只有两个线程针对同一个锁对象加锁时,才会产生锁竞争(互斥)。
  • 读写锁一个线程对于数据的访问,主要存在两种操作:读数据和写数据读写锁就是把读操作和写操作区分对待。对于读写锁来说,分为三个操作:读加锁,写加锁,解锁

读写锁中约定:

  1. 读锁和读锁之间,不会锁竞争,不会产生阻塞等待。(不会影响程序的速度,代码还是可以跑的很快)
  2. 写锁和写锁之间,有锁竞争。
  3. 读锁和写锁之间,也有锁竞争(2,3这两种,速度虽然减慢,但是保证了准确性)。

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

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

1.4.1、读写锁的使用场景(适用于"频繁 读,不频繁写"的场景) 

比如学习通,每节课老师都要使用学习通点名,点名就需要查看班级的同学列表(读操作),这个操作可能要每周执行好几次,而什么时候修改同学列表呢(写操作)?就是有同学加入这个班级的时候,可能一年都不必改一次。

再比如,同学使用学习通查看作业时(读操作),一个班级的同学很多,多操作一天就要进行几十次,但是这一节课的作业,老师只是布置了一次(写操作)。

❗❗❗总结

  • 读写锁在多个线程进行读一个数据的时候,此时并没有线程安全问题,直接并发的读取即可
  • 多个线程都要写一个数据的时候,有线程安全问题。这个时候就需要对这个数据进行加锁。
  • 多个线程,一些在读数据,一些在修改这些数据,也存在线程安全问题,这个时候就要正对这个数据进行读加锁和写加锁。(在写的时候,不允许读;再读的时候,不允许写)
  • synchronized不是读写锁

1.5、可重入锁 vs 不可重入锁 

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

  • 出现了死锁,就是不可重入锁;
  • 不出现死锁,就是可重入锁

Java里面只要是以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有线程的Lock实现类,包括synchronized关键字锁都是可重入的。

我们通过一个伪代码来了解一下可重入锁和不可重入锁。

class BlockingQueue{
    synchronized void put(){
        this.size();
    }
    
    synchronized int size(){
    }
}

public static void main(String[] ){
    BlockingQueue queue = new BlockingQueue();
    Thread t = new Thread(()->{
        queue.put();
    });

这个时候,两个方法的锁对象都是queue,t线程调用put方法,针对锁对象queue,put方法进行了加锁,这个时候t线程被占用了,但是在执行put的方法体的时候,size方法也针对queue对象加锁,这个时候第二个锁尝试加锁,需要等待第一个锁被释放。第一个锁要释放,就需要第二个锁加锁成功。这在逻辑上就矛盾了。也就形成了死锁。这样的锁称为不可重入锁。上述使用synchronized对不可重入锁进行了逻辑上的讲解,但是synchronized是可重入锁。

❓❓❓上述这种情况在我们的日常开发中很容易遇到,当遇到这种情况的时候,就真的的死锁了吗?


❗❗❗当然是不会,因为我们的synchronized是个"可重入锁"。在上述的场景中不会死锁,一个线程在第二次对同一个锁对象加锁的时候会判定一下,看当前尝试申请锁的线程是不是已经就是锁的拥有者了,如果是,则直接放行。


1.5.1、死锁的多种情况

1️⃣上述说到的,一个线程,一把锁,加锁两次,可重入锁没事,不可重入锁死锁了

2️⃣两个线程两把锁,即使是可重入锁,也会死锁。

3️⃣ N个线程,M把锁。

这里通过哲学家,就餐问题来了解这种死锁情况。

✨死锁的四个必要条件(只要发生死锁,这四个条件都有体现。)

  1. 互斥使用:一个线程拿到一把锁之后,另一个线程不能使用。(锁的基本特点)
  2. 不可抢占:一个线程拿到所,只能自己主动释放,不能被其他线程强行占有【挖墙脚行为是不行的】(锁的基本特点)
  3. 请求和保持:就像上面的例子,哲学家拿到一个筷子之后,去哪另一支筷子,拿到的绝不放手。【吃着碗里的,惦记锅里的】(代码的特点,看我们自己怎样设计代码)
  4. 循环等待:上面的哲学家例子中,同时5个哲学家同时拿起左手边的筷子,想要拿起另一支筷子吃面条。这个时候就形成了循环等待。(代码的特点)

 1.5.2、死锁的破解方法

 ❓❓❓死锁是一个比较严重的bug,实践中如何避免出现死锁呢?


❗❗❗这个时候,很多老铁想到了银行家算法,但是这里我们并不推荐这个写法,因为银行家算法实现起来比较复杂,再开发中,追求的是简单可靠。

  1. 所以这里我们推荐一个简单有效的做法,可以通过破解死锁的必要条件中的一个,就可以避免死锁的发生。这里最好破解的就是循环等待这个条件。
  2. 我们针对锁进行编号,如果需要同时获取多把锁,约定加锁顺序,务必是先对小的编号加锁,后对大的编号加锁。这里还是通过哲学家就餐的例子来理解破解的方法

 再代码的设计时,让多个线程按照顺序加锁就可以了,多个线程多把锁,让其中两个线程同时获取最小的一把锁,这个时候就会形成一个线程一个锁都没有拿到。这个时候就会将死锁破解。


1.6、公平锁 vs 非公平锁

  • 公平锁:多个线程等待同一个锁的时候,谁先来,谁就先获取到这把锁(遵守先来后到)
  • 非公平锁:多个线程在等待同一个锁的时候,不遵守先来后到(每个等待的线程获取到锁的概率时均等的)。

❗❗❗注意:

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

 2、CAS(Compare and swap)

CAS的全称就是Compare and swap(比较和交换)

这里是将寄存器A的值和内存M的值进行对比,如果值相同,就把寄存器B内存M的值进行交换

我们通过下面的这个不是原子的伪代码来了解CAS的硬件指令,真实的CAS是原子的硬件指令来完成的,这个伪代码只是辅助理解CAS的工作流程。

此处所谓的CAS指的是CPU提供的一个单独的CAS指令,通过这一条指令,就完成上述伪代码描述的过程,CPU指令已经是不可分割的最小单位。当多个线程同时对某个资源进行CAS操作,只能由一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。

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

CAS最大的意义可以让我们在写多线程代码的时候不加锁,就能够保证线程安全。

 当多线程编程的时候,我们既要保证线程安全,又不想加锁,就可以使用CAS进行"无锁编程",下面的原子类和自旋锁都是无锁编程中的一些具体实现。


2.1、CAS的应用 

2.1.1、实现原子类

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

我们创建AtomicInteger类对象,在线程t1和t2线程中对这个对象进行自增50000次。查看最终结果。


import java.util.concurrent.atomic.AtomicInteger;
public class ThreadDemo26 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger num = new AtomicInteger(0);
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                //num++  后置++
                num.getAndIncrement();
//                //++num  前置++
//                num.incrementAndGet();
//                //--num  前置--
//                num.decrementAndGet();
//                //num--  后置--
//                num.getAndDecrement();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                num.getAndIncrement();
            }
        });
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        //get 获取到数据
        System.out.println(num.get());
    }
}

 通过这个伪代码来,了解AtomicInteger类。

❓❓❓上述伪代码中在执行CAS之前,已经将value的值赋给了oldValue,这个时候在使用CAS进行比较value和oldValue的值相等,是不是没有意义?


❗❗❗肯定是有意义的,因为在多线程的环境下,线程的调度是随机的,可能线程1在执行完oldvalue = value,这个时候线程2将,线程2寄存器中的oldvalue值修改了并且传给了内存(value改变了),这个时候再执行线程t1,这个时候内存中的值(value)就和t1线程中寄存器中的值(oldvalue)不相同了。

2.1.2、实现自旋锁

我们还是通过下面的伪代码来了解CAS实现自旋锁。

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

2.2、 CAS的ABA问题

2.2.1、什么是ABA问题

假设存在两个线程t1和t2,有一个共享变量num在内存中,初始值为A。

接下来,线程t1想使用CAS把内存中的值(num)改成Z,那么就需要

  • 先从内存中读取num的值,记录到oldNum变量(寄存器)中。
  • 使用CAS判定当前内存中的值num和寄存器中的值oldNum是否相等为A,为A,就将内存中的值修改成B。

但是内存中的值(num)和寄存器中的值(oldNum)相等中间存在两种情况。

  1. t1线程在执行上述两个操作的时候,中间没有其他线程修改内存中num的值,一直就是A
  2. t1线程在执行这两个操作时,可能将第一个操作执行完,t2线程被系统调度给CPU,内存中的值(num)被t2线程改成了B,又将B改成了A。

第二种情况,t1线程就无法区分但钱这个变量始终是A,还是经历了一个变化过程。(就好比我们买手机,买了一个翻新机,但是我们看不出来。)CAS只能对比值是否相同,不能确定这个值是否中间发生过改变

 2.2.2、解决CAS中的ABA问题

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

  1. 我们要解决这个问题,可以通过约定数据只能单方向变化(只能增加,或者只能减小),问题就迎刃而解了。
  2. 但是如果我们的需求是该数值,既能增加也能减小,这个时候我们可以引入另外一个版本号变量,约定版本号只能增加(每次修改,都会增加一个版本号),这样每次CAS对比的时候,就不是对比数值本身,而是对比版本号。

下面的图不是完全正确,但是在大体范围内描述了 使用版本号解决CAS的ABA问题。

 只要约定版本号,只能递增或者递减,就能保证此时不会出现ABA反复横跳的问题,以版本号为基准,而不是以变量数值为基准了。


3、synchroinzed原理

上面说到的CAS,我们用来解释了自旋锁的实现。这里来了解synchronizedla工作过程,来看synchronized里面具体都干了啥。

3.1、synchronized的基本特点

根据前面所说的所策略,我们就可以总结出,synchronized具有一下特性(只考虑JDK1.8)

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

 3.2、synchronized加锁工作过程(锁膨胀/锁升级)

JVM将synchronized锁分为无锁,偏向锁、轻量级锁、重量级锁状态。会根据锁的竞争激烈程度,对锁状态进行升级。

  • 锁的级别按照下面的先后顺序升级,我们把这个升级过程称为"锁膨胀"。
  • 锁的升级是单向的,也就是说只能从低到高升级,不会出现降级的情况。

当锁升级为轻量级锁的时候,如果当前锁竞争非常的激烈,比如10个线程,竞争1个锁,1个竞争上 了,另外9个等待,也就是说这10个线程都在轻量级锁策略的情况下,那么9个线程进行自旋等待(忙等),CPU的消耗就非常大,既然如此就要将锁升级为重量级锁,在内核里进行阻塞等待,这个时候就意味着等待的线程暂时放弃CPU,有内核进行后续调度。 

 上述锁状态中只有偏向锁,没有介绍,我们在这里来了解一下偏向锁。

3.2.1、偏向锁

  • 偏向锁不是真的"加锁"只是给对象头中做一个"偏向锁标记"记录这个锁属于那个线程。
  • 如果后续没有其他线程来竞争该锁,那么此时就不用真的加锁了(避免了加锁和解锁的开销)
  • 但是一旦有别的线程尝试来竞争这个锁,于是偏向锁就会立即升级为真的锁(轻量级锁),此时别的线程只能等待。(偏向锁的策略,既保证了效率,又保证了线程安全

3.3、锁消除

  • 上面的锁升级是在代码运行阶段进行的优化手段,这里的锁消除是在编译阶段进行的优化手段。
  • 锁消除:编译器+JVM会检测当前代码是否是多线程执行,是否有必要加锁,如果没有必要,在编写的时候又把锁给写上了,就会在编译过程中自动把锁去掉。

例如我们之前说到的StringBuffer,他是一个线程安全的字符串类 ,它的关键方法都加了synchronized关键字。

如果是单线程情况下使用StringBuffer,不会涉及线程安全问题,不需要使用synchronized关键字,但是StringBuffer类的关键方法中都加了synchronized关键字,这个时候每次调用StringBuffer类中的方法,就会经行加锁和解锁,加锁和解锁这个操作,会浪费一些资源。所以使用消除锁的策略,就能在编译阶段消除这个问题。


3.4、 锁粗化

  • 锁的粒度:synchronized代码块,包含代码的多少(代码越多,粒度越大 ;代码越少,粒度越细)。
  • 一般写代码的时候,多数情况下,是希望锁的粒度更小一点(串行执行的代码少,并发执行的代码就多)。串行代码越少越少,程序执行就越快。

 但是事无绝对,有的时候并不是锁的粒度越小越好,如果频繁加锁和解锁,此时编译器就可能把这个操作优化成一个粒度更粗的锁。因为每次加锁解锁,都会有开销的,尤其是释放锁之后,想要重新加锁,还需要重新竞争。

 

我们举个例子来看:

 滑稽老哥当了领导, 给下属交代工作任务:

方式一:

  • 打电话, 交代任务1, 挂电话.
  • 打电话, 交代任务2, 挂电话.
  • 打电话, 交代任务3, 挂电话.

方式二:

  • 打电话, 交代任务1, 任务2, 任务3, 挂电话.

显然, 方式二是更高效的方案

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

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

相关文章

计算机专业学习的核心是什么?

既然是学习CS&#xff0c;那么在这里&#xff0c;我粗浅的把计算机编程领域的知识分为三个部分&#xff1a; 基础知识 特定领域知识 框架和开发技能 基础知识是指不管从事任何方向的软件工程师都应该掌握的&#xff0c;比如数据结构、算法、操作系统。 特定领域知识就是你…

Python花瓣雨

目录 前言 小海龟 花朵类 移动函数 画花朵 尾声 前言 来啦来啦来啦&#xff0c;小伙伴们快快来领取七彩花瓣雨吧&#xff01;&#xff01; 小海龟 老生常谈啦&#xff0c;在用python画樱花树前&#xff0c;我们先来了解一下turtle吧&#xff01; 小海龟(Turtle)是P…

Java学习路线(13)——Collection集合类:List集合与Set集合

一、集合类体系结构 二、部分Collection类型对象 Collection集合特点 List系列集合是有序、可重复、有索引。 ArrayList&#xff1a;有序、可重复、有索引LinkedList&#xff1a;有序、可重复、有索引 Set系列集合是无序、不重复、无索引。 HashSet&#xff1a;无序、不重复…

0202条件过滤-自动装配原理-springboot2.7.x系列

1前言 在springboot的自动装配过程中&#xff0c;执行完候选配置类导入后&#xff0c;会进行条件过滤。那么在讲解条件过滤前&#xff0c;我们先来了解springboot常用的条件注解&#xff0c;以及它们底层执行原理。 在Spring Boot中&#xff0c;条件&#xff08;Condition&am…

使用qemu模拟CXL.mem设备

CXL可以说是自PCIe技术诞生几十年以来最具变革性的新技术了。可以想象有了CXL以后机箱的边界将被彻底打破&#xff0c;服务器互相使用对方的内存&#xff0c;网卡&#xff0c;GPU 。整个机架甚至跨机架的超级资源池化成为可能&#xff0c;云计算也将进入一个新的时代。 当前In…

C++寄存器优化

在C里面有个有趣的现象&#xff0c;先看代码 #include<iostream> using namespace std; int main() {int const tmp 100; //定义常量tmp tmp不能修改int const* p &tmp; //不能通过指针修改指向的值 int* const q const_cast<int*>(p); //去常属性 可以通过…

【C++11】C++11新增语法特性 右值引用/移动语义/完美转发

C11 右值引用 1 右值引用1.1 左值 、 右值1.2 左值引用 VS 右值引用1.3 谈谈C11引入右值引用的意义1.4 左值引用和右值引用的一些细节问题 2 移动语义3 完美转发4 总结 1 右值引用 1.1 左值 、 右值 在C中所有的值不是左值就是右值。左值是指表达式结束后依然存在的持久化对象…

【JavaSE】Java基础语法(二十二):包装类

文章目录 1. 基本类型包装类2. Integer类3. 自动拆箱和自动装箱4. int和String类型的相互转换 1. 基本类型包装类 基本类型包装类的作用 将基本数据类型封装成对象的好处在于可以在对象中定义更多的功能方法操作该数据常用的操作之一&#xff1a;用于基本数据类型与字符串之间的…

Goby 漏洞更新|锐捷网络 NBR路由器 webgl.data 信息泄露漏洞

漏洞名称&#xff1a;锐捷网络 NBR路由器 webgl.data 信息泄露漏洞 English Name&#xff1a;Ruijie NBR Router webgl.data information CVSS core: 7.5 影响资产数&#xff1a;204290 漏洞描述&#xff1a; 锐捷网络NBR700G路由器是锐捷网络股份有限公司的一款无线路由设…

口碑超好的挂耳式耳机盘点,这几款蓝牙耳机值得一看!

运动已成为人们业余生活中不可缺少的组成部分&#xff0c;徒步、夜跑、骑行等运动项目受到越来越多的人的喜欢&#xff0c;运动与耳机的搭配也是当代年轻人喜爱的行为方式&#xff0c;在颠簸的运动项目中耳机的稳固性和舒适性是非常主要的&#xff0c;现在新推出的开放式耳机深…

Ps:移除工具

移除工具 Remove Tool是一款简单易用、功能强大的工具&#xff0c;可快速、轻松地清去除图片中的干扰元素或不需要的区域。 快捷键&#xff1a;J 就如同使用画笔工具一样&#xff0c;只要在不需要的对象上涂抹&#xff08;描边&#xff09;即可将其去除。 移动工具基于人工智能…

几号发工资就能看出公司的好坏?(文末附招聘岗位)

作为一名资深的职场搬砖人&#xff0c;不知道各位最近有没有跟我一样关注到这个话题 ​#发工资时间看公司#小编刚看到这个话题的第一印象&#xff0c;想的是发工资时间无非是公司实力的体现&#xff0c;工资发的越早证明这个公司的现金流越稳定强大。 打开评论区&#xff0c;不…

Linux——进程优先级

1.什么是优先级&#xff1f; 优先级和权限息息相关。权限的含义为能还是不能做这件事。而优先级则表示&#xff1a;你有权限去做&#xff0c;只不过是先去做还是后去做这件事罢了。 2.为什么会存在优先级&#xff1f; 优先级表明了狼多肉少的理念&#xff0c;举个例子&#xff…

Processing通过编程实现艺术设计_实现艺术和现实的交互---数据设计分析002

还记得这个生命的游戏,也是在这上面有 https://processing.org/ 官网是这个 使用Processing可以用编程的方式来创作艺术 Processing是一门开源编程语言,可以直接对用它来做艺术创作, 可以看一些它的作品 https://processing.org/examples/gameoflife.html 官网是这个,完…

作为C/C++程序员你可以不使用但你必须会的Linux调试器-gdb(GNU Debugger)

gdb(GNU Debugger) 是一个用于调试 Linux 系统的软件工具。在学习 Linux 的过程中&#xff0c;gdb 的重要性不言而喻。以下是 gdb 在 Linux 学习者中的重要性的详细说明: 帮助理解 Linux 系统的运作方式:gdb 是一个强大的调试工具&#xff0c;可以帮助学习者深入了解 Linux 系统…

代码随想录算法训练营day53 | 1143.最长公共子序列,1035.不相交的线,53. 最大子序和 动态规划

代码随想录算法训练营day53 | 1143.最长公共子序列&#xff0c;1035.不相交的线&#xff0c;53. 最大子序和 动态规划 1143.最长公共子序列解法一&#xff1a;动态规划 1035.不相交的线解法一&#xff1a;动态规划 53. 最大子序和 动态规划解法一&#xff1a;动态规划解法二&am…

Python学习38:凯撒密码——解密

类型&#xff1a;字符串‪‬‪‬‪‬‪‬‪‬‮‬‪‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‪‬‭‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‮‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬‪‬‪‬‪‬‪‬‪‬‮‬‭‬‫‬‪‬‪‬‪‬‪‬‪‬‮‬‫‬‪‬ 描述‪‬‪…

【Netty】Netty 编码器(十三)

文章目录 前言一、MessageToByteEncoder 抽象类二、MessageToMessageEncoder 抽象类总结 前言 回顾Netty系列文章&#xff1a; Netty 概述&#xff08;一&#xff09;Netty 架构设计&#xff08;二&#xff09;Netty Channel 概述&#xff08;三&#xff09;Netty ChannelHan…

notepad++查询指定内容并复制

背景说明 记录一下使用notepad进行文本内容查找以及替换的相关场景,简单记录方便后期查看,场景内容: 1.从指定的给出内容中筛选出所有的人员id集合 2.将每一行后面添加逗号 1.从指定的给出内容中筛选出所有的人员id集合 要求从指定的给出内容中筛选出所有的人员id集…

便携式明渠流量计的使用特点

便携式明渠流量计使用特点&#xff1a; 便携式明渠流量计使用特点&#xff0c;首先了解相关要求&#xff1a; 随着新标准JJG711-1990《明渠堰槽流量计试行检定规程》、HJ/T15-2019《超声波明渠污水流量计技术要求及检测方法》、HJ 354-2019《水污染源在线监测系统(CODCr、NH3-N…