主頁 > 後端開發 > JNI-Thread中start方法的呼叫與run方法的回呼分析

JNI-Thread中start方法的呼叫與run方法的回呼分析

2020-11-07 03:33:23 後端開發

前言

在java編程中,執行緒Thread是我們經常使用的類,那么創建一個Thread的本質究竟是什么,本文就此問題作一個探索,

內容主要分為以下幾個部分

1.JNI機制的使用

2.Thread創建執行緒的底層呼叫分析

3.系統執行緒的使用

4.Thread中run方法的回呼分析

5.實作一個jni的回呼

1.JNI機制的基本使用

當我們new出一個Thread的時候,僅僅是創建了一個java層面的執行緒物件,而只有當Thread的start方法被呼叫的時候,一個執行緒才真正開始執行了,所以start方法是我們關注的目標

查看Thread類的start方法

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
        }
    }
}

Start方法本身并不復雜,其核心是start0(),真正地將執行緒啟動起來,

接著我們查看start0()方法

private native void start0();

可以看到這是一個native方法,這里我們需要先解釋一下什么是native方法,

眾所周知java是一個跨平臺的語言,用java編譯的代碼可以運行在任何安裝了jvm的系統上,然而各個系統的底層實作肯定是有區別的,為了使java可以跨平臺,于是jvm提供了叫java native interface(JNI)的機制,當java需要使用到一些系統方法時,由jvm幫我們去呼叫系統底層,而java本身只需要告知jvm需要做的事情,即呼叫某個native方法即可,

例如,當我們需要啟動一個執行緒時,無論在哪個平臺上,我們呼叫的都是start0方法,由jvm根據不同的作業系統,去呼叫相應系統底層方法,幫我們真正地啟動一個執行緒,因此這就像是jvm為我們提供了一個可以作業系統底層方法的介面,即JNI,java本地介面,

在深入查看start0()方法之前,我們先實作一個自己的JNI方法,這樣才能更好地理解start0()方法是如何呼叫到系統層面的native方法,

首先我們先定義一個簡單的java類

package cn.tera.jni;

public class JniTest {
    public native void jniHello();

    public static void main(String[] args) {
        JniTest jni = new JniTest();
        jni.jniHello();
    }
}

在這個類中,我們定義了一個jniHello的native方法,然后在main方法中對其進行呼叫,

接著我們呼叫javac命令將其編譯成一個class檔案,但和平時不同,我們需要加一個-h引數,生成一個頭檔案

javac -h . JniTest.java

注意-h后面有一個.,意思是生成的頭檔案,存放在當前目錄

這時我們可以看到在當前目錄下生成了2個新檔案

JniTest.class:JniTest類的位元組碼

cn_tera_jni_JniTest.h:.h頭檔案,這個檔案是C和C++中所需要用到的,其中定義了方法的引數、回傳型別等,但不包含實作,類似java中的介面,而java代碼正是通過這個“介面”找到真正需要執行的方法,

我們查看該.h檔案,其中就包含了jniHello方法的定義,當然需要注意到的是,這里的方法名和.h檔案本身的命名是jni根據我們類的包名和類名確定出來的,不能修改,

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class cn_tera_jni_JniTest */

#ifndef _Included_cn_tera_jni_JniTest
#define _Included_cn_tera_jni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     cn_tera_jni_JniTest
 * Method:    jniHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

既然我們有了.h頭檔案,那么自然需要.c或者.cpp的定義實際執行內容的檔案,即介面的實作,

我們希望該方法簡單地輸出一個"hello jni",于是定義如下方法,并將其保存在cn_tera_jni_JniTest.c檔案中(這里檔案名不需要一致,不過為了可維護性,我們應當定義一致)

#include "cn_tera_jni_JniTest.h"

JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello(JNIEnv *env, jobject c1){
    printf("hello jni\n");
}

在該檔案中,引入了之前生成.h檔案(類似于java指定了類實作了哪個介面),并且定義了簽名完全一致的Java_cn_tera_jni_JniTest_jniHello方法,此時我們已經有了“介面”和“實作”,接著生成元件即可,

Mac系統運行命令:

gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include cn_tera_jni_JniTest.c -o libJniTest.jnilib 

Linux系統運行命令:

