【并发编程】从AQS机制到同步工具类

AQS机制

Java 中常用的锁主要有两类,一种是 Synchronized 修饰的锁,被称为 Java 内置锁或监视器锁。另一种就是在 JUC 包中的各类同步器,包括 ReentrantLock(可重入锁)、Semaphore(信号量)、CountDownLatch 等。

所有的同步器都是基于AQS机制来构建的,而 AQS 类的核心数据结构是一种名为CLH锁的变体。

CLH锁

CLH锁是一种基于链表的自旋锁,它通过维护一个隐式的等待队列来实现线程的公平性和高效性。CLH锁的核心思想是每个线程在进入临界区时都会在队列尾部排队,并且自旋等待前驱节点的状态变化。CLH 锁的特点是,它将等待线程的状态信息保存在前驱节点中,而不是在本线程中,这样就避免了过多的缓存一致性流量。

隐式双向链表

加锁过程:

  1. 初始化:CLH 锁初始化时,Tail 指向一个状态为 false 的空节点。
  2. 线程入队:
    • 线程尝试获取锁时,创建一个状态为 true 的新节点,表示正在等待锁。
    • 线程通过 CAS 操作将新节点插入队列尾部,并更新 Tail 指针。
  3. 轮询前驱节点状态:线程不断轮询其前驱节点的状态,直到前驱节点的状态变为 false,表示可以获取锁。

解锁过程:

  1. 释放锁:线程完成临界区访问后,将当前节点的状态设置为 false,表示释放锁。
  2. 后继节点获取锁:后继节点检测到前驱节点状态变化,获取锁并进入临界区。

AQS对CLH锁的改造

CLH锁存在缺点:

  • 自旋操作,当锁持有时间长时会带来较大的 CPU 开销。
  • 基本的 CLH 锁功能单一,不改造不能支持复杂的功能。

Java 的 AbstractQueuedSynchronizer(AQS)借鉴了 CLH 锁的思想,并在此基础上做了诸多改进,使其更适合构建高效、可扩展的同步器。以下是 AQS 对 CLH 锁所做的一些主要改造:

显式双向链表

AQS 使用了显式的双向链表来维护等待队列,而不是隐式的单向链表。这样改进的好处是,它允许 AQS 更方便地处理队列中的节点操作,比如取消、唤醒特定节点等。

AQS 加锁过程:

  • 初始化:AQS 初始化时,等待队列为空,headtail 指针均为 null
  • 线程入队
    • 线程尝试获取锁时,会检查当前锁的状态(state)。如果锁已被占用,线程会创建一个新的节点(Node),表示自己需要等待锁。
    • 线程通过 CAS 操作将新节点原子性地插入到等待队列的尾部,并更新 tail 指针指向新节点。
    • 如果队列为空,当前节点会成为队列中的第一个节点。

  • 线程阻塞与等待:如果当前线程无法立即获取锁(state 不为 0),线程会进入阻塞状态,调用 LockSupport.park() 挂起自己,直到被唤醒为止。

AQS 解锁过程:

  1. 释放锁:持有锁的线程完成临界区的操作后,会调用 release(int arg) 方法将 state 变量设置为 0,表示锁已释放。
  2. 唤醒后继节点:AQS 会调用 LockSupport.unpark(Thread) 来唤醒后继节点的线程,使其从 park() 的阻塞状态中恢复。
  3. 后继节点获取锁:被唤醒的后继节点线程会重新尝试获取锁,通过 CAS 操作将 state 从 0 更新为 1。如果成功获取锁,线程将进入临界区执行任务。

多种同步模式

AQS 提供了独占锁(exclusive)和共享锁(shared)两种模式。例如,ReentrantLock 使用的是独占模式,而 SemaphoreCountDownLatch 使用的是共享模式。

一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现钩子方法中的tryAcquire-tryReleasetryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock

同步状态变量

