使用线程池的目的
降低资源消耗
通过重复利用已创建的线程降低线程创建和销毁造成的消耗
提高响应效率
当任务到达时,任务可以不需要等到线程创建就能立即执行
方便管理
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用。
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
通过Executors(JDK1.5并发包)四种创建方式
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool创建一个定长线程池,支持定时及周期性任务执行。
schedule(Runable command, long delay, TimeUnit unit):定时开始执行
scheduleAtFixedRate(Runable command, long initialDelay, long period, TimeUnit unit): 第一次定时开始执行之后,周期性的继续执行(周期频率为:上一次任务开始执行时间为起点,过了period时间,调度下一次任务,当然如果上一次任务执行还没结束,就算到了period时间也不会执行下一次任务)。
scheduleWithFixedFixedDelay(Runable command, long initialDelay, long delay, TimeUnit unit): 第一次定时开始执行之后,周期性的继续执行。(周期频率为:上一个任务结束后,经过delay时间再进行下一次任务调度)。
newSingleThreadExecutor创建一个单线程话的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。
ThreadPoolExecutor上述4中方法实际上底层实现是ThreadPoolExecutor的4个构造器。
Executors的submit方法
以submit(Callable task)方法开起的线程可以获得线程返回值,通过get()获得当前线程的返回值,当获取返回值得时候如果这个线程的写操作还没有完成则会等待(通常建立一个新的子线程来获取,这样不会阻塞主线程)。在获取该线程的返回值之前,是不会影响其他线程的执行的。
底层实现:future模式(以后说)
shutdown()
关闭线程池,不会暴力关闭,而是等待所有任务执行完成后再关闭。只是发送了一个关闭信号,shutdown()方法执行后,这个线程池不会再接收新的任务。
配置线程池
CPU密集
该任务执行的时候,不会产生大量IO阻塞,CPU运行速度快
配置最大线程数=CPU核数
IO密集
该任务需要大量I/O操作,产生阻塞,如果是在单线程中执行,可以使用多线程技术,CPU运算能力不会浪费等待资源
配置最大线程数:2*CPU核数
CPU的数量*CPU使用率*(1+等待时间/计算时间)
CPU数量可以通过java中的Runtime.getRuntime().availableProcessors()方法获取。
并发包
stop(弃用)
原因:暴力终止线程,并释放所持有的锁,可能会造成保存的数据不一致,而导致数据永久地破坏和丢失
interrupt()
中断线程
isInterrupted()
判断是否被中断
interrupted()
判断是否被中断,并清除当前中断状态
sleep(xx)
阻塞(xx毫秒后重新进入就绪状态),会由于中断而抛出异常,此时,它会清除中断标记。所以我们可以在catch块中通过interrupt()再次设置中断标记为,就可以防止下次循环无法捕获这个中断的问题
锁对象.wait()
让线程等待,释放锁的资源
锁对象.notify()
唤醒当前对象锁池等待的线程
join(x)
让目标程序先执行,执行结束之后再执行自己(x可选,填的话就是设置最大等待时长,超过这个时长将会回到自己继续执行)
本质是让调用线程wait()再当前线程对象实例上
yield()
让出CPU让别人先执行,之后再重新争抢CUP。(Thread.yeild())
CountDownLatch
倒计时器:new CountDownLatch(x); x表示计数个数。CountDownLatch,await*(方法,说明x个任务全部完成后,才能继续执行
CyclicBarrier
循环栅栏:可反复使用的计数器,当计数任务完成后,进行下一次计数
特有的异常类:BrokenBarrierException表示CyclicBarrier已经破损,无需继续等待
信号量
允许几个线程同时访问
Semaphore类
通过new Semaphore(x)创建对象,“x"表示允许几个线程同时访问
acquire()申请信号量,无法获得会等待,直到有线程释放信号量
acquireUniterruptibly()和acquire()类似,但是不响应中断
tryAcquire()尝试获得一个许可,成功返回true失败返回false,不会进行等待
release()释放信号量
阻塞工具类:LockSupport
park():挂起线程(类似suspend())
unpark():重启线程(类似resume())
拒绝策略:均实现了RejectedExecutionHandler接口,若这些策略扔无法满足应用,则可以自己扩展RejectedExecutionHandler接口(如重写beforeExecute()(任务开始前),afterExecute()(任务结束后)、terminiated()(整个线程池退出)等)
AbortPolicy
直接抛出异常,阻止系统正常工作
GallerRumsPolicy
只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降
DiscardOledestPolicy
丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务
DiscardPolicy
丢弃无法处理的任务,不予任何处理。(任务丢失)
一定要在synchronized里执行,持有同一把锁。
线程池的优化
提交的任务和线程数量并不是一对一的关系。绝大多数情况下,一个物理线程实际上是需要处理多个逻辑任务的。因此每个线程必然需要拥有一个任务队列。在实际执行过程中,可能会出现这么一种情况:线程A已经把自己的任务都执行完成了,而线程B还有一堆任务等着处理,此时线程A就会“帮助”线程B,从线程B的队列中拿一个任务过来处理,尽可能地达到平衡。一个值得注意的地方是,当线程试图帮助别人时,总是从任务队列的底部开始拿数据,而线程试图执行自己的任务时,则是从相反的顶部开始拿。因此这种行为也十分有利于避免数据竞争。
异常处理,帮助我们定位到抛异常的地方:
public class TraceThreadPoolExecutor extends ThreadPoolExecutor{
public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue){
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void execute(Runnable task) {
super.execute(wrap(task, clientTrace(), Thread.currentThread().getName()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
}
private Exception clientTrace(){
return new Exception("Client stack trace");
}
private Runnable wrap(final Runnable task, final Exception clientStack, String clientTreadName){
return new Runnable() {
@Override
public void run() {
try{
task.run();
}catch (Exception e){
clientStack.printStackTrace();
throw e;
}
}
};
}
}
threadlocal
给每个线程提供一个局部变量,但是不是通过threadLocal来完成的,而是需要在应用层面上来保证的,如果在应用上为每一个线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
4个方法
void set(Object value)
public Object get()
public void remove()
protected Object initialValue()
创建threadLocal:
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return 0;
};
};
底层就是一个ThreadLocalMap可以理解为一个map集合,key是当前线程
内存泄露
当我们使用线程池,意味着当前线程未必会退出。我们将一些对象设置到ThreadLocal中(它实际保存在线程持有的ThreadLocalMap内),可能会使系统出现内存泄露的问题(用完未清理),使用完了再也不用了,但是却无法被回收。ThreadLocalMap更加类似WeakHashMap,它的实现使用了弱引用,java虚拟机在垃圾回收时,如果发现弱引用,就会立即回收
解决办法:使用ThreadLocal.remove()方法将变量移除,或者直接赋值null,那么这个对象会更容易被垃圾回收器发现,从而加速回收。(如:t = null,那么这个ThreadLocal对应的所有线程的局部变量都有可能被回收)。
参考书籍《JAVA高并发程序设计》