Java多線程Thread及其原理詳解
1. 實(shí)現(xiàn)多線程的方式
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(); // 避免主線程直接結(jié)束 Thread.sleep(1000); } @Test @SneakyThrows public void test2() { // Lambda 表達(dá)式定義實(shí)現(xiàn) Runnable target, Thread#run 方法最終調(diào)用 target#run Thread thread2 = new Thread(() -> { log.info("implement 自定義變量 target 的 Runnable#run..."); }); thread2.start(); // 避免主線程直接結(jié)束 Thread.sleep(1000); } @Test @SneakyThrows public void test3() { // Lambda 表達(dá)式定義實(shí)現(xiàn) callable#call,可以在主線程中通過 Future#get 阻塞獲取結(jié)果 result FutureTask<String> stringFutureTask = new FutureTask<>(() -> { log.info("implement Callable#call"); return Thread.currentThread().getName(); }); Thread thread3 = new Thread(stringFutureTask); thread3.start(); // 阻塞獲取結(jié)果,不用擔(dān)心主線程直接結(jié)束 log.info("thread3 futureTask callable output = {}", stringFutureTask.get()); } @Test @SneakyThrows public void test4() { // 線程池實(shí)現(xiàn)異步多線程 ExecutorService executorService = Executors.newFixedThreadPool(1); Future<String> stringFuture = executorService.submit(() -> { log.info("thread pool submit"); return Thread.currentThread().getName(); }); // 阻塞獲取結(jié)果,不用擔(dān)心主線程直接結(jié)束 log.info("thead submit output = {}", stringFuture.get()); } }
2. Thread 部分源碼
2.1. native 方法注冊(cè)
public class Thread implements Runnable { // 在 jdk 底層的 Thread.c 文件中定義了各種方法 private static native void registerNatives(); // 確保 registerNatives 是 <clinit> 中第一件做的事 static { registerNatives(); } }
Thread#registerNatives 作為本地方法,主要作用是注冊(cè)一些本地方法供 Thread 類使用,如 start0(), stop0() 等。
該方法被放在一個(gè)本地靜態(tài)代碼塊中,并且該代碼塊被放在類中最靠前的位置,確保當(dāng) Thread 類被加載到 JVM 中時(shí),調(diào)用 第一時(shí)間就會(huì)注冊(cè)所有的本地方法。
所有的本地方法都是定義在 JDK 源碼的 Thread.c 文件中的,它定義了各個(gè)操作系統(tǒng)平臺(tái)都要用到的關(guān)于線程的基本操作。
可以專門去下載 openjdk 1.8 的源碼一探究竟:
或者直接閱讀 openjdk8 在線的源碼:
https://hg.openjdk.org/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/native/java/lang/Thread.c
2.2. Thread 中的成員變量
針對(duì)其中常見的幾個(gè)變量做了中文注釋
// 當(dāng)前線程的名稱 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; // 當(dāng)前線程是否在后臺(tái)運(yùn)行 /* Whether or not the thread is a daemon thread. */ private boolean daemon = false; /* JVM state */ private boolean stillborn = false; // init 構(gòu)造方法中傳入的執(zhí)行任務(wù),當(dāng)其不為空時(shí),會(huì)執(zhí)行此任務(wù) /* What will be run. */ private Runnable target; // 當(dāng)前線程所在的線程組 /* The group of this thread */ private ThreadGroup group; // 當(dāng)前線程的類加載器 /* The context ClassLoader for this thread */ private ClassLoader contextClassLoader; /* The inherited AccessControlContext of this thread */ private AccessControlContext inheritedAccessControlContext; // 被用來定義 "Thread-" + nextThreadNum() 的線程名,自增的序號(hào)在線程池打印日志中很常見 // 靜態(tài)變量 threadInitNumber 在 static synchronized 方法中自增,這個(gè)方法被調(diào)用時(shí)在 Thread.class 類上加 synchronized 鎖,保證單臺(tái) JVM 虛擬機(jī)上都通過 Thread.class 并發(fā)創(chuàng)建線程 init 時(shí),線程自增序號(hào)的并發(fā)安全 /* For autonumbering anonymous threads. */ private static int threadInitNumber; private static synchronized int nextThreadNum() { return threadInitNumber++; } // 每個(gè)線程都維護(hù)一個(gè) ThreadLocalMap,這個(gè)在保障線程安全的 ThreadLocal 中經(jīng)常出現(xiàn) /* 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 構(gòu)造方法與初始化
構(gòu)造方法:
Thread 具有多個(gè)重載的構(gòu)造函數(shù),內(nèi)部都是調(diào)用 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 變量的賦值。后面可以看到,如果在構(gòu)造器中就傳入這個(gè) Runnable,Thread#run 就會(huì)執(zhí)行這個(gè) 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 線程狀態(tài)與操作系統(tǒng)狀態(tài)
public enum State { /** * Thread state for a thread which has not yet started. */ // 初始化狀態(tài) 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. */ // 可運(yùn)行狀態(tài),可運(yùn)行狀態(tài)可以包括:運(yùn)行中狀態(tài)和就緒狀態(tài)。 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}. */ // 線程阻塞狀態(tài) 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. */ // 等待狀態(tài) 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> */ // 超時(shí)等待狀態(tài) TIMED_WAITING, /** * Thread state for a terminated thread. * The thread has completed execution. */ // 線程終止?fàn)顟B(tài) TERMINATED; }
- NEW: 初始狀態(tài),線程被構(gòu)建,但是還沒有調(diào)用 Thread#start() 方法
- RUNNABLE: 可運(yùn)行狀態(tài),包括運(yùn)行中和就緒狀態(tài)。從源碼的注釋中可以看出來,就緒狀態(tài)就是線程在 JVM 中有資格運(yùn)行,但是由于操作系統(tǒng)調(diào)度的原因尚未執(zhí)行,可能線程在等待操作系統(tǒng)釋放資源,比方說處理器資源。
- BLOCKED: 阻塞狀態(tài),處于這個(gè)狀態(tài)的線程等待別的線程釋放 monitor 鎖以進(jìn)入 synchronized 塊;或者調(diào)用 Object#wait() 方法釋放鎖進(jìn)入等待隊(duì)列后(此時(shí)是 WAITING 狀態(tài)),被其他線程 notify() 喚醒時(shí)不能立刻從上次 wait 的地方恢復(fù)執(zhí)行,再次進(jìn)入 synchronized 塊還需要和別的線程競(jìng)爭(zhēng)鎖。
- 總結(jié)來說,線程因?yàn)楂@取不到鎖而無法進(jìn)入同步代碼塊時(shí),處于 BLOCKED 阻塞狀態(tài)。
- WAITING: 等待狀態(tài),處于該狀態(tài)的線程需要其他線程對(duì)其進(jìn)行通知或者中斷等操作,從而進(jìn)入下一個(gè)狀態(tài)。
- TIMED_WAITING: 超時(shí)等待狀態(tài),相比于 WAITING 狀態(tài)持續(xù)等待,該狀態(tài)可以在一定時(shí)間后自行返回
- TERMINATED: 終止?fàn)顟B(tài),當(dāng)前線程執(zhí)行完畢
下面就用一張圖表示了Java線程各種狀態(tài)的流轉(zhuǎn),其中夾雜著操作系統(tǒng)線程的狀態(tài)定義,其中標(biāo)紅的部分表示 Java 狀態(tài)
對(duì)比操作系統(tǒng)線程狀態(tài),包括 new、terminated、ready、running、waiting,除去初始化 new 和 terminated 終止?fàn)顟B(tài),一個(gè)線程運(yùn)行中的狀態(tài)只有:
- ready: 線程已創(chuàng)建,等待系統(tǒng)調(diào)度分配 CPU 資源
- running: 線程獲得了 CPU 使用權(quán),正在運(yùn)算
- waiting: 線程等待(或者說掛起),讓出 CPU 資源給其他線程使用
其對(duì)應(yīng)關(guān)系我理解如下:
其中 Java 線程狀態(tài) RUNNABLE 包括操作系統(tǒng)狀態(tài)的運(yùn)行 running 和就緒 ready,操作系統(tǒng)的 waiting 包含了 BLOCKED 阻塞掛起狀態(tài)。
2.4. start() 與 run() 方法
新線程構(gòu)造之后,只有調(diào)用 start() 才能讓 JVM 創(chuàng)建線程并進(jìn)入運(yùn)行狀態(tài),Thread#start() 源碼如下,主要包含幾大步驟:
- 判斷線程狀態(tài)是否為 NEW 初始化
- 加入線程組
- 調(diào)用 native 方法 start0() 通知底層 JVM 啟動(dòng)一個(gè)線程,start0() 就是前面 registerNatives() 本地方法注冊(cè)的一個(gè)啟動(dòng)方法
- 如果啟動(dòng)失敗,把線程從線程組中刪除
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. 判斷線程狀態(tài)是否為 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); // 線程是否已經(jīng)啟動(dòng)標(biāo)志位,啟動(dòng)后設(shè)置為 true boolean started = false; try { // 3. 調(diào)用本地方法啟動(dòng)線程 start0(); // 啟動(dòng)后設(shè)置標(biāo)志位為 true started = true; } finally { try { // 4. 如果啟動(dòng)失敗,把線程從線程組中移除 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 真正啟動(dòng)線程的本地方法 private native void start0();
從 start() 源碼中可以看出來以下幾點(diǎn):
- start() 加上 synchronized 關(guān)鍵詞,單個(gè) Thread 實(shí)例在這個(gè) JVM 進(jìn)程中運(yùn)行是同步的,因此不會(huì)出現(xiàn)并發(fā)問題。同步檢查該線程的狀態(tài),如果不是初始化狀態(tài)則拋出異常。
- start() 方法并沒有直接調(diào)用我們定義的 run() 方法,是因?yàn)?Thread#start() 底層調(diào)用 Thread#start0(),start0() 的本地方法邏輯中會(huì)調(diào)用 run() 方法
- 直接調(diào)用 Thread#run() 方法或者 Runnable#run() 方法不會(huì)創(chuàng)建新線程執(zhí)行任務(wù),而是在主線程直接串行執(zhí)行,如果要?jiǎng)?chuàng)建新線程執(zhí)行任務(wù),需要調(diào)用 Thread#start() 方法
調(diào)用邏輯圖如下:
Thread#run() 源碼如下:
// 自定義重寫 Thread#run() 或者傳入 Runnable,最終都會(huì)調(diào)用該線程的 run() 方法邏輯 // 如果傳入了 Runnable 就會(huì)走進(jìn)這個(gè)方法運(yùn)行 target.run(),有點(diǎn)裝飾器模式的感覺 @Override public void run() { if (target != null) { target.run(); } }
至于為何最終 start0() 還是調(diào)用了 Thread#run(),這就需要去看 jdk 源碼了,我剛好也硬著頭皮去挖了下:
首先看到 Thread.c 文件中 registerNatives 里面注冊(cè)的這些本地方法,start0() 會(huì)去調(diào)用 JVM_StartThread
在 jvm.cpp 文件中找出 JVM_StartThread 方法,其底層調(diào)用 new JavaThread()方法
最終該方法真的會(huì)去 thread.cpp 里調(diào)用創(chuàng)建操作系統(tǒng)線程的方法 os::create_thread
new JavaThread() 方法里面會(huì)引用 jvm.cpp 文件中的 thread_entry 方法,這個(gè)方法最終就會(huì)調(diào)用 vmSymbols::run_method_name(),看起來是個(gè)虛擬機(jī)內(nèi)注冊(cè)的方法
全局檢索一下,其實(shí)就是在 vmSymbols.hpp 頭文件中定義的許多通用方法和變量,run 方法剛好是其中定義的一個(gè),也就是 Thread#run()。
還可以看到許多其他常見的方法,比方說類的初始化方法,是 jvm 第一次加載 class 文件時(shí)調(diào)用,包括靜態(tài)變量初始化語句和靜態(tài)塊執(zhí)行。
2.5. sleep() 方法
Thread#sleep() 方法會(huì)讓當(dāng)前線程休眠一段時(shí)間,單位為毫秒,由于是 static 方法,所以是讓直接調(diào)用 Thread.sleep() 的休眠,這里需要注意的是:
調(diào)用 sleep() 方法使線程休眠以后,不會(huì)釋放自己占有的鎖。
// 本地方法,真正讓線程休眠的方法 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++; } // 調(diào)用本地方法 sleep(millis); }
2.6. join() 方法
這個(gè)方法是目前我覺得 Thread 里面最難理解的方法了,涉及到 synchronized 鎖、wait、notify 原理,以及線程調(diào)用主體之間的辨析,參考 【Java】Thread類中的join()方法原理,我的理解如下:
首先看下 Thread#join() 方法的源碼:
非靜態(tài)方法,是類中的普通方法,比方說 Main 線程調(diào)用 ThreadA.join(),就是 Main 線程會(huì)等待 ThreadA 執(zhí)行完成
// 調(diào)用方法,比方說 Main 線程調(diào)用 ThreadA.join(),就是 Main 線程會(huì)等待 ThreadA 執(zhí)行完成 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 結(jié)束,其實(shí)內(nèi)部最后是在 ThreadA 結(jié)束時(shí)被 notify while (isAlive()) { wait(0); } } else { // 該分支時(shí)等待有限的時(shí)間,如果 ThreadA 在 delay 時(shí)間以后還未結(jié)束,等待線程也返回了 while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
重點(diǎn)關(guān)注下其中會(huì)出現(xiàn)的兩個(gè) wait():
首先我們知道,Object#wait() 需要放在 synchronized 代碼塊中執(zhí)行,即獲取到鎖以后再釋放掉鎖。
這個(gè) synchronized 鎖就是在 Thread#join() 方法上,成員方法上加了 synchronized 說明就是 synchronized(this), 假設(shè) Main 線程調(diào)用 ThreadA.join(),那么這個(gè) this 就是指調(diào)用 ThreadA.join() 的 ThreadA 對(duì)象本身,最終效果就是,調(diào)用方 Main 線程持有了 ThreadA 對(duì)象的 Monitor 鎖,被記錄在 ThreadA 對(duì)象頭上。
有了 Object#wait() 就需要有對(duì)應(yīng)的 Object#notify() 將其喚醒,這又得看到 jvm 源碼里面去了
在 openjdk/hotspot/src/share/vm/runtime/thread.cpp 的 JavaThread::exit 方法中,這其實(shí)是線程退出時(shí)會(huì)執(zhí)行的方法,有個(gè) ensure_join() 方法
ensure_join() 方法的源碼如下:
上面的 this 就是指 ThreadA,就是下面方法入?yún)⒅械?thread??梢钥闯鰜?,當(dāng)線程 ThreadA 執(zhí)行完成準(zhǔn)備退出時(shí),jvm 會(huì)自動(dòng)喚醒等待在 threadA 對(duì)象上的線程,在我們的例子中就是主線程。
總結(jié)如下:
Thread.join() 方法底層原理是 synchronized 方法 + wait/notify。主線程調(diào)用 ThreadA.join() 方法,通過 synchronized 關(guān)鍵字獲取到 ThreadA 的對(duì)象鎖,內(nèi)部再通過 Object#wait() 方法等待,這里的執(zhí)行方和調(diào)用方都是主線程,最終當(dāng) ThreadA 線程退出的時(shí)候,jvm 會(huì)自動(dòng) notify 喚醒等待在 ThreadA 上的線程,也就是主線程。
2.7. interrupt() 方法
Thread#interrupt 是中斷被調(diào)用線程的方法,它通過設(shè)置線程的中斷標(biāo)志位來中斷被調(diào)用線程,通常調(diào)用會(huì)拋出 java.lang.InterruptedException 異常。
這種中斷線程的方法比較安全,能夠使正在執(zhí)行的任務(wù)繼續(xù)能夠執(zhí)行完,而不像 stop() 方法那樣強(qiáng)制關(guān)閉。
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; } } // 調(diào)用本地方法中斷線程 interrupt0(); }
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
Spring Boot配置線程池拒絕策略的場(chǎng)景分析(妥善處理好溢出的任務(wù))
本文通過實(shí)例代碼給大家介紹下如何為線程池配置拒絕策略、如何自定義拒絕策略。對(duì)Spring Boot配置線程池拒絕策略的相關(guān)知識(shí)感興趣的朋友一起看看吧2021-09-09Java 實(shí)戰(zhàn)項(xiàng)目之畢業(yè)設(shè)計(jì)管理系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)畢業(yè)設(shè)計(jì)管理系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11JavaWeb實(shí)現(xiàn)同一帳號(hào)同一時(shí)間只能一個(gè)地點(diǎn)登陸(類似QQ登錄的功能)
最近做了企業(yè)項(xiàng)目,其中有這樣的需求要求同一帳號(hào)同一時(shí)間只能一個(gè)地點(diǎn)登陸類似QQ登錄的功能。下面小編通過本文給大家分享實(shí)現(xiàn)思路,感興趣的朋友參考下吧2016-11-11SpringBoot基于Redis實(shí)現(xiàn)生成全局唯一ID的方法
在項(xiàng)目中生成全局唯一ID有很多好處,生成全局唯一ID有助于提高系統(tǒng)的可用性、數(shù)據(jù)的完整性和安全性,同時(shí)也方便數(shù)據(jù)的管理和分析,所以本文給大家介紹了SpringBoot基于Redis實(shí)現(xiàn)生成全局唯一ID的方法,文中有詳細(xì)的代碼講解,需要的朋友可以參考下2023-12-12線程池調(diào)用kafka發(fā)送消息產(chǎn)生的內(nèi)存泄漏問題排查解決
這篇文章主要為大家介紹了線程池調(diào)用kafka發(fā)送消息產(chǎn)生的內(nèi)存泄漏問題排查解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08Java基礎(chǔ)類學(xué)習(xí)之String詳解
這篇文章主要為大家詳細(xì)介紹了Java基礎(chǔ)類中String的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Java有一定的幫助,需要的可以參考一下2022-12-12