1. 不推荐在线程中重写start方法;
emmm,我想没人会试图重写Thread的start()方法
自定义代码
public class MultiThread extends Thread{
//启动线程
@Override
public synchronized void start() {
//super.start();
//自己启动线程...代码逻辑
}
//线程执行逻辑
@Override
public void run() {
super.run();
}
}
注意:继承自Thread类的多线程类不必重写start方法。
因为Thread里面的start方法实际上会调用native方法,在底层实现启动线程、申请栈空间、运行run方法修改线程状态等职责,线程管理和栈内存管理都是JVM负责的,如果覆盖start方法,也就是撤销了线程管理和栈内存管理的能力。
2. 终止线程的方法
stop方法是一个弃用的方法(注意是弃用)...
1. 使用Thread的stop方法终止
线程启动完毕之后,在运行时可能需要终止,Java提供的终止方法只有一个stop,但是并不推荐使用这个方法。
- stop方法是过时的方法。
- stop是一种“恶意”的中断,一旦执行stop方法,即中断当前正在运行的线程,不管线程是否执行完毕。会导致代码逻辑的不完整。
- stop方法会破坏原子逻辑。
2. 使用volatile修饰的标志位终止
volatile保证的是代码的可见性。即线程每次在主存中获取volatile变量,即线程修改volatile变量之后,所有线程立即可见。注意,只能保证变量值到达操作栈顶是正确的,而后可能被其他线程修改,即只能保证其可见性,不能保证其原子性。
操作代码:
public class SafeStopThread extends Thread {
//此变量必须加上volatile
private volatile boolean stop = true;
@Override
public void run() {
while (stop) {
System.out.println("Running...");
}
}
//线程终止
public void terminate() {
stop = false;
}
}
3. interrupt中断方法
interrupt名字看上去是像是终止一个线程的方法,但是它不能终止一个正在执行着的线程,他只是修改中断标志而已。
//执行方法
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
while(true){
System.out.println("Running...");
}
}
};
//启动线程
thread.start();
//中断线程
thread.interrupt();
}
执行上面这段代码,会发现一直有Running
在输出,永远不会停止,似乎执行了interrupt
没有任何变化。那是因为interrupt方法不能终止一个线程状态,他只会改变中断标志位(如果在thread.interrupt()
前后输出thread.isInterrupted()
则会发现分别输出了false
和true
),如果需要终止该程序,还需要自行进行判断,例如我们可以使用interrupt
编写出更加简洁、安全的终止线程代码。
interrupt正确终止线程
//执行方法
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
while(!isInterrupted()){
System.out.println("Running...");
}
}
};
//启动线程
thread.start();
//中断线程
thread.interrupt();
}
4. 线程池关闭线程
如果我们使用线程池(比如ThreadPoolExecutor类),那么可以通过shutdown方法逐步关闭线程池中的线程,它采用的是比较温和、安全的关闭线程方法。
3. 线程优先级只使用三个等级
线程优先级(Priority)决定了线程获得CPU运行的机会,优先级越高的获取的运行机会越大。
线程优先级有2个特点:
- 并不是严格遵循线程优先级别来执行的。
- 但是优先级差别越大,运行机会差别越明显。
所以在Thread类中,默认设置了三个优先级,此意就是告诉开发者,建议使用优先级常量,而不是1到10随机数字。常量代码如下:
//最低的优先级
public final static int MIN_PRIORITY = 1;
//默认的优先级
public final static int NORM_PRIORITY = 5;
//最高的优先级
public final static int MAX_PRIORITY = 10;
在编码时,直接使用这些常量,可以在大部分情况下MAX_PRIORITY的线程会比NORM_PRIOPITY的线程先运行,但也不是绝对。不能把这个优先级作为核心业务的必然条件。Java无法保证优先级高肯定会先执行,只能保证优先级高的有更多机会执行。故建议在开发时只使用此三类优先级。
4. 使用线程异常处理器提升系统可靠性
如何实现线程异常的捕获或者处理呢?
线程Thread的run方法是不会抛出任何检查型异常(即不会throws Exception,但是可以try-catch),但是他自身却可能因为一个异常而中止,导致线程的终结。
在Java 1.5之后,在Thread类中增加了
setUncaughtExceptionHandler
方法,实现了线程异常的捕获和处理,
1. 主动方法解决运行时异常:
public class InitiativeCaught {
void threadDeal(Runnable r,Throwable t){
System.out.println("===Exception"+t.getMessage());
}
//成员内部类
class InitialtiveThread implements Runnable{
public void run() {
Throwable thrown=null;
try {
System.out.println(3/2);
System.out.println(3/0);
System.out.println(3/1);
}catch (Throwable e){
thrown=e;
}finally {
threadDeal(this,thrown);
}
}
}
public static void main(String[] args) {
ExecutorService exec= Executors.newCachedThreadPool();
exec.execute(new InitiativeCaught().new InitialtiveThread());
exec.shutdown();
}
}
上面介绍一种主动的方法来解决未检查异常。在Thread API中也可以使用UncaughtExceptionHandler
,同样也能检测出多线程某个由于未捕获的异常而终结的情况。
这两种方法是互补的,通过将二者结合在一起,就能有效地防止线程泄露的情况。
2. UncaughtExceptionHandler的用法:
public class WitchCaughtThread {
public static void main(String[] args) {
Thread thread=new Thread(new WitchCaughtThread().new Task());
thread.setUncaughtExceptionHandler(new WitchCaughtThread().new ExceptionHandler());
thread.start();
}
//自定义异常handler
class ExceptionHandler implements Thread.UncaughtExceptionHandler{
public void uncaughtException(Thread t, Throwable e) {
System.out.println("==Exception:"+e.getMessage());
}
}
//实例内部类
public class Task implements Runnable{
public void run() {
System.out.println(3/2);
System.out.println(3/0);
System.out.println(3/1);
}
}
}
注意,我们可以为所有的Thread类设置一个默认的UncaughtExceptionHandler
。
3. 设置Thread默认的UncaughtExceptionHandler
Thread.setDefaultUncaughtExceptionHandler(new
WitchCaughtThread().new ExceptionHandler());
4. 线程池如何使用UncaughtExceptionHandler:
只有通过execute提交的任务,才能将它抛出的异常交给UncaughtExceptionHandler,而通过submit提交的任务,无论是抛出Exception异常还是RuntimeException异常,都认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。
4.1 使用execute方法捕获异常:【异常捕获失败】
public class ExecuteCaught {
public static void main(String[] args) {
//创建线程池
ExecutorService exec= Executors.newCachedThreadPool();
//创建线程
Thread thread=new Thread(new WitchCaughtThread().new Task());
//设置线程异常处理机制
thread.setUncaughtExceptionHandler(new WitchCaughtThread().new ExceptionHandler());
//执行线程
exec.execute(thread);
//终止线程
exec.shutdown();
}
}
运行结果
可以看到,异常被抛出,但是并没有捕获异常。进行处理。
1
Exception in thread "pool-1-thread-1" java.lang.ArithmeticException: / by zero
at com.bascis.ThreadDemo.WitchCaughtThread$Task.run(WitchCaughtThread.java:24)
at java.lang.Thread.run(Thread.java:748)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
4.2 将异常的捕获封装到Runnable或Callable中:【异常捕获成功】
public class ExecuteCaught {
public static void main(String[] args) {
//创建线程池
ExecutorService exec= Executors.newCachedThreadPool();
//执行线程
exec.execute(new ThreadPoolTask());
//终止线程
exec.shutdown();
}
}
class ThreadPoolTask implements Runnable{
public void run() {
//在Runnable中处理异常
Thread.currentThread().setUncaughtExceptionHandler(new WitchCaughtThread().new ExceptionHandler());
System.out.println(3/2);
System.out.println(3/0);
System.out.println(3/1);
}
}
执行结果
1
==Exception:/ by zero
但是我们线程池若是使用submit()方法提交...
submit提交线程:【异常捕获失败】
public static void main(String[] args) {
//创建线程池
ExecutorService exec= Executors.newCachedThreadPool();
//执行线程
exec.submit(new ThreadPoolTask());
//终止线程
exec.shutdown();
}
下面为了证实submit提交的线程,异常信息保存到了Future的get方法中:
submit提交的完整代码:
public class ExecuteCaught {
public static void main(String[] args) {
//创建线程池
ExecutorService exec= Executors.newCachedThreadPool();
//执行线程
Future<?> submit = exec.submit(new ThreadPoolTask());
//终止线程
exec.shutdown();
try {
Object o = submit.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
执行结果
1
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at com.bascis.ThreadDemo.ExecuteCaught.main(ExecuteCaught.java:14)
Caused by: java.lang.ArithmeticException: / by zero
at com.bascis.ThreadDemo.ThreadPoolTask.run(ExecuteCaught.java:28)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
4.2 使用UncaughtException注意事项
在实际开发中,需要注意以下三个方面
1. 共享资源锁定
如果线程异常产生的原因是资源被锁定,自行重启应用只会增加系统的负担,无法提供不间断服务。例如一个即时通信服务器出现信息不能写入的情况,即使再怎么重启服务,也是无法解决问题的。在此情况下最好的办法就是停止所有的线程,释放资源。
2. 脏数据引起系统逻辑异常
异常的产生中断了正在执行的业务逻辑,特别是如果正在执行一个原子操作,但如果此时抛出了RuntimeException就有可能会破坏正常的业务逻辑,在这种情况下重启应用服务器,虽然可以提供服务,但对部分用户产生了逻辑异常。
3. 内存溢出
线程异常了,但由该线程创建的对象不会马上回收,如果在重启新线程,在创建一批对象,特别是场景接管,就非常危险了。例如即时通讯服务,重新启动一个新线程必须保证原在线用户的透明性,即用户不会察觉服务器重启,在此种情况下,就需要线程初始化时加载大量对象以保证用户的状态信息,如果线程反复重启,很可能会造成OOM内存泄露问题 。
5. 异步运算考虑使用Callable接口
多线程应用有两种实现方式,一种是实现Runnable接口,另一种是继承Thread类。这两种方式都有缺陷:run方法没有返回值,并且不能抛出异常。
在JDK1.5 开始引入了一个新的接口:Callable,它类似与Runable接口,实现它就可以实现多线程任务。
Callable接口定义:
public interface Callable<V> {
//具有返回值,并可抛出异常
V call() throws Exception;
}
需要注意的是:实现Callable接口的类,只是表明它是一个可调用的任务,并不表示它具有多线程运算能力,还是需要执行器来执行的。我们先编写一个实现Callable接口的任务类:
任务类:
//收税计算器
public class TaxCalculator implements Callable<Integer> {
//本金
private int seedMoney;
public TaxCalculator(int seedMoney) {
this.seedMoney = seedMoney;
}
public Integer call() throws Exception {
//模拟复杂的计算
Thread.sleep(10000);
return seedMoney/10;
}
}
这里模拟了一个复杂的运算:税收计算器,该运算可能花费10s时间,但是不能让用户一直等待,需要给用户输出点什么,让用户知道系统还在运行,是系统友好性的体现:用户输入即有输出,若耗时较长,则显示运算进度。如果我们直接计算,就只有一个main线程,是不可能有友好提示的,如果税金不计算完毕,也不会有后续动作的,所有,此时最好的办法就是重启一个线程来计算,让main做进度提示,代码如下:
public static void main(String[] args) throws InterruptedException, ExecutionException {
//创建一个线程
ExecutorService es= Executors.newSingleThreadExecutor();
//执行线程
Future<Integer> future = es.submit(new TaxCalculator(200));
//查看任务是否执行完毕
while(!future.isDone()){
//若是还没有运算完毕,等到200ms
Thread.sleep(200);
//输出进度符号
System.out.print("#");
}
System.out.println("\n计算完毕,税金是:"+future.get()+"元");
es.shutdown();
}
执行结果
##################################################
计算完毕,税金是:20元
5.2 Callable总结
- 尽可能多的占用系统资源,提供快速运算;
- 尽可能监控线程执行的情况,比如是否执行完毕,是否有返回值,是否异常。
- 可以为用户提供更好的支持,比如例子中的运算进度等。
6. 优先选择线程池
在java1.5之前,实现多线程编程比较麻烦,需要自己启动线程,并关注同步资源,防止出现死锁等问题,在JDK1.5之后引入了并行计算框架,大大简化了多线程开发。
线程一般有五个状态:新建状态(New)、可运行状态(Runnable)、阻塞状态(Blocked)、等待状态(Waiting)、结束状态(Terminated),线程的状态只能由新建转变为运行态才可能被阻塞或等待,最后终结,不可能出现本末倒置的情况。
线程池的实现涉及到三个名词:
1. 工作线程(Worker)
线程池中的线程,只有两个状态:可运行状态和等待状态,没有任务时他们就处于等待状态,运行时可以循环地执行任务。
2. 任务接口(Task)
这是每个任务必须实现的接口,以供工作线程调度器调度,他主要规定了任务的入口、任务执行完的场景处理,任务的执行状态等。这里有两种类型的任务:具有返回值(或异常)的Callable接口任务和无返回值并兼容旧版本的Runnable接口任务。
3. 任务队列(Work Queue)
也叫做工作队列,用于存放等待处理的任务,一般是BlockingQueue的实现类,用于实现任务的排队处理。
线程池的工作原理:创建一个阻塞队列以容纳任务,在第一次执行任务时创建足够多的线程(不超过许可线程数),并处理任务,之后每隔工作线程从任务队列中获取任务,直到任务队列中的任务数量为0为止。此时线程将处于等待状态,一旦有任务再加入到队列中,即唤醒工作线程进行处理,实现线程的可复用性。
7. 适时选择不同的线程池来实现
Java的线程池实现从最根本上来说只有两个:
ThreadPoolExecutor
和ScheduledThreadExecutor
['skedʒəld]
类,这两个类还是父子类的关系,但是JAVA为了简化并行计算,还提供了一个Executors的静态类。
7.1 线程池的成员含义
ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
这个是ThreadPoolExecutor最完整的构造方法,其他的构造函数都是引用该构造函数实现的,我们需要了解一下参数的含义:
corePoolSize:核心线程池大小。线程池启动后,在池中保持线程的最小数量。需要说明的是线程数量是逐步到达corePoolSize值的。
maximumPoolSize:最大线程池数量。池中最大能容纳的最大线程数量,如果超出,则使用RejectedExecutionHandler拒绝策略来处理。
keepAliveTime:线程最大生命期。这里的生命期有2个约束条件:一是该参数针对的是超过corePoolSize数量的线程;二是处于非运行状态的线程。这么说吧,如果corePoolSize为10,maximumPoolSize为20,此时线程池中有15个线程在运行,一段时间之后,其中有3个线程处于等待状态的时间超过了keepAliveTime指定的时间,则结束这3个线程,此时线程池还有12个线程正在运行。
unit:keepAliveTime的时间单位。
workQueue:任务队列。当线程池中的线程都处于运行状态,而此时任务数量继续增加,则需要一个容器来容纳这些任务,这就是任务工厂。
threadThread:线程工厂。定义如何启动一个线程,可以设置线程的名称,并且可以确定是否是后台线程等。
handler:拒绝任务处理器。由于超出线程数量和队列容量而对继续增加的任务进行处理的程序。
7.2 线程池的管理过程
- 首先创建线程池,然后根据任务的数量逐步将线程增大到corePoolSize数量。
- corePoolSizePool已满,则放置到workQueue中,直到workQueue爆满为止。
- 继续增加池中的线程数量(增强处理能力),最终达到
maximumPoolSize
。 - 若还有任务进来,那么就采用拒绝策略handler来处理。
在任务队列和线程池都饱和的情况下,一旦有线程处于等待(任务处理完毕,没有新任务增加)状态时间超过keepAliveTime,则该线程终止。也就是说池中的线程数量会逐渐降低,直至为corePoolSize数量为止。
7.3 Executors的使用
- newSingleThreadExecutor:单线程池。
一个池中只有一个线程在运行,该线程永不超时。由于只有一个线程,当多个任务需要处理的时候,会将他们放置到一个无界阻塞队列中逐步处理。实现代码:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
使用代码
public static void main(String[] args) {
//创建线程池
ExecutorService es= Executors.newSingleThreadExecutor();
//执行任务
es.submit(new Callable<String>(){
public String call() throws Exception {
return null;
}
});
//关闭执行器
es.shutdown();
}
- newCachedThreadPool:缓冲功能的线程池
建立一个线程池,而且线程的数量没有限制(当然不能超过Integer的最大值),新增一个任务即有一个线程处理,或复用之前空闲的线程,或新启用一个线程,但是一个线程在60s内一直是一个等待状态(也及时一分钟没有工作可做)就会被终止。
源代码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这里需要说明的是,任务队列使用了同步阻塞队列,这意味着想队列中新加入一个元素,即可唤醒一个线程(新创建的线程或复用线程池中空闲线程)来处理,这种队列已经没有队列深度的概念了。
- newFixedThreadPool:固定线程数量的线程池
在初始化时已经决定了线程的最大数量,若任务添加的能力超过了线程处理能力,则建立阻塞1队列容纳多余的任务。
源代码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
我们可以看到,它的corePoolSize和maximumPoolSize是相等的。
总结:
我们可以看到,Executors中的阻塞队列均是使用了无限的阻塞队列,若是在生产环境中使用,可能造成内存溢出。故不推荐使用。
推荐阅读