Java并发:线程池的基本原理总结

10 篇文章 0 订阅
5 篇文章 0 订阅

为何选择线程池,不用时创建线程呢?

池化技术:提前准备一些资源,在需要时可以重复使用这些预先准备的资源。常见的池化技术的使用有:线程池、内存池、数据库连接池、HttpClient 连接池。

线程池作为池化技术的一种实践,本质上也是同样的思想,提前备好资源以备不时之需。因此,线程池相比较任务出现再创建线程具有以下的优点:

  • 降低资源损耗:通过重复利用已创建的线程降低线程创建和销毁造成的损耗。
  • 提高响应速度:当任务到达时,可以不需要等到线程创建完毕就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。

当一个任务进入线程池,发生了什么?

在这里插入图片描述
如上图所示,一个新任务进入到线程池时,处理流程如下:

判断核心线程池的线程是否都在执行任务,如果不是,则创建一个工作线程来执行此任务
当核心线程池已满时,进入工作队列等待
当工作队列已满,判断线程池是否达到最大线程数,不是,则创建一个工作线程来执行此任务
如果工作队列和线程池都满了,则交给饱和策略来处理这个任务

进程和线程的区别

在操作系统中,正在运行的程序就是进程。比如:QQ,游戏,idea工具等等
说起进程就要提一下程序。程序是指令和数据的有序集合,本身没有运行的含义,是一个静态的概念。
进程就是执行程序的一次过程,是一个动态的概念。系统资源分配的单位。
通常一个进程中可以包含多个线程,并且,一个进程中至少有一个线程。
线程是CPU调度和执行的单位。

如何使用一个线程池?

JAVA有几种线程池

  1. newFixedTreadPool方法创建一个固定长度的线程池。
  2. newCachedTreadPool方法创建一个可缓存的线程池。如果线程池数量超过需求,可以回收空闲线程;如果不够用,则创建新线程。
  3. newSingleThreadExecutor方法创建一个单线程化的线程池。只用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行。
  4. newSingleTreadScheduledExecutor方法返回一个ScheduleExectorService对象,线程大小为1,可以在固定延时之后执行,或者周期性执行某个功能。

线程池七个核心参数的含义

  1. corePoolSize:线程池中常驻的核心线程数。
  2. maximumThreadPool:线程池中能够同时执行的最大线程数。
  3. keepAliveTime:多余的空闲线程存活时间。当线程池中超过corePoolSize的线程,达到
    keepAliveTime时间就会被销毁,保留corePoolSize数量的线程
  4. unit:keepAliveTime的时间单位。
  5. workeQueue:任务队列。线程被提交尚未执行的任务。
  6. threadFactory:线程工厂。用于创建线程。
  7. handler:拒绝策略。当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何拒绝来请求的Runnable的策略。

JAVA有几种线程池

  • newFixedTreadPool方法创建一个固定长度的线程池。
  • newCachedTreadPool方法创建一个可缓存的线程池。如果线程池数量超过需求,可以回收空闲线程;如果不够用,则创建新线程。
  • newSingleThreadExecutor方法创建一个单线程化的线程池。只用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO、LIFO、优先级)执行。
  • newSingleTreadScheduledExecutor方法返回一个ScheduleExectorService对象,线程大小为1,可以在固定延时之后执行,或者周期性执行某个功能。

都不推荐使用!建议使用自定义线程池!
在这里插入图片描述

线程池的创建

我们在创建线程池的过程中,使用底层的new ThreadPollExecutor,


package com.markor.template.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * @describe:
 * @author: caichangmeng <modules@163.com>
 * @since: 2018/10/22
 */
@Configuration
public class ThreadConfig {
    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(3);
        taskExecutor.setMaxPoolSize(7);
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
        return taskExecutor;
    }
}


其源码如下

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0) //检查输入参数是否异常
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException(); //检查工作队列、线程工厂、拒绝策略是否空指针
              //对属性开始赋值
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