gcc -shared -I /usr/lib/jdk1.8.0_241/include cn_tera_jni_JniTest.c -o libJniTest.so

-dynamiclib、-shared:表示我們需要生成一個元件

-I:之前在.h頭檔案中我們需要引入jni.h,而該檔案位與jdk的目錄下,這里-I就是include的意思

-o:表示輸出的檔案

? 在Mac系統下,鏈接庫的擴展名為jnilib,命名的格式為libXXX.jnilib

? 在Linux系統下,鏈接庫擴展名為so,命名格式為libXXX.so

? 其中的XXX是在運行時加載動態庫時用到的名字

此時在目錄下就會多出一個libJniTest.jnilib或者libJniTest.so的元件,

最后我們回到一開始的java檔案中,引入該庫即可,修改JniTest.java

package cn.tera.jni;

public class JniTest {
    static {
        //設定查找路徑為當前專案路徑
        System.setProperty("java.library.path", ".");
        //加載動態庫的名稱
        System.loadLibrary("JniTest");
    }

    public native void jniHello();

    public static void main(String[] args) {
        JniTest jni = new JniTest();
        jni.jniHello();
    }
}

重新編譯.class檔案,記得將其放到./cn/tera/jni目錄下(包名是啥,目錄就是啥),然后執行即可,

java cn.tera.jni.JniTest
hello jni

此時我們先總結一下JNI的基本使用順序

1)在.java檔案中定義native方法

2)生成相應的.h頭檔案(即介面)

3)撰寫相應的.c或.cpp檔案(即實作)

4)將介面和實作鏈接到一起,生成元件

5)在.java中引入該庫,即可呼叫native方法

2.Thread創建執行緒的底層呼叫分析

了解了jni的基本使用流程之后,我們回到Thread的start0方法

為了探究start0()方法的原理,自然需要看看jvm在幕后為我們做了什么,

首先我們需要下載jdk和jvm的原始碼,因為openjdk和oraclejdk差別很小,而openjdk是開源的,所以我們以openjdk的代碼為參考,版本是jdk8

下載地址:http://hg.openjdk.java.net/jdk8

因為C和C++的代碼對于java程式員來說比較晦澀難懂,所以在下方展示原始碼的時候我只會貼出我們關心的重點代碼,其余的部分就省略了

在jdk原始碼的目錄src/java.base/share/native/libjava目錄下能看到Thread.c檔案,對應的是jni中的“實作”

#include "jni.h"
#include "jvm.h"

#include "java_lang_Thread.h"
...
static JNINativeMethod methods[] = {
    {"start0",           "()V",        (void *)&JVM_StartThread},
    ...
};
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

按照之前我們自己定義的jni實作,該檔案中應當有一個Java_java_lang_Thread_start0的方法定義,然而其中實際上只有一個Java_java_lang_Thread_registerNatives的方法定義,對應的正是Thread.java中的registerNatives方法:

class Thread implements Runnable {
    private static native void registerNatives();
    static {
        registerNatives();
    }
    ...
}

由此我們可以發現,Thread類在實作jni的時候并非是將每一個native方法都直接定義在自己的頭檔案中,而是通過一個registerNatives方法動態注冊的,而注冊所需要的資訊都被定義在了methods陣列中,包括方法名、方法簽名和介面方法,介面方法的定義被統一放到了jvm.h中(#include "jvm.h"),這個時候該jni介面方法的名字就不再受到固定格式限制了,這個機制以后用單獨的文章來解釋,現在先關心Thread的本質,

接下去我會按照呼叫鏈從上至下的順序列出檔案和方法

1)jvm.h,hotspot目錄src/share/vm/prims

既然start0方法的介面方法被定義在jvm.h中,那么我們先查看jvm.h,就可以找到JVM_StartThread的定義了:

JNIEXPORT void JNICALL
JVM_StartThread(JNIEnv *env, jobject thread);

2)jvm.cpp,hotspot目錄src/share/vm/prims

接著我們查看jvm.cpp,這里能看到JVM_StartThread的具體實作,關鍵點是通過創建一個JavaThread類創建執行緒,注意這里JavaThread是C++級別的執行緒:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;
  bool throw_illegal_thread_state = false;

  {
      ...
      /**
       * 創建一個C++級別的執行緒
       */
      native_thread = new JavaThread(&thread_entry, sz);
      ...
  }
  ...
