一、线程池
- 创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。
- 从JDK1.5开始,Java API提供了Executor框架可以创建不同的线程池。比如单线程池,每次处理一个任务;数目固定的线程池或者是缓存线程池(一个适合很多生存期短的任务的程序的可扩展线程池)
new Thread的弊端
每次new Thread新建对象性能差。线程缺乏统一管理,可能无限制新建线程,相互之间竞争及可能占用过多系统资源导致死机或oom(OutOfMemoryError)。
二、线程池的工作原理
- 线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。
- 线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步。
- 线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
corePoolSize 线程池的核心大小
maximumPoolSize 线程池中允许的最大线程数
workQueue 阻塞任务队列
// 创建线程池
ExecutorService es = Executors.newCachedThreadPool();
// 提交任务到线程池中
Future f = es.submit(() -> {
int sum = 0;
for (int i = 0; i <= 100; i++)
sum += i;
return sum;
});
Object res = f.get();
System.out.println(res);
常见问题总结:
- 一般创建线程对象时不建议使用extends Thread方法,因为单根继承;
- 一般没有返回结果时建议使用Runnable接口,如果有返回值一般建议使Callable接口;
- 如果使用线程比较频繁建议使用线程池
daemon线程(守护线程) - 守护线程是为其它线程提供服务的线程。
- 守护进程的特点是当程序中主线程结束时,守护线程会自动中止。
如何将一个子线程设置为守护线程。 - 在一个线程调用start启动之前,调用方法thread.setDaemon(true);就可以将thread线程设置为守护线程。
- 守护线程一般应该是一个独立的线程,它的run()方法是一个无限循环
- 守护线程与其它线程的区别是,如果守护线程是唯一运行着的线程,程序会自动退出。
public class T2 {
public static void main(String[] args) {
System.out.println("开始程序------");
Thread t = new Thread() {
public void run() {
while (true) {
System.out.println("守护线程~~~");
}
}
};
t.setDaemon(true);
t.start();
System.out.println("程序结束-----");
}
}
注意:
有时虽然主线程结束了,但是守护线程还是会执行几次。是因为 JAVA中线程不能立即停止。
线程组
1、所有线程都隶属于一个线程组。那可以是一个默认线程组【main】,亦可是一个创建线程时明确指定的组。
2、在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组
3、若创建多个线程而不指定一个组,它们就会与创建它的线程属于同一个组。
ThreadGroup myThreadGroup = new ThreadGroup("My Group");//创建线程组
Thread myThread = new Thread(myThreadGroup,"a thread"); //定义线程时指定对应的线程组
theGroup = myThread.getThreadGroup();//获取线程对象所属的线程组
ThreadGroup group=new ThreadGroup("yan1");
Thread t1=new Thread(group,
()->{
for(int i=0;i<100;i++)
System.err.println(Thread.currentThread());
}
);
t1.start();
1.corePoolSize
线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行。
2.maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize。
3.keepAliveTime
FutureTask future = new FutureTask(callable);newThread(future).start();线程空闲时的存活时间。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,keepAliveTime参数也会起作用,直到线程池中的线程数为
4.unit
keepAliveTime参数的时间单位。
5.workQueue
任务缓存队列,用来存放等待执行的任务。如果当前线程数为corePoolSize,继续提交的任务就会被保存到任务缓存队列中,等待被执行。一般来说,这里的BlockingQueue有以下三种选择:
- SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。因此,如果线程池中始终没有空闲线程(任务提交的平均速度快于被处理的速 度),可能出现无限制的线程增长。
- LinkedBlockingQueue:基于链表结构的阻塞队列,如果不设置初始化容量,其容量Integer.MAX_VALUE,即为无界队列。因此,如果线程池中线程数达到了corePoolSize,且始终没有空闲线程(任务提交的平均速度快于被处理的速度),任务缓存队列可能出现无限制的增长。
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务。
6.threadFactory
线程工厂,创建新线程时使用的线程工厂。
7.handler
任务拒绝策略,当阻塞队列满了,且线程池中的线程数达到maximumPoolSize,如果继续提交任务,就会采取任务拒绝策略处理该任务,线程池提供了4种任务拒绝策略:
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,默认策略;
- CallerRunsPolicy:由调用execute方法的线程执行该任务;
- DiscardPolicy:丢弃任务,但是不抛出异常;
- DiscardOldestPolicy:丢弃阻塞队列最前面的任务,然后重新尝试执行任务(重复此过程)。当然也可以根据应用场景实现。
- RejectedExecutionHandler接口自定义饱和策略,如记录日志或持久化存储不能处理的任务。
Executors创建线程池
1、newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收重用时则新建线程用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。
2、newFixedThreadPool 创建一个固定大小的定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
3、newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
4、newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
5、newWorkStealingPool 创建一个拥有多个任务队列的线程池,可以减少连接数
提交任务的方式
submit()和execute(),
通过submit()方法提交的任务可以返回任务执行的结果,
通过execute()方法提交的任务不能获取任务执行的结果。
关闭线程池
1、shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表
2、shutdown:当调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务。
volatile关键字
volatile是java提供的一种同步手段,只不过它是轻量级的同步,为什么这么说,因为volatile只能保证多线程的内存可见性,不能保证多线程的执行原子性。而最彻底的同步要保证有序性、可见性和原子性的synchronized任何被volatile修饰的变量,都不拷贝副本到工作内存;
任何修改都及时写在主存。因此对于volatile修饰的变量的修改,所有线程马上就能看到,但是volatile不能保证对变量的修改是原子的。
特性:
1、保证可见性:当写一个 volatile 变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存,使其他线程立即可见.
2、保证有序性:当变量被修饰为 volatile 时,JMM 会禁止读写该变量前后语句的大部分重排序优化,以保证变量赋值操作的顺序与程序中的执行顺序一致。
3、部分原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性