Java多线程Thread及其原理深度解析

文章目录

  • 1. 实现多线程的方式
  • 2. Thread 部分源码
    • 2.1. native 方法注册
    • 2.2. Thread 中的成员变量
    • 2.3. Thread 构造方法与初始化
    • 2.4. Thread 线程状态与操作系统状态
    • 2.4. start() 与 run() 方法
    • 2.5. sleep() 方法
    • 2.6. join() 方法
    • 2.7. interrupt() 方法

本文参考:

线程的创建 — Thread与Runnable详解

[聊一聊多线程的 run() 和 start(),挖一挖start0 ]

【高并发】Thread类的源码精髓

【Java】Thread类中的join()方法原理

Java 线程状态之 BLOCKED

java线程状态与操作系统线程状态的关系

1. 实现多线程的方式

package com.jxz.threads;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

/**
 * @Author jiangxuzhao
 * @Description
 * @Date 2024/9/10
 */
@Slf4j
public class ThreadCreateTest {
    @Test
    @SneakyThrows
    public void test1() {
        // 匿名类重写 Thread#run
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                log.info("extend thread#run...");
            }
        };
        thread1.start();
        // 避免主线程直接结束
        Thread.sleep(1000);
    }

    @Test
    @SneakyThrows
    public void test2() {
        // Lambda 表达式定义实现 Runnable target, Thread#run 方法最终调用 target#run
        Thread thread2 = new Thread(() -> {
            log.info("implement 自定义变量 target 的 Runnable#run...");
        });
        thread2.start();
        // 避免主线程直接结束
        Thread.sleep(1000);
    }

    @Test
    @SneakyThrows
    public void test3() {
        // Lambda 表达式定义实现 callable#call,可以在主线程中通过 Future#get 阻塞获取结果 result
        FutureTask<String> stringFutureTask = new FutureTask<>(() -> {
            log.info("implement Callable#call");
            return Thread.currentThread().getName();
        });
        Thread thread3 = new Thread(stringFutureTask);
        thread3.start();
        // 阻塞获取结果,不用担心主线程直接结束
        log.info("thread3 futureTask callable output = {}", stringFutureTask.get());
    }

    @Test
    @SneakyThrows
    public void test4() {
        // 线程池实现异步多线程
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<String> stringFuture = executorService.submit(() -> {
            log.info("thread pool submit");
            return Thread.currentThread().getName();
        });
        // 阻塞获取结果,不用担心主线程直接结束
        log.info("thead submit output = {}", stringFuture.get());
    }
}

2. Thread 部分源码

2.1. native 方法注册

public
class Thread implements Runnable {
  	// 在 jdk 底层的 Thread.c 文件中定义了各种方法
    private static native void registerNatives();
  
  	// 确保 registerNatives 是 <clinit> 中第一件做的事
    static {
        registerNatives();
    }
}

Thread#registerNatives 作为本地方法,主要作用是注册一些本地方法供 Thread 类使用,如 start0(), stop0() 等。

该方法被放在一个本地静态代码块中,并且该代码块被放在类中最靠前的位置,确保当 Thread 类被加载到 JVM 中时,调用 第一时间就会注册所有的本地方法。

所有的本地方法都是定义在 JDK 源码的 Thread.c 文件中的,它定义了各个操作系统平台都要用到的关于线程的基本操作。

可以专门去下载 openjdk 1.8 的源码一探究竟:

在这里插入图片描述

或者直接阅读 openjdk8 在线的源码:

https://hg.openjdk.org/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/native/java/lang/Thread.c

2.2. Thread 中的成员变量

针对其中常见的几个变量做了中文注释

// 当前线程的名称
private volatile String name;
private int            priority;
private Thread         threadQ;
private long           eetop;

/* Whether or not to single_step this thread. */
private boolean     single_step;

// 当前线程是否在后台运行
/* Whether or not the thread is a daemon thread. */
private boolean     daemon = false;

/* JVM state */
private boolean     stillborn = false;

