java内存模型(JMM)
共享变量存储于主内存中,每个线程都可以访问
每个线程都私有的工作内存或者称为本地内存
工作内存只存储该线程对共享变量的副本
线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存
工作内存和java内存模型一样也是一个抽象的概念,它其实不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等
Volatile关键字
语义
保证不同线程对共享变量操作的可见性
禁止对指令进行重排序
不保证原子性
原理
volatile关键字修饰的变量存在一个“lock”前缀,lock相当于是一个内存屏障
确保指令重排序时不会将其后面的代码排到内存屏障之前
确保指令重排序不会将其前面的代码排到内存屏障之后
确保在执行到内存屏障修饰的指令时前面的代码全部执行完成
强制将线程工作内存中的值刷新到主内存中
如果是写操作,则会导致其他线程工作内存中的缓存数据失效
volatile和synchronized区别
使用上的区别
volatile关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量常量等
synchronized关键字不能修饰变量,只能用于修饰方法或者代码块
volatile修饰的变量可以为null,synchronized同步语句块的monitor对象不能为null
对原子性的保证
volatile无法保证原子性
synchronized是一种排他机制,被synchronized修饰的同步代码是无法被中途打断的,可以保证代码的原子性
可见性保证
两者均可保证共享资源在多线程间的可见性
对有序性的保证
volatile关键字禁止JVM编译器记忆处理器对其重排序,所以能够保证有序性
synchronized是通过串行化换来的,synchronized修饰的代码块中可能会出现重排序情况,最终输出结果和代码编写顺序一致
其他
volatile不会使线程陷入阻塞
synchronized会使线程陷入阻塞
wait和sleep的区别
wait和sleep都可以使线程进入阻塞状态
wait和sleep方法均是可中断方法,被中断后都会收到中断异常
wait是Object的方法,sleep是Thread的方法
wait需要再同步方法中执行,sleep不需要
线程在同步方法中执行sleep方法,并不会释放monitor锁,而wait方法会释放monitor锁
sleep方法短暂休眠后会主动退出阻塞,而wait方法(没有指定wait时间)则需要被其他线程中断后才能退出阻塞
ThreadLocl
原理
ThreadLocalMap中key是当前ThreadLocal实例,value是设置的值
key是弱引用,GC前会被回收掉,使用不当可能会出现内存泄露情况,在使用结束的时候最好能调用remove方法
Hook线程
Hook线程只有在收到退出信号的时候会被执行,如果在kill的时候使用了参数-9,那么Hook线程是不会得到执行,进程将立即退出,因此lock文件将得不到清理
Hook线程中也可以执行一些释放动作,比如关闭文件句柄、socket链接、数据库connection等。
尽量不要再Hook线程中执行一些耗时非常大厂的操作,会导致程序迟迟不能退出。
多线程简介
创建线程的三种方式
1. 继承Thread
2.实现Runnable
3.使用Callable和Future创建线程,有返回值
线程的生命周期
<span style="font-size: inherit;">New:NewThread()创建一个Thread对象,线程已经被创建,但是还不允许分配CPU执行。操作系统层面线程还未真正创建</span><br>
Runnable:thread.start()此时操作系统层面线程创建,等待CPU调度
Running:CPU分配资源,线程执行
BLOCKED:线程阻塞
sleep:不会释放资源
wait:释放锁资源
Terminated:线程结束
线程正常运行结束
线程运行出错意外结束
JVM Crash导致所有的线程都结束
Thread的api
interrupt 打断线程阻塞状态(wait、sleep、join、interruptibleChannel的io操作、Select偶然的wakeUp方法都会使线程进入阻塞状态)
sleep 线程休眠指定毫秒数、不会放弃锁的所有权
yield 提醒调度器放弃当前CPU资源,如果CPU资源不紧张,则会忽略这种提醒
join join方法会使当前线程永远的等待,直到期间别另外的线程中断,或者join的线程执行完毕。也可指定join毫秒数
getId 获取线程ID
getName 获取线程名称
setPriority 设置线程执行优先级,大于1小于10,默认线程优先级为0。避免业务严重依赖线程优先级
守护线程
一般用于处理后台工作,正常线程结束后,守护线程也会结束
thread.setDaemon(true);设置为守护线程,只能在线程启动前才会生效
thread.isDaemon();判断是否为守护进程
如何关闭一个线程
线程结束生命周期正常结束
捕获中断信号关闭线程。通过isInterrupted方法检查线程interrupt的标识来决定是否退出
使用volatile开关控制。由于线程的interrupt标识可能会被擦除,或者逻辑单元中不会调用任何中断方法,所以使用volatile修饰flag开关关闭线程也可行。
异常退出。抛出异常结束生命周期
线程安全
synchronized
提供了一种锁的机制,能确保共享变量的互斥访问,防止出现数据不一致问题
包括monitor enter和monitor exit两个JVM指令能够保证任何时候线程执行到monitor enter成功之前必须从主内存中获取数据,在monitor exit运行成功之后,共享变量被更新后的值必须刷新到主内存中
严格遵守happens-before规则,一个monitor exit指令之前必须有一个monitor enter
可以用作代码块或者方法上
死锁
死锁原因
交叉锁可导致死锁
一问一答的数据交换
数据库锁
死循环引起死锁
文件锁
内存不足导致死锁
预防死锁,破坏任意一条
互斥。共享资源X和Y只能被同一个线程占用
占用且等待。线程T1已经取得共享资源X,在等待共享资源Y的时候,不释放共享资源X
不可抢占。其他线程不能强行抢占线程T1占有的资源
循环等待。线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源
线程池
为什么使用线程池
主要目的是为了重复利用线程,提高系统效率
创建Thread是一个重量级的资源,创建、启动以及销毁都是比较耗费系统资源的
核心参数
corePoolSize
线程池保有的最小线程数。即使整个线程池都很闲,没有任务可执行,也需要保留corePoolSize线程。
maximumPoolSize
线程池创建的最大线程数。当任务很多时,就需要增加线程来处理了,最多增加到maximumPoolSize个线程。当任务减少线程空闲下来时,会回收线程,保留corePoolSize个线程。
workQueue
工作队列。当线程数达到corePoolSize数量时,再有任务提交,会被放入内存队列中,如果该队列是有界队列,当该队列满了时,会继续创建不超过maximumPoolSize线程数的线程。当有线程执行完任务会从该队列中取任务进行执行,使用无界队列会有OOM的风险。
threadFactory
通过这个参数可以自定义如何创建线程。例如:你可以给线程指定一个有意义的名字
handler
拒绝策略。通过该参数可以自定义任务的拒绝策略。如果线程池内的线程都在忙碌,并且workQueue队列也已经满了(前提是队列是有界队列),此时提交线程线程池就会执行拒绝策略,至于拒绝的策略是什么,可以通过handler这个参数来指定。
ThreadPoolExecutor提供的四种拒绝策略
CallerRunsPolicy:提交任务的线程自己去执行该任务。
AbortPolicy:默认的拒绝策略,会throwsRejectedException。
DiscardPolicy:直接丢弃任务,没有任何异常抛出
DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后新任务加入到工作队列。
创建多少个线程合适
I/O密集型
单核计算方式:最佳线程数 = 1+(I/O耗时/CPU耗时)
多核计算方式:最佳线程数 = CPU核数*[1 + (I/O耗时/CPU耗时)]