AQS 使用一个 int 型变量(称为同步状态 state)来表示锁的状态,而不是像 CLH 锁那样依赖前驱节点的布尔变量。AQS 通过 CAS 操作来修改这个状态,确保线程安全。

  • ReentrantLock 的可重入性ReentrantLock 通过内部的 state 变量表示锁的占用状态。初始 state 为 0,表示未锁定。当线程 A 调用 lock() 时,通过 tryAcquire() 方法尝试获取锁并将 state 加 1。若获取成功,线程 A 可以多次获取同一锁,state 会累加,体现可重入性。释放时,state 减 1,直到回到 0,锁才真正释放,其他线程才有机会获取锁。
  • CountDownLatch 的倒计时CountDownLatch 使用 state 变量表示剩余的倒计时数。初始 state 为 N,表示 N 个子线程。每个子线程执行完任务后调用 countDown()state 减 1。所有子线程执行完毕(state 变为 0)后,主线程被唤醒,继续执行后续操作。

实现同步器

  • AQS 的设计:AQS 提供了一个基础的框架和队列管理功能,但具体的同步逻辑并没有在 AQS 中实现,而是留给具体的同步器来定义。这就是模板方法模式的典型应用:AQS 提供了模板方法,这些模板方法依赖于子类实现的钩子方法。
  • 重写钩子方法:具体的同步器,如 ReentrantLockSemaphore,会通过其内部定义的 Sync 类(继承自 AQS)来重写这些钩子方法。这些重写的方法决定了锁的行为(如是否公平、是否可重入、许可的数量等)。

什么是钩子方法? 钩子方法是一种被声明在抽象类中的方法,一般使用 protected 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。

除了这些钩子方法,AQS类中其他方法都是final关键字修饰的,无法被重写。

//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryAcquire(int)
//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)
//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected int tryAcquireShared(int)
//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean tryReleaseShared(int)
//该线程是否正在独占资源。只有用到condition才需要去实现它。
protected boolean isHeldExclusively()

常见同步工具类

ReentrantLock

ReentrantLock是一种可重入的互斥锁,允许同一个线程在持有锁的情况下多次获取锁。它提供了更灵活的锁机制,可以显式地获取和释放锁,还支持公平锁和非公平锁的选择。通常用来实现线程间的同步,防止多个线程同时访问共享资源。

ReentrantLock 有一个内部类 SyncSync 继承 AQS,添加锁和释放锁的大部分操作实际上都是在 Sync 中实现的。Sync公平锁 FairSync非公平锁 NonfairSync 两个子类。

公平锁/非公平锁

  1. 抽象类Sync继承自AbstractQueuedSynchronizer,实现了AQS的部分方法;
  2. NonfairSync继承自Sync,实现了Sync中的方法,主要用于非公平锁的获取;
  3. FairSync继承自Sync,实现了Sync中的方法,主要用于公平锁的获取。
abstract static class Sync extends AbstractQueuedSynchronizer {}static final class NonfairSync extends Sync {}static final class FairSync extends Sync {}

可以通过构造方法实现公平锁或非公平锁。

private final Sync sync;// 默认构造方法,ReentrantLock默认是非公平锁
public ReentrantLock() {sync = new NonfairSync();
}// 自己可选择使用公平锁还是非公平锁,传入true是公平锁,传入false是非公平锁
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

公平锁和非公平锁只有两处不同:

  • 公平锁: 在调用lock()后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。但是公平锁会先判断等待列中是否有处于等待状态的线程,如果有的话,就乖乖加入到等待线程中去排队,而不能直接插队获取锁。

  • 非公平锁: 在调用lock()中的第一次CAS 失败后,调用的是nonfairTryAcquire()非公平方法,如果发现锁这个时候被释放了(state == 0),非公平锁就会直接 CAS 抢锁,不会管当前等待队列中有没有等待线程。但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。

可中断锁

可中断锁与不可中断锁的区别在于:线程尝试获取锁操作失败后,在等待过程中,如果该线程被其他线程中断了,它是如何响应中断请求的。lock方法会忽略中断请求,继续获取锁直到成功;而lockInterruptibly则直接抛出中断异常来立即响应中断,由上层调用者处理中断。

  • lock()适用于锁获取操作不受中断影响的情况,此时可以忽略中断请求正常执行加锁操作,因为该操作仅仅记录了中断状态(通过Thread.currentThread().interrupt()操作,只是恢复了中断状态为true,并没有对中断进行响应)。
  • 如果要求被中断线程不能参与锁的竞争操作,则应该使用lockInterruptibly方法,一旦检测到中断请求,立即返回不再参与锁的竞争并且取消锁获取操作(即finally中的cancelAcquire操作)