// init 构造方法中传入的执行任务,当其不为空时,会执行此任务
/* What will be run. */
private Runnable target;

// 当前线程所在的线程组
/* The group of this thread */
private ThreadGroup group;

// 当前线程的类加载器
/* The context ClassLoader for this thread */
private ClassLoader contextClassLoader;

/* The inherited AccessControlContext of this thread */
private AccessControlContext inheritedAccessControlContext;

// 被用来定义 "Thread-" + nextThreadNum() 的线程名,自增的序号在线程池打印日志中很常见
// 静态变量 threadInitNumber 在 static synchronized 方法中自增,这个方法被调用时在 Thread.class 类上加 synchronized 锁,保证单台 JVM 虚拟机上都通过 Thread.class 并发创建线程 init 时,线程自增序号的并发安全
/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

// 每个线程都维护一个 ThreadLocalMap,这个在保障线程安全的 ThreadLocal 中经常出现
/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

/*
 * The requested stack size for this thread, or 0 if the creator did
 * not specify a stack size.  It is up to the VM to do whatever it
 * likes with this number; some VMs will ignore it.
 */
private long stackSize;

/*
 * JVM-private state that persists after native thread termination.
 */
private long nativeParkEventPointer;

/*
 * Thread ID
 */
private long tid;

/* For generating thread ID */
private static long threadSeqNumber;

/* Java thread status for tools,
 * initialized to indicate thread 'not yet started'
 */

private volatile int threadStatus = 0;


private static synchronized long nextThreadID() {
    return ++threadSeqNumber;
}

/**
 * The argument supplied to the current call to
 * java.util.concurrent.locks.LockSupport.park.
 * Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
 * Accessed using java.util.concurrent.locks.LockSupport.getBlocker
 */
volatile Object parkBlocker;

/* The object in which this thread is blocked in an interruptible I/O
 * operation, if any.  The blocker's interrupt method should be invoked
 * after setting this thread's interrupt status.
 */
private volatile Interruptible blocker;
private final Object blockerLock = new Object();

/* Set the blocker field; invoked via sun.misc.SharedSecrets from java.nio code
 */
void blockedOn(Interruptible b) {
    synchronized (blockerLock) {
        blocker = b;
    }
}

/**
 * The minimum priority that a thread can have.
 */
public final static int MIN_PRIORITY = 1;

/**
 * The default priority that is assigned to a thread.
 */
public final static int NORM_PRIORITY = 5;

/**
 * The maximum priority that a thread can have.
 */
public final static int MAX_PRIORITY = 10;

2.3. Thread 构造方法与初始化

构造方法:

Thread 具有多个重载的构造函数,内部都是调用 Thread#init() 方法初始化,我们常用的就是传入 Thread(Runnable target) 以及 Thread(Runnable target, String name)

public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}


Thread(Runnable target, AccessControlContext acc) {
    init(null, target, "Thread-" + nextThreadNum(), 0, acc, false);
}

public Thread(ThreadGroup group, Runnable target) {
    init(group, target, "Thread-" + nextThreadNum(), 0);
}

public Thread(String name) {
    init(null, null, name, 0);
}

public Thread(ThreadGroup group, String name) {
    init(group, null, name, 0);
}

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}

public Thread(ThreadGroup group, Runnable target, String name) {
    init(group, target, name, 0);
}

public Thread(ThreadGroup group, Runnable target, String name,
              long stackSize) {
    init(group, target, name, stackSize);
}

init 初始化方法:

主要完成成员变量赋值的操作,包括 Runnable target 变量的赋值。后面可以看到,如果在构造器中就传入这个 Runnable,Thread#run 就会执行这个 Runnable.

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;

    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        /* Determine if it's an applet or not */

        /* If there is a security manager, ask the security manager
           what to do. */
        if (security != null) {
            g = security.getThreadGroup();
        }

        /* If the security doesn't have a strong opinion of the matter
           use the parent thread group. */
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }

    /* checkAccess regardless of whether or not threadgroup is
       explicitly passed in. */
    g.checkAccess();

    /*
     * Do we have the required permissions?
     */
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
  
  	// 就是上面成员变量中的 target,在这里赋值
    this.target = target;
    setPriority(priority);
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    /* Set thread ID */
    tid = nextThreadID();
}

