Java語言中使用java.lang.Thread類代表線程對象,它繼承了Object類型,並且實現了java.lang.Runnable接口,在JVM調度運行Thread的用戶代碼時就調用Thread.run()方法。在Thread對象內部還包含一個Runnable target對象,如果target對象不爲空的話運行Thread.run()方法就會執行target.run()方法。
public class Thread implements Runnable {
private Runnable target;
// 其他代碼忽略
@Override
public void run() {
// 線程執行入口
if (target != null) {
target.run();
}
}
}
現在從最基本的線程創建和運行接口來學習Java線程編程接口。
創建與運行
創建線程必須要指定需要在新線程中執行的任務代碼,創建線程有兩種方式,一種是向Thread對象傳遞Runnable的target對象,另外一種就是創建Thread的子類並且覆蓋run()方法。
Thread thread = new Thread(new Runnable() {
public void run() {
System.out.println("I am first Thread!");
}
});
thread.start();
Thread thread2 = new Thread() {
public void run() {
System.out.println("I am first Thread!");
}
};
thread2.start();
指定了要在線程中的代碼後構造出Thread對象後調用它的start()方法線程就會被提交給JVM自動執行。需要注意的是線程的run()方法和start()方法之間的區別,如果直接調用run()相當於當前的任務還是在創建Thread對象的線程執行,並沒有開啓新的線程執行;調用start()方法纔會真正地創建線程執行單元並且併發的執行run()內的代碼。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": I am first Thread!");
}
});
thread.start(); // 執行start方法
thread.run(); // 執行run()方法
// ~執行結果
main: I am first Thread!
Thread-0: I am first Thread!
如果在線程start()啓動運行之後再次調用start()方法啓動該線程會出現什麼情況呢?
Exception in thread "main" Thread-0: I am first Thread!
java.lang.IllegalThreadStateException
at java.lang.Thread.start(Unknown Source)
at com.example.ThreadTest2.main(ThreadTest2.java:12)
從控制檯打印出來的日誌可以看到進程拋出了IllegalThreadStateException,這是因爲線程內部使用了狀態機管理當前線程的運行狀態,一旦用戶啓動過線程,之後的線程狀態變遷都由JVM來管理,不允許用戶再直接修改當前線程的狀態。查看JDK中Thread類的源代碼可以看到線程的狀態有以下幾種值:
NEW: Thread對象剛被創建,但是線程執行單元還沒有被創建
RUNNABLE: 線程執行單元被創建,隨時可以被運行
BLOCKED:線程正在等待進入監控器對象
WAITING: 線程進入監控器對象但是運行條件不滿足,執行wait()方法等待運行條件滿足
TIMED_WAITING:線程進入監控器對象但是運行條件不滿足,執行有限時間的等待
TERMINATED:線程執行結束
暫停與中斷
線程在運行時可能會發現一些資源不滿足條件,如果強行繼續運行就會導致邏輯錯誤甚至應用崩潰,這時就需要將當前的線程任務暫停下來,等到條件滿足的時候再繼續執行。查看條件是否滿足有兩種方式,一種時候輪詢也就是說線程每隔一段時間就查看一下當前的資源是否滿足,滿足就運行否則繼續等待;還有一種是通知,在資源不足的時候線程自動暫停等待,等到資源滿足條件別的線程通知當前線程條件滿足可以繼續運行。
sleep()方法能夠讓當前線程暫時休眠等待,用戶可以傳入等待的時間,等待時間過去之後線程會被喚醒繼續運行。
suspend()方法也能夠讓當前線程暫停,但是suspend方法可能會導致線程死鎖問題,目前已經被廢棄,不再推薦使用。
join()方法不會使當前線程執行被暫停,但會使調用了這個方法的線程被暫停直到當前線程執行完成。
Thread thread = new Thread() {
public void run() {
// thread中執行的代碼
System.out.println(Thread.currentThread().getName() + ": I am first Thread!");
}
};
// mainThread中執行的代碼
thread.start();
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ": I am first Thread!");
// ~ 執行結果
Thread-0: I am first Thread!
main: I am first Thread!
上面的例子裏thread.start()和thread.join()都是運行在main方法的線程裏,thread.join()方法會使main方法線程被暫停直到thread中執行的任務結束纔會返回。
Object對象包含了wait()/notify()/notifyAll()三個方法,它們都必須在獲取到監控器對象之後才能調用,如果線程任務的run()方法中調用wait()就會使當前線程主動放棄監控器對象的鎖並且暫停執行,等到別的線程準備好資源後通過調用notify()/notifyAll()通知當前線程資源就緒,當前線程就會從wait()方法中返回繼續執行。
public class ThreadTest4 {
public static int count = 0;
public static void main(String[] args) {
Object lock = new Object();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(lock) {
while (count < 5) {
try {
System.out.println("Waiting for count to be 5");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Arrive next step");
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
while (count < 5) {
for (int i = 0; i < 100000; i++) {
System.out.println("count = " + count + ", i = " + i);
}
count++;
if (count >= 5) {
synchronized(lock) {
System.out.println("Notify count to 5");
lock.notify();
}
}
}
}
});
thread1.start();
thread2.start();
}
}
前面講述了暫停線程通常是由於資源未就緒需要等待,假如用戶在線程暫停的過程中突然發現不需要再運行之前的任務了,這時就需要打斷線程的暫停狀態。interrupt()方法就能夠打斷sleep()、join()和wait()方法導致的線程暫停狀態,查看這三個方法的聲明可以看到它們都會拋出InterruptedException異常。
// Thread類的方法
public static void sleep(long millis, int nanos)
throws InterruptedException
public final void join() throws InterruptedException
// Object類的方法
public final native void wait(long millis, int nanos)
throws InterruptedException;
在interrupt()方法調用後如果線程由於上述三個方法導致的暫停,就會在調用上述三方法的地方拋出InterruptedException並繼續執行。不過在拋出異常後線程的isInterrupted()方法返回值會被置爲false,如果想保留之前的中斷狀態需要調用Thread.interrupted()方法重新設置中斷狀態。
Object lock = new Object();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized(lock) {
try {
System.out.println("Waiting...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
Waiting...
java.lang.InterruptedException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Unknown Source)
at com.example.ThreadTest5$1.run(ThreadTest5.java:12)
at java.lang.Thread.run(Unknown Source)
線程的運行、暫停、中斷都已經討論過了,接着看一下如何退出線程運行。在Thread類中有stop()方法能夠停止當前線程的運行,但是它和suspend()方法一樣都會導致多線程的死鎖問題,目前該方法已經被廢棄,不再推薦使用該方法。
每個線程創建的時候都會有指定的任務代碼,這些任務代碼在執行完成之後線程就自然終止了,這種線程終止方法很安全,通常建議使用這種退出運行方式。
異常處理
任務在資源數據正常的時候會安全執行完成,如果資源數據是有問題的,比如在計算除法運算時除數的值等於0,就會導致程序異常,拋出的異常如果沒有處理還會導致進程的整體退出。在Android應用開發過程中NullPointerException是很常見的運行時異常,如果線上應用在運行時拋出該異常就會導致Android應用直接退出。通常應用都會有異常上報功能,當出現這類異常時系統會記錄異常日誌,後面應用重啓就會上報異常日誌供開發人員參考修復。
Thread.setUncaughtExceptionHandler()方法可以設置當前線程執行過程中發生異常事件時的回調處理,但是該方法只會針對設置了處理方法的線程的異常。Thread.setDefaultUncaughtExceptionHandler()靜態方法會爲所有的線程都設置異常處理方法,不管是哪個線程執行過程中拋出的異常都會回調該方法設置的全局異常處理方法。
Android異常日誌
想要記錄Android應用在運行過程中拋出的運行時異常就必須修改默認的異常處理機制。Android系統在應用出現異常時會打開一個ForceClose的強制關閉對話框通知用戶應用出現錯誤,在用戶確認後就強制殺死異常應用。Android默認的異常處理的邏輯可以通過Thread.getDefaultUncaughtExceptionHandler()獲取得到,在記錄完異常日誌信息之後再調用Android系統默認異常處理邏輯。修改的動作需要放到Android最早執行的代碼處,也就是Application.onCreate()方法裏,首先自定義MyApplication類。
public class MyApplication extends Application {
private static final String TAG = "MyApplication";
@Override
public void onCreate() {
super.onCreate();
CrashLogManager.init(this);
final Thread.UncaughtExceptionHandler defaultHandler =
Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.e(TAG, e.getMessage());
CrashLogManager.getInstance().save(e);
RestartService.restart(getApplicationContext());
defaultHandler.uncaughtException(t, e);
}
});
CrashLogManager.getInstance().upload();
}
}
除了自定義Application類一定要記得到AndroidManifest.xml文件中的application節點,使用android:name屬性指定自定義的MyApplication創建應用對象,這樣前面增加的記錄異常日誌纔會被執行.
<application
....
android:name=".MyApplication">
....
/>
接着就是定義異常日誌記錄和上報管理對象,這裏爲了演示功能只是用了SharedPreferences來保存並且只能保存一條異常日誌,真正實現的時候可以使用數據保存日誌,用網絡請求框架上報日誌數據。
public class CrashLogManager {
private static final String TAG = "CrashLogManager";
private static final String CRASH_LOG = "crash_log";
private static final String KEY_CRASH_LOG = "key_crash_log";
private SharedPreferences preferences;
private static CrashLogManager crashLogManager;
private static Context sContext;
public static void init(Context context) {
sContext = context.getApplicationContext();
}
public CrashLogManager() {
preferences = sContext.getSharedPreferences(CRASH_LOG, Context.MODE_PRIVATE);
}
public synchronized static CrashLogManager getInstance() {
if (crashLogManager == null) {
crashLogManager = new CrashLogManager();
}
return crashLogManager;
}
public void save(Throwable e) {
StringBuilder stringBuilder = new StringBuilder();
String msg = e.getLocalizedMessage();
stringBuilder.append(msg).append("\n");
StackTraceElement[] stackTraceElement = e.getStackTrace();
for (StackTraceElement element : stackTraceElement) {
stringBuilder.append(element.toString()).append("\n");
}
/**
* 保存到數據庫,這裏使用preferences
*/
Log.e(TAG, "save: " + stringBuilder.toString());
preferences.edit().putString(KEY_CRASH_LOG, stringBuilder.toString()).commit();
}
public void upload() {
/**
* 從數據庫讀取保存的異常日誌,這裏使用preferences
*/
String log = preferences.getString(KEY_CRASH_LOG, "");
if (!TextUtils.isEmpty(log)) {
Log.e(TAG, "upload: " + log);
/**
* 上傳到網絡服務器
*/
}
}
}
線程的異常處理機制在Android應用開發中相當有用,除了前面提到的異常日誌記錄上報,還可以在應用異常退出情況下重新打開新的應用,最大限度的提高應用的留存度。
Android崩潰重啓
在應用崩潰的時候如果在本進程重新啓動MainActivity,隨後調用Android默認異常處理還是會導致進程被殺死,因此重啓應用的發起動作必須要在另外一個進程中執行。在Android應用中如果想在另外一個進程執行代碼最簡單的方式就是通過配置Service節點的android:process="com.example.remote"這種方式讓服務運行在另外一個進程中,之後通過startService方法向遠程Service傳遞要執行的動作和參數,Service在新進程中就能夠執行新應用的啓動操作。
public class RestartService extends IntentService {
public RestartService() {
super("RestartService");
}
public static void restart(Context context) {
Intent intent = new Intent(context, RestartService.class);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
Intent mainIntent = new Intent(getApplication(), MainActivity.class);
mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(mainIntent);
}
}
<service android:name=".RestartService"
android:process="com.example.remote">
</service>
到目前爲止Java線程接口已經基本介紹完成,其他的諸如線程名、線程優先級和後臺線程這類的屬性相對比較容易,這裏就不多做介紹了。