可以看到,线程池创建的七大参数:

  • corePoolSize:线程池的基本大小
  • runnableTaskQueue:任务队列
  • ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂来给线程设置有意义的名字
  • RejectedExecutionHandler:饱和策略,默认情况下是AbortPolicy,JDK1.5提供了一下四种策略
  1. AbortPolicy:直接抛出异常
  2. CallerRunsPolicy:只用调用者所在线程来运行任务
  3. DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务
  • DiscardPolicy:不处理丢弃
  • keepAliveTime:线程池的工作线程空闲后,保持存活的时间
  • TimeUnit:线程活动保持时间的单位

向线程池提交任务

目前分为executre()和submit()方法

//execute()提交不需要返回值的任务
threadPool.execute(new Runnable() {
            @Override
            public void run() {
                //TODO 
            }
        });
//submit()提交需要返回值的任务
Callable myCallable = new Callable() {
  @Override
  public String call() throws Exception {
    Thread.sleep(3000);
    return "call方法返回值";
  }
};
Future future = threadPool.submit(myCallable);
String futureRes = future.get();

关闭线程池

//会停止所有正在执行任务的线程
threadPool.shutdownNow();
//只中断没有执行任务的线程
threadPool.shudown();

简述线程池的处理流程

  • 当线程池处理任务时,首先判断核心线程是否已满,没有满则创建核心线程执行。
  • 如果核心线程满了,则判断任务队列是否已满,没有满则把任务放到任务队列中。
  • 如果任务队列也满了,则判断是否达到最大线程数,没有达到则创建临时线程执行。
  • 如果已达到最大线程数,则根据拒绝策略处理任务。

线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?

普通队列只能作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了;
阻塞队列可以保留住当前想要继续入队的任务,使得线程进入wait状态,释放cpu资源,阻塞队列自带阻塞和唤醒的功能。

在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。

合理的配置线程池

1.线程数的配置:
目前公司的项目,用简单的HttpServer暴露prometheus的metrics的内容,设置的是fixThreadPool,线程数设置为5,就是根据Ncpu * 2得到的经验值。

2.建议使用有界队列
有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点儿,比如几千。

threadlocal

threadlocal类的作用

threadlocal类用来提供线程内的局部变量,在多线程环境下访问时,多个线程内的变量不会互相干扰。

threadlocal和synchronized的区别

都用于多线程并发访问变量的问题。但是处理角度和思路不同

  • synchronized关键字原理:同步机制采用了以时间换空间的方式,只提供一份变量,让不同的线程排队访问。(侧重于多个线程之间访问资源的同步)
  • threadlocal类原理:采用了以空间换时间的方式,为每一个线程提供变量的副本,实现同时访问而相互不干扰。(侧重于多个线程访问,让每个线程的数据相互隔离)

volatile

volatile关键字的作用和原理

实现多线程访问共享变量的可见性:一个线程修改了变量,其他的线程可见。
JMM的Java内存模型,共享变量都保存在主内存中。各个线程中使用的都是从主内存中复制的变量副本。使用volatile关键字修饰共享变量时,在第三步子线程修改了共享变量的值并提交给了主内存中,CPU会通过一种机制,让主线程中的该变量副本失效,让后从主内存中重新获取最新的变量副本。

在这里插入图片描述

volatile和synchronized的区别

  • volatile只能修饰实例变量和类变量;synchronized可以修饰方法和代码块。
  • volatile保证数据的可见性,但是不保证数据的原子性(多线程写操作,不保证线程安全); synchronized是一种排它机制,保证可见性,也保证原子性。
  • volatile用于禁止指令重排。

线程

线程的生命周期和状态

线程的5个状态:
创建状态:Thread t = new Thread线程一旦创建就进入到新生状态。(new)
就绪状态:当调用start()方法,线程立即进入就绪状态;但并不一定立即被调用执行。
运行状态:获得CPU调度进入运行状态;进入运行状态,线程才开始执行线程体的代码块。
阻塞状态:当调用sleep、wait或同步锁时,线程进入阻塞状态(即代码不再执行),阻塞事件解除后,重新进入就绪状态,等待CPU调度。
死亡状态:线程中断或者结束,线程进入死亡状态;一旦进入死亡状态,就不能再次启动。

