多线程
概念
-
线程:一个顺序的单一的程序执行流程就是一个线程,代码一句一句的有先后顺序的执行,线程背设计成进程的执行路径.(线程是CPU调度的基本单位 进程是资源分配的基本单位、进程可以说成是一个执行中的程序)
-
多线程:多个单一顺序执行的流程并发运行,造成感官上同时运行的效果.
-
并发:多个线程走走停停,线程调度程序将CPU运行时间划分为若干个时间片段尽可能分配给每个线程(线程是CPU调度的基本单位),CPU执行时间在纳秒级别,微观走走停停,宏观一起运行.
线程的芝士
1.线程的分类
-
用户线程:是程序中普通的线程,当所有的用户线程都执行完毕后,Java虚拟机会退出.
-
守护线程:用于提供后台服务和支持性工作.用户线程结束后,即使守护线程还未执行完毕,Java虚拟机也会退出,比如垃圾回收线程.
2.wait/notify/notifyAll方法应用
wait方法使当前线程进入等待状态 notify方法只能唤醒同一对象一个线程 notifyAll唤醒所有调用wait方法的对象
和sleep的区别
-
sleep暂停当前线程的执行 进入阻塞状态 wait进入等待状态
-
sleep时线程对象调用的 wait是对象调用的
-
sleep不会释放线程持有的锁 wait主动释放锁
-
sleep时间到自动唤醒 wait需要以上两种方式唤醒
线程的创建?
-
继承Thread并重写run方法(结构简单,匿名内部类)
-
实现Runnable接口单独定义线程任务
-
线程池创建
线程的使用?
-
Runable
实现该接口后使用接口方法 但是不返回结果
-
Callable
实现Callable<V>接口后 返回结果 比如我们的项目中使用多线程调用数据接口后需要返回响应
-
Future
对callable的封装 对功能进行了增强
实现Future<V>接口 可以通过cancel isCanceled isDone 来取消任务 判断任务是否完成
-
CompletableFuture
对Future 和 线程池 的包装
runAsync 没有返回值
supplyAsync 有返回值
获取值的方法 Join Get getNow
项目中有没有用过多线程?
场景
同时发起多个请求,实现并发执行的效果 并发执行 --同时干活,性能更高
什么情况下可以并发执行?相互之间没有顺序依赖
你用过什么线程池?
使用过CompletableFuture
线程池
线程池是一种基于池化技术的多线程管理技术,预先创建一定数量的线程放入一个容器(线程池),这些线程都处于睡眠状态,等待任务分配,有任务时,不用创建一个新线程,从线程池取出一个线程来执行这个任务,执行任务完毕后,不会销毁,回到线程池中等待下一个任务.
优点:显著减少线程创建和销毁,提高系统的响应速度和吞吐量,还可以通过线程池中线程的数量来限制系统的并发量,保护资源不被过渡消耗
Executors 提供了方便的创建线程池的方法,但是不建议使用,应该通过new ThreadPoolExecutor来结合具体的业务设置合理的参数来创建线程池
线程池中常见的参数:
-
corePoolSize(核心池大小):表示线程池中核心线程的数量.
-
maxinumPoolSize(最大池大小):线程池中最大线程的数量.当工作队列已满且池中的线程池数小于 最大池大小时,线程池会创建新的线程处理任务.
-
keepAliveTime(线程空闲时间):线程池中线程的数量超过核心池大小时,空闲线程的存活时间,空闲时间超过keepAliveTime时,多余的线程会被销毁,直到数量等于核心池大小
-
unit(时间单位):用于指定keepAliveTIme的时间单位
-
workQueue(工作队列/任务队列):用于存储等待执行的任务,默认情况下使用无界队列
-
threadFactory(线程工厂):用于创建新线程.
-
handler(拒绝策略):线程池队列已满,无法接受新的任务,Java提供了几种内置的拒绝策略
线程池拒绝的策略?
-
AboutPolicy:丢弃任务并抛出异常(默认拒绝策略)
-
DiscardPolicy:丢弃任务,不抛出异常
-
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务
-
CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
线程池执行任务的过程?
以ThreadPoolExcutor为例,它是Java中线程池的实现类,实现了ExecutorService接口,用于执行任务的管理和调度.下面是ThreadPoolExecutor对象执行任务的过程:
-
状态检查:在执行任务之前,ThreadPoolExecutor会检查自身的状态,例如是否关闭
-
判断核心线程数:ThreadPoolExecutor会检查当前线程池中的线程数量,如果未达到核心线程数(通过corePoolSize指定),则创建新的线程来执行任务
-
任务队列处理:如果已达到核心线程数,ThreadPoolExecutor会将任务放入到阻塞队列(BlockingQueue)实现,等待执行.
-
判断最大线程数:如果任务队列已满并且当前线程池中的线程数量还未达到最大线程数(通过maximumPoolSize指定),则创建新的线程来执行任务
-
拒绝策略:任务队列已满,且当前线程池中的线程数量已达到最大线程数,ThreadPoolExecutor会根据设置的拒绝策略来决定如何处理无法接受的新任务.
-
任务执行:ThreadPoolExecutor会在任务队列中取出任务并将任务分配给工作线程来执行,工作线程会执行任务中的run()方法.
-
结果返回和异常处理:任务执行完成后,可以通过Future对象获取任务的执行结果.如果任务执行过程中发生异常,ThreadPoolExecutor会捕获并处理异常
-
线程池关闭:当需要关闭线程池时,可以调用shutdown()方法.ThreadPoolExecutor会停止接受新任务,并尽量将已提交的任务执行完毕,如果需要立即停止所有的任务并返回尚未执行的任务列表,可以调用shutdowNow()方法.
核心线程池大小如何设置?
-
CPU密集型任务(N+1)(利用CPU计算能力 ):这种任务主要消耗CPU资源,可以将线程数设置为CPU核心数+1,防止线程偶发的缺页中断,或者任务队列暂停,多出来的线程可以利用CPU的空闲时间.
-
I/O密集型任务(2N+1)(网络读取,文件读取):这种任务应用起来,会用大部分时间来处理I/O交互,而线程在处理I/O时不会占用CPU,这时候可以将CPU交出给其它线程使用,所以可以分2N.
Volatile关键字的理解?
在Java中,volatile是一个关键字,用来修饰变量.
-
可见性:保证变量的可变性.一个线程修改了volatile修饰的变量的值时,其它线程可以立即看到修改后的值,不会使用缓存中的旧值
-
禁止指令重排序:禁止了对修饰变量的指令重排序.指令重排序是编译器和处理器为了提高执行效率而进行的重排序的操作,可能会导致多线程程序出现问题
-
不保证原子性:volatile关键字不能保证变量操作的原子性.如果一个操作涉及到多个步骤,且需要保证原子性,需要使用synchronized关键字
什么是线程不可见性?
AB两个线程共同操作共享变量,线程之间相互独立,每个线程都有一个私有的本地内存.两个线程之间是不可见的,并且每个线程在操作共享变量时,是从主内存中把共享变量拷贝到本地内存中的.这也是JMM(Java Memory Model)
解决方案:缓存一致性 MESI协议
为了解决多个核心之间的数据传播问题,提出了总线嗅探策略,就是把所有的请求通过总线广播给所有的核心(通俗的讲 就是BUS 告诉其它所有的线程),让这些核心去嗅探这些请求,再根据本地的状态进行响应.
-
M--已修改Modified:缓存行的数据是脏的,与主内存里的数据不一样。如果其他内核要读主内存的这块数据,该缓存行必须回写到主内存,状态变为共享。
-
E--独占Exclusive:缓存行只在当前缓存中,但是⼲净的,缓存数据等于主内存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。
-
S--共享Shared:缓存行的数据也存在于其它缓存中且是干净的。缓存行可以在任意时刻抛弃。
-
I--⽆效Invalid:缓存行的数据是⽆效的。
ThreadLocal的理解
ThreadLocal是一个线程局部变量.它的主要作用是为每个线程提供一个独立的变量副本,每个线程可以独立地操作自己的副本,互不干扰.
ThreadLocal并不能解决所有地线程安全问题,只是提供了一种隔离机制.
多线程并发安全问题
概念
当多个线程并发操作同一临界资源,由于线程切换时机不确定,导致操作临界资源的顺序出现严重混乱,甚至可以导致系统瘫痪.
临界资源
操作该资源的全过程只能被单个线程完成.
如何解决并发安全问题?
解决并发安全问题的本质就是将多个线程并发操作改为同步操作来解决
synchronized关键字
synchronized有两种使用方式
-
在方法上修饰,此时该方法变为一个同步方法
-
同步块,可以更准确的锁定需要排队的代码片段
-
在静态方法上使用,由于静态方法属于类,所以静态方法使用的不同监视器对象为当前类的类对象(Class的实例)
无论是给方法上上锁还是给代码块上锁,底层实现其实都是一样的,在进入同步代码之前
先获取锁,获取到锁之后锁的计数器+1,同步代码执行完,锁的计数器-1
通过字节码反编译工具查看字节码内容可以发现两者区别:
-
同步方法:通过flags标志
synchronized作用在方法上没有monitorenter monitorexit监视器进入和退出,在flags多出了一个ACC_SYNCHRONIZED标记
当线程要执行的方法被标注上ACC_SYNCHRONIZED时,执行线程先获取monitor,再执行代码,之后再释放monitor
-
同步代码块:通过monitorenter和mointorexit操作指令
在执行monitorenter之前要尝试获得锁,获得锁之后锁的计数器+1,执行monitorexit指令时,锁的计数器也会减1.
什么是Monitor?
不管是flags或monitorexit/enter 都是在操作monitor,每个Java对象天生就有监视器锁,也就是Synchronized的对象锁