1、定义任务
并发编程可以使我们将程序划分为多个分离的、独立运行的任务,任务本身不会执行。通过使用多线程机制,这些任务将会由执行线程来驱动处理。
Java里面准备好了使用底层线程机制的逻辑,对于应用研发人员来说,只要按部就班的调用即可。
Runnable就是描述任务的一种方式。实现Runnable接口,并且编写run()方法。它本身不具备产生线程的能力,要想实现线程行为,必须显式的将一个任务附着到线程上。
线程调度器Thread.yield(),将CPU从一个线程转移到另一个线程。表示当前线程已经执行完了最重要的部分,可以切换CPU给其他任务。
2、Thread类
Thread不能称之为线程,但是可以理解它具备产生线程的能力,也就是Runnable要附着在的东西上。
Thread.start(),是线程执行必需的初始化工作。main方法中执行Thread.start()。程序会同时执行两个方法,一个是main、一个是Thread里面的Runnable.run(),由两个线程独立驱动。
在正常的垃圾回收中,因为Thread对象在执行完后,且没有其他的引用,应该会被回收。但是在这个场景中,Thread对象不会被回收。每个Thread都注册了他自己,因此缺失有个对它的引用,在他的任务退出其run()并死亡之前,垃圾回收器不会回收他。
3、使用Executor
executor是jdk提供的管理Thread对象的执行器。用来创建和管理系统中的所有任务。
shutdown()方法是为了防止新任务提交给Executor,而已经提交的任务将会被继续执行。
Executor可以创建3种线程池:FixedThreadPool、CachedThreadPool、SingleThreadExecutor。在任何线程池中,现有线程在可能的情况下,都会被自动复用。底层都是ThreadPoolExecutor。
FixedThreadPool,一次性执行代价高昂的线程分配,但是也限制了线程的数量。
CachedThreadPool,在程序执行过程中,通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程。
SingleThreadExecutor,就像线程数量为1的FixedThreadPool,也可以提交多个任务,排队执行。
4、从任务中产生返回值
Runnable是执行工作的独立任务,但不会产生返回值。如果需要返回值,则需要实现Callable接口,而不是Runnable;同时提交到线程池时使用submit()方法,而不是execute()方法。
submit()方法会产生Future对象,Future是个泛型包装类,泛型类则是Callable返回结果的类型。可以用isDone()方法判断Future是否已完成。Future.get()是获取里面的结果,get()方法具有设置超时时间的机制,不设置则会一直等Future完成,即线程执行完后。
5、休眠
Thread.sleep()可以让线程停止执行一段时间。可能会抛出InterruptedException,需要在使用的地方捕获处理。
TimeUnit,可以指定线程延迟的单元,比如延迟几秒、几分钟、几小时等。
6、优先级
线程的优先级传给调度器,但是调度器不一样按此处理。在线程run()方法里面开头设置优先级setPriority()。JDK有很多优先级,但是操作系统的优先级是不确定的,两者之间不能好好映射,如果使用则建议用高MAX_PRIORITY、中NORM__PRIORITY、低MIN_PRIORITY三个级别。
7、让步
yield(),表示你的工作已经差不多了,让可以让出CPU给别的线程。调用该方法时,建议相同优先级的其他线程可以执行。
与休眠sleep()的差异:
a 优先级区分,sleep()会给其他任何优先级的线程机会,而yield()则会给同级别或更高优先级线程机会。
b 运行状态区分,sleep()会阻塞,时间到了后变成就绪,yield()会直接就绪。
c 抛出异常之分,sleep()会跑出InterruptedException,yield()则不会。
8、后台线程
后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的贤臣,并且这种线程并不属于程序中不可获取的部分。
与优先级setPriority()类似,要想设置后台线程,要在线程启动之前调用setDaemon()方法;与setPriority()区别是,线程start()方法之前这是,优先级是在run()里面设置。
后台线程创建的其他线程都是后台线程。
9、编码的变体
可以在一个类中,创建一个内部类,实现Runnable、继承Thread来创建任务,或者使用匿名内部来,共四种情况。
10、术语
主要为了区分任务、线程。我们实现Runnable、继承Thread不过是创建了一个任务,线程本身并非我们创建,我们也不用关注线程创建的细节,只知道这样会有线程驱动我们的任务执行。
11、加入一个线程
一个线程在其他线程之上调用join()方法,效果是等一段时间直到第二个线程结束才继续执行。如A线程里面调用B.join(),后续要等到B线程继续才会继续。join()方法可以设置超时。
针对这种场景CyclicBarrier可能是更优的解决方案。
12、创建有相应的用户界面
使用线程的动机之一就是建立有响应的用户界面。可以通过建立一个后台线程,实时监听用户输入并且做出响应,同时用户可以在界面上进行其他操作,实时监听程序不至于将程序卡死。
13、线程组
线程组是持有一个线程的集合。是一次不成功的尝试。
14、捕获异常
由于线程的本质特性,异常可能逃出任务的run()方法,直到main()方法中。解决的方法是先建立一个实现Thread.UncaughtExceptionHandler的类MyHandler,在创建完任务后Thread t = new Thread(),将其附着在线程上,t.setUncaughtExceptionHandler(new MyHandler())。
也可以创建一个全局的未捕获异常处理器:Thread.setDefaultUncaughtExceptionHandler()。
相当于注册了一个监听器,如果出现异常则会调用。感觉意义不大。