可重入锁

可重入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的首先需要解决以下两个问题:

  • 线程再次获取锁: 所需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次获取成功;
  • 锁的最终释放: 线程重复n次获取了锁,随后在第n次释放该锁后,其它线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前线程被重复获取的次数,而被释放时,计数自减,当计数为0时表示锁已经成功释放。

首先会通过compareAndSetState(int, int)方法来尝试修改同步状态,如果修改成功则表示获取到了锁,然后调用setExclusiveOwnerThread(Thread)方法来设置获取到锁的线程。

该方法继承自AbstractOwnableSynchronizer类,它的主要作用就是记录获取到独占锁的线程,AOS类的定义很简单:

public abstract class AbstractOwnableSynchronizerimplements java.io.Serializable {private static final long serialVersionUID = 3737899427754241961L;protected AbstractOwnableSynchronizer() { }// The current owner of exclusive mode synchronization.private transient Thread exclusiveOwnerThread;protected final void setExclusiveOwnerThread(Thread thread) {exclusiveOwnerThread = thread;}protected final Thread getExclusiveOwnerThread() {return exclusiveOwnerThread;}
}

CountDownLatch

CountDownLatch是一个计数器,它允许一个或多个线程等待其它线程完成操作后再继续执行,通常用来实现一个线程等待其它多个线程完成操作之后再继续执行的操作。

CountDownLatch内部维护了一个计数器,该计数器通过CountDownLatch的构造方法指定。当调用await()方法时,它将一直阻塞,直到计数器变为0。当其它线程执行完指定的任务后,可以调用countDown()方法将计数器减一。当计数器减为0,所有的线程将同时被唤醒,然后继续执行。

常用方法

  • CountDownLatch(int count): CountDownLatch的构造方法,可通过count参数指定计数次数,但是要大于等于0,小于0会抛IIegalArgumentException异常。
  • void await(): 如果计数器不等于0,会一直阻塞(在线程没被打断的情况下)。
  • boolean await(long timeout,TimeUnit unit): 除非线程被中断,否则会一直阻塞,直至计数器减为0或超出指定时间timeout,当计数器为0返回true,当超过指定时间,返回false。
  • void countDown(): 调用一次,计数器就减1,当等于0时,释放所有线程。如果计数器的初始值就是0,那么就当没有用CountDownLatch吧。
  • long getCount(): 返回当前计数器的数量,可以用来测试和调试。

使用实例

定义线程任务,实现Runnable接口

@AllArgsConstructor
public class CountWork implements Runnable {private CountDownLatch countDownLatch;@Overridepublic void run() {System.out.println("执行任务");countDownLatch.countDown();}
}

定义测试类,使用for循环执行任务,知道任务结束完毕后打印结果。

public class CountDownLatchTest {public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(3);CountWork countWork = new CountWork(countDownLatch);for (int i = 0; i < 3; i++) {new Thread(countWork).start();}countDownLatch.await();System.out.println("所有任务执行完毕");}
}

执行结果:

可以发现,当所有任务执行完毕后,才执行了测试类后续的打印任务。

但是如果使用构造函数创建了4个计数new CountDownLatch(4),但实际只有3个线程,则测试类阻塞,无法打印结果。

CyclicBarrier

CyclicBarrier是一个同步屏障,它允许多个线程相互等待,直到到达某个公共屏障点,才能继续执行。通常用来实现多个线程在同一个屏障处等待,然后再一起继续执行的操作。

CyclicBarrier也维护了一个类似计数器的变量,通过CyclicBarrier的构造函数指定,需要大于0,否则抛IllegalArgumenException异常。当线程到达屏障位置时,调用await()方法进行阻塞,直到所有线程到达屏障位置时,所有线程才会被释放,而屏障将会被重置为初始值以便下次使用。