JVM_END

3)thread.cpp,hotspot目錄src/share/vm/runtime

查看thread.cpp,可以看到JavaThread的建構式,其中創建了一個系統執行緒:

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                       Thread()
{
  ...
  /**
   * 創建系統執行緒
   */
  os::create_thread(this, thr_type, stack_sz);
}

4)os_linux.cpp,hotspot目錄src/os/linux/vm

我們能在hotspot原始碼目錄的src/os下找到不同系統的方法,我們以linux系統為例,

查看os_linux.cpp,找到create_thread方法:

bool os::create_thread(Thread* thread, ThreadType thr_type,
                       size_t req_stack_size) {
    ...
    pthread_t tid;
    int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
    ...
}

這個pthread_create方法就是最終創建系統執行緒的底層方法

因此java執行緒start方法的本質其實就是通過jni機制,最終呼叫系統底層的pthread_create方法,創建了一個系統執行緒,因此java執行緒和系統執行緒是一個一對一的關系

3.系統執行緒的使用

接著我們來簡單使用一下這個創建執行緒的方法,創建如下的.c檔案,在main方法中創建一個執行緒,并讓2個執行緒不斷列印一些文案

#include <pthread.h>
#include <stdio.h>

pthread_t pid;

void* thread_entity(void* arg){
    while (1) {
        printf("i am thread\n");
    }
}

int main(){
    pthread_create(&pid,NULL,thread_entity,NULL);
    while (1) {
        printf("i am main\n");
    }
    return 1;
}

編譯該檔案

gcc threaddemo.c -o threaddemo.out

-o:編譯后的執行檔案為threaddemo.out

運行該out檔案后就能看到2個文案在不斷重復列印了,也就是成功通過pthread_create方法創建了一個系統級別的執行緒,

4.Thread中run方法的回呼分析

到這里我們的探究并沒有結束,在java的Thread類中,我們會傳入一個執行我們指定任務的Runnable物件,在Thread的run()方法中呼叫,當java通過jni呼叫到pthread_create創建完系統執行緒后,又要如何回呼java中的run方法呢?

前面的探究我們是從java層開始,從上往下找,此時我們要反過來,從下往上找了,

1)pthread_create

先看pthread_create方法本身,它接收4個引數,其中第三個引數start_routine是系統執行緒創建后需要執行的方法,就像前面我們創建的簡單示例中的thread_entity,而第四個引數argstart_routine方法需要的引數

pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

2)os_linux.cpp

查看create_thread方法中呼叫pthread_create的代碼,可以看到thread_native_entry就是系統執行緒所執行的方法,而thread則是傳遞給thread_native_entry的引數:

int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);

查看thread_native_entry方法,它獲取的引數正是一個Thread,并呼叫其run()方法,注意這個Thread是C++級別的執行緒,來自于pthread_create方法的第4個引數:

static void *thread_native_entry(Thread *thread) {
  ...
  // call one more level start routine
  thread->run();
  ...
  return 0;
}

3)thread.cpp

查看JavaThread::run()方法,其主要的執行內容在thread_main_inner方法中:

void JavaThread::run() {
  /**
   * 主要的執行內容
   */
  thread_main_inner();
}

查看JavaThread::thread_main_inner()方法,其內部通過entry_point執行回呼:

void JavaThread::thread_main_inner() {
  ...
  /**
   * 呼叫entry_point,執行外部傳入的方法,注意這里的第一個引數是this
   * 即JavaThread物件本身,后面會看到該方法的定義
   */
  this->entry_point()(this, this);
  ...
}

查看JavaThread::JavaThread建構式,可以看到這里的entry_point是從外部傳入的

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
                       Thread()
{
  ...
  set_entry_point(entry_point);
  ...
}

4)jvm.cpp

查看JVM_StartThread方法,可以看到傳給JavaThread的entry_pointthread_entry

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
  JVMWrapper("JVM_StartThread");
  JavaThread *native_thread = NULL;
  bool throw_illegal_thread_state = false;

  {
      ...
      /**
       * 傳給建構式的entry_point是thread_entry
       */
      native_thread = new JavaThread(&thread_entry, sz);
      ...
  }
  ...
