JAVA并发(线程池及其他并发相关内容)
前言
OOM : 内存泄露异常
线程池
概念
线程池主要是控制运行线程的数量,将待处理的任务放到等待队列,然后创建线程执行这些任务,如果超过了最大线程数,则等待。
优点:线程复用;控制最大并发数;管理线程
线程复用:不用一直new新线程,重复利用已经创建的线程来降低线程的常见和销毁开销,节省系统资源。
提高响应速度:当任务达到时,不用创建新的线程,直接利用线程池中的线程
管理线程:可以控制最大并发数,控制线程的创建等。
Executor->ExecutorService->AbstractExecutorService->ThreadPoolExecutor。
Executor有自己的工具类:Executors
ThreadPoolExector
Executors.newFixedThreadPool(int) 固定数量的线程池,用于执行长期任务,性能会很好。
Executors.newSingleThreadExecutor() 一池只有一个线程。
以上两种LinkedBlockingQueue实现,队列长度默认为Integer.MAX_VALUE,约为21亿,可能会堆积大量请求,从而导致OOM。
Executors.newCachedThreadPool() 使用SynchronousQueue实现,变长线程池。执行很多短期异步任务,线程池根据需要创建线程,但在先前勾线的线程可用时将重用他们。可自动扩容。
此方式允许创建的线程数量为Integer.MAX_VALUE,可能会创建大量线程,从而导致OOM。
除此之外,还有Executors.newScheduledThreadPool(),Executors.newWorkStealingPool()等
阿里规范中,不允许使用Executors创建,必须通过ThreadPoolExecutor方式创建。
除此之外,还有
线程池底层构造方法源码
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
七个参数:
corePoolSize 线程池中的常驻核心线程数
maximumPoolSize 线程池中能容纳的同时执行的最大线程数,必须大于1
keepAliveTime 多余的空闲线程(超过corePoolSize的数量的线程)的存活时间
unit keepAliveTime的存活时间单位
workQueue 任务队列 new LinkedBlockingQueue<>(int)
threadFactory 生成线程池中的工作线程的线程工厂,用于创建线程,一般默认
handler 拒绝策略,当队列满满,且工作线程大于等于maximumPoolSize,拒绝请求执行的runnable的策略
拒绝策略(阻塞队列)
AbortPolicy,默认策略,直接抛出RejectedException。
new ThreadPoolExecutor.AbortPolicy();
CallRunnablePolicy,不会抛出异常,也不会终止任务,而是将任务返回给调用者(主线程),从而降低新任务的流量。
new ThreadPoolExecutor.CallRunnablePolicy();
DiscardOldestPolicy ,抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交任务。
new ThreadPoolExecutor.DiscardOldestPolicy();
DiscardPolicy该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,只是最好的方法。
new ThreadPoolExecutor.DiscardPolicy();
参数选择
maximumPoolSize选择:
对于CPU密集型任务,最大线程数是CPU线程数+1。
对于IO密集(多文件上传下载),需要尽量多的线程数,可以是CPU线程数*2,或者CPU线程数/(1-阻塞系数)。
workQueue大小选择
maximumPoolSize相同或maximumPoolSize - corePoolSize,具体得根据实际情况
死锁
各自持有一把锁,又想获取对方的锁
示例
class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(String lockA, String lockB) {
this.lockA = lockA;
this.lockB = lockB;
}
@Override
public void run() {
synchronized (lockA) {
System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockA + "\t尝试获取:" + lockB);
try {
TimeUnit.SECONDS.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockB + "\t尝试获取:" + lockA);
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA, lockB), "ThreadA").start();
new Thread(new HoldLockThread(lockB, lockA), "ThreadB").start();
}
}
如何找出死锁
先用jps -l命令获取进程号
再通过jstack [进程号]查看
LockSupport
等待唤醒方式:
1、Object的wait()方法让线程等待,Object的notify()方法唤醒
2、JUC包中Contidion的await()方法让线程等待,使用signal()、signalAll()方法唤醒
以上两种方法必须在同步块或同步方法里且成对出现使用。
3、LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对于的唤醒方法。(调用的是Unsafe里的native方法)
LockSupport类使用了一种名为Permit(许可)的概念做到阻塞和唤醒线程的功能,每个线程都有一个permit,permit只有两个值,0和1,默认是0。(类似Semaphore,但是许可的累加上限是1)
Thread t1 = new Thread(()->{
//开始等待,当许可证不为0时-1后继续执行
LockSupport.park();
});
t1.start();
new Thread(()->{
//唤醒t1,许可证变为1
LockSupport.unpark(t1);
}).start();
连续调用unpark只相当于调用一次,连续调用unpark再连续调用park时只有第一次会通过,之后的会等待。
AQS(AbstractQueuedSynchronizer抽象同步队列)
通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作,将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配,有一个int类变量表示持有锁的状态,通过CAS完成对status值的修改(0表示没有,1表示阻塞)
双向链表,将每一个线程封装成为一个Node对象,在Node节点中包含节点状态()和上一个节点(pre),下一个节点(next)。
非公平锁模式:当资源释放时,先唤醒第一个节点,若节点已取消,则将后续节点一起唤醒,谁抢到则谁占用。
公平锁模式:当资源释放时,先唤醒对位节点,让队尾节点开始递归判断前置节点是否为待唤醒状态,找到最靠前的待唤醒节点,将其唤醒,使其使用资源。
Callable接口
Runnable方法的run()无法带返回值
1、Callable带返回值
2、会抛异常
3、覆写call()方法,而不是run()方法
Runnable和Future的子接口RunnableFuture,实现类为FutureTask
创建时需要传入实现Callable接口的类。
通过get方法获取返回值
用法
//实现Callable接口
class MyCallnable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
try {
//todo
} catch (InterruptedException e) {e.printStackTrace(); }
//返回结果
return 0;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建FutureTask类,接收MyCallnable 。
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallnable ());
//将FutureTask对象放到Thread类的构造器里面。
new Thread(futureTask).start();
//用FutureTask的get方法得到返回值。
int result = futureTask.get();
}
}