Lock与ReentrantLock

Lock

Lock位于java.util.concurrent.locks包下,是一种线程同步机制,就像synchronized块一样。但是,Lock比synchronized块更灵活、更复杂。

1. Lock继承关系

2. 官方文档解读

 

 

3. Lock接口方法解析

public interface Lock {// 获取锁。如果锁不可用,则当前线程将出于线程调度目的而禁用,并处于休眠状态,直到获得锁为止。void lock();// 如果当前线程未被中断,则获取锁。如果锁可用,则获取锁并立即返回。// 如果锁不可用,出于线程调度目的,将禁用当前线程,该线程将一直处于休眠状态。// 下面两种情形会让当前线程停止休眠状态:// 1.锁由当前线程获取。// 2.其他一些线程中断当前线程,并且支持对锁获取的中断。// 当前线程出现下面两种情况时,将抛出InterruptedException,并清除当前线程的中断状态。// 1.当前线程在进入此方法时,已经设置为中断状态。// 2. 当前线程在获取锁时被中断,并且支持对锁获取中断。void lockInterruptibly() throws InterruptedException;// 尝试获取锁,如果锁处于空闲状态,则获取锁,并立即返回true。如果锁不可用,则立即返回false。// 典型用法:// 确保解锁前一定获取到锁// if (lock.tryLock()) {//     try {//             // manipulate protected state//     } finally {//          lock.unlock();//     }// } else {//     // perform alternative actions// }boolean tryLock();// 该方法为tryLock()的重载方法,两个参数分别表示为:// time:等待锁的最长时间// unit:时间单位// 如果在给定的等待时间内是空闲的并且当前线程没有被中断,则获取锁。如果锁可用,则此方法立即获取锁并返回true,如果锁不可用,出于线程调度目的,将禁用当前线程,该线程将一直处于休眠状态。// 如果指定的等待时间超时,则返回false值。如果时间小于或等于0,则该方法永远不会等待。// 下面三种情形会让当前线程停止休眠状态:// 1.锁由当前线程获取。// 2.其他一些线程中断当前线程,并且支持对锁获取的中断。// 3.到了指定的等待时间。// 当前线程出现下面两种情况时,将抛出InterruptedException,并清除当前线程的中断状态。// 1.当前线程在进入此方法时,已经设置为中断状态。// 2.当前线程在获取锁时被中断,并且支持对锁获取中断。boolean tryLock(long time, TimeUnit unit) throws InterruptedException;// 释放锁,与lock()、tryLock()、tryLock(long , TimeUnit)、lockInterruptibly()相对应。void unlock();// 返回绑定到此锁实例的Condition实例。当前线程只有获得了锁,才能调用Condition实例的方法。Condition newCondition();
}

ReentrantLock

ReentrantLock位于java.util.concurrent(J.U.C)包下,是Lock接口的实现类,属于独占锁。可重入特性与synchronized相似,但拥有扩展的功能。

ReentrantLock表现为API层面的互斥锁,通过lock()和unlock()方法完成,是显式的,而synchronized表现为原生语法层面的互斥锁,是隐式的。

在JDK 1.6之后,虚拟机对于synchronized关键字进行整体优化后,在性能上synchronized与ReentrantLock已没有明显差距,因此在使用选择上,需要根据场景而定,大部分情况下我们依然建议是synchronized关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。而ReentrantLock提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择ReentrantLock

ReentrantLock都是把具体实现委托给内部类(Sync、NonfairSync、FairSync)

1. 可重入性

可重入性:任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,synchronized和Reentrant都是可重入的,隐式显式之分。

实现条件:

  1. 锁需要去识别获取锁的线程是否是当前占据锁的线程,如果是的话,就成功获取。
  2. 锁获取一次,内部锁计数器需要加一,释放一次减一,计数为零表示为成功释放锁。

ReentrantLock的重入计数是使用AbstractQueuedSynchronizer的state属性的,state大于0表示锁被占用,等于0表示空闲,小于0则是重入次数太多导致溢出了。

2. 公平锁模式和非公平锁模式

ReentrantLock的构造函数接受可选的公平参数,参数为true则表示获取一个公平锁,不带参数或者false表示一个非公平锁。

构造方法:

    // 无参构造方法// 默认是非公平锁模式public ReentrantLock() {sync = new NonfairSync();}// 有参构造方法public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();}

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