常用方法

  • CyclicBarrier(int parties): CyclicBarrier的构造方法,可通过parties参数指定需要到达屏障的线程个数,但是要大于0,否则会抛IllegalArgumentException异常。
  • CyclicBarrier(int parties,Runnable barrierAction): 另一个构造方法,parties作用同上,barrierAction表示最后一个到达屏障点的线程要执行的逻辑。
  • int await(): 表示线程到达屏障点,并等待其它线程到达,返回值表示当前线程在屏障中的位置(第几个到达的)。
  • int await(long timeout,TimeUnit unit): 与await()类似,但是设置了超时时间,如果超过指定的时间后,仍然还有线程没有到达屏障点,则等待的线程会被唤醒并执行后续操作。
  • void reset(): 重置屏障状态,即将屏障计数器重置为初始值。
  • int getParties(): 获取需要同步的线程数量。
  • int getNumberWaiting(): 获取当前正在等待的线程数量。

使用实例

定义线程执行的任务,当线程执行完打印任务后,阻塞等待其他线程。

@AllArgsConstructor
public class BarrierTask implements Runnable{private CyclicBarrier barrier;@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " 正在执行");try {barrier.await();} catch (InterruptedException | BrokenBarrierException e) {throw new RuntimeException(e);}}
}

定义最终执行的业务逻辑

public class FinalTask implements Runnable{@Overridepublic void run() {System.out.println("所有线程执行完毕");}
}

定义测试类,当所有线程执行到屏障后,触发最终的业务逻辑。

public class CyclicBarrierTest {public static void main(String[] args) {CyclicBarrier cyclicBarrier = new CyclicBarrier(4, new FinalTask());BarrierTask barrierTask = new BarrierTask(cyclicBarrier);for (int i = 1; i <= 4; i++) {new Thread(barrierTask, "线程-" + i).start();}}
}

执行结果:

对比CountDownLatch

  • CyclicBarrier维护线程的计数,而CounDownLatch维护任务的计数。
  • 可重用性: 两者最明显的差异就是可重用性。CyclicBarrier所有线程都到达屏障后,计数会重置为初始值。而CountDownLatch永远不会重置。

Semaphore

Semaphore是一个计数信号量,它允许多个线程同时访问共享资源,并通过计数器来控制访问数量。它通常用来实现一个线程需要等待获取一个许可证才能访问共享资源,或者需要释放一个许可证才能完成的操作。

Semaphore维护了一个内部计数器(许可permits),主要有两个操作,分别对应Semaphore的acquire和release方法。acquire方法用于获取资源,当计数器大于0时,将计数器减1;当计数器等于0时,将线程阻塞。release方法用于释放资源,将计数器加1,并唤醒一个等待中的线程。

常用方法

  • Semaphore(int permits): 构造方法,permits表示Semaphore中的许可数量,它决定了同时可以访问某个资源的线程数量。
  • Semaphore(int permits,boolean fair): 构造方法,当fair为ture,设置为公平信号量。
  • void acquire(): 获取一个许可,如果没有许可,则当前线程被阻塞,直到有许可。如果有许可该方法会将许可数量减1。
  • void acquire(int permits): 获取指定数量的许可,获取成功同样将许可减去指定数量,失败阻塞。
  • void release(): 释放一个许可,将许可数加1。如果有其他线程正在等待许可,则唤醒其中一个线程。
  • void release(int n): 释放n个许可。
  • int availablePermits(): 当前可用许可数。

使用实例

信号量的构造方法传入参数为5,设置六个进程获取这5个资源。

public class SemaphoreTest {Semaphore park;public SemaphoreTest(int permits) {park = new Semaphore(permits);}public void enter() throws InterruptedException {park.acquire();System.out.println(Thread.currentThread().getName() + " 进入");}public void leave() {park.release();System.out.println(Thread.currentThread().getName() + " 驶出");}public static void main(String[] args) {SemaphoreTest test = new SemaphoreTest(5);for (int i = 1; i <= 6; i++) {new Thread(() -> {try {test.enter();Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}test.leave();}, "车牌号" + i).start();}}
}

