背景:

  ReentrantReadWriteLock把锁进行了细化,分为了写锁和读锁,即独占锁和共享锁。独占锁即当前所有线程只有一个可以成功获取到锁对资源进行修改操作,共享锁是可以一起对资源信息进行查看。即写同时只能一个人写,读可以大家一起读。

  

ReentrantReadWriteLock的结构

  ReentrantReadWriteLock并没有继承ReentrantLock,也并没有实现Lock接口,而是实现了ReadWriteLock接口,该接口提供readLock()方法获取读锁,writeLock()获取写锁。

public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}

  ReentrantReadWriteLock内部结构:

public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private final ReentrantReadWriteLock.ReadLock readerLock;//读锁内部类
private final ReentrantReadWriteLock.WriteLock writerLock;//写锁内部类
final Sync sync;//同步器
public ReentrantReadWriteLock(boolean fair) {//默认使用非公平锁
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 外部获得WriteLock、ReadLock的方法,由ReentrantReadWriteLock构造方法创建/
* 获取实例:
* ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
* ReentrantReadWriteLock.ReadLock readLock = rw.readLock();
* ReentrantReadWriteLock.WriteLock writeLock = rw.writeLock();
*/
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
//ReadLock读锁内部类源码
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
public void lock() {//共享锁获取
sync.acquireShared();//调用AQS的acquireShared
}
public void unlock() {//共享锁释放
sync.releaseShared();//调用AQS的releaseShared
}
}
//WriteLock写锁内部类源码
public static class WriteLock implements Lock, java.io.Serializable {private final Sync sync;
public void lock() {//排他锁获取
sync.acquire();//调用AQS的acquire
}
public void unlock() {//排他锁释放
sync.release();//调用AQS的release
}
}
//内部类,同步器Sync,实现tryAcquireShared、tryAcquire、tryRelease,即具体实现锁的获取和释放
abstract static class Sync extends AbstractQueuedSynchronizer{
//.....................
} }

  读锁获取ReadLock.lock()

    调用AQS的acquireShared,内部方法有tryAcquireShared和doAcquireShared,tryAcquireShared是AQS的抽象方法,由ReentrantReadWriteLock的同步器Sync实现,doAcquireShared由AQS实现。

public abstract class AbstractQueuedSynchronizer{
//ReadLock的lock调用
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)//获取锁失败
doAcquireShared(arg);//加入等待队列中
}
//acquireShared调用,由ReentrantReadWriteLock的Sync实现
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
}

  在讲tryAcquireShared之前需要解决一些概念问题,由ReentrantReadWriteLock来区别管理读写锁,那么需要一套数据结构来管理每个线程持有的锁状态(读/写)、锁数量、重入/非重入等问题。这些问题的解决就是在ReentrantReadWriteLock的内部类Sync中使用了ThreadLocal来解决,缓存每个线程的锁数量和线程id。

    abstract static class Sync extends AbstractQueuedSynchronizer{
/**
* int为4字节,共有32位:1111 1111 1111 1111 - 1111 1111 1111 1111
* 高16位(读状态)为读锁持有数量sharedCount;低16位(写状态)为写锁持有数量exclusiveCount;
* 0000 0000 0000 0011 0000 0000 0000 0000表示有3个线程获取了读锁,0个线程获取了写锁
*/
static final int SHARED_SHIFT = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//最多持有数量为65535
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }//返回某状态c中共享锁(读锁)使用的次数,相当于是获取高16位,忽略低位
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }//返回某状态c中独占锁(写锁)使用的次数,相当于是获取低16位,忽略高位
//每个线程获取的读锁数量,用ThreadLocal来维护,数据缓存到cachedHoldCounter
static final class HoldCounter {
int count = 0;//初始值为0
// 使用id而不是引用来避免垃圾保留【垃圾保留:无法回收的垃圾】
final long tid = getThreadId(Thread.currentThread());
}
//维护HoldCounter
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();//为每个线程创建一个HoldCounter,保存到ThreadLocal中
}
}
//readHolds当前线程持有的数据对象,包含锁数量和线程id,当线程数量为0时会移除该对象
private transient ThreadLocalHoldCounter readHolds;
//缓存上一个成功获取读锁的线程的数据对象,减少ThreadLocal的查找操作
private transient HoldCounter cachedHoldCounter;
//第一个读线程的对象
private transient Thread firstReader = null;
//第一个读线程持有的锁数量
private transient int firstReaderHoldCount;
Sync() {
readHolds = new ThreadLocalHoldCounter();//创建当前线程数据对象
setState(getState()); // 更新数据,确保readHolds的可见性
}
}

  tryAcquireShared获取锁:

        protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();//获得当前线程
