基本的线程机制
线程是比进程更轻量级的调度执行单位,各个线程既可以共享进程资源(内存地址、文件IO),又可以独立调度(线程是CPU调度的最小单元)。
多进程和多线程的区别:
1、本质区别在于每个进程拥有自己一整套变量,线程则是共享数据。因此共享变量使线程之间的通信比进程之间的通信更有效、更容易。
2、与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小的多。
一、定义任务
1)通过实现Runnable接口并编写run()方法,使得任务可以执行我们的命令。
Runnable执行工作的独立任务,但不会返回任何值。
class LiftOff implements Runnable{
protected int countDown = 10;
private static int taskCount = 0;
private final int id = taskCount++;
@Override
public void run() {
while (countDown-- > 0){
System.out.println(countDown);
Thread.yield();
}
}
}
2)希望任务结束后有一个返回值,需要实现Callable接口并编写call方法。Callable是一个具有类型参数的泛型,它的类型参数表示是从方法call中返回的值,并且必须使用ExecutorService.submit()方法调用它。
class TaskWithResult implements Callable<String>{
private int id;
public TaskWithResult(int id){
this.id = id;
}
@Override
public String call() throws Exception {
return "result of TaskWithResult " + id;
}
}
public static void main(String[] args){
ExecutorService exec = Executors.newCachedThreadPool();
ArrayList<Future<String>> results = new ArrayList<>();
for (int i = 0; i < 10; i++){
results.add(exec.submit(new TaskWithResult(i)));
}
try {
for (Future<String> future: results){
future.get();
}
}catch (ExecutionException e){
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
submit()方法会产生Future对象,它用Callable返回结果的特定类型进行参数化,isDone()方法用来查询Future是否已经完成,任务完成时,它具有一个结果,可以调用get()获取该结果。也可以不用isDone()检查直接调用get(),get()方法将阻塞,直至结果准备就绪。
二、线程的创建
1)Thread类来创建线程。Thread构造器只需一个Runnable对象。调用Thread对象的start()方法为该线程执行必需的初始化操作,然后调用Runnable的run()方法,以便在这个新线程中启动该任务
Thread t = new Thread(new LiftOff());
t.start();
start()调用后会快速的返回,run()方法会在新线程中执行
2)使用Executor创建线程。执行器(Executor)管理Thread对象,从而简化并发编程。Executor在客户端和任务执行之间提供了一个间接层;与客户端直接执行任务不同,中介对象将执行任务。Executor允许你管理异步任务的执行,而无需显式地管理线程的生命周期。
一般单个的Executor被用来创建和管理系统中所有的任务。shutDown()方法的调用可以防止新任务被提交给这个Executor。
FixedThreadPool可以一次性预先执行高昂的线程分配,限定线程的数量。可以节省时间,因为不用为每个线程都固定地付出创建线程的开销。
CachedThreadPool通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。因此是首选,当这种方式会引发问题时,才需要切换到FixedThreadPool.
SingleThreadExecutor创建一个线程,每个任务都会在下一个任务开始之前运行结束,所有任务都使用相同的线程
三、线程常见名词
3.1休眠 sleep()
调用sleep()将使任务终止执行给定的时间
对sleep()的调用可以抛出InterruptedException异常,异常不能夸线程传播,所以必须在本地处理所有任务内部产生的异常
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
3.2线程优先级
线程的优先级将线程的重要性传递给调度器,尽管CPU处理线程集的顺序不确定,但是调度器将倾向于让优先级高的线程先执行。并不是优先级底的线程得不到执行,仅仅是执行的评率较低。
获取线程优先级Thread.currentThread().getPriority();
设置线程优先级Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
设置优先级实在run()方法的开头设定的,在构造器中设定不会有任何好处,因为Executor在此刻还没有执行任务
JDK有10个优先级,因为与操作系统不能很好的映射,只使用MAX_PRIORITY, MIN_PRIORITY,NORM_PRIORITY三种级别。
3.3让步 yield()
通过调用yield()可以给线程调度器暗示,让别的线程使用CPU。yield()的调用也是建议具有相同优先级的线程可以运行。对于任何重要的控制或在调整应用时,都不能依赖于yield()。
3.4后台线程 daemon
后台线程指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程不属于程序中不可或缺的部分。所有非后台线程结束时,程序也就终止了,同时会杀死所有后台线程。
必须在线程启动之前调用setDaemon()方法,才能把它设置为后台线程。
Thread t = new Thread(new LiftOff());
t.setDaemon(true);
t.start();
通过定制ThreadFaactory可以定制由Executor创建的线程的属性(后台、优先级、名称)
class DaemonThreadFactory implements ThreadFactory{
@Override
public Thread newThread(@NonNull Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
}
}
ExecutorService exec = Executors.newCachedThreadPool(new DaemonThreadFactory());
可以通过isDaemon()来判断一个线程是否为后台线程。如果一个线程为后台线程那么它创建的任何线程都是后台线程。
注意:当最后一个非后台线程终止时,后台线程会“突然”终止,finally子句也不会执行
3.5加入一个线程join()
一个线程可以在其他线程之上调用join()方法,效果为等待一段时间直到第二个线程结束才继续执行。如某个线程在另一个线程t上调用t.join(),此线程将被挂起,直到目标线程t结束才恢复。
也可以在join()方法中带一个参数,如果目标线程在参数时间到期还没有结束,join()方法总能返回。
3.6捕获异常
由于线程的本质特性,不能捕获从线程中逃逸的异常。一旦异常逃逸出任务的run()方法,就会向外传播到控制台,除非采取特殊的步骤捕获异常。
Thread.UncaughtExceptionHandler允许在每个Thread对象上附着一个异常处理器
Thread t = new Thread(new LiftOff());
t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
}
});
t.start();