在Android系统中,如果用户界面失去响应超过5s之后,系统会提示用户是否需要强制关闭app。因此,当需要在程序中做一些好事操作时(如网络连接,下载文件等),最好另开一个线程处理耗时操作。Android中采用Java的方法建立和使用线程。
线程的开启
1,Runnable加Thread实现
首先创建一个类实现Runnable接口,或者直接创建一个Runnable对象。然后重写Runnable中的run()方法,run()方法中的代码就是线程执行的部分。
Runnable runnable = new Runnable() {
@Override
public void run() {
}
};
或者
class MyThread implements Runnable{
@Override
public void run() {
}
}
然后通过一个Thread对象将Runnable对象作为参数传递给他,
调用Thread对象的start方法即可开启线程。
new Thread(null,new MyThread(),"myThread").start();
2,Handler加Runnable方法实现线程开启
Android不允许在子线程中更新用户界面,更新UI界面的操作这能在主线程中进行
因为如果可以在子线程中更新UI组件,就会导致用户见面显示的内容处于不确定的状态,而这种不确定的情况可能会给用户
带来困惑。
但有时我们确实需要将线程中的运行结果呈现到用户界面上,这时就需要一个Handler对象来解决上述问题。
Handler是Android给我们提供用来更新UI的一套机制,是一套消息处理机制,可以通过它来发送消息和处理消息。
每一个线程可以一个MessageQueue,而一个MessageQueue通过一个Looper来获取其中的Message,每一个线程同时只能
处理一个Message。MessageQueue是一个消息队列,用于待处理的消息。looper是一个循环提取MessageQueue中消息的机制,
当MessageQueue中有Message时就读取,没有时就阻塞。而Handler就是往MessageQueue中添加,以及处理消息的对象。
主线程在一开始时就创建好了MessageQueue和Looper。
下面一张图展现了他们几个的关系。
那么如何通过Handler对象来开启线程呢?就需要用到Handler的post方法
调用Handler的Post()方法或者postDelayed()方法,启动另一个线程。
创建一个工作线程,实现 Runnable 接口,实现 run 方法,处理耗时操作
创建一个 handler,通过 handler.post/postDelay,投递创建的 Runnable,在 run 方法中进行更新 UI 操作。
Handler处理Runnable任务的常用方法
1)post: 立即启动Runnable任务
2)postDelayed:延迟若干时间后启动Runnable任务
3)postAtTime: 在指定事件启动Runnable任务
4)removeCallbacks:移除指定的Runnable任务
post本质上是将一个Runnable对象封装成一二Message然后传递到主线程的MessageQueue中,一代该消息被主线程的Looper取出,那么在其中的关于更新UI界面的操作就会在主线程中运行。
样例代码:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new Handler();
handler.post(new Runnable() {
@Override
public void run() {
//更新UI的操作
}
});
}
ps:此方法理论上同样可以将一些耗时操作post到主线程中运行,但是不推荐这样做,因为那样和直接在主线程中执行耗时操作没有任何区别。因此通常做法是当要
进行耗时操作时使用第一种方法开启线程,当需要在子线程中更新UI时将需要更新UI的部分post主线程中进行。记得我们老师说过一个准则“主线程不做耗时操作,
子线程不更新UI
线程的关闭
1,interrupt()
之前线程的关闭可以直接通过Thread的stop方法实现,但是该方法存在风险,已经被谷歌弃用。
其实只要将run中的代码执行完线程就会自动关闭,不需要人为的操作
但是有的时候我们在run中写的代码是死循环,根本执行不玩~~比如下面这个例子
开启一个线程,该线程中run是一个死循环不停的做+1,-1的操作,这个时候可以通过Thread的interrupt将线程停下。
public class MainActivity extends AppCompatActivity {
private Thread thread;
private Button buttonStart;
private Button buttonStop;
int i = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
buttonStart = findViewById(R.id.button);
buttonStop = findViewById(R.id.button2);
buttonStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thread = new Thread(runnable);
thread.start();
Log.d("myTag", "onClick:线程开启 ");
}
});
buttonStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
thread.interrupt(); //将线程的中断标志位置true
Log.d("myTag", "onClick:线程关闭 ");
}
});
}
Runnable runnable = new Runnable() {
@Override
public void run() {
while (!thread.isInterrupted()){ //获取当前线程的中断标志位,如果为true则中断,如果为false则继续执行
i++;
i--;
}
}
};
}
运行结果
但是interrupt方法存在局限性,就是不能遇到Thread.sleep()。interrupt方法本质上是将线程的中断标志置为true,
当线程在执行sleep方法时会不停的检测线程的中断标志位,如果为true,会抛出InterruptException。
而Java中凡是抛出InterruptedException的方法,都会在抛异常的时候,将interrupt flag重新置为false,所以原本的
interrupt就没有一点作用了。
将Runnable中的run方法改成
Runnable runnable = new Runnable() {
@Override
public void run() {
while (!thread.isInterrupted()){
i++;
i--;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
Log.d("myTag", "onClick:线程关闭失败 ");
}
}
}
};
此时的运行结果为
而此时的线程其实并未关闭,然而开发过程中,死循环套sleep时线程操作最常用的情况。
那么这种情况该如何停止呢?
获取到InterruptException之后将线程的中断标志位重新置为true
Runnable runnable = new Runnable() {
@Override
public void run() {
while (!thread.isInterrupted()){
i++;
i--;
Log.d("myThread","线程正在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
};
这样就能保证线程被中断而不会继续执行了。
2,volatile关键字
通过一个volatile修饰的boolean变量作为while循环的条件,当需要将线程关闭时将boolean置为false
public class MainActivity extends AppCompatActivity {
private Thread thread;
private Button buttonStart;
private Button buttonStop;
int i = 0;
private volatile boolean flag = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
buttonStart = findViewById(R.id.button);
buttonStop = findViewById(R.id.button2);
buttonStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
flag = true;
thread = new Thread(runnable);
thread.start();
Log.d("myTag", "onClick:线程开启 ");
}
});
buttonStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
flag = false;
Log.d("myTag", "onClick:线程关闭 ");
}
});
}
Runnable runnable = new Runnable() {
@Override
public void run() {
while (flag){
i++;
i--;
Log.d("myThread","线程正在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
};
}
如果针对这种方法,似乎使用一个普通的boolean就可以实现进程的关闭。
那么为什么要使用一个volaltile修饰呢?
这就要涉及到线程的运行机制了!
在多线程的应用中多个线程对 非volatile 变量进行操作,线程在对它们进行操作的时候为了提高性能会将变量从主存复制到 cpu 缓存中。如果你的电脑包含的 cpu 不止一个, 那么每个线程可能会运行于不同的 cpu 上。这意味着,不同线程会将变量复制到不同 cpu 的缓存里。
非volatile 变量不能保证 Java 虚拟机(JVM)何时从主存中将数据读入cpu 缓存,也不能保证何时将数据从 cpu 缓存写入到主存中。
而经过volatile修饰的变量,每次读操作都会直接从计算机的主存中读取,而不是从 cpu 缓存中读取;同样,每次对 volatile 变量的写操作都会直接写入到主存中,而不仅仅写入到 cpu 缓存里。
是想一种情况:
一个变量flag控制了一个或者好几个线程的启停,只用主线程可以对该变量进行写操作,而其他子线程只有读操作。(其实上面讲的例子也是这样的)
变量保存在主存中,因为时普通变量所以各个线程所在的cup的cache中保存者该变量的副本。
此时主线程将flag置为false,想要关闭所有的子线程,会出现一下一种情况
1,主线程的CPUcache中的flag已经置为false,但是主线程还没有将其写入主存,此时其他几个线程读取到的还是flag原先的值,所以会继续执行。
2,主线程已将cache中的数据写入内存,但是其他的线程还没有从主存中读取flag更新自己cache中的flag副本,继续读取副本导致读取到的flag还是原先的true,导致线程继续执行。
只用当主线程将数据写入内存,并且其他子线程从内存中读取后并更新了自己cache中的副本之后,才会让线程停下来,这样会导致线程错误的执行好几次。
而volatile就是保证对变量的每次读都是从主存读的,每次写都是写入到内存为止,这样保证了每次线程读到的变量都是该变量“最新的”值。