int c = getState();//得到状态
/**
* 当前有写线程且本线程不是写线程,不符合重入,失败
* 此处判断逻辑隐含了一个条件,就是当有写锁获取并且是获取写锁的是当前线程,那么不返回-1,允许此写锁获取读锁。【锁降级】
* 锁降级指的是先获取到写锁,然后获取到读锁,然后释放了写锁的过程。
* 流程:writeLock.lock(); readLock.lock(); writeLock.unlock(); readLock.unlock();
       * 锁降级的应用场景: 对于数据比较敏感, 需要在对数据修改以后, 获取到修改后的值, 并进行接下来的其它操作。
* 锁降级中读锁的获取是否必要呢?
* 答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁, 假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
* 保证数据的可见性可以这样理解:假设线程A修改了数据,释放了写锁,这个时候线程T获得了写锁,修改了数据,然后也释放了写锁,线程A读取数据的时候,读到的是线程T修改的,并不是线程A自己修改的,那么在使用修改后的数据时,就会忽略线程A之前的修改结果。当前线程无法感知线程T的数据更新,是说线程A使用数据时,并不知道别的线程已经更改了数据,所以使用的是线程T的修改结果。因此通过锁降级来保证数据每次修改后的可见性。
* 锁是不支持锁升级的(先获取写锁,再获取读锁然后释放读锁),
* 因为第一步获取读锁的时候可能有多个线程获取了读锁,这样如果锁升级的话将会导致写操作对其他已经获取了读锁的线程不可见。
*/
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//得到读锁个数
int r = sharedCount(c);
//如果读不应该阻塞并且当前持有的锁个数小于最大值65535,且可以更新状态值,成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {//读锁个数为0,即第一个读
firstReader = current;//第一个读线程就是当前线程
firstReaderHoldCount = 1;//第一个读线程持有的锁数量为1
} else if (firstReader == current) {//当前线程重入
firstReaderHoldCount++;//锁数量+1
} else {//当前线程和第一个读线程不一致(不是重入),从ThreadLocal中获取当前线程重入读锁的次数,然后自增下。
HoldCounter rh = cachedHoldCounter;//获取最后一次成功获取读锁的线程数据对象
//rh == null(当前线程是第二个获取的),或者当前线程和rh不是同一个
if (rh == null || rh.tid != getThreadId(current))
//把最后一次成功获取读锁的线程数据对象更新为当前线程的数据对象
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)//最后一次成功获取读锁的锁数量为0
readHolds.set(rh);//当前线程设置为HoldCounter,即把当前线程数据存放到ThreadLocal中进行维护
rh.count++;//获取到的读锁数量+1
}
return 1;
}
//如果读锁获取失败,调用该方法进行CAS循环获取
return fullTryAcquireShared(current);
}

  fullTryAcquireShared:

        final int fullTryAcquireShared(Thread current) {
//这段代码在一定程度上与tryacquirered中的代码是冗余的,但总的来说更简单,
//因为它没有使tryacquirered在重试和延迟读锁计数之间的交互复杂化。
HoldCounter rh = null;
//自旋
for (;;) {
int c = getState();
//已经有写锁被获取
if (exclusiveCount(c) != 0) {
//当前线程不是重入,获取失败
if (getExclusiveOwnerThread() != current)
return -1;
} else if (readerShouldBlock()) {//有写锁,读锁被阻塞,可能会造成死锁
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
//第一次循环
if (rh == null) {
rh = cachedHoldCounter;//最后一次成功获取读锁的数据对象
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get(); //把最后一次成功获取读锁的线程数据对象更新为当前线程的数据对象
if (rh.count == 0)//如果当前线程的读锁为0就remove
readHolds.remove();
}
}
//不是第一次循环
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)//读锁数量达到临界值抛出异常
throw new Error("Maximum lock count exceeded");
//尝试CAS设置同步状态
//后续操作和tryAquireShared基本一致
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh;
}
return 1;
}
}
}

  readerShouldBlock的解读:

/**
* 非公平锁的读锁获取策略
*/
final boolean readerShouldBlock() {
//如果当前线程的后续节点为独占式写线程,则返回true(表示当前线程在tryAcquireShared方法中不能立刻获取读锁,需要后续通过fullTryAcquireShared方法取判断是否需要阻塞线程)
//在fullTryAcquireShared方法中会通过判断当前获取读锁线程的读锁数量来判断当前尝试获取读锁的线程是否持有写锁,如果持有写锁则锁降级,需要将当前锁降级的线程添加到阻塞队列中重新获取读锁
//这么做是为了让后续的写线程有抢占写锁的机会,不会因为一直有读线程或者锁降级情况的存在而造成后续写线程的饥饿等待
return apparentlyFirstQueuedIsExclusive();
} final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
} /**
* 公平锁的读锁获取策略
*/
final boolean readerShouldBlock() {
//如果当前线程不是同步队列头结点的next节点(head.next) (判断是否有前驱节点,如果有则返回false,否则返回true。遵循FIFO)
//则阻塞当前线程
return hasQueuedPredecessors();
} public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

  doAcquireShared:

    private void doAcquireShared(int arg) {
//把当前线程封装到一个SHARE类型Node中,添加到SyncQueue尾巴上,即加入等待队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {//前继节点是head节点,下一个就到自己了
int r = tryAcquireShared(arg);//非公平锁实现,再尝试获取锁
if (r >= 0) {//获取成功
setHeadAndPropagate(node, r);//把当前节点更新为head节点并唤醒线程
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//前继节点非head节点,将前继节点状态设置为SIGNAL,通过park挂起node节点的线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

  读锁释放ReadLock.unlock()

  调用AQS的releaseShared,内部方法有tryReleaseShared和doReleaseShared,tryReleaseShared是AQS的抽象方法,由ReentrantReadWriteLock的同步器Sync实现,doReleaseShared由AQS实现。

    public final boolean releaseShared(int arg) {//ReadLock的unlock调用
if (tryReleaseShared(arg)) {//锁释放成功
//此处的doReleaseShared方法与setHeadAndPropagate方法中锁唤醒的节点有所差别
//setHeadAndPropagate方法只唤醒head后继的共享锁节点
//doReleaseShared方法则会唤醒head后继的独占锁或共享锁
doReleaseShared();
return true;
}
return false;
}
//tryReleaseShared调用,由ReentrantReadWriteLock的Sync实现
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}

  tryReleaseShared:

        //读锁释放,实现AQS的tryReleaseShared
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();//获取当前线程
if (firstReader == current) {//当前线程是第一个读线程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)//第一个读线程的锁数量为1
firstReader = null;//可以让其他线程获取锁
else
firstReaderHoldCount--;//锁数量-1
} else {
//获取最后一次成功获取锁的数据对象
HoldCounter rh = cachedHoldCounter;
//获取当前线程的数据对象
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
//获取当前线程持有的读锁数量
int count = rh.count;
if (count <= 1) {//小于1
readHolds.remove();//移除当前线程的数据信息
if (count <= 0)//小于0抛出异常
throw unmatchedUnlockException();
}
--rh.count;//当前线程的读锁数量-1
}
for (;;) {//自旋
int c = getState();//获取状态
//释放后的同步状态
int nextc = c - SHARED_UNIT;
//CAS更新同步状态,成功则返回是否同步状态为0
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

  doReleaseShared:

    /** releaseShared调用
* 把当前结点设置为SIGNAL或者PROPAGATE
* 唤醒head.next(B节点),B节点唤醒后可以竞争锁,成功后head->B,然后又会唤醒B.next,一直重复直到共享节点都唤醒
* head节点状态为SIGNAL,重置head.waitStatus->0,唤醒head节点线程,唤醒后线程去竞争共享锁
* head节点状态为0,将head.waitStatus->Node.PROPAGATE传播状态,表示需要将状态向后继节点传播
*/
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {//head是SIGNAL状态
/* head状态是SIGNAL,重置head节点waitStatus为0,这里不直接设为Node.PROPAGATE,
* 是因为unparkSuccessor(h)中,如果ws < 0会设置为0,所以ws先设置为0,再设置为PROPAGATE
* 这里需要控制并发,因为入口有setHeadAndPropagate跟release两个,避免两次unpark
*/
if (!h.compareAndSetWaitStatus(Node.SIGNAL, 0))
continue;//设置失败,重新循环
/* head状态为SIGNAL,且成功设置为0之后,唤醒head.next节点线程
* 此时head、head.next的线程都唤醒了,head.next会去竞争锁,成功后head会指向获取锁的节点,
* 也就是head发生了变化。看最底下一行代码可知,head发生变化后会重新循环,继续唤醒head的下一个节点
*/
unparkSuccessor(h);
/*
* 如果本身头节点的waitStatus是出于重置状态(waitStatus==0)的,将其设置为“传播”状态。
* 意味着需要将状态向后一个节点传播
*/
}
else if (ws == 0 && !h.compareAndSetWaitStatus(0, Node.PROPAGATE))
continue;
}
if (h == head)//如果head变了,重新循环
break;
}
}

  写锁获取WriteLock.lock()

  调用AQS的acquire,tryAcquire是AQS的抽象方法,由ReentrantReadWriteLock的同步器Sync实现。

    public final void acquire(int arg) {//WriteLock的lock调用
//获取失败则会调用addWaiter方法将线程添加到CLH队列末尾,并调用acquireQueued方法阻塞当前线程
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//acquire调用,由ReentrantReadWriteLock的Sync实现
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}

  tryAcquire:

        protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();//获得当前线程
