Lock锁和ReentrantLock锁


前言

JDK 1.5中提供的锁的接口java.util.concurrent.locks.Lock,其提供了一些ReentrantLock, ReentrantReadWriteLock实现类。

参考JDK文档:Java Platform SE 6


目录

前言

Lock接口

ReentrantLock

公平性和非公平性

公平锁与非公平锁的使用示例 

AQS

AQS核心字段

AQS同步器原理

ReentrantLock实现

公平性锁和非公平性锁父类:Sync

公平性锁实现:FairSync

非公平性锁实现:NonfairSync

重入锁实现:

condition

Condition与Object中的wati,notify,notifyAll区别:


Lock接口

Lock实现提供了比使用Synchronized方法和语句更广泛的搜定操作,此操作允许更灵活的结构,可以具有很大的属性,可以支持多个相关的Condition对象

Lock接口的提供的方法有:

图片.png

public interface Lock {void lock();void lockInterruptibly() throws InterruptedException;boolean tryLock();boolean tryLock(long time, TimeUnit unit) throws InterruptedException;void unlock();Condition newCondition();
}
        //todo//加锁  未抢到锁的阻塞下来,直到抢到锁才会执行后续的逻辑reentrantLock.lock();try {//可中断的加锁reentrantLock.lockInterruptibly();//尝试性加锁 如果加锁成功 返回true  加锁失败会返回false  立即返回reentrantLock.tryLock(1000,TimeUnit.MILLISECONDS);reentrantLock.tryLock();} catch (InterruptedException e) {e.printStackTrace();}//todo//释放锁reentrantLock.unlock();

         Lock对比Synchronized加锁操作

  1. Lock实现类的锁必须是显性的调用加锁、释放锁,且必须成对出现
  2. 加锁操作可以滴方法内部的核心代码片段,加锁粒度更细,意味着并发性更高
  3. 可以通过lockInterruptibly方式添加可中断锁
  4.  可以尝试性加锁,未抢到锁可以立即返回 

ReentrantLock

公平性和非公平性

如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。而该线程被“饥饿致死”正是因为它得不到CPU运行的机会。解决饥饿的方案被称之为“公平性”,即所有线程均能公平地获得运行机会。通俗讲,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。

线程饥饿的原因:

  1. 高优先级的线程会比低优先级的线程优先执行
  2. 线程被阻塞在一个等待进入同步块的状态

公平性锁:

在ReentrantLock中通过FairSync实现公平性锁,其实现是基于一个队列来实现的。

非公平锁:

NonfairSync:在锁中保持一个获取锁的线程信息,当释放锁之后再次抢锁时,通过比较正在抢锁的线程和队列头的线程,如果是上一次获取锁的线程,那么该线程具有优先执行权。

ReentrantLock实现了公平锁和非公平锁。

public ReentrantLock(boolean fair) //参数是Boolean  true:公平性锁  false:非公平性锁
public ReentrantLock ()  //默认值是false 

公平锁与非公平锁的使用示例 

在while循环中进行显性加锁lock,通过打印看是哪个线程抢到了cpu的执行机会,然后再释放锁。

public class NonFairAndFairDemo implements Runnable {private static Integer num = 0;private ReentrantLock rtl;public NonFairAndFairDemo(ReentrantLock rtl) {this.rtl = rtl;}@Overridepublic void run() {while (true) {//显性加锁rtl.lock();num++;System.out.println(Thread.currentThread().getName()+":"+num);rtl.unlock();}}public static void main(String[] args) {//非公平性锁的实现ReentrantLock nonFairLock = new ReentrantLock(true);new Thread(new NonFairAndFairDemo(nonFairLock),"A").start();new Thread(new NonFairAndFairDemo(nonFairLock),"B").start();}
}

非公平性锁运行,传入false就是非公平锁,true就是公平锁:

ReentrantLock nonFairLock = new ReentrantLock(false);

由运行结果可以看出两个线程抢到资源后会一直抢占,经过一段时间后才被另一个线程抢到,是不公平的。 

公平性锁运行结果:

ReentrantLock nonFairLock = new ReentrantLock(true);

公平性锁机制保证了先来的锁优先级高,抢到运行机会的概率大,它的实现机制是基于队列的,因此如果A线程先到,那么等释放锁了之后A线程就会先运行,反之B先运行,这也就是说他们之间的运行时有序的,交替执行的。

AQS

实现提供了一个双向队列,将同步失败线程加入到双向队列,(Node节点包含线程信息,状态waitState),AQS中包含了state.  AQS类与子类层级关系如下:

图片.png

AbstractQueuedSynchronizer是CountDownLatch/FutureTask/ReentrantLock/RenntrantReadWriteLock/Semaphore的基础,因此AbstractQueuedSynchronizer是Lock/Executor实现的前提。公平锁、不公平锁、Condition、CountDownLatch、Semaphore等实现的基础。

AQS核心字段

AQS里面有三个核心字段:

private volatile int state;

private transient volatile Node head;

private transient volatile Node tail;

其中state描述的有多少个线程取得了锁,对于互斥锁来说state<=1。

AQS中的state:

state=0 表示锁是空闲状态

state>0 表示锁被占用

state<0 表示溢出

head/tail加上CAS操作就构成了一个CHL的FIFO队列,下面是Node节点的属性:

 static final class Node {/** 标记表示节点正在共享模式中等待 */static final Node SHARED = new Node();/** 标记表示节点正在独占模式下等待 */static final Node EXCLUSIVE = null;/** * 表示线程已经被取消 * 同步队列中的线程因为超时或中断,需要从同步队列中取消。被取消的节点将不会有任何改变*/static final int CANCELLED = 1;/** * 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后* 继节点,使后继节点的线程得以运行 */static final int SIGNAL = -1;/** * 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法 *  后,该节点将会中等待队列中转移到同步队列中,加入到对同步状态的获取 */static final int CONDITION = -2;/*** 下一次共享模式同步状态获取将会无条件的被传播下去*/static final int PROPAGATE = -3;/***   等待状态,仅接受如下状态中的一个值:*   SIGNAL:  -1*   CANCELLED:   1*   CONDITION:   -2*   PROPAGATE:   -3*   0:  初始化的值** 对于正常的同步节点,它的初始化值为0,对于条件节点它的初始化的值是CONDITION。它使用* CAS进行修改。*/volatile int waitStatus;/***  前驱节点*/volatile Node prev;/*** 后继节点*/volatile Node next;/*** 获取同步状态的线程*/volatile Thread thread;/*** 等待队列中的后继节点。如果当前节点是共享的,那么这个字段是一个SHARED常量,也就是说* 节点类型(独占和共享)和等待队列中的后继节点公用同一个字段*/Node nextWaiter;/*** 如果节点在共享模式下等待则返回true*/final boolean isShared() {return nextWaiter == SHARED;}/*** 获取前驱节点*/final Node predecessor() throws NullPointerException {Node p = prev;if (p == null)throw new NullPointerException();elsereturn p;}Node() {}Node(Thread thread, Node mode) { this.nextWaiter = mode;this.thread = thread;}Node(Thread thread, int waitStatus) { this.waitStatus = waitStatus;this.thread = thread;}
}

volatile int waitStatus:节点的等待状态,一个节点可能位于以下几种状态:

CANCELLED = 1

当前的线程被取消,节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。

SIGNAL = -1

表示当前节点的后继节点包含的线程需要运行,也就是unpark.节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。

CONDITION = -2

当前节点在等待condition,也就是在condition队列中.表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。PROPAGATE=-3当前场景下后续的acquireShared能够得以执行

当前节点在sync队列中,等待着获取锁

正常状态,新生的非CONDITION节点都是此状态。

非负值标识节点不需要被通知(唤醒)。

volatile Node prev;此节点的前一个节点。节点的waitStatus依赖于前一个节点的状态。

volatile Node next;此节点的后一个节点。后一个节点是否被唤醒(uppark())依赖于当前节点是否被释放。

volatile Thread thread;节点绑定的线程。

Node nextWaiter;下一个等待条件(Condition)的节点,由于Condition是独占模式,因此这里有一个简单的队列来描述Condition上的线程节点。

节点(Node)是构成CHL的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程会构建成一个节点并加入到同步器的尾部。CHL的基本结构如下:

image.png

AQS同步器原理

基本的思想是表现为一个同步器,支持下面两个操作:

获取锁:首先判断当前状态是否允许获取锁,如果是就获取锁,否则就阻塞操作或者获取失败,也就是说如果是独占锁就可能阻塞,如果是共享锁就可能失败。另外如果是阻塞线程,那么线程就需要进入阻塞队列。当状态位允许获取锁时就修改状态,并且如果进了队列就从队列中移除。

while(synchronization state does not allow acquire){enqueue current thread if not already queued;possibly block current thread;
}
dequeue current thread if it was queued;

释放锁:这个过程就是修改状态位,如果有线程因为状态位阻塞的话就唤醒队列中的一个或者更多线程。

update synchronization state;
if(state may permit a blocked thread to acquire)unlock one or more queued threads;

要支持上面两个操作就必须有下面的条件:

  • 原子性操作同步器的状态位
  • 阻塞和唤醒线程
  • 一个有序的队列

目标明确,要解决的问题也清晰了,那么剩下的就是解决上面三个问题。

  • 状态位的原子操作

这里使用一个32位的整数来描述状态位,使用CAS操作来修改状态。事实上这里还有一个64位版本的同步器(AbstractQueuedLongSynchronizer),这里暂且不谈。

  • 阻塞和唤醒线程

标准的JAVA API里面是无法挂起(阻塞)一个线程,然后在将来某个时刻再唤醒它的。JDK 1.0的API里面有Thread.suspend和Thread.resume,并且一直延续了下来。但是这些都是过时的API,而且也是不推荐的做法。

在JDK 1.5以后利用JNI在LockSupport类中实现了此特性。

LockSupport.park()

LockSupport.park(Object)

LockSupport.parkNanos(Object, long)

LockSupport.parkNanos(long)

LockSupport.parkUntil(Object, long)

LockSupport.parkUntil(long)

LockSupport.unpark(Thread)

上面的API中park()是在当前线程中调用,导致线程阻塞,带参数的Object是挂起的对象,这样监视的时候就能够知道此线程是因为什么资源而阻塞的。由于park()立即返回,所以通常情况下需要在循环中去检测竞争资源来决定是否进行下一次阻塞。park()返回的原因有三:

  • 其他某个线程调用将当前线程作为目标调用 unpark;
  • 其他某个线程中断当前线程;
  • 该调用不合逻辑地(即毫无理由地)返回。

其实第三条就决定了需要循环检测了,类似于通常写的while(checkCondition()){Thread.sleep(time);}类似的功能。

  • 有序队列

在AQS中采用CHL列表来解决有序的队列的问题。AQS采用的CHL模型采用下面的算法完成FIFO的入队列和出队列过程。对于入队列(enqueue):从数据结构上出发,入列是比较简单的,无非就是当前队列中的尾节点指向新节点,新节点的prev指向队列中的尾节点,然后将同步器的tail节点指向新节点。在AQS中入列的源码如下:

 /*** 为当前线程和给定的模式创建节点并计入到同步队列中** @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared* @return the new node*/
private Node addWaiter(Node mode) {// 创建一个节点Node node = new Node(Thread.currentThread(), mode);// 快速尝试添加尾节点,如果失败则调用enq(Node node)方法设置尾节点Node pred = tail;// 判断tail节点是否为空,不为空则添加节点到队列中if (pred != null) {node.prev = pred;// CAS设置尾节点if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;
}/*** 插入节点到队列中* @param node the node to insert* @return node's predecessor*/
private Node enq(final Node node) {// 死循环 知道将节点插入到队列中为止for (;;) {Node t = tail;// 如果队列为空,则首先添加一个空节点到队列中if (t == null) {if (compareAndSetHead(new Node()))tail = head;} else {// tail 不为空,则CAS设置尾节点node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}
}

从上面源码中我们可以看到,在将节点添加到CHL尾部的时候,使用了一个CAS方法(compareAndSetTail(pred, node)),这里使用CAS的原因是防止在并发添加尾节点的时候出现线程不安全的问题(即有可能出现遗漏节点的情况)

ReentrantLock实现

公平性锁和非公平性锁父类:Sync

static abstract class Sync extends AbstractQueuedSynchronizer {
abstract void lock();//非公平获取,公平锁和非公平锁都需要这个方法
final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {   //state == 0表示无锁//CAS确保即使有多个线程竞争锁也是安全的if (compareAndSetState(0, acquires)) {  //加锁成功//当前哪一个线程获取到锁,将线程信息记录到AQS里面  setExclusiveOwnerThread(current);   //设置当前持有锁的线程return true;                        //获取成功}} else if (current == getExclusiveOwnerThread()) {//当前线程正是锁持有者int nextc = c + acquires;if (nextc < 0) // 被锁次数上溢(很少出现)throw new Error("Maximum lock count exceeded");//锁被持有的情况下,只有持有者才能更新锁保护的资源setState(nextc);return true;}return false;}//释放protected final boolean tryRelease(int releases) {int c = getState() - releases;//只有锁的持有者才能释放锁if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {   //锁被释放free = true;setExclusiveOwnerThread(null);}setState(c);return free;}//当前线程是否持有锁protected final boolean isHeldExclusively() {return getExclusiveOwnerThread() == Thread.currentThread();}final ConditionObject newCondition() {return new ConditionObject();}//锁的持有者final Thread getOwner() {return getState() == 0 ? null : getExclusiveOwnerThread();}//加锁次数final int getHoldCount() {return isHeldExclusively() ? getState() : 0;}//是否上锁,根据state字段可以判断final boolean isLocked() {return getState() != 0;}
}

公平性锁实现:FairSync

final static class FairSync extends Sync {//见AbstractQueuedSynchronizer.java, 4.2节有final void lock() {acquire(1);}//公平版本的tryAcquire,除非是递归调用或没有等待者或者是第一个,否则不授予访问protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {//是等待队列的第一个等待者if (isFirst(current) &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);   //加锁成功return true;}}//当前线程正是线程的持有者else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)  //溢出throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
}isFirst的实现,即等待队列为空或者当前线程为等待队列的第一个元素final boolean isFirst(Thread current) {Node h, s;return ((h = head) == null ||((s = h.next) != null && s.thread == current) ||fullIsFirst(current));
}

非公平性锁实现:NonfairSync

final static class NonfairSync extends Sync {// 执行lock,尝试立即闯入,失败就退回常规流程final void lock() {if (compareAndSetState(0, 1))   //比较并设置state,成功则表示获取成功setExclusiveOwnerThread(Thread.currentThread());//锁持有者elseacquire(1);//获取失败,进入常规流程:acquire会首先调用tryAcquire}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}
}acquire的实现(AbstractQueuedSynchronizer.java)public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

重入锁实现:

  • ReentrantLock都是把具体实现委托给内部类(Sync、NonfairSync、FairSync),
  • ReentrantLock的重入计数是使用AbstractQueuedSynchronizer的state属性的,state大于0表示锁被占用、等于0表示空闲,小于0则是重入次数太多导致溢出了.
  • 可重入锁需要一个重入计数变量,初始值设为0,当成功请求锁时加1,释放锁时减1,当释放锁之后计数为0则真正释放锁;
  • 重入锁还必须持有对锁持有者的引用,用以判断是否可以重入;

condition

synchronized与wait()和notify()/notifyAll()方法相结合可以实现等待/通知模式,类ReentrantLock同样可以实现该功能,但是要借助于Condition对象。

newCondition方法:

public Condition newCondition()

返回用来与此 Lock 实例一起使用的 Condition 实例。

public interface Condition {//使当前线程进入休眠进行等待void await() throws InterruptedException;void awaitUninterruptibly();long awaitNanos(long nanosTimeout) throws InterruptedException;boolean await(long time, TimeUnit unit) throws InterruptedException;boolean awaitUntil(Date deadline) throws InterruptedException;//唤醒因await进入休眠的一个线程void signal();//唤醒因await进入休眠的所有线程void signalAll();
}

使用await、signal、signalAll是必须加锁的,使用重入锁的加锁释放锁await、signal要通知await的线程必须是作用于同一个Condition实例。

eg:ABC三个线程分别打印各自名称,打印结果ABCABC....A线程通知B线程(ab的Condition)、B线程通知C线程(bc的Condition)、C线程通知A线程(ca的Condition)

public class ABCThread implements Runnable {private ReentrantLock lock;private Condition sCondition;private Condition aCondition;public ABCThread(ReentrantLock lock, Condition sCondition, Condition aCondition) {this.lock = lock;this.sCondition = sCondition;this.aCondition = aCondition;}@Overridepublic void run() {int i = 0;while (i < 10) {//加锁lock.lock();try {//接收到C线程通知继续执行,否则就阻塞aCondition.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.print(Thread.currentThread().getName()+" ");//通知B线程sCondition.signal();i++;//释放锁lock.unlock();}}
}ReentrantLock reentrantLock = new ReentrantLock();Condition ab = reentrantLock.newCondition();Condition bc = reentrantLock.newCondition();Condition ca = reentrantLock.newCondition();new Thread(new ABCThread(reentrantLock,ab,ca),"A").start();new Thread(new ABCThread(reentrantLock,bc,ab),"B").start();new Thread(new ABCThread(reentrantLock,ca,bc),"C").start();reentrantLock.lock();ca.signal();reentrantLock.unlock();

Condition与Object中的wati,notify,notifyAll区别:

  1. Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。
  2. Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。对于同一个锁,我们可以创建多个Condition,在不同的情况下使用不同的Condition。例如,假如多线程读/写同一个缓冲区:当向缓冲区中写入数据之后,唤醒"读线程";当从缓冲区读出数据之后,唤醒"写线程";并且当缓冲区满的时候,"写线程"需要等待;当缓冲区为空时,"读线程"需要等待。如果采用Object类中的wait(),notify(),notifyAll()实现该缓冲区,当向缓冲区写入数据之后需要唤醒"读线程"时,不可能通过notify()或notifyAll()明确的指定唤醒"读线程",而只能通过notifyAll唤醒所有线程(但是notifyAll无法区分唤醒的线程是读线程,还是写线程)。   但是,通过Condition,就能明确的指定唤醒读线程。

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

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

相关文章

ReentrantLock源码分析(一)加锁流程分析

一、ReetrantLock的使用示例 static ReentrantLock lock new ReentrantLock(); public static void main(String[] args) throws InterruptedException { new Thread(ClassLayOutTest::reentrantLockDemo, "threadA").start(); Thread.sleep(1000);…

Lock与ReentrantLock

Lock Lock位于java.util.concurrent.locks包下&#xff0c;是一种线程同步机制&#xff0c;就像synchronized块一样。但是&#xff0c;Lock比synchronized块更灵活、更复杂。 1. Lock继承关系 2. 官方文档解读 3. Lock接口方法解析 public interface Lock {// 获取锁。如果锁…

ReentrantLock介绍

文章目录 ReentrantLock1、构造函数2、公平性锁和非公平性锁&#xff08;1&#xff09;公平性锁和非公平性锁示例&#xff08;2&#xff09;公平锁和非公平锁的实现公平性锁&#xff1a;FairLock非公平性锁 &#xff08;3&#xff09;Condition生产者和消费者 循环打印ABC Reen…

ReentrantLock锁相关方法

目录 Lock接口的实现类 ReentrantLock的方法 ReentrantLockTest测试 用于测试的线程 t1测试 正确释放重入锁 获取当前的重入次数 t1t2测试 使用islocked()方法检测锁状态 t1t3测试 使用trylock方法尝试获取锁 使用isHeldByCurrentThread方法检测当前线程是否持有锁 不…

ReentrantLock详解

目录 一、ReentrantLock的含义 二、RerntrantLock当中的常用方法 ①lock()和unlock()方法 ②构造方法 ③tryLock()方法 tryLock()无参数 tryLock(timeout,Times)有参数 ④lockInterruptibly() throws InterruotedException 经典面试问题: ReentrantLock和synchronized有什…

OpenAI最新官方ChatGPT聊天插件接口《智能聊天插件引言》全网最详细中英文实用指南和教程,助你零基础快速轻松掌握全新技术(一)(附源码)

Chat Plugins Limited Alpha 聊天插件 前言IntroductionPlugin flow 插件流其它资料下载 Learn how to build a plugin that allows ChatGPT to intelligently call your API. 了解如何构建允许ChatGPT智能调用API的插件。 前言 在现代的软件开发环境中&#xff0c;使用第三方…

Pycharm快速入门(10) — 插件管理

1、插件安装 File | Settings | Plugins | Marketplace 搜索插件点击Install安装 2、插件卸载 File | Settings | Plugins | Installed 选择需要卸载的插件&#xff0c;点击Uninstall。 3、推荐插件 &#xff08;1&#xff09;、Chinese ​(Simplified)​ Language Pack &am…

chatgpt赋能python:Python编程的好玩之处:用简单的代码创造奇妙的世界

Python编程的好玩之处&#xff1a;用简单的代码创造奇妙的世界 如果你喜欢写代码&#xff0c;那么Python是一个不错的选择。Python语言设计简单&#xff0c;易学易用&#xff0c;同时还拥有丰富的生态系统&#xff0c;支持许多强大的第三方库和框架&#xff0c;可以使你轻松地…

chatgpt赋能python:Python图片拼图的好处和应用

Python图片拼图的好处和应用 Python是一种高级编程语言&#xff0c;已经被广泛应用于数据科学、网络编程、机器学习等领域。其中&#xff0c;Python的图像处理领域也越来越受关注。在本文中&#xff0c;我们将介绍如何使用Python创建图片拼图&#xff0c;并讨论它的好处和应用…

midjourney教程:如何快速生成个性化Logo设计

midjourney是一款基于人工智能技术的Logo设计工具&#xff0c;它可以帮助用户快速生成个性化的Logo设计&#xff0c;而无需具备专业的设计技能。下面将为大家介绍midjourney的使用方法&#xff0c;以帮助大家轻松生成符合自己需求的Logo设计。 第一步&#xff1a;登录midjourn…

chatgpt赋能python:Python添加图片背景的方法

Python添加图片背景的方法 简介 Python是一种开源的高级编程语言&#xff0c;广泛应用于各个行业中&#xff0c;包括图像处理。添加图片背景是图像处理中的常见需求&#xff0c;通过Python可以很方便地实现。 本篇文章将介绍如何使用Python来给图片添加背景&#xff0c;让您…

chatgpt赋能python:Python怎么做图形

Python怎么做图形 在数据可视化和图像处理方面&#xff0c;Python已经成为了最受欢迎的编程语言之一。Python的图形库使得创建各种图形和图表、可视化工具和图像处理应用程序变得容易而简单。 在本文中&#xff0c;我们将会介绍一些最受欢迎的Python图形库&#xff0c;以帮助…

程序员晒追女神聊天截图,坦言第一次没经验,网友直呼凭实力单身

前段时间网络上一名程序员晒出了自己与女神之间的聊天记录的对话截图&#xff0c;通过截图中我们可以看出&#xff0c;应该是这位程序员在追求这位女神&#xff0c;但是短短的十几分钟几条聊天记录&#xff0c;却以女神不再愿意搭理程序员结束&#xff0c;对于这样的结局&#…

程序员给女友4千生活费,收到女友错发信息后分手,神对话!

如何平衡好亲情爱情的关系&#xff0c;是一门学问&#xff0c;有的人就希望自己的另一半过好他们自己的小日子&#xff0c;不要对家里的事情过多的付出&#xff0c;但有人觉得自己父母养大自己不容易&#xff0c;能有能力的话&#xff0c;不光孝敬爹妈&#xff0c;还会帮衬家里…

程序员就是这样聊天把女朋友聊没的

身为程序员 都想当然的认为 身为一个优秀的程序员 我怎么可能会没女票 这不科学啊 工资高&#xff0c;话少 有一天看到了 某个程序员的聊天记录 有女孩主动搭讪 这么绝好的机会 然后你竟然说忙 说忙 忙... 主动找你搭讪 你还不抓紧机会约约约 如果改成&#xff1a…

被程序员的相亲规划整不会了......

近日&#xff0c;北京一程序员将自己7天7场相亲行程规划表发到论坛分享&#xff0c;感叹到&#xff1a;真不是凡尔赛&#xff0c;相亲比上班还难&#xff0c;引来大量网友围观。 相亲也有规划表&#xff1f; 据介绍&#xff0c;该程序员今年刚好30岁&#xff0c;自己平时加班多…

程序员吐槽女朋友狮子大开口

本文转载自程序员八卦 一个程序员发帖吐槽自己的潮汕女朋友&#xff0c;开口要彩礼18万8&#xff0c;楼主在网上查了一下&#xff0c;一般潮汕彩礼是3万到8万&#xff0c;难道外地人要多给一点吗&#xff1f;而且女朋友还一定要楼主父母出彩礼&#xff0c;不能楼主自己出&…

最最普通程序员,如何利用工资攒够彩礼,成为人生赢家

今天我们不讲如何提升你的专业技能去涨工资&#xff0c;不讲面试技巧如何跳槽涨工资&#xff0c;不讲如何干兼职赚人生第一桶金&#xff0c;就讲一个最最普通的程序员&#xff0c;如何在工作几年后&#xff0c;可以攒够彩礼钱&#xff0c;婚礼酒席钱&#xff0c;在自己人生大事…

如何做好小红书?从找好定位开始,这篇文章告诉你

近年来小红书随着用户体量壮大和平台多元化发展&#xff0c;用户的兴趣点&#xff0c;早已从美妆独大变为渗透生活领域的各个方面。与以往相比&#xff0c;大家对小红书的认知也逐渐在发生变化。 如果说去年还有不少商家还经常问我们“为什么要做小红书&#xff1f;”。那么&am…

测试听力口语软件,上、英语系学姐最全整理的34个英语学习App 针对听力、口语、阅读...

英语的重要性不用我多说啦~日常生活、工作&#xff0c;不擅长英语真的会失去很多机会和乐趣 作为英语系学姐今天就给大家总结了一些学习英语的app 有需要就马住&#xff0c;慢慢学习&#xff01; 听力 听力学习&#xff0c;都非常好用 -朗易思听 页面超精美&#xff0c;资源也很…