返回结果:

可以发现同一时刻只有五个线程获取到资源,当有资源释放时(车牌号5 驶出),其他线程才能获取资源(车牌号6 进入)。

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

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

相关文章

Android13 Launcher3 客制化Workspace页面指示器

需求&#xff1a;原生态的workspace页面指示器是个长条&#xff0c;不大好看&#xff0c;需要进行客制化 实现效果如图&#xff1a; 实现原理&#xff1a; 代码实现在WorkspacePageIndicator.java 布局在launcher.xml里 实现在WorkspacePageIndicator.java通过重写onDraw函数…

顺序循环队列

顺序循环队列 队头插入元素&#xff0c;队尾删除元素 本来应该判空和判断是否存满的条件都是&#xff1a;队头 队尾&#xff0c;但这样就没办法区分了&#xff0c;所以&#xff0c;就牺牲一个空间&#xff08;比如长度为10&#xff0c;但只能存9个&#xff09;&#xff0c;这…

auto的使用场景

auto的两面性 合理使用auto 不仅可以减少代码量, 也会大大提高代码的可读性. 但是事情总有它的两面性 如果滥用auto, 则会让代码失去可读性 推荐写法 这里推荐两种情况下使用auto 一眼就能看出声明变量的初始化类型的时候 比如迭代器的循环, 用例如下 #include <iostre…

利用autoDecoder工具在数据包加密+签名验证站点流畅测试

站点是个靶场 https://github.com/0ctDay/encrypt-decrypt-vuls 演示地址http://39.98.108.20:8085/ 不是仅登录位置暴力破解的那种场景&#xff0c;使用autoDecoder&#xff08;https://github.com/f0ng/autoDecoder&#xff09;的好处就是每个请求自动加解密&#xff0c;测…

关于ThinkPHP 5 框架开启自动搜索控制器 无法访问的问题坑

假如当前有一个登陆接口功能 因为后续会有不同版本的 登陆接口 这时候 我们可以在控制器中 新建文件夹 做区分 方便管理即 新建了一个 api 模块 文件路径是 api/controller/V1/Login 正常情况下 controller 目录下 是 控制器文件 login.php 文件&#xff0c;由于我们有多个…

Qt text-align和padding属性

1. text-align属性是用来设置文本的水平对齐方式。 text-align: center 文本将居中显示text-align: left 文本将左对齐显示text-align: right 文本将右对齐显示 2. 内边距padding: 内边距是元素内容与其边框之间的空间 padding-left: 10px; 距离内左边距10个像素点padding-r…

时序预测 | 基于WTC+transformer时间序列组合预测模型(pytorch)

目录 效果一览基本介绍程序设计参考资料 效果一览 基本介绍 WTCtransformer时间序列组合预测模型 WTC,transformer 创新点&#xff0c;超级新。先发先得&#xff0c;高精度代码。 预测主模型transformer也可以改其他WTC-former系列&#xff0c;比如WTC-informer/autoformer等等…

LLaMA Factory微调Llama3模型

LLaMA Factory是一款开源低代码大模型微调框架&#xff0c;集成了业界最广泛使用的微调技术&#xff0c;支持通过Web UI界面零代码微调大模型&#xff0c;目前已经成为开源社区内最受欢迎的微调框架。 &#x1f4a5;GPU推荐使用24GB显存的A10&#xff08;ecs.gn7i-c8g1.2xlarg…

论文泛读: DETRs Beat YOLOs on Real-time Object Detection

