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线程接口已经基本介绍完成,其他的诸如线程名、线程优先级和后台线程这类的属性相对比较容易,这里就不多做介绍了。