打通 Java 任督二脉 —— 并发数据结构的基石

OpenCV函数用法之calibrateCamera 参考链接:opencv中标定函数calibrateCamera_ychl87的专栏-优快云博客 世界坐标系的三维点投影到成像坐标系中的二维点的投影公式如下: 其中(X,Y,Z)为世界坐标系中的三维点; (u,v)为成像面坐标系中的二维点; A为相机的内参数矩阵:(cx,cy)为主光轴点,一般为图像的中心;fx和fy为焦距; [R|t]为相机的外参数矩阵:R为旋转矩阵,t为位移矩阵; 上述公式的简单推理过程如下 考虑到镜头畸变 其中,k1,k2,k3,k4,k5和k6为径向畸变,p1和. 阅读详情

老大难的 Java ClassLoader,到了该彻底理解它的时候了

每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantLock,使用它加锁时如果没有立即加成功,就会阻塞当前的线程等待其它线程释放锁之后再重新尝试加锁,那线程是如何实现阻塞自己的?其它线程释放锁之后又是如果唤醒当前线程的?当前线程是如何得出自己没有加锁成功这一结论的?本篇内容将会从根源上回答上面提到的所有问题

线程阻塞原语

Java 的线程阻塞和唤醒是通过 Unsafe 类的 park 和 unpark 方法做到的。

public class Unsafe {
  ...
  public native void park(boolean isAbsolute, long time);
  public native void unpark(Thread t);
  ...
}

线程从启动开始就会一直跑,除了操作系统的任务调度策略外,它只有在调用 park 的时候才会暂停运行。锁可以暂停线程的奥秘所在正是因为锁在底层调用了 park 方法。

parkBlocker

线程对象 Thread 里面有一个重要的属性 parkBlocker,它保存当前线程因为什么而 park。就好比停车场上停了很多车,这些车主都是来参加一场拍卖会的,等拍下自己想要的物品后,就把车开走。那么这里的 parkBlocker 大约就是指这场「拍卖会」。它是一系列冲突线程的管理者协调者,哪个线程该休眠该唤醒都是由它来控制的。

class Thread {
  ...
  volatile Object parkBlocker;
  ...
}

class LockSupport {
  ...
  public static void park(Object blocker) {
     Thread t = Thread.currentThread();
     setBlocker(t, blocker);
     U.park(false, 0L);
     setBlocker(t, null); // 醒来后置null
  }

  public static void unpark(Thread thread) {
     if (thread != null)
        U.unpark(thread);
     }
  }
  ...
}

排队管理器

当多个线程争用同一把锁时,必须有排队机制将那些没能拿到锁的线程串在一起。当锁释放时,锁管理器就会挑选一个合适的线程来占有这个刚刚释放的锁。每一把锁内部都会有这样一个队列管理器,管理器里面会维护一个等待的线程队列。ReentrantLock 里面的队列管理器是 AbstractQueuedSynchronizer,它内部的等待队列是一个双向列表结构,列表中的每个节点的结构如下。

class AbstractQueuedSynchronizer {
  volatile Node head;  // 队头线程将优先获得锁
  volatile Node tail;  // 抢锁失败的线程追加到队尾
  volatile int state; // 锁计数
}

class Node {
  Node prev;
  Node next;
  Thread thread; // 每个节点一个线程

  // 下面这两个特殊字段可以先不去理解
  Node nextWaiter; // 请求的是共享锁还是独占锁
  int waitStatus; // 精细状态描述字
}

640?wx_fmt=png
图片

AbstractQueuedSynchronizer 类是一个抽象类,它是所有的锁队列管理器的父类,JDK 中的各种形式的锁其内部的队列管理器都继承了这个类,它是 Java 并发世界的核心基石。比如 ReentrantLock、ReadWriteLock、CountDownLatch、Semaphone、ThreadPoolExecutor 内部的队列管理器都是它的子类。这个抽象类暴露了一些抽象方法,每一种锁都需要对这个管理器进行定制。而 JDK 内置的所有并发数据结构都是在这些锁的保护下完成的,它是JDK 多线程高楼大厦的地基。

