一、JUC介绍
JUC就是java.util.concurrent包的缩写,说白了就是并发场景进行多线程编程的工具类
我的总结就是JUC就是在并发场景下,怎么通过有效的硬件,高效的处理请求,并且保证程序的“线程安全”这设计的知识点非常大
二、进程、线程、协程
什么是进程
在操作系统中,进程是基本的资源分配单位,操作系统会通过进程来管理计算机的资源,如CPU、内存、磁盘等。每个进程都有一个唯一的进程标识符(PID),用于区分不同的进程
什么是线程
线程是操作系统中最基本的执行单元,她是进程中的一个实体,是CPU调度和分配的基本单位。一个进程可以包含多个线程,每个线程都可以独立执行不同的任务,但是他们会共享进程的资源。
同一时刻,一个CPU的核心只能运行一个线程,也就是CPU内核和同时运行的线程数量是1:1的关系。
什么是协程
1、一个线程中可以创建多个协程,这些协程之间共享同一个线程的资源
2、协程是在一个进程内部运行的,不需要操作系统的介入,可以在用户空间内实现协作式多任务处理。因此协程的创建和销毁开销很小(由JVM进行创建和销毁),可以更好的利用资源。
Java19才支持虚拟线程(协程)
或者使用第三方库quasar(据说已经入职ORACLE了,并且正式Java虚拟线程的开发者)
三、并发、并行、串行
并发
在一个处理器上处理多条线程的指令,交替执行
并行
在多个处理器上同时运行多条线程的指令,同行执行
串行
一个处理器同一时间只处理一个线程的指令,并且这个请求处理完才处理其他线程的指令
四、CPU的核心数和线程数的关系
CPU都是多核的,线程是CPU调度的最小单位。同一时刻,一个CPU核心只能运行个一个线程,也就是1:1的关系。Inter引进超线程技术后,产生了逻辑处理器的概念,使得核心数与线程数形成了1:2的关系。
在Java中提供了Runtime.getRuntime().avileableProcessors(),可以让我们获取当前的CPU逻辑处理器的数量。获取当前CPU的核心数在并发编程中很重要,并发编程下的性能优化往往与CPU核心数密切相关。
五、上下文切换(context switch)
现在的计算机大部分都是多核CPU,多线程往往会比单线程更快,更能提高并发,但是提高并发并不意味着启动更多的线程来执行,更多的线程意味着线程创建销毁开销加大、上下文切换的非常频繁,你的程序反而不能支持更高的TPS(Tranaaction Per Second)
时间片
多任务系统往往需要同时执行多道任务。作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,如何让用户感觉这些任务在同时进行呢?操作系统的设计者巧妙的利用了时间片轮转的方式
时间片是给每个线程分配的处理时间
线程上下文切换是指某一个时间点CPU寄存器和程序计数器的内容,CPU通过时间片分配算法来循环执行任务(线程),因为时间片非常短,所以CPU通过不停的切换线程执行。
多核CPU可以在一定程度上减小上下文切换
建议:
合理设置线程数目既可以最大化利用CPU又可以减小线程切换的开销。
高并发,低耗时的情况,建议少线程
低并发,高耗时的情况,建议多线程
高并发高耗时,要分析任务类型,增加排队,加大线程数
六、创建线程的三种方式
1、继承Thread类,重写run方法,调用start()方法
2、实现Runable接口,重写run方法,调用start()方法,推荐使用runable接口
3、实现Callable接口,重写call方法,与FutureTask结合使用,可以有返回值,如果调用futureTask的get方法获取返回值需要进行阻塞同步执行(判断是否已经结束,没有执行完需要进行等待)
start()和run()的区别
run方法是同步方法,start是异步方法
run方法的作用是存放任务代码,start方法的作用是启动线程
执行run方法它不会产生新线程,执行start方法会产生一个新的线程
run可以被调用无数次,但是start方法只能被调用一次,因为线程不能被重复启动
七、线程的常用方法
setName:给当前线程起名字
getName:获取当前线程名字
Thread.getCurrentThread:获取当前线程
sleep:让当前线程进行睡眠,让出时间片,不释放锁,其他线程可以使用interrupt打断睡眠的线程,sleep方法此时就会抛出异常,推荐使用TimeUnit进行睡眠,可读性更高
yield:让步,让出时间片,告诉调度器当前线程愿意让出时间片,但是是否让出CPU资源由调度器决定。
getPriority/setPriority(int priority) 得到线程优先级,设置线程优先级默认为1-10,默认是5,还得具体是调度器进行抉择
interrupt:打断,中断线程执行,并不会立即中断,会一直运行,通知线程需要中断,只会给线程做个标记(中断标记为true)。
interrupted:判断当前线程是否被打断,并且清除打断标记
isinterrupted:判断线程是否打断,并且不清楚打断标记
ps:使用interrupt+interrupted+break优雅的结束线程
join:等待该线程执行结束
isAlive:是否存活,存活返回true,不存活返回false
setDaemon(Boolean b):设置线程为守护线程:用户线程结束,守护线程也会立马结束(垃圾回收器就是属于守护线程,Tomcat用来接收处理外部的请求的线程)
getState():获取线程的状态
八、线程的状态
new:初始方法,线程被构建但是没有调用start()方法
runnable:运行状态:Java线程将操作系统的就绪和运行状态统称为运行中
blocked:阻塞状态表示线程阻塞于锁
waiting:等待状态,表示该线程进入等待状态,进入该状态表示该线程需要其他线程通知(notify或者notifyAll)如:join(),wait()
time_waiting:超时等待状态,表示线程可以指定时间自己返回如:sleep()
terminated:终止状态,表示当前线程已经执行完毕
九、线程池
什么是线程池
并发数量大的情况下进行频繁的进行现成的创建和销毁非常的消耗性能,因此我们设计了线程池进行预创建线程并且有需要线程的时候就从中获取,这样可以使得线程的调用得到复用
使用线程池的优点
线程池的主要工作室控制运行的线程数量,处理过程中将任务放进队列,然后在线程创建的时候启动这些任务,如果线程数量超过了最大数量,超出的线程进行排队等候,等其他线程执行完毕,再从队列中取出任务进行执行
他的主要的特点是:线程的复用,控制最大并发量,管理线程
一、降低资源消耗。通过重复利用已将创建的线程降低线程创建和销毁造成的销毁。
二、提高响应速度。当任务到达时,任务不需要进行等待现成的创建就能立即执行
三、提高线程的可管理性。线程是稀缺资源如果无限制的创建,不仅会消耗系统的资源,还和降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池的使用
ThreadPoolExcutor(int corePoolSize(核心线程池数量),
int maximumPoolSize(最大线程数量),
long keepAliveTima(非核心线程的空闲状态的存活时间(数字)),
TimeUnit unit(参数三的时间单位(天、时、分、秒)),
BlockingQueue<Runable> workQueue(工作队列(阻塞队列)),
ThreadFactory threadFactory(线程工厂(创建工厂)),
RejectedExecutionHandler handler(拒绝策略)
1、不执行并抛出一个异常
2、让调用者线程执行
3、从阻塞队列里面剔除出一个最老的线程
4、什么都不做
)
thread.excute()开启线程池
shutdown 执行完阻塞队列在进行关闭
shutdownNow 阻塞队列不执行
十、线程安全
在多线程下并发同时对共享数据进行读写,可能造成数据的紊乱,这就是线程不安全
原子性
原子性指的是单子不可分割的操作,要么全部成功,要么全部失败
一致性
可见性
一个变量修改这个值其他变量立即看见
如何解决线程不安全
1、破坏临界资源
1.1、只读
1.2、局部变量