多线程系列:一、进程与线程、Java中的线程池

进程与线程

两者的区别:

  1. 进程是计算机资源(CPU、内存等)分配基本单位;为避免进程间的互相干扰,每个进程都拥有自己独立的地址空间。

  2. 线程是CPU调度和分派的基本单位,是程序执行时的最小单位;一个进程可以由很多个线程组成;线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。

  3. 线程切换的速度,高于进程切换的速度;线程创建的开销,低于进程创建的开销

  4. 多进程的程序更加健壮。多线程程序中,一个 线程的崩溃 ,很有可能导致整个进程结束,但一个 进程的崩溃 ,并不会影响其他进程。

  5. 线程间的通信可以使用进程中共享的数据来实现,进程间的通信有如下几种方式

进程间的通信方式:

  1. 管道(半双工下的父子进程的通信)》》流管道(全双工的父子进程通信)》》命名管道(全双公共的任意进程的通信)

  2. 信号量:主要应用于进程或同一进程内不同线程的同步

  3. 消息队列:适合需要承载多信息量的通信

  4. 共享内存:开辟一块各个进程都可以访问的内存区域,通常与信号量结合起来实现同步互斥

  5. 套接字(Socket)/ 远程过程调用(RPC) :应用于不同机器,不同进程间的通信

更多关于进程通信的介绍

线程间的通信方式:

  1. 锁机制:包括互斥锁、条件变量、读写锁
    互斥锁:提供了以排他方式防止数据结构被并发修改的方法。
    读写锁:允许多个线程同时共享数据,而对写操作是互斥的。
    条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

  2. 信号量机制(Semaphore):包括无名进程信号量和命名线程信号量

  3. 信号机制(Signal):类似进程间的信号处理

Java中线程间的通信方法:

线程间的同步:

  • 关键字 synchronized 进行加锁同步
  • Lock类下的 lock / trylock 进行加锁同步

线程间的通知

  • Object 类下的 wait / notify
    • 阻塞当前线程,会释放锁(所以该方法的 使用前提是当前线程已经拥有 monitor 锁
    • 唤醒等待当前 monitor 的一个线程
  • Thread 类下的 sleep / yield / join
    • 睡眠线程指定的毫秒,不释放锁
    • 让步当前线程的执行权,有不确定性(使用较少)
    • 父线程阻塞等待子线程执行完成后,再执行

fork函数对进程的复制,执行:

  1. fork函数在复制子线程的过程中,会有关于标准IO库是带缓冲的,缓存区未被刷新,将被子线程复制;缓存区被刷新,子线程不会复制到其中内容(标准输出(printf之类的)的缓冲区由换行符刷新,但换行符不会刷新文件的缓冲区)

  2. fork函数虽然复制了整个进程,但其是从fork的位置下,开始执行子进程的

  3. fork函数将返回一个pid,这个pid在父进程中是子进程的pid值,在子进程中pid为0

记住上面三条后,然后参考两个用例对其的解释:

https://it.baiked.com/dev/1027.html :::::: https://www.cnblogs.com/tp-16b/p/9005079.html

更多Linux进程介绍


线程生命周期中的五种状态

出生、就绪、执行、阻塞、死亡

在这里插入图片描述

线程的创建和创建数量限制:

Java中线程的创建方式:

  1. 继承Thread
  2. 实现Runnable接口
  3. FutureTask和Callable创建有返回值的子线程

三种方式下的多线程使用的安全用例

线程创建的数量限制:

影响 JVM 创建线程数量的两个因素:

  • 系统限制:
    /proc/sys/kernel/pid_max:线程的最大 PID 值
    /proc/sys/kernel/thread-max:最大线程
    max_user_process(ulimit -u):某用户下的最大线程
    /proc/sys/vm/max_map_count:一个进程可以映射虚拟内存区域的数量
  • Java虚拟机本身:-Xms,-Xmx,-Xss

JVM最多能启动的线程数参照公式:

(MaxProcessMemory - JVMMemory – ReservedOsMemory) / (ThreadStackSize) = Number of threads

1. MaxProcessMemory : 进程的最大寻址空间

   32位的 Linux 默认每个进程最多申请3G的地址空间,64位的操作系统可以支持到46位(64TB)的物理地址空间和47位(128T)的进程虚拟地址空间。

2. JVMMemory : JVM内存

​   由Heap区和Perm区组成。通过-Xms和-Xmx可以指定heap区大小,通过-XX:PermSize和-XX:MaxPermSize指定perm区的大小(默认从32MB 到64MB,和JVM版本有关)。

3.ReservedOsMemory

   保留的操作系统内存,如Native heap,JNI之类,一般100多M

4.ThreadStackSize : 线程栈的大小,JVM 启动时可以使用 Xss 指定

   Stack Space的空间是独立分配的,不使用 Heap 空间。Stack Space用来做方法的递归调用时压入 Stack Frame。所以当递归调用太深的时候,就有可能耗尽 Stack Space,抛出 StackOverflow 的错误。

   对于32位 JVM,缺省值为 256KB,对于JDK 8 及以上的 64 位 JVM,缺省值为 1MB。最大值根据平台和特定机器配置的不同而不同。


ThreeadPoolExecutor :

   Executors 为我们提供了简易的线程池构建方法,都是用 ThreeadPoolExecutor 去进行封装的。

   Executors 构建线程池可能会 堆积大量的请求创建大量的线程 ,从而导致OOM,所以应用中禁止使用。应用中的线程池应使用 ThreeadPoolExecutor 构建。

//五个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue)
 
//六个参数的构造函数-1
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory)
 