2.4. Thread 线程状态与操作系统状态

public enum State {
    /**
     * Thread state for a thread which has not yet started.
     */
  	// 初始化状态
    NEW,

    /**
     * Thread state for a runnable thread.  A thread in the runnable
     * state is executing in the Java virtual machine but it may
     * be waiting for other resources from the operating system
     * such as processor.
     */
  	// 可运行状态,可运行状态可以包括:运行中状态和就绪状态。
    RUNNABLE,

    /**
     * Thread state for a thread blocked waiting for a monitor lock.
     * A thread in the blocked state is waiting for a monitor lock
     * to enter a synchronized block/method or
     * reenter a synchronized block/method after calling
     * {@link Object#wait() Object.wait}.
     */
  	// 线程阻塞状态
    BLOCKED,

    /**
     * Thread state for a waiting thread.
     * A thread is in the waiting state due to calling one of the
     * following methods:
     * <ul>
     *   <li>{@link Object#wait() Object.wait} with no timeout</li>
     *   <li>{@link #join() Thread.join} with no timeout</li>
     *   <li>{@link LockSupport#park() LockSupport.park}</li>
     * </ul>
     *
     * <p>A thread in the waiting state is waiting for another thread to
     * perform a particular action.
     *
     * For example, a thread that has called <tt>Object.wait()</tt>
     * on an object is waiting for another thread to call
     * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
     * that object. A thread that has called <tt>Thread.join()</tt>
     * is waiting for a specified thread to terminate.
     */
  	// 等待状态
    WAITING,

    /**
     * Thread state for a waiting thread with a specified waiting time.
     * A thread is in the timed waiting state due to calling one of
     * the following methods with a specified positive waiting time:
     * <ul>
     *   <li>{@link #sleep Thread.sleep}</li>
     *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
     *   <li>{@link #join(long) Thread.join} with timeout</li>
     *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
     *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
     * </ul>
     */
  	// 超时等待状态
    TIMED_WAITING,

    /**
     * Thread state for a terminated thread.
     * The thread has completed execution.
     */
  	// 线程终止状态
    TERMINATED;
}
  • NEW: 初始状态,线程被构建,但是还没有调用 Thread#start() 方法

  • RUNNABLE: 可运行状态,包括运行中和就绪状态。从源码的注释中可以看出来,就绪状态就是线程在 JVM 中有资格运行,但是由于操作系统调度的原因尚未执行,可能线程在等待操作系统释放资源,比方说处理器资源。

  • BLOCKED: 阻塞状态,处于这个状态的线程等待别的线程释放 monitor 锁以进入 synchronized 块;或者调用 Object#wait() 方法释放锁进入等待队列后(此时是 WAITING 状态),被其他线程 notify() 唤醒时不能立刻从上次 wait 的地方恢复执行,再次进入 synchronized 块还需要和别的线程竞争锁。
    总结来说,线程因为获取不到锁而无法进入同步代码块时,处于 BLOCKED 阻塞状态。

    一篇用例子解释的文章参考:Java 线程状态之 BLOCKED

  • WAITING: 等待状态,处于该状态的线程需要其他线程对其进行通知或者中断等操作,从而进入下一个状态。

  • TIMED_WAITING: 超时等待状态,相比于 WAITING 状态持续等待,该状态可以在一定时间后自行返回

  • TERMINATED: 终止状态,当前线程执行完毕

下面就用一张图表示了Java线程各种状态的流转,其中夹杂着操作系统线程的状态定义,其中标红的部分表示 Java 状态

在这里插入图片描述

