Java 提高(6)—– 多线程
1.线程的状态
- 新创建线程 在new Thread(r)后该线程还没有运行,状态还是new
- 可运行线程 线程处于runable状态,可运行的线程可能在运行也可能没有在运行,当获得时间片就是运行否则就是没有运行
- 阻塞线程 当一个线程试图获取被其他对象持有的锁就会进入阻塞状态,这是同步阻塞;当运行sleep或者join这个线程也会进入阻塞状态,这时当sleep超时,join等待线程终止该线程就进入可运行状态
- 等待状态,可以在任何线程中使用Object.wait()来阻塞改线程,阻塞的线程会进入一个阻塞队列,使用Object.notify()唤醒阻塞的队列最先进入的,使、Object.notifyAll()会唤醒全部阻塞的线程
- 终止线程 当一个线程死亡,死亡的方式有run()方法执行完毕而自然死亡,run()方法出现异常而意外死亡
2.守护线程
可以通过t.setDaemon(true)把一个线程设为守护线程,守护线程的唯一用途是为其他线程提供服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者;只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
注意:
(1) thread.setDaemon(true)必须在thread.start()之前设置
(2) 在Daemon线程中产生的新线程也是Daemon的
3.未捕获异常处理器
线程的run方法不能抛出任何能被检测的异常,但是,不可以被检测的异常会导致线程终止,线程死亡;这个时候在线程死亡之前异常会被传到一个用于未捕获异常的处理器,该处理器必须是一个继承Thread.UncaughtExceptionHandler的子类,当我们不做设置时,独立的线程的默认处理器是该线程的ThreadGroup对象(线程组ThreadGroup表示一组线程的集合,一旦一个线程归属到一个线程组之中后,就不能再更换其所在的线程组)
但是我们自己定义一个处理器,在线程开始前使用setUncaughtExceptionHandler()方法设置成自己的处理器,这样当出现异常时,就会执行覆盖的uncaughtException()方法,我们可以在这个方法中保存异常上传到服务器
class LeoHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
e.printStackTrace();
//do something 上传错误到服务器等
}
}
4.Callable 与 Future
Runable是一个封装异步执行的任务,可以把它想象成一个没有参数和返回值的异步方法,Callable与Runable类似,但是有返回值,Callable接口是一个参数化的类型,只有一个方法call;Future保存异步计算的结果,可以启动一个计算,将Future对象交给某个线程,然后忘掉他,Future对象的所有者在计算好之后可以获得它(具体怎么用,各种api建议浏览一遍api文档)
public class FutherDemo {
//写一个Callbale,里面模拟了耗时运算,返回的结果是Integer类型
static class MathDo implements Callable<Integer>{
@Override
public Integer call() throws Exception {
int sum = 0;
for(int i = 0 ; i < 10 ;i++){
sum += 10;
Thread.sleep(500);
}
return sum;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//Callable封装在FutureTask中
FutureTask<Integer> task = new FutureTask<Integer>(new MathDo());
//FutureTask当做thread的参数
Thread thread = new Thread(task);
thread.start();
//通过Future的get方法可以得到,get()方法是阻塞方法,所以我们不用加任何代码防止主线程退出
System.out.println(task.get());
}
}
5.线程池
当我们需要频繁的创建多个线程进行耗时操作室,每次都要通过new Thread()创建一个去直接使用start()执行不是很好的方式,每次new Thread()得到的线程的新建和销毁对象比较差,线程缺乏统一管理,可能无限制创建新线程,相互之间也存在竞争,导致死锁,缺乏定时执行,线程中断等。java提供了四种线程池供我们使用,可以有效的管理线程,有以下好处
- 重用存在的线程,减少线程的新建,销毁的开销;
- 可有效控制最大并发线程数,提高系统利用率(线程一次也不要开很多,和处理器的核数有关,分io任务还是cpu任务,一般是cpu核数加1);
- 提供定时执行,定期执行,单线程,并发数控制等功能;
四个线程池的特点
#创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,线程池为无限大,用空间换时间,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
#创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待,定长线程池的大小最好根据系统资源进行设置。如Runtime.getRuntime().availableProcessors()
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
#创建一个定长线程池,支持定时及周期性任务执行。可以使用schedule()支持定时周期性任务执行
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
#创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
上面的四个线程池最终实现都是依靠ThreadPoolExecutor,我们也可以根据实际情况使用ThreadPoolExecutor来使用线程池
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数
- maximumPoolSize 最大线程池数
- keepAliveTime 表示线程没有任务执行时最多保持多久时间会终止,默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
- unit:参数keepAliveTime的时间单位
- workQueue:一个阻塞队列,用来存储等待执行的任务
- threadFactory:线程工厂,主要用来创建线程
- handler:表示当拒绝处理任务时的策略
总结使用准则
- 不要对那些同步等待其他任务结果的任务排队,可能导致死锁
- 理解任务,调整线程池大小,任务是Io限制还是cpu限制
- 线程池大小的调整,太多太少都不好
- 当任务全部是计算性质Io限制的任务,可以设置大小为N或N+1,N为cpu核数;当任务是io限制的可以设置线程池数目大于cpu数目,因为不是所有线程一直在工作