前言
- 并發(fā)怎燥,這是一個值得深思的話題瘫筐。它似無形卻有形。我們平常的工作都是面向業(yè)務編程铐姚,CRUD居多策肝,基本上與并發(fā)沒什么交集。ok隐绵,并發(fā)是一個廣泛的概念之众。那么咱們來聊聊多線程(java 多線程)。這里咱們來思考下問題:為什么要使用多線程依许?俗話說棺禾,一方有難八方支援。在今年的疫情初期峭跳,武漢的疫情非常嚴峻膘婶,如果僅靠武漢的白衣天使來醫(yī)治病患,這無疑是一個長征項目蛀醉,這就等同于單線程在干活悬襟。于是一批批來自于五湖四海的白衣天使前往武漢進行支援(點贊!)拯刁,此時就是多線程在協同工作脊岳。是的,你沒想錯,使用多線程的就是為了加快程序運行速度割捅。換句話來說奶躯,就是提高cpu利用率。如果把國家的每個行政區(qū)比作cpu的一個核棺牧,那么咱們國家就是一個34核的cpu巫糙。試問下,一個核和34個核的處理速度颊乘,這不用我說参淹,大家都懂吧!
- 上述的描述乏悄,可以得出一個結論:java線程與操作系統(tǒng)是等價的浙值。接下來,咱們來證明下此結論
一檩小、證明java線程等價于os的線程
- 為了能正常的看出效果开呐,我選擇window系統(tǒng)的任務管理器來證明這個結論
- 編寫如下代碼(InitThread.java):
public class InitThread { public static void main(String[] args) { for (int i = 0; i < 100; i++) { new Thread(() -> { try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } } }
-
執(zhí)行代碼之前,打開任務管理器规求,并查看線程數筐付,如下圖所示:
-
運行上述java類,然后再次查看任務管理器
-
綜上阻肿,可以看出瓦戚,我們使用java創(chuàng)建出一個線程與os中創(chuàng)建出一個線程是對等的。因此可以判斷丛塌,
java創(chuàng)建線程時與os函數進行了交互
较解。于是,我們來看下Thread類的start方法 - java.lang.Thread#start
// 上面部分代碼省略 boolean started = false; try { // 調用了此方法開啟了線程 start0(); started = true; } finally { // finally部分代碼省略 }
- java.lang.Thread#start0
// start0方法時一個native方法 private native void start0();
- 可以看到java調用Thread的start方法啟動一個線程時赴邻,最終會調用到start0方法印衔。而start0是native方法,何為native方法姥敛?什么是native方法呢奸焙?為了解釋native方法,這里要描述下java的發(fā)展歷史彤敛,很久很久以前忿偷,c語言很流行,所有的程序基本上都是c寫的臊泌。在1995年,Java誕生了揍拆,它以不需要手動釋放內存的特性深受程序員歡迎渠概,java的開發(fā)團隊為了解決java與c的通訊問題,所以使用c/c++寫出了jvm。jvm在java中起到了非常大的作用播揪,包括垃圾回收器贮喧、java與os的交互、與c語言的交互等等猪狈。native方法就是對應的一個c語言文件后java在調用它時是通過jvm來交互的
二箱沦、使用自定義native方法開啟一個線程
2.1 使用os函數開啟一個線程
ps:此時我選擇的os為centos7 64位的os(擁有c語言編譯環(huán)境)
-
第一步:查看os創(chuàng)建線程的api
#1. 安裝man命令 => 為了查看函數信息 yum install man-pages #2. 執(zhí)行如下命令查看os創(chuàng)建線程api,具體內容查看下圖 man pthread_create
-
第二步:使用os的api(pthread_create)創(chuàng)建一個線程
1.撰寫myThread.c文件
```c
#include "pthread.h" //頭文件雇庙,在pthread_create方法中有明確寫到
#include "stdio.h"pthread_t pid; // 定義一個變量谓形,用來存儲生成的線程id, 在pthread_create方法中也有介紹
/**
- 定義主體函數
/
void run(void* arg) {
while(1) {
printf("\n Execting run function \n");
printf(arg);
sleep(1);
}
}
/**
- 若要編譯成可執(zhí)行文件,則需要寫main方法
*/
int main() {
pthread_create(&pid, NULL, run, "123"); // 調用os創(chuàng)建線程api
while(1) { // 這里必須要寫個死循環(huán)疆前,因為c程序在main方法執(zhí)行結束后寒跳,它內部開的子線程也會關掉
}
}
> 2.編譯c文件成可執(zhí)行命令 ```shell # -pthread參數表示把pthread類庫也添加到編譯范圍 gcc -o myThread myThread.c -pthread
- 定義主體函數
-
第三步:運行并查看結果
運行編譯后的c文件
shell ./myThread
運行結果:
綜上,咱們已經使用os函數啟動了一個線程
2.2 使用java調用自定義的native方法啟動線程
- 第一步:創(chuàng)建
ExecMyNativeMethod.java
類(不用指定在哪個包下竹椒,因為最終要把它放在linux中去執(zhí)行)public class ExecMyNativeMethod { /** * 加載本地方法類庫童太,注意這個名字,后面會用到 */ static { System.loadLibrary("MyNative"); } public static void main(String[] args) { ExecMyNativeMethod execMyNativeMethod = new ExecMyNativeMethod(); execMyNativeMethod.start0(); } private native void start0(); }
- 第二步:將java類編譯成class文件
javac ExecMyNativeMethod.java
- 第三步:將class文件轉成c語言頭文件
javah ExecMyNativeMethod
- 第四步:查看編譯后的頭文件
對于上述內容胸完,我們只需要關注我們定義的native方法(/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class ExecMyNativeMethod */ #ifndef _Included_ExecMyNativeMethod #define _Included_ExecMyNativeMethod #ifdef __cplusplus extern "C" { #endif /* * Class: ExecMyNativeMethod * Method: start0 * Signature: ()V */ JNIEXPORT void JNICALL Java_ExecMyNativeMethod_start0 (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif
JNIEXPORT void JNICALL Java_ExecMyNativeMethod_start0 (JNIEnv *, jobject);
)即可书释,也就是說native方法轉成c語言頭文件后會變成JNIEXPORT void JNICALL Java_類名_native方法名 (JNIEnv *, jobject);
的格式 - 第五步:更新我們剛剛編寫的
myThread.c
文件,為了不造成影響赊窥,我們使用cp命令創(chuàng)建出一個新的c文件myThreadNew.ccp myThread.c myThreadNew.c
- 第六步:修改myThreadNew.c文件為如下內容
#include "pthread.h" // 引用線程的頭文件爆惧,在pthread_create方法中有明確寫到 #include "stdio.h" #include "ExecMyNativeMethod.h" // 將自定義的頭文件導入 pthread_t pid; // 定義一個變量,用來存儲生成的線程id, 在pthread_create方法中也有介紹 /** * 定義主體函數 */ void* run(void* arg) { while(1) { printf("\n Execting run function \n"); printf(arg); sleep(1); } } /** * 此方法就是后面java要調用到的native方法 */ JNIEXPORT void JNICALL Java_ExecMyNativeMethod_start0(JNIEnv *env, jobject c1) { pthread_create(&pid, NULL, run, "Creating thread from java application"); // 調用os創(chuàng)建線程api while(1) {} // 死循環(huán)等待 } /** * 每個要執(zhí)行的c文件都要寫main方法誓琼, * 如果要編譯成動態(tài)鏈接庫检激,則不需要 */ int main() { return 0; }
- 第七步:執(zhí)行如下命令將
myThreadNew.c
文件編譯成動態(tài)鏈接庫,并添加到環(huán)境變量中(否則在啟動java類的main方法時腹侣,在靜態(tài)代碼塊中找不到myNative
類庫)# 1. 編譯成動態(tài)鏈接庫 # 說明下-I后面的參數: 分別指定jdk安裝目錄的include文件夾和include/linux文件夾 # 因為我在環(huán)境變量中配置了JAVA_HOME叔收,所以我直接$JAVA_HOME了 # 后面的libMyNative.so文件,它的格式為lib{xxx}.so # 其中{xxx}為類中System.loadLibrary("yyyy")代碼中yyyy的值 gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -fPIC -shared -o libMyNative.so myThreadNew.c # 2. 將此動態(tài)鏈接庫添加到環(huán)境變量中 # 格式: export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:{libxxxx.so} # 其中{libxxxxNative.so}為動態(tài)鏈接庫的路徑, # 我的libMyNative.so文件在/root/workspace文件夾下 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/root/workspace/libMyNative.so
- 第八步:執(zhí)行如下命令啟動java程序
java ExecMyNativeMethod
- 查看運行結果
綜上傲隶,我們僅僅是使用java調用了自己編寫的native方法啟動了線程饺律。如果我們要和java一樣,自己寫一個run方法跺株,然后啟動線程時复濒,來調用這個run方法的話,要怎么實現呢乒省?別急巧颈,往下看!
2.3 native方法回調java方法
- 第一步:優(yōu)化我們的
ExecMyNativeMethod.java
類袖扛,新增run方法砸泛,具體如下:public class ExecMyNativeMethod { /** * 加載本地方法類庫十籍,注意這個名字,后面會用到 */ static { System.loadLibrary("MyNative"); } public static void main(String[] args) { ExecMyNativeMethod execMyNativeMethod = new ExecMyNativeMethod(); execMyNativeMethod.start0(); } private native void start0(); public void run() { System.out.println("I'm run method.........."); } }
- 第二步:修改上述的
myThreadNew.c
文件為如下內容(用到了JNI
唇礁,這個c文件在jdk的安裝目錄中可以找到勾栗,所以這是jdk提供的功能):#include "stdio.h" #include "ExecMyNativeMethod.h" // 將自定義的頭文件導入 #include "jni.h" /** * 此方法就是后面java要調用到的native方法 */ JNIEXPORT void JNICALL Java_ExecMyNativeMethod_start0(JNIEnv *env, jobject c1) { jclass cls = (*env)->FindClass(env, "ExecMyNativeMethod"); if (cls == NULL) { printf("Not found class!"); return; } jmethodID cid = (*env)->GetMethodID(env, cls, "<init>", "()V"); if (cid == NULL) { printf("Not found constructor!"); return; } jobject obj = (*env)->NewObject(env, cls, cid); if (obj == NULL) { printf("Init object failed!"); return; } jmethodID rid = (*env)->GetMethodID(env, cls, "run", "()V"); jint ret = (*env)->CallIntMethod(env, obj, rid, NULL); printf("Finished!"); }
- 第三步:將
myThreadNew.c
文件編譯成動態(tài)鏈接庫
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -fPIC -shared -o libMyNative.so myThreadNew.c
- 第四步:編譯java類并執(zhí)行它
javac ExecMyNativeMethod.java java ExecMyNativeMethod
-
查看運行結果:
2.4 額外總結
- 關于用戶態(tài)和內核態(tài)。咱們把它理解成兩個角色盏筐。用戶態(tài)理解成普通用戶围俘。內核態(tài)理解成超級管理員。當普通用戶要使用超級管理員的權限時琢融,需要有一個普通用戶轉化為超級管理員的過程界牡。即所說的用戶態(tài)轉內核態(tài)。大家可以想象下吏奸,在ubuntu系統(tǒng)下欢揖,我們的一個普通用戶要使用管理員的權限是不是要在命令前面添加
sudo
命令?這也是一個轉化奋蔚。
三她混、總結
- 綜上,咱們了解了java線程與os的關系泊碑,以及模擬了java調用os函數創(chuàng)建線程的流程坤按。
- 最近在學習并發(fā)相關的知識點, 后續(xù)將會繼續(xù)更新窘拯。下篇文章主題為:
synchronized關鍵字常見api其屏、初始對象頭以及證明hashcode
。 - 并發(fā)模塊對應github地址:傳送門
- I am a slow walker, but I never walk backwards.