int c = getState();//获得状态
//获取写锁数量(w>0表示已经有线程获取写锁)
int w = exclusiveCount(c);
if (c != 0) {//同步状态不为0,说明线程获取到了写锁或读锁
//写锁状态0(表示有线程已经获取读锁(共享锁获取时阻塞独占锁))或者当前线程不是已经获取写锁的线程(独占锁只允许自己持有锁)
//返回false
//此处的处理逻辑也间接验证了获取了读锁的线程不能同时获取写锁
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//大于最大线程数则抛出错误
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//如果写锁状态>0并且当前线程为写锁重入,更新写锁状态
setState(c + acquires);
return true;
}
//如果同步状态等于0
//在尝试获取同步状态之前先调用writerShouldBlock()写等待策略
//ReentrantReadWriteLock中通过FairSync(公平锁)和NonfairSync(非公平锁)重写writerShouldBlock()方法来达到公平与非公平的实现
//NonfairSync(非公平锁)中直接返回false表示不进行阻塞直接获取
//FairSync(公平锁)中需调用hasQueuedPredecessors()方法判断当前线程节点是否为等待队列的head结点的后置节点,是才可以获取锁
//获取成功则将当前线程设置为持有锁线程,并返回true
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}

  写锁释放WriteLock.unlock()

  调用AQS的release,tryRelease是AQS的抽象方法,由ReentrantReadWriteLock的同步器Sync实现。

   public final boolean release(int arg) {//WriteLock的unlock调用
//tryRelease释放锁,直到写锁state等于0(所有重入锁都释放),唤醒后续节点线程
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//release调用,由ReentrantReadWriteLock的Sync实现
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}

  tryRelease:

        protected final boolean tryRelease(int releases) {
//当前线程不是获取了同步状态的线程则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//释放一次锁,则将状态-1
int nextc = getState() - releases;
//重入的线程的锁是否都全释放
boolean free = exclusiveCount(nextc) == 0;
if (free)//全是否
setExclusiveOwnerThread(null);//独占锁设置为null,让其他线程可以获取到锁
setState(nextc);//更新状态
return free;
}

