Java线程和线程池知识整理【较详细】

前言

在讲多线程之前,首先明白几个概念
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操作等多线程场景,我们可以引入线程池,一个对性能有提升,另一个就是可以让管理线程变得更简单。

【参考1】进入
【参考2】进入
【参考3】进入

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值