【多线程】

进程

什么是进程?

进程 = 程序段 + 数据段 + 进程控制块
  • 程序段(代码段):是进程的程序指令在内存中的位置,包含需要执行的指令集合;
  • 数据段:是进程的操作数据在内存中的位置,包含需要操作的数据集合;
  • 程序控制块(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()获取当前线程实例,才能访问和控制当前线程
    • 不能获取异步执行的结果。

使用 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的七个参数

  1. int corePoolSize 核心线程数
  2. int maximumPoolSize 最大线程数
  3. long keepAliveTime 空闲线程存活时间
  4. TimeUnit unit 时间单位
  5. BlockingQueue workQueue, 阻塞队列
    • ArrayBlockingQueue:数组有界阻塞队列,按FIFO排序
    • LinkedBlockingQueue:链表阻塞队列(设置容量是有界,不设置容量是无界),按FIFO排序
    • PriorityBlockingQueue:优先级的无界队列
    • DelayQueue:无界阻塞延迟队列
    • SynchronousQueue(同步队列):是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,特殊:它不会保存提交的任务,而是直接新建一个线程来执行新来的任务。
  6. ThreadFactory threadFactory, 线程工厂,创建新线程
  7. 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()钩子方法之后的状态。
    在这里插入图片描述

线程池的优雅关闭

  1. shutdown:拒绝新任务的提交,并等待所有任务有序地执行完毕
  2. shutdownNow:是JUC提供一个立即关闭线程池的方法,此方法会打断正在执行的工作线程,并且会清空当前工作队列中的剩余任务,返回的是尚未执行的任务。
  3. 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();

线程池的好处:

  1. 降低资源消耗:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,通过重复利用已创建的线程可以降低线程创建和销毁造成的消耗。
  2. 提高响应速度:当任务到达时,可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性:线程池提供了一种限制、管理资源的策略,维护一些基本的线程统计信息,如已完成任务的数量等。通过线程池可以对线程资源进行统一的分配、监控和调优。

对线程池进行分类

IO密集型任务 – IO操作多

此类任务主要是执行IO操作。由于执行IO操作的时间较长,导致CPU的利用率不高,这类任务CPU常处于空闲状态。Netty的IO读写操作为此类任务的典型例子。
  • IO密集型任务的CPU使用率较低,导致线程空余时间很多,核心线程数=最大线程数=CPU核数*2
NettyReactor(反应器)实现类,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信息,不需要参数传递,可在任何地方方便获取参数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值