640?wx_fmt=png
图片

锁管理器维护的只是一个普通的双向列表形式的队列,这个数据结构很简单,但是仔细维护起来却相当复杂,因为它需要精细考虑多线程并发问题,每一行代码都写的无比小心。

JDK 锁管理器的实现者是 Douglas S. Lea,Java 并发包几乎全是他单枪匹马写出来的,在算法的世界里越是精巧的东西越是适合一个人来做。

Douglas S. Lea是纽约州立大学奥斯威戈分校计算机科学教授和现任计算机科学系主任,专门研究并发编程和并发数据结构的设计。他是Java Community Process的执行委员会成员,主持JSR 166,它为Java编程语言添加了并发实用程序。

640?wx_fmt=png
图片

后面我们将 AbstractQueuedSynchronizer 简写成 AQS。我必须提醒各位读者,AQS 太复杂了,如果在理解它的路上遇到了挫折,这很正常。目前市场上并不存在一本可以轻松理解 AQS 的书籍,能够吃透 AQS 的人太少太少,我自己也不算。

公平锁与非公平锁

公平锁会确保请求锁和获得锁的顺序,如果在某个点锁正处于自由状态,这时有一个线程要尝试加锁,公平锁还必须查看当前有没有其它线程排在排队,而非公平锁可以直接插队。联想一下在肯德基买汉堡时的排队场景。

也许你会问,如果某个锁处于自由状态,那它怎么会有排队的线程呢?我们假设此刻持有锁的线程刚刚释放了锁,它唤醒了等待队列中第一个节点线程,这时候被唤醒的线程刚刚从 park 方法返回,接下来它就会尝试去加锁,那么从 park 返回到加锁之间的状态就是锁的自由态,这很短暂,而这短暂的时间内还可能有其它线程也在尝试加锁。

其次还有一点需要注意,执行了 Lock.park 方法的线程自我休眠后,并不是非要等到其它线程 unpark 了自己才会醒来,它可能随时会以某种未知的原因醒来。我们看源码注释,park 返回的原因有四种

  1. 其它线程 unpark 了当前线程

  2. 时间到了自然醒(park 有时间参数)

  3. 其它线程 interrupt 了当前线程

  4. 其它未知原因导致的「假醒」

计算机世界非公平锁的服务效率要高于公平锁,所以 Java 默认的锁都使用了非公平锁。不过现实世界似乎非公平锁的效率会差一点,比如在肯德基如果可以不停插队,你可以想象现场肯定一片混乱。为什么计算机世界和现实世界会有差异,大概是因为在计算机世界里某个线程插队并不会导致其它线程抱怨。

public ReentrantLock() {
    this.sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    this.sync = fair ? new FairSync() : new NonfairSync();
}

共享锁与排他锁

ReentrantLock 的锁是排他锁,一个线程持有,其它线程都必须等待。而 ReadWriteLock 里面的读锁不是排他锁,它允许多线程同时持有读锁,这是共享锁。共享锁和排他锁是通过 Node 类里面的 nextWaiter 字段区分的。

class AQS {
  static final Node SHARED = new Node();
  static final Node EXCLUSIVE = null;

  boolean isShared() {
    return this.nextWaiter == SHARED;
  }
}

640?wx_fmt=png

条件变量

关于条件变量,需要提出的第一个问题是为什么需要条件变量,只有锁还不够么?考虑下面的伪代码,当某个条件满足时,才去干某件事

 void doSomething() {
   locker.lock();
   while(!condition_is_true()) {  // 先看能不能搞事
     locker.unlock();  // 搞不了就歇会再看看能不能搞
     sleep(1);
     locker.lock(); // 搞事需要加锁,判断能不能搞事也需要加锁
   }
   justdoit();  // 搞事
   locker.unlock();
 }

void doSomethingWithCondition() {
  cond = locker.newCondition();
  locker.lock();
  while(!condition_is_true()) {
    cond.await();
  }
  justdoit();
  locker.unlock();
}

640?wx_fmt=png
图片