//六个参数的构造函数-2
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler)
 
//七个参数的构造函数
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

不在构造器中的参数:
/**
 * If false (default), core threads stay alive even when idle.
 * If true, core threads use keepAliveTime to time out waiting
 * for work.
 */
private volatile boolean allowCoreThreadTimeOut;

参数含义:

int corePoolSize:该线程池中核心线程数最大值核心线程

  线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程,核心线程默认情况下会一直存活在线程池中,即使为闲置状态。如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉。

int maximumPoolSize:该线程池中线程总数最大值

线程总数 = 核心线程数 + 非核心线程数。

long keepAliveTime:线程池中线程等待任务的超时时长

一个非核心线程,如果获取任务(即闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉。默认情况下(allowCoreThreadTimeOut = false),只作用于非核心线程,也可以使其作用于核心线程。

TimeUnit unit:keepAliveTime时间单位

BlockingQueue workQueue:该线程池中的任务队列,维护着等待执行的 Runnable 对象

   当核心线程数已经到达最大值时,新添加的任务会被添加到这个队列中等待处理。

常用的workQueue类型:

  • SynchronousQueue
    不存储元素 的阻塞队列,每一个 put 操作必须等待一个 take 操作,否则不能继续添加元素。因此这里的 Synchronous 指的是读线程和写线程需要同步,数据必须从某个写线程交给某个读线程,而不是在队列中等待被消费。

  • LinkedBlockingQueue:链表实现的有界阻塞队列,元素排序按 FIFO。注意:该队列默认大小为 Integer.MAX_VALUE。

ExecutorService fixedThreadPool = new ThreadPoolExecutor(6, 10, 60L, TimeUnit.SECONDS,new LinkedBlockingQueue<>(18));
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            fixedThreadPool.execute(new Runnable(){
                @Override
                public void run() {
                    System.out.println(finalI+"fixedThreadPool "+Thread.currentThread().getName());

                    try {
                        Thread.sleep(50000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            });
        }


print:
0fixedThreadPool pool-1-thread-1
2fixedThreadPool pool-1-thread-3
4fixedThreadPool pool-1-thread-5
1fixedThreadPool pool-1-thread-2
24fixedThreadPool pool-1-thread-7
3fixedThreadPool pool-1-thread-4
5fixedThreadPool pool-1-thread-6
25fixedThreadPool pool-1-thread-8
27fixedThreadPool pool-1-thread-10
java.util.concurrent.RejectedExecutionException:......
  • ArrayBlockingQueue:数组实现的有界阻塞队列,元素排序按 FIFO,使用 ReentrantLock 和两个 Condition 对象完成加锁和阻塞的动作。
ExecutorService fixedThreadPool = new ThreadPoolExecutor(6, 10, 60L, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(18));
        for (int i = 0; i < 100; i++) {
            int finalI = i;
            fixedThreadPool.execute(new Runnable(){
                @Override
                public void run() {
                    System.out.println(finalI+"fixedThreadPool "+Thread.currentThread().getName());

                    try {
                        Thread.sleep(50000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            });
        }
print:
RejectedExecutionException.....
1fixedThreadPool pool-1-thread-2
3fixedThreadPool pool-1-thread-4
5fixedThreadPool pool-1-thread-6
25fixedThreadPool pool-1-thread-8
27fixedThreadPool pool-1-thread-10
2fixedThreadPool pool-1-thread-3
0fixedThreadPool pool-1-thread-1
4fixedThreadPool pool-1-thread-5
26fixedThreadPool pool-1-thread-9
24fixedThreadPool pool-1-thread-7
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列,基于数组的二叉堆实现。添加元素时,如果容量不足会使用 tryGrow 进行扩容。

  • DelayQueue:支持延迟获取元素的无界阻塞队列,队列内元素必须实现 Delayed 接口(Delay接口又继承了 Comparable,需要实现 compareTo 方法)。每个元素都需要指明过期时间,通过 getDelay(unit) 获取元素剩余时间(剩余时间 = 到期时间 - 当前时间)
    当从队列获取元素时,只有过期的元素才会出队列。

  • LinkedTransferQueue:由链表组成的无界 TransferQueue

  • LinkedBlockingDeque:由链表构成的界限可选的双端阻塞队列,如不指定边界,则为 Integer.MAX_VALUE。

ThreadFactory threadFactory

创建线程的方式,这是一个接口,你new他的时候需要实现他的Thread newThread(Runnable r)方法

RejectedExecutionHandler handler

当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理;

jdk1.5 后提供了四种饱和策略 :

  • AbortPolicy :默认。直接抛异常(RejectedExecutionException)。

  • CallerRunsPolicy :当队列也满了时,使用提交任务的主线程执行任务。注意:该模式下将降低新任务提交的速度,可能影响主任务的异步执行后续业务。

  • DiscardOldestPolicy :丢弃任务队列中最久的任务。

  • DiscardPolicy :丢弃当前任务。

任务执行机制:

   接收到任务的时候,如果没有达到 corePoolSize 的值,则 新建核心线程 执行任务;如果核心线程数等于 corePoolSize,则 入队 BlockingQueue 等候;如果队列已满,则 新建非核心线程 执行任务;如果总线程数到了 maximumPoolSize,并且队列也满了,则执行 指定的拒绝策略
在这里插入图片描述
思考:
   阻塞队列满时,会构建非核心线程来处理任务,那么非核心线程先执行阻塞队列中的任务还是溢出队列中的任务?

   非核心线程会先执行入队失败的任务

如何正确的设置 ThreeadPoolExecutor 中的参数:

问题:

  1. 当为 ThreeadPoolExecutor 设置任务队列时,进入队列中的任务很有可能会被延迟执行,对应的业务是否能接受这种延迟?
  2. 当不设置任务队列时,任务数过多后,会出现任务被拒绝抛弃,如何估算一个合适的线程数?

最大线程数 (maximumPoolSize)

CPU数量可以根据 Runtime.getRuntime().availableProcessors() 方法获取。

  • CPU密集型:线程池的大小推荐为 CPU核心数+1,避免开启过多的线程数,导致线程上下文切换次数增加,带来额外的开销。

  • IO密集型,两种方式:

    • 不大于 2 * CPU核心数 ,让 CPU 在等待 IO 的时候,可以切换到其他任务执行
    • 《Java 虚拟机并发编程》:CPU核心数 /(1-阻塞系数),阻塞系数=阻塞时间/(阻塞时间+计算时间)。如果一类任务 90% 的时间都是阻塞状态,那么阻塞系数为0.9。
  • 混合型任务:将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。

阻塞队列

使用有界队列,避免资源耗尽,进而导致 OOM 的情况发生

拒绝策略

默认采用的是 AbortPolicy 拒绝策略,直接在程序中抛出 RejectedExecutionException 异常。可根据情况选择需要的拒绝策略。

美团团队对线程池的使用介绍

任务队列的深入学习

LinkedBlockingQueue 中,是如何入队、出队的?

结论:
   LinkedBlockingQueue 中,对链表两端添加 ReentrantLock(头部为 takeLock,尾部为 putLock),完成入队和出对的阻塞操作。

   元素入队时(put方法),通过 notFull ( putLock 下的 Condition)进行阻塞和唤醒添加任务的线程,并使用 notEmpty(takeLock 下的 Condition)唤醒取任务的线程。
   元素出队时(take方法),通过 notEmpty 进行阻塞和唤醒取任务的线程,并使用 notFull 唤醒添加任务的线程。

源码图解:
   对 java.util.concurrent.LinkedBlockingQueue#put / take 的源码阅读,我们可以更好的了解其原理:

在这里插入图片描述
小问题:
   线程池采用 LinkedBlockingQueue 任务添加机制时,会有问题吗?是怎么样的?

   答案是,在部分特殊情况下,使用 LinkedBlockingQueue 会出现超出预期的任务数情况,然后抛出 RejectedExecutionException 异常的问题,更多内容可以参考本文


思考:

  本篇文章介绍了,进程与线程的区别,线程的创建、管理以及部分原理,但目前 Java 应用大都是结合 Spring 来应用的,那么如何在 Spring 中更好的使用线程池呢?应用重启发布时,如何正确的关闭 Spring 管理的线程池呢?
  下一篇,线程池在 Spring 中的使用与关闭,将会通过实际的案例,去分析、解决这些问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值