进程
什么是进程?
进程 = 程序段 + 数据段 + 进程控制块
- 程序段(代码段):是进程的程序指令在内存中的位置,包含需要执行的指令集合;
- 数据段:是进程的操作数据在内存中的位置,包含需要操作的数据集合;
- 程序控制块(PCB):是进程存在的唯一标志,包含进程的描述信息和控制信息。
线程:
什么是线程?
线程是CPU调度的最小单位。一个进程可以有一个或多个线程,各个线程之间共享进程的内存空间、系统资源,进程仍然是操作系统资源分配的最小单位。
线程 = 线程描述信息 + 程序计数器(PC)+ 栈内存
栈帧: 局部变量表、操作栈、动态链接、方法出口
进程 和 线程的区别:
1)线程是“进程代码段”的一次的顺序执行流程。一个进程由一个或多个线程组成;一个进
程至少有一个线程。
2)线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于
进程,使得多线程程序的并发性高。
3)线程是出于高并发的调度诉求从进程内部演进而来的。线程的出现既充分发挥CPU的计算
性能,又弥补了进程调度过于笨重的问题。
4)进程之间是相互独立的,但进程内部各个线程之间并不完全独立。各个线程之间共享进程
的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。
5)切换速度不同,线程上下文切换比进程上下文切换要快得多。所以,有时线程也称为轻量
级进程。
线程的状态
- 新建
- 就绪
- 调用线程的start()方法,此线程进入就绪状态
- 当前线程的执行时间片用完
- 线程睡眠(sleep)操作结束
- 对其他线程合入(join)操作结束
- 等待用户输入结束
- 线程争抢到对象锁(Object Monitor)
- 当前线程调用了yield()方法出让CPU执行权限
- 运行
- 阻塞
- 线程等待获取锁
- IO阻塞
- 等待
- Object.wait()方法,对应的唤醒方式为:Object.notify() / Object.notifyAll()
- Thread.join()方法,对应的唤醒方式为:被合入的线程执行完毕
- LockSupport.park()方法,对应的唤醒方式为:LockSupport.unpark(Thread)
- 限时等待
- Thread.sleep(time)方法,对应的唤醒方式为:sleep睡眠时间结束
- Object.wait(time)方法,对应的唤醒方式为:调用Object.notify() / Object.notifyAll()去主动唤醒,或者限时结束
- LockSupport.parkNanos(time)/parkUntil(time)方法,对应的唤醒方式为:线程调用配套的
- LockSupport.unpark(Thread)方法结束,或者线程停止(park)时限结束
- 结束
- 正常或者异常
- 正常或者异常
stop:强制终止正在运行的线程,线程不安全,进入终止状态
interrupt:设置中断位,不直接中断线程,只在合适的位置等待中断
yield:让正在执行的线程放弃当前的执行,让出CPU的执行权限,去执行其他的线程(偏向于将执行机会让给优先级较高的线程)。进入就绪状态
daemon:守护线程
用户线程和JVM进程是主动关系,如果用户线程全部终止,JVM虚拟机进程也随之终止;
守护线程和JVM进程是被动关系,如果JVM进程终止,所有的守护线程也随之终止
join:线程A需要在合并点等待,一直等到线程B执行完成,或者等待超时。进入等待状态
//重载版本1:此方法会把当前线程变为WAITING,直到被合并线程执行结束
public final void join() throws InterruptedException://重载版本2:此方法会把当前线程变为TIMED_WAITING,直到被合并线程结束,或者等待被合并线程执行millis 的时间
public final synchronized void join(long millis) throws InterruptedException://重载版本3:此方法会把当前线程变为TIMED_WAITING,直到被合并线程结束,或者等待被合并线程执行
millis+nanos的时间
public final synchronized void join(long millis, int nanos) throws InterruptedException:
甲方线程调用乙方线程的join()方法,在执行流程上将乙方线程合并到甲方线程。甲方线程等待乙方线程执行完成后,甲方线程再继续执行
sleep 和 wait 主要有以下区别:
- 所属类不同:sleep 是 Thread 线程类的静态方法,而 wait 是 Object 顶级类的普通方法。
- 持有锁的状态不同:sleep()方法导致了程序暂停执行指定的时间,让出 CPU 给其他线程,但是他的监控状态依然保持,当指定的时间到了又会自动恢复运行状态。在调用 sleep() 方法的过程中,线程不会释放对象锁。而 wait() 方法则不同,当线程调用 wait() 方法的时候,线程会释放对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify() 方法后本线程才进入对象锁定池准备。
- 应用场景不同:sleep 可以在任何地方使用,而 wait 只能在同步方法或者同步块中使用。
创建线程
继承Thread
实现Runnable接口
-
优点:
- 避免由于Java单继承带来的局限性
- 逻辑和数据更好分离,适合同一个资源被多段业务逻辑并行处理的场景。
在同一个资源被多个线程逻辑去异步、并行处理的场景中,通过实现Runnable接口的方式更好地做到多个线程并发地完成同一个任务,访问同一份数据资源,可以更加方便、清晰地将执行逻辑和数据存储分离,更好地体现了面向对象的设计思想。
//商场商品类型(target销售线程的目标类),一个商品最多销售4次,可以多人销售
public class Goods implements Runnable{
//多人销售可能导致数据出错,使用原子数据类型保障数据安全
private AtomicInteger goodsAmount = new AtomicInteger(5);
public void run(){
for (int i = 0; i <= 5; i++){
if (this.goodsAmount.get() > 0){
System.out.println(getCurThreadName() + " 卖出一件,还剩:" + (goodsAmount.decrementAndGet()));
sleepMilliSeconds(10);
}
}
System.out.println(getCurThreadName() + " 运行结束.");
}
public static void main(String args[]) throws InterruptedException{
Print.hint("商场版本的销售");
Goods goods = new Goods();
// 商场招聘了3个不同的商场销售员,共享了一个Runnable类型的target执行目标实例——Goods实例
for (int i = 1; i <= 3; i++){
Thread thread = null;
thread = new Thread(goods, "商场销售员-" + i);
thread.start();
}
System.out.println(getCurThreadName() + " 运行结束.");
}
}
- 缺点:
- 所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程
类的构造器,才能创建真正的线程 - 如果访问当前线程的属性(甚至控制当前线程),不能直接访问Thread的实例方法,必须
通过Thread.currentThread()获取当前线程实例,才能访问和控制当前线程 - 不能获取异步执行的结果。
- 所创建的类并不是线程类,而是线程的target执行目标类,需要将其实例作为参数传入线程
使用 Callable 和 FutureTask 创建线程
- RunnableFuture继承了Runnable接口,保证了其实例可以作为Thread线程实例的target目标;
- RunnableFuture通过继承Future接口,保证了通过它可以获取未来的异步执行结果;
- RunnableFuture只是一个接口,无法直接创建对象;
Future接口三大功能:
1)cancel():能够取消异步执行中的任务。
2)isDone():判断异步任务是否执行完成。
3)get():获取异步任务完成后的执行结果。
FutureTask类才是真正的在Thread与Callable之间搭桥的类
//①创建一个Callable接口的实现类
public class CallableTask implements Callable<Long> {
//②编写好异步执行的具体逻辑,可以有返回值
public Long call() throws Exception{
long startTime = System.currentTimeMillis();
System.out.println(getCurThreadName() + " 线程运行开始.");
Thread.sleep(1000);
for (int i = 0; i < COMPUTE_TIMES; i++) {
int j = i * 10000;
}
long used = System.currentTimeMillis() - startTime;
System.out.println(getCurThreadName() + " 线程运行结束.");
return used;
}
public static void main(String args[]) throws InterruptedException {
CallableTask task = new CallableTask();//③
FutureTask<Long> futureTask = new FutureTask<Long>(task);//④
Thread thread = new Thread(futureTask, "returnableThread");//⑤
thread.start();//⑥
Thread.sleep(500);
System.out.println(getCurThreadName() + " 让子弹飞一会儿.");
System.out.println(getCurThreadName() + " 做一点自己的事情.");
for (int i = 0; i < COMPUTE_TIMES / 2; i++) {
int j = i * 10000;
}
System.out.println(getCurThreadName() + " 获取并发任务的执行结果.");
try {
System.out.println(thread.getName()+"线程占用时间:" + futureTask.get());//⑦
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(getCurThreadName() + " 运行结束.");
}
}
1)futureTask的结果outcome不为空,callable.call()执行完成。在这种情况下,futureTast.get会
直接取回outcome结果,返回给main线程(结果获取线程)。
2)futureTask的结果outcome为空,callable.call()还没有执行完。在这种情况下,main线程作为
结果获取线程会被阻塞住,一直被阻塞到callable.call()执行完成。当执行完后,最终结果保存到
outcome中,futureTask会唤醒main线程去提取callable.call()执行结果。
线程池
Executors类
-
newSingleThreadExecutor 创建一个线程的线程池,
LinkedBlockingQueue无界的阻塞队列
-
newFixedThreadPool(int nThreads) 创建固定大小的线程池,
LinkedBlockingQueue无界的阻塞队列
,适用于处理CPU密集型的任务,在CPU被工作线程长时间使用的情况下,能确保尽可能少地分配线程 -
newCachedThreadPool() 创建一个不限制线程数量的线程池(依赖于操作系统(或者说JVM)能够创建的最大线程大小),
SynchronousQueue同步队列
,任何提交的任务都将立即执行,但是空闲线程会得到及时回收,适用场景:需要快速处理突发性强、耗时较短的任务场景,如Netty的NIO处理场景、REST API接口的瞬时削峰场景
可以无限制创建线程,不会有任务等待,所以才使用SynchronousQueue。
当“可缓存线程池”有新任务到来时,新任务会被插入到SynchronousQueue实例中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程,则线程池会创建一个线程来执行该任务。
SynchronousQueue是一个比较特殊的阻塞队列实现类,SynchronousQueue没有容量,每一个插入操作都要等待对应的删除操作,反之每个删除操作都要等待对应的插入操作。也就是说,如果使用SynchronousQueue,提交的任务不会被真实地保存,而是将新任务交给空闲线程执行,如果没有空闲线程,就创建线程,如果线程数都已经大于最大线程数,就执行拒绝策略。使用这种队列需要将maximumPoolSize设置得非常大,从而使得新任务不会被拒绝。
- newScheduledThreadPool() (调用了ScheduledThreadPoolExecutor构造器)创建一个可定期或者延时执行任务的线程池,
DelayedWorkQueue无界工作队列
,
Executors去创建线程池缺点:
(1)FixedThreadPool和SingleThreadPool 这两个工厂方法所创建的线程池,
工作队列
(任务排队的队列)长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM(即耗尽内存资源)。
(2)CachedThreadPool和ScheduledThreadPool
这两个工厂方法所创建的线程池允许创建的线程数量
为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM问题。
ThreadPoolExecutor的七个参数
- int corePoolSize 核心线程数
- int maximumPoolSize 最大线程数
- long keepAliveTime 空闲线程存活时间
- TimeUnit unit 时间单位
- BlockingQueue workQueue, 阻塞队列
- ArrayBlockingQueue:数组有界阻塞队列,按FIFO排序
- LinkedBlockingQueue:链表阻塞队列(设置容量是有界,不设置容量是无界),按FIFO排序
- PriorityBlockingQueue:优先级的无界队列
- DelayQueue:无界阻塞延迟队列
- SynchronousQueue(同步队列):是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,特殊:它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。
- ThreadFactory threadFactory, 线程工厂,创建新线程
- RejectedExecutionHandler handler) 拒绝策略
- AbortPolicy:拒绝策略,拒绝新任务(默认),抛出 RejectedExecutionException 异常
- DiscardPolicy:抛弃策略,新任务就会直接被丢掉,并且不会有任何异常抛出。
- DiscardOldestPolicy:抛弃最老任务策略,将最早进入队列的任务抛弃
- CallerRunsPolicy:调用者执行策略,提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务
- 自定义异常,实现RejectedExecutionHandler,重写rejectedExecution方法
向ExecutorService线程池提交异步执行target目标任务的常用方法有:
//方法一:执行一个 Runnable类型的target执行目标实例,无返回
void execute(Runnable command);
//方法二:提交一个 Callable类型的target执行目标实例,返回一个Future异步任务实例
<T> Future<T> submit(Callable<T> task);
//方法三:提交一个 Runnable类型的target执行目标实例,返回一个Future异步任务实例
Future<?> submit(Runnable task);
execute(…)与submit(…)方法的区别:
- Submit():
- 可以接收Callable、Runnable两种类型的参数;
- 提交任务后有返回值;
- 允许抛出异常,Future.get()方法获取执行结果时,可以捕获异步执行过程中抛出的受检异常和运行时异常,并进行对应的业务处理
- execute():
- 只能接收Runnable类型的参数;
- 提交任务后无返回值;
- 不允许抛出异常
钩子方法
//任务执行之前的钩子方法(前钩子)
protected void beforeExecute(Thread t, Runnable r) { }
//任务执行之后的钩子方法(后钩子)
protected void afterExecute(Runnable r, Throwable t) { }
//线程池终止时的钩子方法(停止钩子)
protected void terminated() { }
线程池的五种状态:
- RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
- SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
- STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
- TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。
- TERMINATED:执行完terminated()钩子方法之后的状态。
线程池的优雅关闭
- shutdown:拒绝新任务的提交,并等待所有任务有序地执行完毕
- shutdownNow:是JUC提供一个立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。
- awaitTermination:等待线程池完成关闭。在调用线程池的shutdown()与shutdownNow()方法时,当前线程会立即返回,不会一直等待直到线程池完成关闭。如果需要等到线程池关闭完成,可以调用awaitTermination()方法。
shutdown()的原理
public void shutdown(){
// 加锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try{
// 检查调用者是否具有执行线程池关闭的Java Security权限
checkShutdownAccess();
// 设置线程池状态,不再接受新提交的任务
// 如果还继续往线程池提交任务,将会使用线程池拒绝策略响应
advanceRunState(SHUTDOWN);
// 中断空闲线程
interruptIdleWorkers();
// 钩子函数,主要用于清理一些资源
onShutdown();
} finally{
mainLock.unlock();
}
tryTerminate();
}
shutdownNow()的原理
// 加锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
// 检查状态
checkShutdownAccess();
// 将线程池状态变为 STOP
advanceRunState(STOP);
// 中断所有线程,包括工作线程以及空闲线程(interrupt()实例方法设置了中断状态)
interruptWorkers();
// 丢弃工作队列中剩余任务
tasks = drainQueue();
线程池的好处:
- 降低资源消耗:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,通过重复利用已创建的线程可以降低线程创建和销毁造成的消耗。
- 提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:线程池提供了一种限制、管理资源的策略,维护一些基本的线程统计信息,如已完成任务的数量等。通过线程池可以对线程资源进行统一的分配、监控和调优。
对线程池进行分类
IO密集型任务 – IO操作多
此类任务主要是执行IO操作。由于执行IO操作的时间较长,导致CPU的利用率不高,这类任务CPU常处于空闲状态。Netty的IO读写操作为此类任务的典型例子。
- IO密集型任务的CPU使用率较低,导致线程空余时间很多,
核心线程数=最大线程数=CPU核数*2
Netty的Reactor(反应器)实现类,IO事件处理线程数默认值为CPU核数的两倍
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1,
SystemPropertyUtil.getInt("io.netty.eventLoopThreads",
// 获取CPU数
Runtime.getRuntime().availableProcessors() * 2)
);
}
CPU密集型任务 – 计算多
此类任务主要是执行计算任务。由于响应时间很快,CPU一直在运行,这种任务CPU的利用率很高。
核心线程数=最大线程数 = CPU数
混合型任务
此类任务既要执行逻辑计算,又要进行IO操作(如RPC调用、数据库访问)。相对来说,由于执行IO操作的耗时较长(一次网络往返往往在数百毫秒级别),这类任务的CPU利用率也不是太高。Web服务器的HTTP请求处理操作为此类任务的典型例子。
最佳线程数目 =(线程等待时间与线程CPU时间之比 + 1)* CPU核数
ThreadLocal 线程本地变量
创建一个ThreadLocal实例,在访问这个变量值时,每个线程都会拥有一个独立的、自己的本地值,不受其他线程干扰。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。
ThreadLocal内部有个静态内部类ThreadLocalMap(去掉桶结构,当发生哈希碰撞会将相同的Entry放到槽点后面相邻的空闲位 – 开放定址法
),
- ThreadLocalMap是ThreadLocal的一个静态内部类,其实现了一套简单的Map结构
- Entry,Entry 的 Key 需要使用弱引用
- 每一个Thread实例拥有一个Map实例
- Key = ThreadLocal实例
- Value = 待保存的值
方法 | 说明 |
---|---|
set(T value) | 设置当前线程在“线程本地变量”实例中绑定的本地值 |
T get() | 获得当前线程在“线程本地变量”实例中绑定的本地值 |
remove() | 移除当前线程在“线程本地变量”实例中绑定的本地值 |
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//根据key的HashCode,找到key在数组上的槽点i
int i = key.threadLocalHashCode & (len-1);
// 从槽点i开始向后循环搜索,找空余槽点(空余位置)或者找现有槽点
//如果没有现有槽点,则必定有空余槽点,因为没有空间时会扩容
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//找到现有槽点:Key值为ThreadLocal实例
if (k == key) {
e.value = value;
return;
}
//找到异常槽点:槽点被GC掉,重设Key值和Value值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//没有找到现有的槽点,增加新的Entry
tab[i] = new Entry(key, value);
//设置ThreadLocal数量
int sz = ++size;
//清理Key为null的无效Entry
//没有可清理的Entry,并且现有条目数量大于扩容因子值,进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
Entry 的 Key 需要使用弱引用,避免内存泄漏
什么是弱引用呢?
仅有弱引用(Weak Reference)指向的对象只能生存到下一次垃圾回收之前。换句话说,当GC发生时,无论内存够不够,仅有弱引用所指向的对象都会被回收。而拥有强引用指向的对象则不会被直接回收。
什么叫作内存泄漏?
不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。对于持续运行的服务进程必须及时释放内存,否则内存占用量越来越高,轻则影响系统性能,重则导致进程崩溃。
当线程tn执行完funcA()方法后,栈帧将被销毁,强引用local的值也就没有了,
但此时线程的ThreadLocalMap中对应的Entry的Key引用还指向了ThreadLocal实例
如果Entry的Key引用是强引用,就会导致Key引用指向的ThreadLocal实例及其Value值都不能被GC回收,这将造成严重的内存泄漏
ThreadLocal会发生内存泄漏的前提条件如下:
- 线程长时间运行而没有被销毁。
- ThreadLocal引用被设置为null,且后续在同一Thread实例的执行期间,没有发生对其他ThreadLocal实例的get()、set()或remove()操作。去触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉ThreadLocal弱引用为null的Entry,
编程规范推荐使用 private static final 修饰 ThreadLocal 对象,用完记得remove
- private:缩小使用的范围,尽可能不让他人引用
- static:确保ThreadLocal实例的全局唯一,一个线程内所有操作是共享,静态变量会在类第一次被使用时装载,只会分配一次存储空间,此类的所有实例都会共享这个存储空间
- final:防止其在使用过程中发生动态变更
- remove:防止内存泄漏,用static、final修饰,使得Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏,使用完后必须使用remove()进行手动释放
使用场景
(1)线程隔离:
多线程环境下,防止自己的变量被其他线程篡改,各线程数据隔离,避免同步锁带来的性能损失。
- 数据库连接独享
- session数据管理
(2)跨函数传递数据:
同一个线程内,跨类、跨方法传递数据时,使用ThreadLocal设置后,可以在任意地方获取,避免耦合
- 为每个线程绑定一个session信息,不需要参数传递,可在任何地方方便获取参数