进程与线程
两者的区别:
-
进程是计算机资源(CPU、内存等)分配基本单位;为避免进程间的互相干扰,每个进程都拥有自己独立的地址空间。
-
线程是CPU调度和分派的基本单位,是程序执行时的最小单位;一个进程可以由很多个线程组成;线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。
-
线程切换的速度,高于进程切换的速度;线程创建的开销,低于进程创建的开销
-
多进程的程序更加健壮。多线程程序中,一个 线程的崩溃 ,很有可能导致整个进程结束,但一个 进程的崩溃 ,并不会影响其他进程。
-
线程间的通信可以使用进程中共享的数据来实现,进程间的通信有如下几种方式
进程间的通信方式:
-
管道(半双工下的父子进程的通信)》》流管道(全双工的父子进程通信)》》命名管道(全双公共的任意进程的通信)
-
信号量:主要应用于进程或同一进程内不同线程的同步
-
消息队列:适合需要承载多信息量的通信
-
共享内存:开辟一块各个进程都可以访问的内存区域,通常与信号量结合起来实现同步互斥
-
套接字(Socket)/ 远程过程调用(RPC) :应用于不同机器,不同进程间的通信
线程间的通信方式:
-
锁机制:包括互斥锁、条件变量、读写锁
互斥锁:提供了以排他方式防止数据结构被并发修改的方法。
读写锁:允许多个线程同时共享数据,而对写操作是互斥的。
条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。 -
信号量机制(Semaphore):包括无名进程信号量和命名线程信号量
-
信号机制(Signal):类似进程间的信号处理
Java中线程间的通信方法:
线程间的同步:
- 关键字 synchronized 进行加锁同步
- Lock类下的 lock / trylock 进行加锁同步
线程间的通知
- Object 类下的 wait / notify
- 阻塞当前线程,会释放锁(所以该方法的 使用前提是当前线程已经拥有 monitor 锁)
- 唤醒等待当前 monitor 的一个线程
- Thread 类下的 sleep / yield / join
- 睡眠线程指定的毫秒,不释放锁
- 让步当前线程的执行权,有不确定性(使用较少)
- 父线程阻塞等待子线程执行完成后,再执行
fork函数对进程的复制,执行:
-
fork函数在复制子线程的过程中,会有关于标准IO库是带缓冲的,缓存区未被刷新,将被子线程复制;缓存区被刷新,子线程不会复制到其中内容(标准输出(printf之类的)的缓冲区由换行符刷新,但换行符不会刷新文件的缓冲区)
-
fork函数虽然复制了整个进程,但其是从fork的位置下,开始执行子进程的
-
fork函数将返回一个pid,这个pid在父进程中是子进程的pid值,在子进程中pid为0
记住上面三条后,然后参考两个用例对其的解释:
https://it.baiked.com/dev/1027.html :::::: https://www.cnblogs.com/tp-16b/p/9005079.html
线程生命周期中的五种状态
出生、就绪、执行、阻塞、死亡
线程的创建和创建数量限制:
Java中线程的创建方式:
- 继承Thread
- 实现Runnable接口
- 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 中的参数:
问题:
- 当为 ThreeadPoolExecutor 设置任务队列时,进入队列中的任务很有可能会被延迟执行,对应的业务是否能接受这种延迟?
- 当不设置任务队列时,任务数过多后,会出现任务被拒绝抛弃,如何估算一个合适的线程数?
最大线程数 (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 中的使用与关闭,将会通过实际的案例,去分析、解决这些问题。