最近因为项目中使用到了帧动画。众所周知,帧动画极耗资源,如果一旦涉及到的图片比较多,很容易OOM。
因此为了解决这个问题,采用了自定义SurfaceView,并在里面使用子线程逐帧绘制bitmap的方式来实现。由于这个Surface所要播放的图片list是可以被设置的。也就是说可能上一个动画还没播放完,我就设置了一个新的图片list进来。这样就需要停止掉旧的动画线程,并开启一个新的线程来播放新的动画。于是就涉及到了标题中的这个问题:java如何优雅的停止一个原生Thread线程。
大家都知道Thread类中有一个stop方法可以用来停止线程。那么我们可以直接使用这个方法吗?答案是不可以。因为stop方法已经被官方明确废弃。主要原因就是因为这个方法实在是太简单粗暴了。会造成至少以下几个问题:
(1)stop方法会破坏线程操作的原子性。比如我们对一个线程操作要求是同步的,因为可能在后面有几个很重要的方法必须执行,又或者必须要释放一些资源。这时调用stop方法停止线程,他根本不管什么同不同步,这一套流程有没有完整走完。直接给你停止。
基于以上原因,你现在再去调用stop方法,会直接收到一个UnsupportedOperationException的异常。
既然stop方法不能用。那有没有什么别的办法可以做到呢?有朋友记起Thread类里面有一个interrupt方法,interrupt不就是中断么?那我调用这个方法是不是就可以了。
这里先直接上3点明确的结论吧:
(1)这个方法名起得很具有迷惑性。
(2)这个方法其实本身无法停止一个线程。
(3)有时候我们写的线程因为这个方法停止了,其实并非如此,停止这个线程的根本原因是异常。
接下来上两个例子:
(1)
private Thread mThread;
private boolean mIsRunning = true;
private void startThread(){
mThread = new Thread(
new Runnable() {
@Override
public void run() {
int i = 0;
try {
while(mIsRunning) {
Log.d(
"MyThreadDemo",
String.valueOf(i)
);
mThread.sleep(1000);
i++;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
mustDoThings();
}
}
}
);
mThread.start();
}
private void interruptThread(){
if(mThread!=null){
mThread.interrupt();
}
}
private void mustDoThings(){
Log.d("MyThreadDemo","必须要执行的代码");
}
我们先调用startThread方法开启一个线程,待线程跑一会。之后再调用interruptThread方法。看一下Log日志:
06-27 17:56:18.762 2480-2935/com.wm.mythreaddemo D/MyThreadDemo: 4
06-27 17:56:19.763 2480-2935/com.wm.mythreaddemo D/MyThreadDemo: 5
06-27 17:56:20.764 2480-2935/com.wm.mythreaddemo D/MyThreadDemo: 6
06-27 17:56:21.764 2480-2935/com.wm.mythreaddemo D/MyThreadDemo: 7
06-27 17:56:22.765 2480-2935/com.wm.mythreaddemo D/MyThreadDemo: 8
06-27 17:56:23.766 2480-2935/com.wm.mythreaddemo D/MyThreadDemo: 9
06-27 17:56:24.766 2480-2935/com.wm.mythreaddemo D/MyThreadDemo: 10
06-27 17:56:25.433 2480-2935/com.wm.mythreaddemo W/System.err: java.lang.InterruptedException
at java.lang.Thread.sleep(Native Method)
06-27 17:56:25.434 2480-2935/com.wm.mythreaddemo W/System.err: at java.lang.Thread.sleep(Thread.java:1031)
at java.lang.Thread.sleep(Thread.java:985)
at com.wm.mythreaddemo.MainActivity$1.run(MainActivity.java:74)
at java.lang.Thread.run(Thread.java:818)
06-27 17:56:25.434 2480-2935/com.wm.mythreaddemo D/MyThreadDemo: 必须要执行的代码
你看线程这不是停止了么?而且finally里面必须调用的代码mustDoThings方法也成功调用了。但再仔细看看就可以发现,在finally之前,有一个InterruptedException被抛出。所以,这次线程的停止和mustDoThings方法的调用完全是因为这个异常。
private Thread mThread;
private boolean mIsRunning = true;
private void startThread(){
mThread = new Thread(
new Runnable() {
@Override
public void run() {
int i = 0;
while(mIsRunning) {
Log.d(
"MyThreadDemo",
String.valueOf(i)
);
i++;
}
}
}
);
mThread.start();
}
private void interruptThread(){
if(mThread!=null){
mThread.interrupt();
}
}
我们还是先startThread,再interruptThread。会发现,这次怎么调用interruptThread方法你都停不了这个线程。
其实interrupt方法只是设置了一个停止线程的标识位。如果线程处于阻塞状态,调用interrupt方法会抛出InterruptException异常,并且清除停止线程的标识位。
既然知道了stop不能用,interrupt方法又不能直接停止一个线程。停止线程的正确姿势是什么?这里提供比较常用的两种:
(1)自己维护停止线程的标识位
private Thread mThread;
private boolean mIsRunning = true;
private void startThread(){
mThread = new Thread(
new Runnable() {
@Override
public void run() {
int i = 0;
while(mIsRunning){
Log.d(
"MyThreadDemo",
String.valueOf(i)
);
try {
mThread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
mustDoThings();
}
}
);
mThread.start();
}
private void stopBoolThread(){
mIsRunning = false;
}
private void mustDoThings(){
Log.d("MyThreadDemo","必须要执行的代码");
}
这就是完全自己维护一个标识位来控制while循环是否需要继续执行。如果不再继续,在while之后进行必要操作的处理。
(2)利用interrupt方法设置的标识位
既然Thread提供了一个设置标识位,那么我们可以利用这个方法来实现。Thread还提供了一个isInterrupted方法来获取这个标识位。
private Thread mThread;
private void startThread(){
mThread = new Thread(
new Runnable() {
@Override
public void run() {
int i = 0;
try {
while(!mThread.isInterrupted()){
Log.d(
"MyThreadDemo",
String.valueOf(i)
);
mThread.sleep(1000);
i++;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mustDoThings();
}
}
}
);
mThread.start();
}
private void interruptThread(){
if(mThread!=null){
mThread.interrupt();
}
}
一定要注意一下try-catch的包裹范围问题。因为就像之前所说的,如果你对一个阻塞的线程调用interrupt方法,他会清除停止线程的标识位,即会让这个标识位变成false。如果你写了以下代码,通过interrupt方法你是停止不了这个线程的。
一个错误的演示:
private void startThread(){
mThread = new Thread(
new Runnable() {
@Override
public void run() {
int i = 0;
while(!mThread.isInterrupted()){
Log.d(
"MyThreadDemo",
String.valueOf(i)
);
try {
mThread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
i++;
}
mustDoThings();
}
}
);
mThread.start();
}