sync继承于AQS(AQS参考解析AQS实现原理_我不是欧拉_的博客-CSDN博客,CAS参考解析CAS原理_我不是欧拉_的博客-CSDN博客)

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)) {  // 加锁成功,state状态改为1// 当前哪一个线程获取到锁,将线程信息记录到AQS里面// 设置当前持有锁的线程setExclusiveOwnerThread(current);   return true;                      }} else if (current == getExclusiveOwnerThread()) {// 当前线程正是锁持有者,此段逻辑体现可重入性// 同一个线程可以在不获取锁的情况再次进入// nextc表示被加锁次数,即重入次数int nextc = c + acquires;if (nextc < 0) // 被锁次数上溢(很少出现)throw new Error("Maximum lock count exceeded");// 设置加锁次数,lock几次就要unlock几次,否则无法释放锁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

static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;final void lock() {acquire(1);}// 方法逻辑与Snyc中的nonfairTryAcquire方法一致,只是加了线程排队的逻辑protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// !hasQueuedPredecessors()// 判断队列中是否有其他线程,没有才进行锁的获取,否则继续排队// 体现公平性if (!hasQueuedPredecessors() &&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;}}

非公平锁模式:NonfairSync

static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;/*** Performs lock.  Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {// 不管锁是否已经被占用,采用CAS的方式进行锁竞争// 抢锁成功,则把当前线程设置为活跃的线程// 抢锁失败则走acquire(1)逻辑// 体现非公平性,线程不必排队if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());else// 抢锁失败,调用tryAcquire方法,最终调用nonfairTryAcquire方法acquire(1);}protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}

3. condition

在并发编程中,每个Java对象都存在一组监视器方法,如wait()、notify()以及notifyAll()方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),如生产者-消费者模式、等待-通知模式,而且这些方法必须配合着synchronized关键字使用。

与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点:

  1. 通过Condition能够精细的控制多线程的休眠与唤醒。
  2. 对于一个锁,我们可以为多个线程间建立不同的Condition。

Condition相关方法

    // 返回绑定到此锁实例的Condition实例。当前线程只有获得了锁,才能调用Condition实例的方法。Condition newCondition();
public interface Condition {// 线程程进入等待状态直到被通知(signal)或中断// 当其他线程调用singal()或singalAll()方法时,该线程将被唤醒// 当其他线程调用interrupt()方法中断当前线程// await()相当于synchronized等待唤醒机制中的wait()方法void await() throws InterruptedException;// 与wait()方法相同,唯一的不同点是,该方法不会再等待的过程中响应中断void awaitUninterruptibly();// 当前线程进入等待状态,直到被唤醒或被中断或超时// 其中nanosTimeout指的等待超时时间,单位纳秒long awaitNanos(long nanosTimeout) throws InterruptedException;// 同awaitNanos,但可以指明时间单位boolean await(long time, TimeUnit unit) throws InterruptedException;// 线程进入等待状态,直到被唤醒、中断或到达某个时// 间期限(deadline),如果没到指定时间就被唤醒,返回true,其他情况返回falseboolean awaitUntil(Date deadline) throws InterruptedException;// 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须// 获取与Condition相关联的锁,功能与notify()相同void signal();// 唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须// 获取与Condition相关联的锁,功能与notifyAll()相同void signalAll();
}

使用await之前必须加锁,使用signal、signalAll之后记得释放锁。

Condition实现原理

Condition的具体实现类是AQS的内部类ConditionObject,前面我们分析过AQS中存在两种队列,一种是同步队列,一种是等待队列,而等待队列就相对于Condition而言的。注意在使用Condition前必须获得锁,同时在Condition的等待队列上的结点与前面同步队列的结点是同一个类即Node,其结点的waitStatus的值为CONDITION。在实现类ConditionObject中有两个结点分别是firstWaiter和lastWaiter,firstWaiter代表等待队列第一个等待结点,lastWaiter代表等待队列最后一个等待结点,代码如下:

public class ConditionObject implements Condition, java.io.Serializable {private static final long serialVersionUID = 1173984872572414699L;/** First node of condition queue. */// 等待队列第一个等待结点private transient Node firstWaiter;/** Last node of condition queue. */// 等待队列最后一个等待结点private transient Node lastWaiter;/*** Creates a new {@code ConditionObject} instance.*/public ConditionObject() { }
}

每个Condition都对应着一个等待队列,也就是说如果一个锁上创建了多个Condition对象,那么也就存在多个等待队列。等待队列是一个FIFO的队列,在队列中每一个节点都包含了一个线程的引用,而该线程就是Condition对象上等待的线程。当一个线程调用了await()相关的方法,那么该线程将会释放锁,并构建一个Node节点封装当前线程的相关信息加入到等待队列中进行等待,直到被唤醒、中断、超时才从队列中移出。Condition中的等待队列模型如下:

Node节点的数据结构,在等待队列中使用的变量与同步队列是不同的,Condtion中等待队列的结点只有直接指向的后继结点并没有指明前驱结点,而且使用的变量是nextWaiter而不是next。

firstWaiter指向等待队列的头结点,lastWaiter指向等待队列的尾结点,等待队列中结点的状态只有两种即CANCELLED和CONDITION,前者表示线程已结束需要从等待队列中移除,后者表示条件结点等待被唤醒。再次强调每个Codition对象对于一个等待队列,也就是说AQS中只能存在一个同步队列,但可拥有多个等待队列

实现代码:

await方法:

public final void await() throws InterruptedException {// 判断线程是否被中断if (Thread.interrupted())throw new InterruptedException();// 创建新结点加入等待队列并返回Node node = addConditionWaiter();// 释放当前线程锁即释放同步状态int savedState = fullyRelease(node);int interruptMode = 0;// 判断结点是否同步队列(SyncQueue)中,即是否被唤醒while (!isOnSyncQueue(node)) {// 挂起线程LockSupport.park(this);// 判断是否被中断唤醒,如果是退出循环。if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 被唤醒后执行自旋操作争取获得锁,同时判断线程是否被中断if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// clean up if cancelledif (node.nextWaiter != null) // 清理等待队列中不为CONDITION状态的结点unlinkCancelledWaiters();if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}private Node addConditionWaiter() {Node t = lastWaiter;// 判断是否为结束状态的结点并移除if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters();t = lastWaiter;}// 创建新结点状态为CONDITIONNode node = new Node(Thread.currentThread(), Node.CONDITION);// 加入等待队列if (t == null)firstWaiter = node;elset.nextWaiter = node;lastWaiter = node;return node;
}

signal()方法:

 public final void signal() {// 判断是否持有独占锁,如果不是抛出异常// 从这点也可以看出只有独占模式先采用等待队列,而共享模式下是没有等待队列的,也就没法使用Conditionif (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;// 唤醒等待队列第一个结点的线程if (first != null)doSignal(first);}private void doSignal(Node first) {do {// 移除条件等待队列中的第一个结点,// 如果后继结点为null,那么说没有其他结点将尾结点也设置为nullif ( (firstWaiter = first.nextWaiter) == null)lastWaiter = null;first.nextWaiter = null;// 如果被通知节点没有进入到同步队列并且条件等待队列还有不为空的节点,则继续循环通知后续结点} while (!transferForSignal(first) &&(first = firstWaiter) != null);}// transferForSignal方法
final boolean transferForSignal(Node node) {// 尝试设置唤醒结点的waitStatus为0,即初始化状态// 如果设置失败,说明当期结点node的waitStatus已不为// CONDITION状态,那么只能是结束状态了,因此返回false// 返回doSignal()方法中继续唤醒其他结点的线程,注意这里并// 不涉及并发问题,所以CAS操作失败只可能是预期值不为CONDITION,// 而不是多线程设置导致预期值变化,毕竟操作该方法的线程是持有锁的。if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;// 加入同步队列并返回前驱结点pNode p = enq(node);int ws = p.waitStatus;// 判断前驱结点是否为结束结点(CANCELLED=1)或者在设置// 前驱节点状态为Node.SIGNAL状态失败时,唤醒被通知节点代表的线程if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))// 唤醒node结点的线程LockSupport.unpark(node.thread);return true;}

流程:signal()被调用后,先判断当前线程是否持有独占锁,如果有,那么唤醒当前Condition对象中等待队列的第一个结点的线程,并从等待队列中移除该结点,移动到同步队列中,如果加入同步队列失败,那么继续循环唤醒等待队列中的其他结点的线程,如果成功加入同步队列,那么如果其前驱结点是否已结束或者设置前驱节点状态为Node.SIGNAL状态失败,则通过LockSupport.unpark()唤醒被通知节点代表的线程,到此signal()任务完成。

注意被唤醒后的线程,将从前面的await()方法中的while循环中退出,因为此时该线程的结点已在同步队列中,那么while (!isOnSyncQueue(node))将不在符合循环条件,进而调用AQS的acquireQueued()方法加入获取同步状态的竞争中,这就是等待唤醒机制的整个流程实现原理,流程如下图所示(注意无论是同步队列还是等待队列使用的Node数据结构都是同一个,不过是使用的内部变量不同罢了) 

流程图:

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

 

利用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 {// 接收到线程通知则继续执行,否则就阻塞aCondition.await();} catch (InterruptedException e) {e.printStackTrace();}System.out.print(Thread.currentThread().getName()+" ");// 唤醒其他线程线程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();

 

参考:Lock锁和ReentrantLock锁_resumebb的博客-CSDN博客_lock reentrantlock

深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理_w_s_h_y的博客-CSDN博客  

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

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

相关文章

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;资源也很…

小红书怎么运营好?分享小红书的一些经验让你少走弯路

每次讲小红书运营&#xff0c;我都尽量把一个问题拆的特别细&#xff0c;揉碎了讲&#xff0c;说实话挺不容易的。之前也发过&#xff0c;这次分享又是小红书&#xff0c;没办法&#xff0c;小红书的流量非常大&#xff0c;而且粉丝精准度也很不错。 分享的这些都是经验&#…

chatgpt赋能python:Python手机密码解锁-打开手机的一条捷径

Python手机密码解锁-打开手机的一条捷径 我们都遇到过忘记手机密码的经历。不管是因为长时间不用手机导致遗忘&#xff0c;还是输入错误太多次&#xff0c;导致手机被锁定&#xff0c;让我们感到非常困扰和苦恼。虽然我们可以通过向手机厂商寻求帮助或找专业维修技术人员来解锁…