阻塞在条件变量上的线程可以有多个,这些阻塞线程会被串联成一个条件等待队列。当 signalAll() 被调用时,会唤醒所有的阻塞线程,让所有的阻塞线程重新开始争抢锁。如果调用的是 signal() 只会唤醒队列头部的线程,这样可以避免「惊群问题」。

await() 方法必须立即释放锁,否则临界区状态就不能被其它线程修改,condition_is_true() 返回的结果也就不会改变。 这也是为什么条件变量必须由锁对象来创建,条件变量需要持有锁对象的引用这样才可以释放锁以及被 signal 唤醒后重新加锁。创建条件变量的锁必须是排他锁,如果是共享锁被 await() 方法释放了并不能保证临界区的状态可以被其它线程来修改,可以修改临界区状态的只能是排他锁。这也是为什么 ReadWriteLock.ReadLock 类的 newCondition 方法定义如下

public Condition newCondition() {
    throw new UnsupportedOperationException();
}

条件等待队列

当多个线程 await() 在同一个条件变量上时,会形成一个条件等待队列。同一个锁可以创建多个条件变量,就会存在多个条件等待队列。这个队列和 AQS 的队列结构很接近,只不过它不是双向队列,而是单向队列。队列中的节点和 AQS 等待队列的节点是同一个类,但是节点指针不是 prev 和 next,而是 nextWaiter。

class AQS {
  ...
  class ConditionObject {
    Node firstWaiter;  // 指向第一个节点
    Node lastWaiter;  // 指向第二个节点
  }

  class Node {
    static final int CONDITION = -2;
    static final int SIGNAL = -1;
    Thread thread;  // 当前等待的线程
    Node nextWaiter;  // 指向下一个条件等待节点

    Node prev;
    Node next;
    int waitStatus;  // waitStatus = CONDITION
  }
  ...
}
640?wx_fmt=png
图片

ConditionObject 是 AQS 的内部类,这个对象里会有一个隐藏的指针 this$0 指向外部的 AQS 对象,ConditionObject 可以直接访问 AQS 对象的所有属性和方法(加锁解锁)。位于条件等待队列里的所有节点的 waitStatus 状态都被标记为 CONDITION,表示节点是因为条件变量而等待。

队列转移

当条件变量的 signal() 方法被调用时,条件等待队列的头节点线程会被唤醒,该节点从条件等待队列中被摘走,然后被转移到 AQS 的等待队列中,准备排队尝试重新获取锁。这时节点的状态从 CONDITION 转为 SIGNAL,表示当前节点是被条件变量唤醒转移过来的。

class AQS {
  ...
  boolean transferForSignal(Node node) {
    // 重置节点状态
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
      return false
    Node p = enq(node); // 进入 AQS 等待队列
    int ws = p.waitStatus;
    // 再修改状态为SIGNAL
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
       LockSupport.unpark(node.thread);
       return true;
  }
  ...
}

640?wx_fmt=png
Java 并发包常用类库依赖结构

ReentrantLock 加锁过程

下面我们精细分析加锁过程,深入理解锁逻辑控制。我必须肯定 Dough Lea 的代码写成下面这样的极简形式,阅读起来还是挺难以理解的。

class ReentrantLock {
    ...
    public void lock() {
        sync.acquire(1);
    }
    ...
}

class Sync extends AQS {
  ...
  public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
  }
  ...
}

如果在循环重试加锁过程中被其它线程打断了,acquireQueued 方法就会返回 true。这时候线程就需要调用 selfInterrupt() 方法给当前线程设置一个被打断的标识位。

// 打断当前线程,其实就是设置一个标识位
static void selfInterrupt() {
        Thread.currentThread().interrupt();
}

acquireQueued 和 addWaiter 方法由 AQS 类提供,tryAcquire 需要由子类自己实现。不同的锁会有不同的实现。下面我们来看看 ReentrantLock 的公平锁 tryAcquire 方法的实现

640?wx_fmt=png
图片

这里有个 if else 分支,其中 else if 部分表示锁的重入,当前尝试加锁的线程是已经持有了这把锁的线程,也就是同一个线程重复加锁,这时只需要增加计数值就行了。锁的 state 记录的就是加锁计数,重入一次就 +1。AQS 对象里有一个 exclusiveOwnerThread 字段,记录了当前持有排他锁的线程。