JVM_END

查看thread_entry,其中呼叫了JavaCalls::call_virtual去回呼java級別的方法,其實看到它的方法簽名就能猜到個大概了

static void thread_entry(JavaThread* thread, TRAPS) {
  HandleMark hm(THREAD);
  /**
   * obj正是根據thread物件獲取到的,JavaThread在呼叫時會傳入this
   */
  Handle obj(THREAD, thread->threadObj());
  /**
   * 回傳結果是void
   */
  JavaValue result(T_VOID);
  /**
   * 回呼java級別的方法
   */
  JavaCalls::call_virtual(&result,//回傳物件
                          //實體物件
                          obj,
                          //類
                          KlassHandle(THREAD, SystemDictionary::Thread_klass()),
                          //方法名
                          vmSymbols::run_method_name(),
                          //方法簽名
                          vmSymbols::void_method_signature(),
                          THREAD);
}

5)vmSymbols.hpp,hotspot目錄src/share/vm/classfiles

我們查看獲取方法名run_method_name和方法簽名void_method_signature的部分,可以看到正是獲取一個方法名為run,且不獲取任何引數,回傳值為void的方法:

template(run_method_name,                           "run")
...
template(void_method_signature,                     "()V")

于是系統執行緒就能成功地回呼java級別的run方法了!

這里我整理了一下Thread的start0方法的呼叫上下游關系,方便大家整體把握

Thread.java

-------->jvm.cpp

? -------->thread.cpp

? -------->os_linux.cpp

? -------->pthread_create

5.實作一個jni的回呼

最后我們嘗試自己實作一個簡單的方法回呼,

修改一開始的JniTest.java,新增一個回呼方法:

package cn.tera.jni;

public class JniTest {
    static {
        //設定查找路徑為當前專案路徑
        System.setProperty("java.library.path", ".");
        //加載動態庫的名稱
        System.loadLibrary("JniTest");
    }

    public native void jniHello();
    
    //新增一個回呼方法
    public void callBack(){
        System.out.println("this is call back");
    }

    public static void main(String[] args) {
        JniTest jni = new JniTest();
        jni.jniHello();
    }
}

修改cn_tera_jni_JniTest.c檔案,原先只是簡單輸出一個文案,現在改為回呼java方法,可以看到這個流程和java中的反射機制非常相似:

#include "cn_tera_jni_JniTest.h"

JNIEXPORT void JNICALL Java_cn_tera_jni_JniTest_jniHello(JNIEnv *env, jobject c1){
    //獲取類資訊
    jclass thisClass = (*env)->GetObjectClass(env, c1);
    //根據方法名和簽名獲取方法的id
    jmethodID midCallBack = (*env)->GetMethodID(env, thisClass, "callback", "()V");
    //呼叫方法
    (*env)->CallVoidMethod(env, c1, midCallBack);
}

重新生成元件、編譯.class檔案、運行:

gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home/include cn_tera_jni_JniTest.c -o libJniTest.jnilib
javac JniTest.java
java cn.tera.jni.JniTest

成功得到輸出結果:

this is call back

當然,對于有引數的、有回傳結果的回呼等,jni也提供了不同的呼叫方法,這個就不在本文中展開了,有興趣的同學可以自己去看下jni.h檔案

還要提一點,上面展示的回呼只是最基本的使用,而jvm中的官方回呼方法,因為涉及到了java的父類繼承關系、方法句柄、vtable等等內容,這里也就不展開了,同學們自己研究吧

最后,總結一下本文的內容

1.實作一個jni只需要4個東西,.java檔案,.h頭檔案(相當于介面),.c或.cpp檔案(相當于實作),生成的元件,

2.java的Thread是通過jni機制最終呼叫到了系統底層的pthread_create方法創建執行緒的,

3.Thread的jni呼叫鏈:Thread.java->jvm.cpp->thread.cpp->os_linux.cpp->pthread_create

4.jni也可以回呼java方法,從呼叫到回呼完成了一個demo

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/205042.html

標籤:Java

上一篇:Java自學入門精選教程-適合零基礎

下一篇:springboot 配置日志 列印不出來sql

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more