什么是多线程?
线程是cpu调度的最小单位。java中,一个虚拟机实例对应着一个进程,进程中有n个线程,java为每个进程分配一定的内存区域。
- 方法区
- 堆
- java栈
- 本地方法栈
- 程序计数器
其中,方法区和堆区是进程内所有线程所共享的,每个线程都有自己的程序计数器,java栈以及本地方法栈。
线程与进程之间的简要关系如上,借用马士宾老师一句最浅显易懂的话来形容线程就是:
线程是程序不同的执行分支。
线程同步问题
上文提到不同线程有各自的专属内存区域,包括java栈、本地方法栈、程序计数器,java栈中存放着一个个的栈帧,线程中执行的方法就对应着一个个栈帧。如果不同的线程同时写一个int,需要先将int加载到自己的java栈中再来写入,相当于不同的线程都在写int的副本,最后int的值为多少,实在无法控制,所以需要引入同步机制,只有获得同步锁对象后,才能写int,这样每个线程加载的int的副本值都是可确定的对象,数据写入才能安全。
正常使用同步锁,理解同步锁是什么,这对于理解线程同步问题有非常大的帮助。
private Object lock = new Object();
public void demo(){
synchronized (lock) {
/* 同步锁是lock */
}
}
public synchronized void demo2(){
/* 同步锁是类的实例 */
}
public static synchronized void demo3(){
/* 同步锁是类 */
}
线程状态说明
- NEW:线程已经创建,但还没有start
- RUNNABLE:线程处于可运行状态,一切就绪
- BLOCKED:线程处于阻塞状态,比如正在等待某个锁的释放
- WAITING:线程处于等待状态,调用wait方法导致
- TIMED_WAITING:等待特定的时间,一般指调用sleep方法
- TERMINATED:终止运行
BLOCKED状态,指还未获取到同步锁,仍在等待当中;WAITING是指调用wait方法,释放自己已经得到的同步锁,同时让自己处于等待状态当中;TIMED_WAITING一般是指调用sleep方法,线程睡了一段时间之后就继续运行。这也间接说明了sleep方法与wait方法的不同,sleep只是单纯让线程睡眠一段时间就立即重新执行,与同步毫无关系。
同步时常用方法说明
- wait:释放同步锁,使线程进入等待状态当中,等待同步锁对象再次调用notify或notifyAll方法唤醒线程,执行线程其余的逻辑
- notify:释放同步锁,唤醒其它wait的线程,常与wait搭配使用,进行线程间的通信
- join:等待调用线程结束,再来执行当前线程的内容
- sleep:单纯地让线程睡眠一段时间,时间一过,线程恢复成执行状态
- interrupt:顾名思义,打断线程,如果一个线程正被阻塞在wait上或者线程调用了join方法、sleep方法,此时调用interrupt方法,将清除线程的中断状态,并且抛出异常,可接收此异常消息退出线程等。
正确理解volatile
volatile,是一种并不安全的线程同步方式,但在一定程序上也是有效的。相信大家都在大学里上过计算机基础课程,cpu在执行命令时,先将数据从硬盘上加载到内存中,再将数据从内存中读入到寄存器中。cpu其实是在操纵寄存器当中的数据,在一定的时候再寄存器的数据写回到内存中。cpu并不会在操作数据前都重新从内存中加载到寄存器中,所以寄存器中的数据有可能是老旧的数据,这样也会导致程序异常。
public volatile static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(){
public void run() {
int i = 0;
while (!stop) {
i ++;
}
System.out.println("thread stop i = " + i);
}
};
thread.start();
Thread.sleep(1000);
stop = true;
System.out.println("main thread stop = " + stop);
thread.join();
}
请看上面的代码,如果不用volatile修饰变量stop,那么这个程序将永远无法停止。使用volatile修饰变量,在使用变量的时候将重新从内存中读取,这在一定程度上将能解决线程同步问题。如果是多线程同时写,使用volatile修饰依然无用。
handler原理
hander原理,网上已经有很多的详细解释了,本人就不再详细解释了,不过需要强调的一句是
如果不特别指定hander的Looper对象,那么hander还是在主线程运行,不要在主线程中做特别耗时的事。但hander确实是异步的。
hander不断向消息列队中发送消息,Looper执行死循环,从消息队列中取出消息,将消息交给handler处理执行。
class WorkThread extends Thread{
public void run() {
Looper.prepare();
Handler handler = new Handler(){
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
Looper.loop();
}
}
一般handler的使用如上,执行Looper.prepare()方法时,将创建消息队列,创建Looper对象,并将Looper对象保存在ThreadLocal中,ThreadLocal类似于map类的键值对容器,它的key很奇特,就是当前的线程。
public void set(T value) {
Thread currentThread = Thread.currentThread();
Values values = values(currentThread);
if (values == null) {
values = initializeValues(currentThread);
}
values.put(this, value);
}
handler将消息发送给与自己相关联的Looper对象,Looper对象在死循环中(执行loop方法)不停处理消息队列中的消息。请注意,Looper对象的loop方法是在与Looper对象关联的线程中执行的,至此,handler与线程关联起来,可以使用handler处理工作线程中的逻辑。
HandlerThread的源码较简单,类似上述Handler使用代码,不再详述。
AsyncTask使用
AsyncTask,是Android工作线程的一种实现,可以AsyncTask中处理复杂逻辑,并在主线程中更新UI,非常地方便。但人们在使用AsyncTask时常常存在着误区
如果存在多个AsyncTask实例,那么这些AsyncTask将同步执行,不会异常执行,必须等待上一个AsyncTask实例执行完毕,才能执行下一个AsyncTask实例。
本人第一次读AsyncTask源码时,非常惊讶,专门为Android定制的工作线程,居然是同步线性执行的。
AsyncTask中定义着两个线程池,一个负责调度,一个负责真正执行。调度线程池中有一个Runnable队列,调度线程池不停地从队列中取出Runnable对象,交给真正执行的线程池执行。
调度线程池:
private static class SerialExecutor implements Executor {
final ArrayDeque mTasks = new ArrayDeque();
Runnable mActive;
public synchronized void execute(final Runnable r) {
mTasks.offer(new Runnable() {
public void run() {
try {
r.run();
} finally {
scheduleNext();
}
}
});
if (mActive == null) {
scheduleNext();
}
}
protected synchronized void scheduleNext() {
if ((mActive = mTasks.poll()) != null) {
THREAD_POOL_EXECUTOR.execute(mActive);
}
}
}
mTasks是保存Runable对象的队列,在execute方法中,mTasks向自身添加Runable对象,并且将原本的Runable对象进行一定的修改,当前的Runable对象run方法执行完成后再从mTasks对象中取出第二个Runable对象来执行。
mActive 对象,在第一个AsyncTask启动时肯定为空,mTasks从队列中取值,赋值给mActive对象,并将mActive 交给真正的执行线程池THREAD_POOL_EXECUTOR执行。待mActive逻辑执行完毕,再次从mTasks取值执行。实现前文所提的,多个AsyncTask实例,同步线性执行。
执行线程池配置如下:
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
可详细查看AsyncTask源码,学习执行线程池的配置,加深线程池的学习及使用。
更多形式的线程使用
Callable,接口,可以获取返回值
Future, 接口,可查询任务执行的情况,是否完成等
-
FutureTask,类,实现了future接口以及runable接口
Thread只支持Runable接口。通过工作线程进行某耗时任务,有时需要取得返回值,但Runable接口无返回值。为了解决此问题,引出两个新的接口,Callable和Future。Callable接口包含返回值,而Future接口,可用于查看当前工作线程的执行情况,是否已经执行完成等。
FutureTask实现了Future和Runable接口,它的构造函数中还包含Callable接口,在它的run方法中,执行Callable接口的实现逻辑,并将结果保存起来,当工作线程线束时可返回结果。void innerRun() { if (!compareAndSetState(READY, RUNNING)) return; runner = Thread.currentThread(); if (getState() == RUNNING) { // recheck after setting thread V result; try { result = callable.call();//调用callable的接口实现逻辑 } catch (Throwable ex) { setException(ex); return; } set(result);//保存执行结果 } else { releaseShared(0); // cancel } }
FutureTask,是一个可返回任务执行结果的Runable实现类。示例如下:
public class DemoFutureTask {
static class CallableImpl implements Callable{
int result = 0;
@Override
public Integer call() throws Exception {
while (result < 10) {
Thread.sleep(1000);
result ++;
}
return result;
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
CallableImpl callableImpl = new CallableImpl();
FutureTask task = new FutureTask<>(callableImpl);
new Thread(task).start();
System.out.println("workthread has started");
while (!task.isDone()) {
System.out.println("task is not done");
Thread.sleep(1000);
}
System.out.println("task is done and result = " + task.get());
}
}
结果为:
workthread has started
task is not done
......
task is not done
task is done and result = 10