多线程高并发编程(4) -- ReentrantReadWriteLock读写锁源码分析的更多相关文章

  1. 多线程高并发编程(5) -- CountDownLatch、CyclicBarrier源码分析

    一.CountDownLatch 1.概念 public CountDownLatch(int count) {//初始化 if (count < 0) throw new IllegalArg ...

  2. 多线程高并发编程(6) -- Semaphere、Exchanger源码分析

    一.Semaphere 1.概念 一个计数信号量.在概念上,信号量维持一组许可证.如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它.每个release()添加许可证,潜在地释 ...

  3. java并发锁ReentrantReadWriteLock读写锁源码分析

    1.ReentrantReadWriterLock 基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读 ...

  4. java读写锁源码分析(ReentrantReadWriteLock)

    读锁的调用,最终委派给其内部类 Sync extends AbstractQueuedSynchronizer /** * 获取读锁,如果写锁不是由其他线程持有,则获取并立即返回: * 如果写锁被其他 ...

  5. 并发编程(四)—— ThreadLocal源码分析及内存泄露预防

    今天我们一起探讨下ThreadLocal的实现原理和源码分析.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两 ...

  6. 【Java并发编程】19、DelayQueue源码分析

    DelayQueue,带有延迟元素的线程安全队列,当非阻塞从队列中获取元素时,返回最早达到延迟时间的元素,或空(没有元素达到延迟时间).DelayQueue的泛型参数需要实现Delayed接口,Del ...

  7. 【Java并发编程】17、SynchronousQueue源码分析

    SynchronousQueue是一种特殊的阻塞队列,不同于LinkedBlockingQueue.ArrayBlockingQueue和PriorityBlockingQueue,其内部没有任何容量 ...

  8. Java并发编程笔记之Semaphore信号量源码分析

    JUC 中 Semaphore 的使用与原理分析,Semaphore 也是 Java 中的一个同步器,与 CountDownLatch 和 CycleBarrier 不同在于它内部的计数器是递增的,那 ...

  9. 【Java并发编程】18、PriorityBlockingQueue源码分析

    PriorityBlockingQueue是一个基于数组实现的线程安全的无界队列,原理和内部结构跟PriorityQueue基本一样,只是多了个线程安全.javadoc里面提到一句,1:理论上是无界的 ...

随机推荐

  1. Functional mechanism: regression analysis under differential privacy_阅读报告

    Functional mechanism: regression analysis under differential privacy 论文学习报告 组员:裴建新   赖妍菱    周子玉 2020 ...

  2. JSP(二)----指令,注释,内置对象

    ##  JSP 1.指令 *  作用:用于配置JSP页面,导入资源文件 *  格式: <%@  指令名称  属性名1=属性值1  属性名2=属性值2  %> <%@ page con ...

  3. 基于KNN的发票识别

    项目概况: 有一个PDF文件,里面的每页都是一张发票,把每页的发票单独存为一个PDF并用该发票的的发票号码进行文件的命名,发票号码需要OCR识别,即识别下图中红色方块的内容. 一:拆分PDF 现有一个 ...

  4. OpenCV-Python | 图像的基本操作 十

    目标 学会: 访问像素值并修改它们 访问图像属性 设置感兴趣区域(ROI) 分割和合并图像 本节中的几乎所有操作都主要与Numpy相关,而不是与OpenCV相关.要使用OpenCV编写更好的优化代码, ...

  5. Python python 函数参数:参数组合

    '''在Python中定义函数,可以用必选参数.默认参数.可变参数和关键字参数, 这4种参数都可以一起使用,或者只用其中某些 参数定义的顺序必须是:必选参数.默认参数.可变参数和关键字参数 ''' d ...

  6. 监控一哥Prometheus你可认识?

    先大致认识认识普罗米修斯 —— Prometheus. 依据官方文档https://prometheus.io/docs/introduction/overview/ 大概能够了解到 Promethe ...

  7. 认识STM32芯片

    STM32中的ST指的是意法半导体,M是Microelectronics的缩写,32表示32位,即意法半导体公司开发的32位微控制器 ST官网:https://www.st.com/content/s ...

  8. Win32程序:与"LPCWSTR"类型的形参不兼容

    出现该问题的原因是通常手动输入的字符串都是LPCSTR类型的, 解决办法如下: 在工程处右键,属性-常规-字符集,将Unicode字符集改为为多字节字符集,应用并确认即可.   字符串常量报错: 在常 ...

  9. 类实例调用静态方法(Java)

    前言 第一次看到在Java中是可以通过类实例调用静态方法,当然不推荐这么做,接下来会讲到,但是在C#中通过类实例调用静态方法在编译时就不会通过,这里做下记录. 类实例调用静态方法 首先我们来看一个简单 ...

  10. autojs,autojs 发送http请求,autojs 解析json数据

    如题,我这个就直接上代码吧 (function () { let request = http.request; // 覆盖http关键函数request,其他http返回最终会调用这个函数 http ...