详情:https://www.kuangstudy.com/bbs/1363147273537597441#header19

sleep()、wait()、join()、yield()的区别

等待池

等待池是针对wait方法的,当我们调用wait方法时,线程会进入等待池中,等待池中的线程不会去竞争锁,只用调用了notify、notifyall等待池中的线程才会去竞争锁。
notify方法是随机让等待池中的一个线程放入锁池,notifyall是让等待池中所有的线程进入锁池。

锁池

所有需要竞争同步锁的线程会放在锁池中,比如当前对象的锁被一个线程拿到,那其他的线程就需要在锁池中等待,当前面的线程释放锁后,其他线程再去竞争锁,拿到锁的线程就会进入就绪状态,等待CPU的分配。

区别

  • sleep是thread类的一个静态方法,wait是Object类的本地方法。
  • sleep方法不会释放锁,wait方法会释放锁,并进入到等待队列中。
  • sleep方法不依赖于同步器synchronized;但是wait需要依赖synchronized关键字使用。
  • sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要唤醒。(不指定时间需要被别人唤醒)。
  • sleep一般用于当前线程休眠,或者轮循暂停操作;wait则多用于多线程之间的通信。
  • yield()方法执行后,线程进入就绪状态,释放CPU的执行权,让CPU重新进行调度。
  • join执行后线程进入阻塞状态;比如B线程调用A线程的join方法,B线程进入阻塞状态,直到A线程线程或中断,才会继续执行B线程的代码。

死锁

什么是死锁?怎么解决死锁

多个线程持有共享资源的一部分,都互相需要对方手里的资源才能执行,产生了僵持,都不往下执行。比如:线程A持有锁A,想要获得锁B;线程B持有锁B,想要获得锁A。

产生死锁的四个必要条件

互斥条件:当资源被一个进程使用时,别的进程不能使用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持占用。
不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

上面列出了死锁的四个必要条件,我们只要想办法破其中的任意一个或多个条件就可以避免死锁发生。

互斥条件是非共享资源所必须的,应加以保持不应破坏;所以破坏其他三个条件解决死锁:

1.破坏请求与保持条件:所有的进程在开始运行之前,必须一次性地申请所有需要的资源。如果无法一次性申请,那就进行等待。
2.破坏不剥夺条件: 当某个进程获得一部分资源,要去申请另一个资源时,如果申请不到,那就主动释放自己占用的所有资源。
3.破坏循环等待条件:可以给每个资源标上序号,按序申请,先申请资源序号小的,再申请资源序号大的,就可以避免循环等待。

还有著名的银行家算法避免死锁。

https://www.kuangstudy.com/bbs/1363147273537597441#header35

线程通信的方式

主要是通过wait方法、notify方法进行线程之间的通信。比如:

管程法:并发协作模型“生产者/消费者模式”>管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
  • 消费者:负责处理数据的模块(可能是方法,对象,线程,进程);
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区”,生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据。

信号灯法:并发协作模型“生产者/消费者模式”->信号灯法

设置标志位,通过标志位来进行通知需要等待和需要唤醒的线程。
比如:演员表演时,观众需要等待(true);演员等待时(false),观众进行观看。

https://www.bilibili.com/video/BV1V4411p7EF?p=24

谈谈你对线程安全的理解

线程安全就是内存安全,堆是共享的内存,可以被所有的线程访问。

线程安全

当多个线程访问同一个对象,如果不进行额外的同步控制,调用这个对象的行为都可以获得正确的结果(和单线程执行的结果一致),我们称之为线程安全。

  • 堆是进程和线程共有的空间,分全局堆和局部堆;全局堆就是所有没有分配的空间,局部堆就是分配给进程的空间。
  • 堆是在操作系统对进程进行初始化的时候分配的,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏。
    (在Java中,堆是在虚拟机启动时创建。堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。)

栈是每个线程独有的,保存其运行状态和局部自动变量的。
栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的。
操作系统在切换线程的时候会自动切换栈。(栈空间不需要在高级语言里面显式的分配和释放)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

LC超人在良家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值