if(c == 0) 意味着当前锁是自由态,计数值为零。这时就需要争抢锁,因为同一时间可能会有多个线程在调用 tryAcquire。争抢的方式是用 CAS 操作 compareAndSetState,成功将锁计数值从 0 改成 1 的线程将获得这把锁,将当前的线程记录到 exclusiveOwnerThread 中。

代码里还有一个 hasQueuedPredecessors() 判断,这个判断非常重要,它的意思是看看当前的 AQS 等待队列里有没有其它线程在排队,公平锁在加锁之前需要 check 一下,如果有排队的,自己就不能插队。而非公平锁就不需要 check,公平锁和非公平锁的全部的实现差异就在于此,就这一个 check 决定了锁的公平与否。

下面我们再看看 addWaiter 方法的实现,参数 mode 表示是共享锁还是排他锁,它对应 Node.nextWaiter 属性。

640?wx_fmt=png
图片

640?wx_fmt=png
图片

640?wx_fmt=png
图片

addWaiter 需要将新的节点添加到 AQS 等待队列的队尾。如果队尾 tail 是空的意味着队列还没有初始化,那就需要初始化一下。AQS 队列在初始化时需要一个冗余的头部节点,这个节点的 thread 字段是空的。

将新节点添加到队尾也是需要考虑多线程并发的,所以代码里再一次使用了 CAS 操作 compareAndSetTail 来竞争队尾指针。没有竞争到的线程就会继续下一轮竞争 for(;;) 继续使用 CAS 操作将新节点往队尾添加。

下面我们再看看 acquireQueue 方法的代码实现,它会重复 park、尝试再次加锁、加锁失败继续 park 的循环过程。

640?wx_fmt=png
图片

acquireQueue 在尝试加锁之前会先看看自己是不是 AQS 等待队列的第一个节点,如果不是它就继续去 park。这意味着不管是公平还是非公平锁,在这里它们都统一采取了公平的方案,看看队列中是不是轮到自己了。也就是说「一朝排队,永远排队」。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

同时我们还可以注意到锁是可以取消的 cancelAcquire(),准确地说是取消处于等待加锁的状态,线程处于 AQS 的等待队列中等待加锁。那什么情况下才会抛出异常而导致取消加锁呢,唯一的可能就是 tryAcquire 方法,这个方法是由子类实现的,子类的行为不受 AQS 控制。当子类的 tryAcquire 方法抛出了异常,那 AQS 最好的处理方法就是取消加锁了。cancelAcquire 会将当前节点从等待队列中移除。

ReentrantLock 解锁过程

解锁的过程要简单一些,将锁计数降为零后,唤醒等待队列中的第一个有效节点。

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
         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;
}

读写锁

读写锁分为两个锁对象 ReadLock 和 WriteLock,这两个锁对象共享同一个 AQS。AQS 的锁计数变量 state 将分为两个部分,前 16bit 为共享锁 ReadLock 计数,后 16bit 为互斥锁 WriteLock 计数。互斥锁记录的是当前写锁重入的次数,共享锁记录的是所有当前持有共享读锁的线程重入总次数。

读写锁同样也需要考虑公平锁和非公平锁。共享锁和互斥锁的公平锁策略和 ReentrantLock 一样,就是看看当前还有没有其它线程在排队,自己会乖乖排到队尾。非公平锁策略不一样,它会比较偏向于给写锁提供更多的机会。如果当前 AQS 队列里有任何读写请求的线程在排队,那么写锁可以直接去争抢,但是如果队头是写锁请求,那么读锁需要将机会让给写锁,去队尾排队。

写锁加锁过程

读写锁的写锁加锁在整体逻辑上和 ReentrantLock 是一样的,不同的是 tryAcquire() 方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
         if (w == 0 || current != getExclusiveOwnerThread())
              return false;
         if (w + exclusiveCount(acquires) > MAX_COUNT)
              throw new Error("Maximum lock count exceeded");
         setState(c + acquires);
         return true;
     }
     if (writerShouldBlock() ||
           !compareAndSetState(c, c + acquires))
         return false;
     setExclusiveOwnerThread(current);
     return true;
}

