前言
在讲多线程之前,首先明白几个概念
1 程序:计算机中,程序是一个有序的有限指令序列,一般是按照有顺序执行,直至遇到跳转程序或者出现中断,注意程序是静态的,不运行就是一系列的代码。
2 进程:首先明确,进程是计算机最小的资源分配单位,可以把它理解为正在运行的程序实例,广义上也认为是具有一定独立功能的程序在相关数据上的运行活动,通常一个进程中包含了若干线程。比如我们打开微信等客户端程序就在打开一个进程。多进程的作用不是提高执行速度,而是提高CPU的使用率;进程和进程之间的内存是独立的;
3 线程:线程是计算机中最小的执行单元,是独立运行和独立调度的基本的单位,它与资源分配没有关系。多线程作用不是为了提高执行速度,而是提高应用程序的使用率;线程和线程共享“堆内存和方法区内存”,栈内存是独立的,一个线程一个栈。
4 时间片:又称处理器片(processor slice),是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间,在抢占内核中是:从进程开始运行到被抢占花费的时间。
java命令会启动JVM,等于启动了一个应用程序即进程,该进程会自动启动一个主线程,然后主线程去调用某个类的main方法,所以main方法运行在主线程中
线程生命周期
Java中多线程的实现
1 继承Thread类实现,其中该类实现了Runnable接口
/**
* 1.创建线程:继承 Thread 类 + 重写 run();
* 2.使用线程:实例化线程对象 + 对象.start();
*/
class TestThread1 extends Thread{
public void run(){
for(int i=0;i<100;i++){
System.out.println(this.getName()+"==="+i);
}
}
}
class TestThread2 extends Thread{
public void run(){
for(int i=0;i<100;i++){
System.out.println(this.getName()+"=="+i);
}
}
}
public static void main(String[] args){
TestThread1 tt1=new TestThread1();
TestThread2 tt2=new TestThread2();
tt1.start();
tt2.start();
}
执行的结果其实是没有规则的,比较随机:
2 实现Runnable接口
/**
* 1.实现 Runnable 接口 + 重写 run(); -->真实角色
* 2.启动多线程,使用静态代理 Thread
* 1)创建真实角色
* 2)创建代理角色+真实角色引用
* 3)调用.start();
* 3.推荐通过实现 Runnable 接口创建线程:
* 1)避免单继承的局限性
* 2)便于共享资源
*/
class TestThread3 implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("=1_Runnable="+i);
}
}
}
class TestThread4 implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("=2_Runnable="+i);
}
}
}
/*
后台进程测试
*/
public static void main(String[] args) throws InterruptedException {
//真是执行线程
TestThread3 tt3=new TestThread3();
TestThread4 tt4=new TestThread4();
//创建线程代理
Thread th1=new Thread(tt3);
Thread th2=new Thread(tt4);
System.out.println("th1:"+th1.getPriority());
System.out.println("th2:"+th2.getPriority());
//线程start 默认会执行 run()
th1.start();
th2.start();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+i);
}
结果仍然随机
3 通过Callable和Future来创建,有返回值
/**
* FutureTask 是 RunnableFuture 接口的实现类(RunnableFuture 继承于 Runnable 接口,Future 接口)
* 1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值
* 2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值
* 3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程
* 4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值
*/
class TestThread5 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int index=0;
for(;index<10;index++){
System.out.println(Thread.currentThread().getName()+"=="+index);
}
return index;
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
TestThread5 tt5=new TestThread5();
FutureTask<Integer>ft=new FutureTask<Integer>(tt5);
//ft.run();//将当前线程(main函数)执行callable的call函数,没有返回值
// System.out.println("====12="+ft.get());//可以通过get()获取返回值
Thread tt6=new Thread(ft,"callable test");//将任务装进线程
tt6.start();//开始执行,但是如果ft.run()在前,此方法不执行
System.out.println("====="+ft.get());//可以通过get()获取返回值,
}
以上为三种常用的线程实现的方式,基本都是使用了new Thread()进行处理,使用此的缺点如下:
a. 每次new Thread新建对象性能差。
b.线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或oom。
c.缺乏更多功能,如定时执行、定期执行、线程中断。
为解决以上问题,Java中提供了线程池来解决以上问题,那么什么是线程池呢?
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。【百度】
使用线程池的优点是:
a. 重用存在的线程,减少对象创建、消亡的开销,性能佳。
b.可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
c. 提供定时执行、定期执行、单线程、并发数控制等功能。
java 中提供五种线程池(JDK8新增了newWorkStealingPool()),分别是:
1 newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。用的ThreadPoolExecutor类
2 newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。用的ThreadPoolExecutor类
3 newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。用的ScheduledThreadPoolExecutor
4 newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。用的Executors.FinalizableDelegatedExecutorService
5 newWorkStealingPool,这个是 JDK1.8 版本加入的一种线程池,stealing 翻译为抢断、窃取的意思,它实现的一个线程池和上面4种都不一样,用的是 ForkJoinPool 类
以上的线程池创建是通过Executors类中的静态方法来定义
java线程池的执行主要有两种方式:
1 线程池中的execute方法,即开启线程执行池中的任务。
2 还有一个方法submit也可以做到,它的功能是提交指定的任务去执行并且返回Future对象,即执行的结果。
两者的主要区别:
1、接收的参数不一样,execute 方法执行 runnable 任务,submit 方法执行 callable 任务,对于 Runnable,task 是 MyRunner,对于 Callable,task 是 FutureTask;
2、submit有返回值,而execute没有
3、submit方便Exception处理
意思就是如果你在你的task里会抛出checked或者unchecked exception,
而你又希望外面的调用者能够感知这些exception并做出及时的处理,那么就需要用到submit,通过捕获Future.get抛出的异常。
线程池关闭也有两种常用的方式shutdown()和shutdownNow()
区别如下,具体请参见:
1 shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。
2 shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。
1 newSingleThreadExecutor
【解释】
线程池中使用的从始至终都是单个线程,所以这里的线程名字都是相同的,而且下载任务都是一个一个的来,直到有空闲线程时,才会继续执行任务,否则都是等待状态。
源码:
单核心线程池,最大线程也只有一个,这里的时间为 0 意味着无限的生命,就不会被摧毁了
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
submit()执行
public static void main(String[] args){
//创建线程池
ExecutorService es= Executors.newSingleThreadExecutor();
TestThread5 tt5=new TestThread5();
Future<Integer> ft=null;
for(int i=0;i<20;i++){
System.out.println("main==="+i);//为主线程语句,先执性
ft=es.submit(tt5);//提交即执行
//tt5以及主线程执行11次就停止,
if(i==10){
es.shutdown();
}
}
//由于11次已经停止是,所以这句话打印不出来
System.out.println("result:=="+ft.get()+" == "+ft.isDone()+" "+ft.isCancelled());}
class TestThread5 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int index=0;
for(;index<10;index++){
System.out.println(Thread.currentThread().getName()+"=="+index);
}
return index;
}
}
2 newFixedThreadPool 线程池
需要传入一个固定的核心线程数,并且核心线程数等于最大线程数,而且它们的线程数存活时间都是无限的,看它的创建方式:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
【解释】如果newFixedThreadPool(3)
可以做到并发的下载,三个下载任务可以同时进行,并且所用的线程始终都只有三个,因为它的最大线程数等于核心线程数,不会再去创建新的线程了。
2.1 execute方式执行
public static void main(String[] args){
//创建线程池
ExecutorService es= Executors.newFixedThreadPool(3);
TestThread5 tt5=new TestThread5();
TestThread4 tt4=new TestThread4();
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"=="+i);
//Thread.sleep(2000);
es.execute(tt4);
}
class TestThread4 implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"===="+i);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
由于结果是循环打印10次,且在任务中设置了线程阻塞 (sleep),相当于执行快的线程在等执行慢的线程,线程池大小固定是3,所以每次打印了3个值,同时我们发现这三个值是一样的,最后一次(第10次)是任意一个线程,打印一次。但是如果将 sleep设置的小一点或者不设置,也是开3个线程,但是执行就不规则了。也就是说,出现下面的输出是将线程的sleep时间设置的大了点,所以每个线程都是按序执行。
sleep设置小 或者 不设置,注意执行方式
使用submit()
public static void main(String[] args){
//创建线程池
ExecutorService es= Executors.newFixedThreadPool(3);
TestThread5 tt5=new TestThread5();
Future<Integer> ft=null;
for(int i=0;i<20;i++){
System.out.println("main==="+i);//为主线程语句,先执性
ft=es.submit(tt5);//提交即执行
//tt5以及主线程执行11次就停止,
if(i==10){
es.shutdown();
}
}
//由于11次已经停止是,所以这句话打印不出来
System.out.println("result:=="+ft.get()+" == "+ft.isDone()+" "+ft.isCancelled());}
class TestThread5 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int index=0;
for(;index<10;index++){
System.out.println(Thread.currentThread().getName()+"=="+index);
}
return index;
}
}
可以看到执行结果和execute是类似的,但是不同的是,这里可以有返回值,通过Future对象来获得。由于 设置了在 第11次进行关闭线程池,也就是主线程main其实也只执行了11次,总共20次循环没有执行完毕,会抛异常:Exception in thread “main” java.util.concurrent.RejectedExecutionException,程序就中断了,这个是需要注意的。
所抛异常:
3 newCachedThreadPool
可以进行缓存的线程池,意味着它的线程数是最大的,无限的。但是核心线程数为 0。这里要考虑线程的摧毁,因为不能够无限的创建新的线程,所以在一定时间内要摧毁空闲的线程。看看创建的源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
【解释】由于没有核心线程数,最大线程数没有限制,所以一点全部开始下载,就会创建出 多个新的线程同时执行任务,类似于某云盘的下载。但是由于这种线程池创建时初始化的都是无界的值,一个是最大线程数,一个是任务的阻塞队列,都没有设置它的界限,可能会导致计算机或者网络的阻塞。
public static void main(String[] args){
//创建线程池
ExecutorService es= Executors.newCachedThreadPool();
TestThread5 tt5=new TestThread5();
Future<Integer> ft=null;
for(int i=0;i<20;i++){
System.out.println("main==="+i);//为主线程语句,先执性
ft=es.submit(tt5);//提交即执行
//tt5以及主线程执行11次就停止,
if(i==10){
es.shutdown();
}
}
//由于11次已经停止是,所以这句话打印不出来
System.out.println("result:=="+ft.get()+" == "+ft.isDone()+" "+ft.isCancelled());}
class TestThread5 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int index=0;
for(;index<10;index++){
System.out.println(Thread.currentThread().getName()+"=="+index);
}
return index;
}
}
可以看到这个激活了11个线程,也就是这个循环多少次停止,就开多少个线程,当然开多了会让电脑爆出内存超出的异常也就是 OOM
4 newScheduledThreadPool
这个表示的是有计划性的线程池,就是在给定的延迟之后运行,或周期性地执行。
构造函数源码:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
内部有一个延时的阻塞队列(DelayedWorkQueue)来维护任务的进行,延时也就是在这里进行的。此线程池有较多的方法实现,一般不常用,可以自行搜索。
下面这个结果不知道为什么只打印了前5个?
public class DaemonTest implements Runnable {
/**
* 线程安全的队列
*/
static Queue<String> queue = new ConcurrentLinkedQueue<String>();
static {
//入队列
for (int i = 0; i < 9; i++) {
queue.add("task-" + i);
}
for (String s : queue) {
System.out.println(s);
}
}
public static void main(String[] args) throws Exception {
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
System.out.println(queue.size());
for (int i = 0; i < queue.size(); i++) {
ScheduledFuture<String> scheduledFuture = executorService.schedule(new Callable<String>() {
@Override
public String call() throws Exception {
String value = DaemonTest.queue.poll();
if (value != "" && null != value) {
System.out.println("时间:" + sdf.format(new Date())+"线程" + Thread.currentThread().getName() + " 执行了task: " + value);
}
return "call";
}
}, 1, TimeUnit.SECONDS);
System.out.println(scheduledFuture.get());
}
executorService.shutdown();
}
}
5 newWorkStealingPool,这个是 JDK1.8 版本加入的一种线程池,stealing 翻译为抢断、窃取的意思,它实现的一个线程池和上面4种都不一样,用的是 ForkJoinPool 类,
构造函数代码如下:
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
它是一个并行的线程池,参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别,前面4种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。此外还要注意,这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作。
【注意】
newWorkStealingPool适合使用在很耗时的操作,但是newWorkStealingPool不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中。
以上是5种线程池,但是一般使用newCachedThreadPool线程池。
总结
总结一下线程池在哪些地方用到,比如网络请求、下载、I/O操作等多线程场景,我们可以引入线程池,一个对性能有提升,另一个就是可以让管理线程变得更简单。