[toc[ DETRs Beat YOLOs on Real-time Object Detection 论文地址: https://openaccess.thecvf.com/content/CVPR2024/html/Zhao_DETRs_Beat_YOLOs_on_Real-time_Object_Detection_CVPR_2024_paper.html 代码地址: https://zhao-yian.github.io/RTDETR 动机 现状 YOLO系列因…

ubuntu设置为自己需要的屏幕分辨率

先说一下我处理该问题的大体背景&#xff1a;我是学习Linux的新手&#xff0c;刚学完嵌入式Linux驱动开发相关课程。现在想接着学习一下QT开发。我是在电脑上装了虚拟机之后安装的ubuntu系统。因为换了电脑&#xff0c;所以重新装了ubuntu系统。但是&#xff0c;装完ubuntu系统…

SELF-INSTRUCT: Aligning Language Modelswith Self-Generated Instructions 学习

指令微调就是要训练模型执行用户的要求的能力。 文章首先说“指令微调”数据集经常是人工生成&#xff0c;有数量少等缺点。文章提供了一个让语言模型自己生成指令微调数据&#xff0c;自己学习的方法。首先会让一个语言模型自己生成要求&#xff0c;输入和输出&#xff0c;然…

【JS】使用MessageChannel实现深度克隆

前言 通常使用简便快捷的JSON 序列化与反序列化实现深克隆&#xff0c;也可以递归实现或者直接使用lodash。 但 JSON 序列化与反序列化 无法处理如下的循环引用&#xff1a; 实现 MessageChannel 内部使用了浏览器内置的结构化克隆算法&#xff0c;该算法可以在不同的浏览器上…

redis集群部署

因为Redis是c开发的,因此安装redis需要c语言的编译环境,即先需要安装gcc. 1.解压包 [rootredis01 Redis]# tar -zvxf redis-3.2.9.tar.gz 查看是否存在Makefile文件,存在则直接make编译redis源码 2.编译文件 [rootredis01 redis-3.2.9]# make 安装编译好的文件 [rootredi…

上传拍摄素材和后期剪辑素材太慢?镭速助力企业加速大文件传输

随着时光的流逝&#xff0c;当代人对视觉体验的要求越来越高&#xff0c;每一帧画面都追求极致的清晰度与细腻感。这无疑为影视制作带来了机遇&#xff0c;同时也带来了挑战。高清4K、8K视频等大文件的传输需求日益增长&#xff0c;传统的FTP、HTTP等数据传输方式已难以满足行业…

华硕天选Air:开学季的性价比之巅

正值开学季&#xff0c;华硕天选Air全能本以8999元的首发价回归&#xff0c;为学生和需求高性能笔记本的用户带来了超值的选择。 这款笔记本以其轻薄设计和强悍性能&#xff0c;成为市场上的热点。 轻薄设计&#xff0c;潮流先锋 华硕天选Air 2024采用了全新模具设计&#xf…

零基础学习Python(七)

1. 字符串常用方法 lower()、upper()&#xff1a;转换为小写字符串、大写字符串 split(str)&#xff1a;按照指定字符串str进行分割&#xff0c;结果为列表&#xff1a; email "123qq.com" print(email.split("")) [123, qq.com] count(str)&#xf…

python12 中,No module named‘distutils‘错误

python12跑redis的时候&#xff0c;突然发现报错“ No module nameddistutils ” distutils在python标准库从2012年就开始断更了&#xff0c;python12中已经移除该库&#xff0c;可以安装以下库进行解决。 pip install setuptools --upgrade “setuptools”是一个处理Python软…

OceanBase 功能解析之 Binlog Service

前言 MySQL&#xff0c;是在全球广泛应用的开源关系型数据库&#xff0c;除了其稳定性、可靠性和易用性&#xff0c;他早期推出的二进制日志功能&#xff0c;即binlog&#xff0c;也是MySQL广受欢迎的原因。 MySQL binlog&#xff0c;即二进制日志&#xff0c;是 MySQL 中用于…

爆品是测出来的,不是选出来的

我在亚马逊摸爬滚打了五年&#xff0c;深深感受到了"七分选品&#xff0c;三分运营"的重要性。不管你的产品图片、描述多么精美&#xff0c;如果不去精选和测试&#xff0c;很难保证能出单。我见过很多跨境新手在选品上卡了几个月&#xff0c;纠结于卖什么。但实际上…

光敏电阻传感器详解(STM32)

目录 一、介绍 二、传感器原理 1.光敏电阻传感器介绍 2.原理图 三、程序设计 main.c文件 ldr.h文件 ldr.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 光敏电阻器是利用半导体的光电导效应制成的一种电阻值随入射光的强弱而改变的电阻器&#xff0c;又称为光…