如果计数值为零,那就开始争抢锁。取决于锁是否公平,在争抢之前调用 writerShouldBlock() 方法看看自己是否需要排队,如果不需要排队,就可以使用 CAS 操作来争抢,成功将计数值从 0 设置为 1 的线程将独占写锁。

读锁加锁过程

读锁加锁过程比写锁要复杂很多,它在整体流程上和写锁一样,但是细节差距很大。特别是它需要为每一个线程记录读锁计数,这部分逻辑占据了不少代码。

public final void acquireShared(int arg) {
    // 如果尝试加锁不成功, 就去排队休眠,然后循环重试
    if (tryAcquireShared(arg) < 0)
        // 排队、循环重试
        doAcquireShared(arg);
}

wlock.lock();
if(whatever) {
  // 降级
  rlock.lock();
  wlock.unlock();
  doRead();
  rlock.unlock();
} else {
  // 不降级
  doWrite()
  wlock.unlock();
}

private transient ThreadLocalHoldCounter readHolds;

static final class HoldCounter {
    int count;
    final long tid = LockSupport.getThreadId(Thread.currentThread());
}

static final class ThreadLocalHoldCounter
            extends ThreadLocal<HoldCounter> {
   public HoldCounter initialValue() {
        return new HoldCounter();
   }
}

private transient HoldCounter cachedHoldCounter;

private transient Thread firstReader;
private transient int firstReaderHoldCount;

final int getReadHoldCount() {
    // 先访问锁全局计数的读计数部分
    if (getReadLockCount() == 0)
        return 0;

    // 再访问 firstReader
    Thread current = Thread.currentThread();
    if (firstReader == current)
         return firstReaderHoldCount;

    // 再访问最近的读线程锁计数
    HoldCounter rh = cachedHoldCounter;
    if (rh != null && rh.tid == LockSupport.getThreadId(current))
        return rh.count;

    // 无奈读 ThreadLocal 吧
    int count = readHolds.get().count;
    if (count == 0) readHolds.remove();
    return count;
}

读加锁还有一个自旋的过程,所谓自旋就是第一次加锁失败,那就直接循环重试,不休眠,听起来有点像死循环重试法。

final static int SHARED_UNIT = 65536
// 读计数是高16位