对比操作系统线程状态,包括 new、terminated、ready、running、waiting,除去初始化 new 和 terminated 终止状态,一个线程运行中的状态只有:

  • ready: 线程已创建,等待系统调度分配 CPU 资源
  • running: 线程获得了 CPU 使用权,正在运算
  • waiting: 线程等待(或者说挂起),让出 CPU 资源给其他线程使用

其对应关系我理解如下:

其中 Java 线程状态 RUNNABLE 包括操作系统状态的运行 running 和就绪 ready,操作系统的 waiting 包含了 BLOCKED 阻塞挂起状态。

在这里插入图片描述

2.4. start() 与 run() 方法

新线程构造之后,只有调用 start() 才能让 JVM 创建线程并进入运行状态,Thread#start() 源码如下,主要包含几大步骤:

  1. 判断线程状态是否为 NEW 初始化

  2. 加入线程组

  3. 调用 native 方法 start0() 通知底层 JVM 启动一个线程,start0() 就是前面 registerNatives() 本地方法注册的一个启动方法

  4. 如果启动失败,把线程从线程组中删除

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
  	// 1. 判断线程状态是否为 NEW 初始化,否则直接抛出异常
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
  	// 2. 加入线程组
    group.add(this);
	
  	// 线程是否已经启动标志位,启动后设置为 true
    boolean started = false;
    try {
      	// 3. 调用本地方法启动线程
        start0();
      	// 启动后设置标志位为 true
        started = true;
    } finally {
        try {
          	// 4. 如果启动失败,把线程从线程组中移除
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

// JVM 真正启动线程的本地方法
private native void start0();

从 start() 源码中可以看出来以下几点:

  1. start() 加上 synchronized 关键词,单个 Thread 实例在这个 JVM 进程中运行是同步的,因此不会出现并发问题。同步检查该线程的状态,如果不是初始化状态则抛出异常。
  2. start() 方法并没有直接调用我们定义的 run() 方法,是因为 Thread#start() 底层调用 Thread#start0(),start0() 的本地方法逻辑中会调用 run() 方法
  3. 直接调用 Thread#run() 方法或者 Runnable#run() 方法不会创建新线程执行任务,而是在主线程直接串行执行,如果要创建新线程执行任务,需要调用 Thread#start() 方法

调用逻辑图如下:

在这里插入图片描述

Thread#run() 源码如下:

// 自定义重写 Thread#run() 或者传入 Runnable,最终都会调用该线程的 run() 方法逻辑
// 如果传入了 Runnable 就会走进这个方法运行 target.run(),有点装饰器模式的感觉
@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

至于为何最终 start0() 还是调用了 Thread#run(),这就需要去看 jdk 源码了,我刚好也硬着头皮去挖了下:

  1. 首先看到 Thread.c 文件中 registerNatives 里面注册的这些本地方法,start0() 会去调用 JVM_StartThread在这里插入图片描述

  2. 在 jvm.cpp 文件中找出 JVM_StartThread 方法,其底层调用 new JavaThread()方法
    在这里插入图片描述

    最终该方法真的会去 thread.cpp 里调用创建操作系统线程的方法 os::create_thread

在这里插入图片描述

  1. new JavaThread() 方法里面会引用 jvm.cpp 文件中的 thread_entry 方法,这个方法最终就会调用 vmSymbols::run_method_name(),看起来是个虚拟机内注册的方法

    在这里插入图片描述

    全局检索一下,其实就是在 vmSymbols.hpp 头文件中定义的许多通用方法和变量,run 方法刚好是其中定义的一个,也就是 Thread#run()。
    还可以看到许多其他常见的方法,比方说类的初始化方法,是 jvm 第一次加载 class 文件时调用,包括静态变量初始化语句和静态块执行。参考init和clinit何时调用
    在这里插入图片描述

2.5. sleep() 方法

Thread#sleep() 方法会让当前线程休眠一段时间,单位为毫秒,由于是 static 方法,所以是让直接调用 Thread.sleep() 的休眠,这里需要注意的是:

调用 sleep() 方法使线程休眠以后,不会释放自己占有的锁。

// 本地方法,真正让线程休眠的方法
public static native void sleep(long millis) throws InterruptedException;

/**
 * Causes the currently executing thread to sleep (temporarily cease
 * execution) for the specified number of milliseconds plus the specified
 * number of nanoseconds, subject to the precision and accuracy of system
 * timers and schedulers. The thread does not lose ownership of any
 * monitors.
 *
 * @param  millis
 *         the length of time to sleep in milliseconds
 *
 * @param  nanos
 *         {@code 0-999999} additional nanoseconds to sleep
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative, or the value of
 *          {@code nanos} is not in the range {@code 0-999999}
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          <i>interrupted status</i> of the current thread is
 *          cleared when this exception is thrown.
 */
public static void sleep(long millis, int nanos)
throws InterruptedException {
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
        millis++;
    }
		
  	// 调用本地方法
    sleep(millis);
}

2.6. join() 方法

这个方法是目前我觉得 Thread 里面最难理解的方法了,涉及到 synchronized 锁、wait、notify 原理,以及线程调用主体之间的辨析,参考 【Java】Thread类中的join()方法原理,我的理解如下:

首先看下 Thread#join() 方法的源码:

非静态方法,是类中的普通方法,比方说 Main 线程调用 ThreadA.join(),就是 Main 线程会等待 ThreadA 执行完成

// 调用方法,比方说 Main 线程调用 ThreadA.join(),就是 Main 线程会等待 ThreadA 执行完成
public final void join() throws InterruptedException {
    join(0);
}

public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
      	// 该分支是无限期等待 ThreadA 结束,其实内部最后是在 ThreadA 结束时被 notify 
        while (isAlive()) {
            wait(0);
        }
    } else {
      	// 该分支时等待有限的时间,如果 ThreadA 在 delay 时间以后还未结束,等待线程也返回了
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

重点关注下其中会出现的两个 wait():

首先我们知道,Object#wait() 需要放在 synchronized 代码块中执行,即获取到锁以后再释放掉锁。

这个 synchronized 锁就是在 Thread#join() 方法上,成员方法上加了 synchronized 说明就是 synchronized(this), 假设 Main 线程调用 ThreadA.join(),那么这个 this 就是指调用 ThreadA.join() 的 ThreadA 对象本身,最终效果就是,调用方 Main 线程持有了 ThreadA 对象的 Monitor 锁,被记录在 ThreadA 对象头上。

在这里插入图片描述

有了 Object#wait() 就需要有对应的 Object#notify() 将其唤醒,这又得看到 jvm 源码里面去了

在 openjdk/hotspot/src/share/vm/runtime/thread.cpp 的 JavaThread::exit 方法中,这其实是线程退出时会执行的方法,有个 ensure_join() 方法
在这里插入图片描述

ensure_join() 方法的源码如下:

上面的 this 就是指 ThreadA,就是下面方法入参中的 thread。可以看出来,当线程 ThreadA 执行完成准备退出时,jvm 会自动唤醒等待在 threadA 对象上的线程,在我们的例子中就是主线程。

在这里插入图片描述

总结如下:

Thread.join() 方法底层原理是 synchronized 方法 + wait/notify。主线程调用 ThreadA.join() 方法,通过 synchronized 关键字获取到 ThreadA 的对象锁,内部再通过 Object#wait() 方法等待,这里的执行方和调用方都是主线程,最终当 ThreadA 线程退出的时候,jvm 会自动 notify 唤醒等待在 ThreadA 上的线程,也就是主线程。

2.7. interrupt() 方法

Thread#interrupt 是中断被调用线程的方法,它通过设置线程的中断标志位来中断被调用线程,通常调用会抛出 java.lang.InterruptedException 异常。

这种中断线程的方法比较安全,能够使正在执行的任务继续能够执行完,而不像 stop() 方法那样强制关闭。

public void interrupt() {
    if (this != Thread.currentThread())
        checkAccess();

    synchronized (blockerLock) {
        Interruptible b = blocker;
        if (b != null) {
            interrupt0();           // Just to set the interrupt flag
            b.interrupt(this);
            return;
        }
    }
  	// 调用本地方法中断线程
    interrupt0();
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV_最简单的鼠标截取ROI区域

在OpenCV中也存在鼠标的操作&#xff0c;今天我们先介绍一下鼠标中的操作事件 void setMousecallback(const string& winname, MouseCallback onMouse, void* userdata0) setMousecallback参数说明&#xff1a; winname:窗口的名字 onMouse:鼠标响应函数&#xff0c;回调…

接口加解密及数据加解密

目录 一、 加解密方式介绍 1.1 Hash算法加密 1.2. 对称加密 1.3 非对称加密 二、 我们要讲什么&#xff1f; 三、 接口加解密 四、 数据加解密 一、 加解密方式介绍 所有的加密方式我们可以分为三类&#xff1a;对称加密、非对称加密、Hash算法加密。 算法内部的具体实现…

【后端开发】JavaEE初阶—线程的理解和编程实现

前言&#xff1a; &#x1f31f;&#x1f31f;本期讲解多线程的知识哟~~~&#xff0c;希望能帮到屏幕前的你。 &#x1f308;上期博客在这里&#xff1a;【后端开发】JavaEE初阶——计算机是如何工作的&#xff1f;&#xff1f;&#xff1f;-CSDN博客 &#x1f308;感兴趣的小伙…

【设计模式】UML类图

目录 前言 一、类图概述 二、类图的作用 三、类图表示法 四、类之间关系的表示方法 1. 关联关系 1.1 单向关联 1.2 双向关联 1.3 自关联 2. 聚合关系 3. 组合关系 4. 依赖关系 5. 继承关系 6. 实现关系 总结 前言 统一建模语言&#xff08; Unified Modeling La…

游戏如何对抗定制挂

近年来&#xff0c;游戏安全对抗强度相比以往更加激烈&#xff0c;具体表现在“定制挂”趋势显著。在近期收集的近万款外挂样本中&#xff0c;定制挂约占比78%&#xff0c;常见的内存修改器、变速器等通用作弊手段占比正在下降。 所谓定制挂&#xff0c;是指针对某款游戏单独开…

九章云极DataCanvas公司荣获2024年服贸会“科技创新服务示范案例”

9月15日&#xff0c;2024年中国国际服务贸易交易会&#xff08;服贸会&#xff09;示范案例交流会暨颁奖典礼在北京国家会议中心举行&#xff0c;九章云极DataCanvas 公司自研的DataCanvas Alaya NeW智算操作系统凭借卓越的AI创新实力、前瞻性的市场布局以及突破性的技术革新成…

Python脚本每日自动备份MySQL数据库,无需mysqldump

编写一个Python脚本&#xff0c;每天凌晨3点开始备份 脚本具有以下特点 不需要安装mysql-client&#xff0c;并且Windows Linux都可以使用支持多个数据库连接的备份每个数据库支持多个表备份日志保存下来&#xff0c;方便第二天早上查看备份结果 首先安装需要的库 pip3 ins…

Mybatis Plus分页查询返回total为0问题

Mybatis Plus分页查询返回total为0问题 一日&#xff0c;乌云密布&#xff0c;本人看着mybatis plus的官方文档&#xff0c;随手写了个分页查询&#xff0c;如下 Page<Question> questionPage questionService.page(new Page<>(current, size),questionService.g…

[C语言]连子棋游戏

文章目录 一、前言二、游戏思路三、游戏方法1、初始化2、判断胜利3、交互4、电脑下棋 四、核心方法说明1、初始化游戏2、销毁棋盘3、显示游戏4、电脑下棋5、用户下棋6、判断游戏状态7、游戏交互 五、游戏效果展示与源码分享1、游戏效果2、源代码 一、前言 对于指针和数组理解尚…

DataGrip在Windows和MacOS平台上的快捷键

0. 背景信息 No.说明1测试DataGrip版本号 : 2024.2.2 1. Windows下快捷键 2. MacOS下快捷键

基于波特图的控制系统设计算法

波特图&#xff08;Bode Plot&#xff09;是一种用于描述线性控制系统频率响应的图形表示方法&#xff0c;通常用于分析和设计控制系统。它以控制系统的传递函数&#xff08;或频域传递函数&#xff09;为基础&#xff0c;将系统的幅频特性&#xff08;振幅-频率响应&#xff0…

PyCharm与Anaconda超详细安装配置教程

1、安装Anaconda&#xff08;过程&#xff09;-CSDN博客 2.创建虚拟环境conda create -n pytorch20 python3.9并输入conda activate pytorch20进入 3.更改镜像源conda/pip(只添加三个pip源和conda源即可) 4.安装PyTorch&#xff08;CPU版&#xff09; 5.安装Pycharm并破解&…

LED灯、蜂鸣器、继电器的控制

LED灯的控制 该专栏所有文章都默认使用STM32F103ZET6开发板 目录 LED灯的控制 一、简单的LED灯控制 1、初始化函数 led灯 2、应用函数 2、蜂鸣器 3、继电器 一、简单的LED灯控制 编程框架&#xff1a;初始化函数和应用函数 1、初始化函数 初始化函数一般包括&#xf…

SVTR文字识别

论文地址&#xff1a;https://arxiv.org/abs/2205.00159 notes&#xff1a; 论文2.5中说的N nodes&#xff0c;就是输出的类别数量&#xff0c;英文37&#xff0c;中文6625&#xff0c;英文37说的是最简单的英文文字识别任务&#xff0c;不区分大小写&#xff0c;就是26个字母…

软件测试 BUG 篇

目录 一、软件测试的生命周期 二、BUG 1. bug的概念 2. 描述bug的要素 3. bug的级别 4. bug的生命周期 5. 与开发产生争执怎么办&#xff1f;&#xff08;面试高频考题&#xff09; 5.1 先检查自身&#xff0c;是否bug描述不清楚 5.2 站在用户角度考虑并抛出问题 5.3 …

nginx架构篇(三)

文章目录 一、Nginx实现服务器端集群搭建1.1 Nginx与Tomcat部署1. 环境准备(Tomcat)2. 环境准备(Nginx) 1.2. Nginx实现动静分离1.2.1. 需求分析1.2.2. 动静实现步骤 1.3. Nginx实现Tomcat集群搭建1.4. Nginx高可用解决方案1.4.1. Keepalived1.4.2. VRRP介绍1.4.3. 环境搭建环境…

【已解决】SpringBoot3项目整合Druid依赖:Druid监控页面404报错

文章标题 问题描述原因分析解决方案参考资料 问题描述 最近&#xff0c;笔者在SpringBoot3项目中整合Druid连接池时&#xff0c;偶然翻到一条介绍Druid监控的短视频&#xff0c;兴致盎然之下尝试设置了一下Druid监控。 But&#xff0c;按照视频中提供的yml参数对照设置&#x…

【全网最全】2024华为杯数学建模C题高质量成品查看论文!【附带全套代码+数据】

题 目&#xff1a; ___基于数据驱动下磁性元件的磁芯损耗建模 完整版获取&#xff1a; 点击链接加入群聊【2024华为杯数学建模助攻资料】&#xff1a;http://qm.qq.com/cgi-bin/qm/qr?_wv1027&kxtS4vwn3gcv8oCYYyrqd0BvFc7tNfhV7&authKeyedQFZne%2BzvEfLEVg2v8FOm%…

山体滑坡检测系统源码分享

山体滑坡检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

使用vue创建项目

一、安装环境 二、创建vue框架&#xff08;创建文件夹&#xff0c;摁shift鼠标右键 打开&#xff09; 1、项目配置 2、新增目录 三、路径别名配置 输入/ ,VSCode会联想出src下的所有子目录和文件&#xff0c;统一文件路径访问时不容易出错 四、ElementPlus配置 1、组件分为…