final int fullTryAcquireShared(Thread current) {
  for(;;) {
    int c = getState();
    // 如果有其它线程加了写锁,还是返回睡觉去吧
    if (exclusiveCount(c) != 0) {
        if (getExclusiveOwnerThread() != current)
            return -1;
    ...
    // 超出计数上限
    if (sharedCount(c) == MAX_COUNT)
       throw new Error("Maximum lock count exceeded");
    if (compareAndSetState(c, c + SHARED_UNIT)) {
       // 拿到读锁了
       ...
       return 1
    }
    ...
    // 循环重试
  }
}

protected final boolean tryReleaseShared(int unused) {
   ...
   for (;;) {
       int c = getState();
       int nextc = c - SHARED_UNIT;
       if (compareAndSetState(c, nextc)) {
         return nextc == 0;
       }
   }
   ...
}

小结

Java 并发能力的基石是 park() 和 unpark() 方法、volatile 变量、synchronized、CAS 操作和 AQS 队列,深究进去这几个知识点都不简单,本节内容提及到的锁相关知识点也不是特别完备,其中还有很多细节我自己也还没有完全吃透,那么关于锁更多的细节我们后续再讲吧。

老大难的 Java ClassLoader,到了该彻底理解它的时候了

640?wx_fmt=png

阅读更多精品文章,长按图片识别二维码关注「码洞」

virtuoso--DFF 27-实验课-std_cell2 阅读详情

相关推荐

基于docker进行安装部署seata,解决Seata的版本与Nacos的版本不匹配

本文分享了在Docker环境下部署Seata时遇到Nacos 3.x API不兼容问题的解决方案。由于Seata 2.0.0内置的nacos-client 1.4.2无法调用Nacos 3.x重构后的接口,导致注册失败。通过下载Seata 2.6.0源码自行构建Docker镜像,并调整启动方式(直接执行Java命令绕过Shell脚本),最终成功实现与Nacos 3.x的集成。关键步骤包括:创建自定义Dockerfile、配置application.yml、处理启动脚本兼容性问题,最终采用java直接运行方案

Dd52698的博客 410

jdk8 新特性之StampedLock锁

参考《java并发编程之美》 对于java8新增的特性来看,主要是 1,接口方法可以提供默认的实现, 2,及函数式编程, 目前大部分说到java8 新特性都会想到以上两点,却很少人知道java8 还新增了其他的新特性,今天来聊聊java8 新增的锁的新特性,StampedLock。 StampedLock是并发包里面jdk8版本新增的一个锁,该锁提供了3种模式的读写控制,当调用该锁...

zhoujie的博客 657

YOLOv5 改进点总览:轻量化、高精度的背后有哪些魔改?

YOLOv5是由Ultralytics团队开发的高效目标检测模型,其核心改进包括:主干网络采用CSPDarknet53提升梯度传播效率;特征融合使用PANet增强小目标识别;输出结构采用解耦头设计优化定位和分类任务。此外,YOLOv5通过CIoU损失函数、Mosaic数据增强和SimOTA标签分配机制进一步提升性能。模型支持多种推理格式(ONNX/TensorRT等),并具备自动锚框匹配和多尺度训练功能,使其在轻量化(yolov5s仅7MB)与高性能(最高51.2% mAP)间取得平衡,但缺乏正式论文支撑是

guoguozgw的博客 1806

JUC锁: ReentrantReadWriteLock详解

JUC锁: ReentrantReadWriteLock详解

blblccc 380

生信人的一天~HIFI数据+HIC数据组装基因组

HIFI+HIC数据组装基因组!

qq_43337286的博客 9531

【VAX 小番茄安装详细步骤】

本文详细介绍了在VS2022环境下安装Visual Assist X(番茄助手)的步骤及常见问题解决方案。主要内容包括:安装前的兼容性设置(需修改为Windows7兼容模式)、安装流程(卸载旧版本、勾选对应VS版本、替换关键dll文件)以及常见问题处理(如安装失败、工具栏图标缺失等)。文章提供了完整的下载链接和操作截图,特别强调了安装失败后的清理方法(注册表、安装目录和残余文件删除)。最后给出了安装成功后的测试运行指引。

qq_23962581的博客 1719

并发编程-ReentrantReadWriteLock读写锁详解(1)

说明:Sync抽象类继承自AQS抽象类,Sync类提供了对ReentrantReadWriteLock的支持。(2)类的内部类Sync类内部存在两个内部类,分别为HoldCounter和ThreadLocalHoldCounter,其中HoldCounter主要与读锁配套使用,其中,HoldCounter源码如下。// 计数器// 计数// 获取当前线程的TID属性的值。

2401_83915900的博客 1086

Unity 接入HybridCLR的点点滴滴,亲测三平台(PC、Android、WebGL)妥妥的。-问题分享

一入热更深似海啊,没有热更是真恼火啊,干啥啥不方便,动不动就得重新发包;说实在的,也是工作之余研究这个,在原有框架基础上接入这个热更,既要保持原有功能,还要支持热更,实实在在、断断续续搞了这么久,终于是接入并测通了,这一路是坎坎坷坷,下面把走过的弯弯绕绕记录下,希望对后来想接入的小伙伴有帮助吧。

GarFe的博客 1万+

java 加锁_打通 Java 任督二脉 —— 并发数据结构的基石

每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantLock,使用它加锁时如果没有立即加成功,就会阻塞当前的线程等待其它线程释放锁之后再重新尝试加锁,那线程是如何实现阻塞自己的?其它线程释放锁之后又是如果唤醒当前线程的?当前线程是如何得出自己没有加锁成功这一结论的?本篇内容将会从根源上回答上面提到...

weixin_39721953的博客 114

《YOLOv8实例分割实战:训练自己的数据集 》(视频课程)

本课程将手把手地教大家使用labelme标注和使用YOLOv8训练自己的数据集,完成一个多目标实例分割实战项目。

bai666ai的博客 790

模型训练篇 | 手把手教你使用ResNet训练自己数据集和推理,ResNet50通用物体分类识别,含完整代码和数据集,全网最详细教程

手把手教你使用ResNet训练自己数据集和推理,ResNet50通用物体分类识别,含完整代码和数据集,全网最详细教程。本篇文章将带你学习 ResNet50 模型构建、ResNet50训练自己数据集、数据集处理、推理和验证、模型剪枝与模型改进等。 本篇教程适配各种分类识别任务,可以说是一个通用的教程,只需修改自己的数据集即可完成自己领域的任务和系统实现。

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 736

LoRA训练失败排查指南:从数据到参数的系统方法

在深度学习模型微调领域,LoRA通过冻结预训练权重并插入低秩矩阵,以极小参数量实现高效适配,成为AIGC内容生成中常用的轻量级微调方案。然而,许多开发者在实际训练中频繁遇到loss异常、过拟合、生成效果不稳定等问题。真正影响训练成败的往往不是学习率或rank等表面参数,而是数据质量、打标规范、环境一致性和效果验证方法。本文从数据预处理、环境搭建、最小配置跑通到多维度评估,系统梳理一条可复现的LoRA训练排查路径,适合使用Kohya、diffusers等工具的工程师参考,帮助你从盲目调参转向系统性定位问题,提

weixin_33796205的博客 388

MuJoCo 入门教程(四)建模

在 MJCF 中,一个体可以包含多个关节,因此无需引入虚拟体来创建复合关节。相反,只需在同一主体中定义构成所需复合关节的所有原始关节即可。例如,可以使用两个滑块和一个铰链来模拟在平面内运动的肢体。其他 MJCF 元素可以在嵌套的主体元素所创建的树中定义,特别是关节、几何体、场地、摄像机、灯光。当一个元素被定义在一个肢体内时,它就固定在该肢体的局部坐标系上,并始终随其移动。涉及多个机构或根本不涉及机构的元素,将在运动树之外的单独部分中定义。

机器人控制与仿真应用 4165

Java程序员必看经典书籍,助你早日打通任督二脉

不知不觉,已经在开发界混迹了三年半,由于转行的缘故,感觉自己依旧是一个小菜鸟,既然你是一个非计算机专业的,还想鸠占鹊巢,那私底下的努力必定是少不了的,要想人前显贵,必定人后受罪。 一路走来,磕磕巴巴,有心酸也有收获。 话不多说,跟着节奏走,不要乱,action! 1、《Java核心技术卷一》 Java四大神作之一。 唯一可以和《Java编程思想》媲美的一本Java入门书籍。单从技术的角度来看,前者更好一些。但上升到思想层面嘛,自然后者更好,两者的偏重点不同。 2、《编写高质量代码:改善J.

学Java,找哪吒 1万+

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

《Java数据结构与算法》特别篇:泛型——打通数据结构的任督二脉

本文从Java数据结构学习中的泛型困惑切入,通过具体实例深入解析了泛型的核心概念。首先展示了非泛型代码的类型安全问题,进而阐释泛型如何通过编译时类型检查保障安全性。重点剖析了类型擦除机制,即泛型信息仅在编译期存在而被运行时擦除的特性。详细讲解了泛型类和泛型方法的基本语法,包括类型参数声明和使用规则,并介绍了通配符解决容器类型继承问题的原理。文章采用学习者视角,通过"盒子"等生活化类比,将抽象概念具象化,最终回归到ArrayList等数据结构的实际应用场景,为后续学习奠定基础。

2502_91523773的博客 1284

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

想快速入门二叉树?一篇文章打通你任督二脉

如果叶子结点个数为n0,度为2的非叶子结点个数为n2,则n0 = n2 + 1。(度为0的结点数总比度为2的结点数多一个)有n个结点,按从上到下、左到右的顺序编号,想知道序号i的结点有多少,按如下规则分辨,2. 若规定只有根结点的二叉树深度为1,则深度为k的二叉树。有n个结点,则其深度为log2(n + 1) 上取整。同前序遍历一样,有两种解法,思路相同,我只写第一种。同前序遍历一样,有两种解法,思路相同,我只写第一种。公式:二叉树的层数为K,节点总数为2^k -1。二叉树中又有两种特殊的二叉树,

2301_79417489的博客 1263

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

2024年最新Java程序员必看经典书籍,助你早日打通任督二脉,刷java面试题的网站

JVM,JAVA集合,JAVA多线程并发,JAVA基础,Spring原理,微服务,Netty与RPC,网络,日志,Zookeeper,Kafka,RabbitMQ,Hbase,MongoDB,Cassandra,设计模式,负载均衡,数据库,一致性哈希,JAVA算法,数据结构,加密算法,分布式缓存,Hadoop,Spark,Storm,YARN,机器学习,云计算…本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录。

2401_84584817的博客 733

Controller层代码这么写,任督二脉全打通

往期热门文章: 1、深拷贝、浅拷贝和clone、new方法到底哪个效率更高? 2、别再踩坑!电话号码,用 int 还是用 String 存储? 3、告别类型错误:Java泛型上下界,这些坑你千万别踩! 4、IDEA 接入 DeepSeek,辅助编程非常丝滑! 5、在 SpringBoot 项目中如何动态切换数据源、数据库? 作者丨gelald来源丨juejin.cn/post/712309104...

Java后端技术 99

60小时打通Python任督二脉

重磅:教育部考试中心已经公布,在计算机二级考试加入“Python语言程序设计”科目,将于2018年9月全国计算机等级考试中首次亮相。这将促进Python在大学生中的普及。...

架构文摘 622

硬件嵌入式学习路线大总结(一):C语言与linux。内功心法——从入门到精通,彻底打通你的任督二脉!

本文总结了嵌入式工程师学习路线中的C语言核心内容。文章指出C语言是嵌入式开发的"屠龙宝刀",因其贴近硬件、高效灵活的特性成为嵌入式领域的王者语言。全文分为三个阶段: 基础阶段(2-4周): 重点掌握数据类型、运算符、控制流、函数和数组 强调理解变量作用域、内存连续性等底层概念 进阶阶段(3-6周): 深入讲解指针和内存管理,称为C语言的"灵魂" 详细解析结构体、共用体、枚举和预处理宏 介绍文件I/O操作 算法阶段(4-8周): 讲解C语言在算法题中的优势 演示链表实现

c5dnuser的博客 1797

Java集合框架:从数据结构原理到面试源码的完整脉络

数据结构是程序设计的基石,而Java集合框架正是这些抽象模型在工程中的活体实现。顺序表对应ArrayList,链表对应LinkedList,哈希表对应HashMap,红黑树对应TreeMap,每个经典结构都有对应的Java类。理解集合框架的底层原理,如HashMap的哈希冲突处理与扩容机制、红黑树的平衡旋转,不仅有助于面试中应对八股文追问,更能指导日常开发中的选型和性能调优。面向接口编程、合理设置初始容量、正确使用线程安全容器,都是工程实践中的关键技能。从ArrayList的1.5倍扩容到Concurren

weixin_30768881的博客 185

《100天精通Python——基础篇 2025 第19天:玩转文件与目录,打通Python数据持久化任督二脉》

本文详解 Python 中文件与目录的操作,覆盖文件 I/O 原理、open() 函数用法、读写模式及常见操作流程。深入讲解序列化与反序列化、JSON 与 pickle 的应用,介绍 shutil 模块进行文件管理,并补充了 CSV 文件的读写方式。帮助读者掌握数据持久化核心技能,为实战开发奠定基础。

XAMO Lab|代码、调试与复盘笔记 1120
上一篇: 保卫 Java 应用程序的安全沙箱机制你需要了解一下
下一篇: 一本《Redis 深度历险》,我能躺挣多少钱?
码洞
博客等级 码龄10年 41粉丝 137原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值