Java线程
2023-12-26 19:01:39 42 举报
AI智能生成
Java线程
作者其他创作
大纲/内容
重排序
为了提升性能,处理器和编译器尝尝会对指令进行重排序
需要满足的条件
不能改变程序运行的结果
两个原则
as-if-serial
保证单线程内执行结果不会改变
happens-before
保证正确同步的多线程执行结果不会改变
存在数据依赖的不允许重排序
虽然不会影响单线程的执行结果,但会破坏多线程的执行语义
synchronized
volatile
Lock
CAS
volatile
Lock
CAS
synchronized
悲观锁、可用于类,方法,代码块,会阻塞线程
使用对象头中的mark down实现加锁
volatile
提供线程间共享变量,禁止指令重排序,只保证可见性不保证原子性,不会阻塞线程
Lock
只能给代码块加锁,必须手动加锁、解锁
ReentrantLock
Lock实现类,底层调用的是unsafe的park实现加锁
CAS
基于冲突解决的乐观锁(自旋)
CAS
(compare and swap)
(compare and swap)
乐观锁的一种实现
包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。
如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。
CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。
如果内存地址里面的值和 A 的值是一样的,那么就将内存里面的值更新成 B。
CAS是通过无限循环来获取数据的,若果在第一轮循环中,a 线程获取地址里面的值被b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。
会出现问题
ABA问题
一个线程 one 从内存位置 V 中取出 A,
这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成A,
这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。
这时候另一个线程 two 也从内存中取出 A,并且 two 进行了一些操作变成了 B,然后 two 又将 V 位置的数据变成A,
这时候线程 one 进行 CAS 操作发现内存中仍然是 A,然后 one 操作成功。
解决方法
jdk1.5之后再Atomic包中增加了
AtomicMarkableReference(引入一个boolean值来判断中间是否变动过),
AtomicStampedReference(通过引入一个int累计来判断中间是否变动过)来解决ABA问题
AtomicMarkableReference(引入一个boolean值来判断中间是否变动过),
AtomicStampedReference(通过引入一个int累计来判断中间是否变动过)来解决ABA问题
循环时间开发大
资源竞争大时,自旋概率会变大,浪费CPU资源
Excutor
Excutors
Excutors
Excutor
执行策略调用,调度,执行和控制异步任务的框架
执行线程任务
ExecutorService
(继承Executor)
(继承Executor)
提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值
提供了更多的方法我们能获得任务执行的状态并且可以获取任务的返回值
Excutors
线程池创建框架
Future
表示异步任务,是一个可能还没有完成的异步任务结构
Callable产生结果,Future获取结果
CLH(Craig,Landin,and Hagersten)队列
一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。
AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
Runnable/Callable
相同点
都采用Thread.start()启动线程
不同点
Runnable
不会返回结果,且不会抛出返回结构异常
Callable
call方法有返回值,配合Future\Future Task可以获取返回结果
call方法允许抛出异常,可以捕获异常信息
监视器
显示监视器(Lock)
隐示监视器(Synchroniezd)
每个监视器与一个对象引用关联
每个对象都有一把锁
基本概念
实现同步的方法
锁
同步方法
同步代码块
重入锁
特殊变量与volatile
注意事项
Wait(),notify(),notifyAll()需要在获得对象锁的情况下调用
使用完threadlocal后,需要手动调用remove()方法
不remove是否真的就有问题?
线程池中:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
long\dubbo类型不具有原子性,多个线程并发下数据会有线程安全性问题((64位字段会被拆成两个32位字段处理)可能读到的值,不是任何一个线程所赋的值)
threadLocal/threadLocalMap
优/缺点
优点
充分利用多核CPU的性能,使CPU的计算发挥到极致
缺点
内存占用
上下文切换
资源竞争
安全问题及解决方案
线程切换导致的原子性问题
使用Atomic原子类、Sychronized、Lock解决
缓存导致的可见性问题
使用Synchronized\Lock\Volatile解决
编译器优化带来的有序性问题
Happen-Before原则
守护线程
1.将一个线程设置成守护进程(setDaemon())必须在线程启动之前;
2.守护线程产生的也是守护线程;
3.不是所有线程都可以成为守护线程,比如读写操作和逻辑运算;
4.守护线程中不能依靠finally块来确保关闭或释放资源。因为没有非守护线程运行,程序会直接结束。
2.守护线程产生的也是守护线程;
3.不是所有线程都可以成为守护线程,比如读写操作和逻辑运算;
4.守护线程中不能依靠finally块来确保关闭或释放资源。因为没有非守护线程运行,程序会直接结束。
死锁四个条件
互斥性
一个资源一个线程获取
请求与保持
请求资源阻塞时,不释放获取的资源
解决:一次性获取所有锁
不可剥夺
线程获取的资源,不可被其他线程强行剥夺
解决:主动释放
循环等待
多个线程行程循环
解决:顺序获取资源,反序释放资源锁
创建线程的方式
继承thread类
实现runnable接口
实现callable接口
线程池
run()\start()方法
run()
线程具体执行的内容
start()
启动一个线程,使线程进入一个准备就绪状态
线程五个基本状态
(1) wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
(5)yield 方法让出了对 cpu 的占用权利;
(2)sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理 InterruptedException 异常;
(3)notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且与优先级无关;
(4)notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
(5)yield 方法让出了对 cpu 的占用权利;
线程调度
按照特定机制给多线程分配CPU使用权利
线程调度不受虚拟机控制,由系统控制
线程调度不受虚拟机控制,由系统控制
调度策略
分时调度:平分每个线程CPU占用时间
抢占调度(jvm使用方式):优先级高的线程先执行
调度器
1.是个操作系统服务;
2.将cpu可用的时间片分配给各个Runnabel状态的线程;
2.将cpu可用的时间片分配给各个Runnabel状态的线程;
时间分片
将可用的CPU时间分配给Runnable状态的线程
分配方式
线程优先级
线程等待时间
线程中断
interrupt()
interrupt()
线程中断仅仅是线程的中断状态,线程不会立即停止,需要用户去监视线程的状态并做处理,支持线程中断的方法(也就是线程中断后会抛出interruptedException 的方法)就是在监视线程的中断状态,一旦线程的中断状态被置为“中断状态”,就会抛出中断异常。
LockSupport.park()\wait()\sleep均响应中断
interrupt
interrupted
isInterrupted
interrupted
isInterrupted
interrupt
用于中断线程,调用该方法的线程状态将被设置为“中断”
interrupted
静态方法,
查看状态是True还是False
会消耗中断状态
查看状态是True还是False
会消耗中断状态
isInterrupted
查看当前中断信号
基础方法
wait()
会释放锁
wait()的使用必须要被唤醒才能重新执行
应该在循环中调用
因为有可能被唤醒之后“锁”等条件不满足需要继续等待
线程进入锁对象的等待池,等待被唤醒
wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。
notify()
由JVM确定唤醒哪个线程,且与优先级无关
notifyAll()
唤醒所有等待状态的线程竞争
会唤醒等待池中所有线程参与竞争,
竞争成功就继续执行,
失败的在锁池继续等待锁释放参与竞争
竞争成功就继续执行,
失败的在锁池继续等待锁释放参与竞争
yield()
只会给相同优先级或者更高优先级线程运行机会
sleep()
不会释放锁
调用sleep()让出CPU时,不会考虑线程优先级,会给低优先级线程运行机会
通信协作常见方式
syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
通过中断和共享变量
线程间直接数据交换
使用管道进行线程间通信
LockSupport
同步与互斥
互斥
共享的进程系统资源任意时刻最多只允许一个线程使用
同步
用户模式
不需要切换内核态,只在用户态下完成
方法
原子操作
临界区
内核模式
利用系统内核对象的单一性进行同步;
需要切换内核 态和用户态
需要切换内核 态和用户态
方法
事件
信号量
互斥量
实现同步的方法
同步方法(Synchronized)
同步代码块(Synchronized、Lock)
volatile修饰变量
线程优先级
总的有1-10个级别,数值越大优先级越高
java对线程的调度会委托给系统,系统不同而不同,所以不建议使用线程优先级
线程dump文件
进程的内存镜像,把程序的运行状态通过调试器保存在dump文件中
获取方式
Linux
以通过命令 kill -3 PID (Java 进程的进程 ID)来获取 Java应用的 dump 文件。
Windows
按下 Ctrl + Break 来获取。
这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。
这样 JVM 就会将线程的 dump 文件打印到标准输出或错误文件中,它可能打印在控制台或者日志文件中,具体位置依赖应用的配置。
并发关键字
volatile
保证内存可见性(缓存一致性协议),禁止重排序(内存屏障)
保证修改的值会被立即更新到主存中,其他地方读取也是从主存中读取
不保证原子性,只保证可见性和有序性
synchronized
概述
1.6之前,重量级锁,效率低
(因为监视器依赖底层的Mutex Lock实现,
java的线程是映射到底层的原生线程上,要唤醒、挂起线程都需要操作系统帮忙,而操作系统切换线程需要重用户态转换到内核态(这一步比较耗时))
(因为监视器依赖底层的Mutex Lock实现,
java的线程是映射到底层的原生线程上,要唤醒、挂起线程都需要操作系统帮忙,而操作系统切换线程需要重用户态转换到内核态(这一步比较耗时))
1.6之后,使用"自旋锁","适应性自旋锁","锁消除","锁粗化","偏向锁","轻量锁"来减少锁的开销
synchronized加载静态方法和synchronized(Class)都是给类加锁
synchronized记在实例方法上,是给对象加锁
synchronized关键字是不能继承的
不要使用synchronized(String a),因为JVM中有字符串常量池
自旋
让获取锁的线程不阻塞,而是在synchronized边界做盲目循环
底层原理
可以使用命令>>javap -c 名字 查看
监视器功能;主要为monitor获取锁,monitorexit释放锁;其中有两个monitorexit;后面一个monitorexit为保证异常退出也会释放锁
可重入锁原理
底层维护计数器
锁升级原理
锁对象的对象头有一个threadid字段,第一次进来为空,jvm让其持有偏向锁,并将threadid设置为当前线程ID;
再次有线程进入时,如果线程ID一致则直接使用,如果不一致则升级锁为轻量锁;
通过自旋获取锁,自旋一定次数之后则升级轻量级锁为重量级锁
再次有线程进入时,如果线程ID一致则直接使用,如果不一致则升级锁为轻量锁;
通过自旋获取锁,自旋一定次数之后则升级轻量级锁为重量级锁
四种状态
无状态锁
偏向锁
轻量级锁
重量级锁
锁升级原理
随着资源竞争的激烈,状态会依次升级,锁只能升级,不会降级
锁
死锁
活锁
饥饿
活锁
饥饿
死锁
因争夺资源而导致的相互等待、无外力作用将无法推进下去
活锁
任务为阻塞,但由于某些条件未满足,导致一直重试、失败、重试。。。
饥饿
一个或多个线程因为某种原因而一直无法获取资源,导致一直无法执行
原因
高优先级一直吞噬低优先级CPU时间
一直阻塞在等待同步块的状态
线程本身等待一个处于永久等待的对象(比如调用对象的wait()方法-)
synchronized
ReentrantLock
(可重入锁)
(可重入锁)
实现原理
1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;
2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。
支持公平锁和非公平锁
公平锁
获取锁的顺序符合请求上的先后性
非公平锁
ReentrantReadWriteLock
(读写锁)
(读写锁)
读写分离,读锁共享,写锁分离
三个特征
公平选择
支持公平与非公平的锁获取方式,非公平式的吞吐量高于公平式的吞吐量
重进入
读锁、写锁都支持重进入
锁降级
遵循获取写死在获取读锁的次序,写锁能够降级为读锁
FutureTask
表示一个异步计算任务
可以传入一个Cllable异步运算,获取异步运算结果
可以对调用了Callable,Runable对象进行包装
是Runable接口的实现类,也可以放入线程中执行
原子操作
(不可被中断的一个或一系列操作)
(不可被中断的一个或一系列操作)
原理:CAS(自旋)+volatile(内存可见):仅有一个线程能成功,而未成功的线程可以像自旋锁一样,继续尝试,一直等到执行成功。
原子类
AtomicBoolean
AtomicInteger
AtomicLong
AtomicReference
原子数组
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
原子属性更新器
AtomicLongFieldUpdater
AtomicIntegerFieldUpdater
AtomicReferenceFieldUpdater
并发工具
CountDownLatch(倒计时器)
强调一个线程等多个线程完成某件事情
CyclicBarrier(循环栅栏)
多个线程互等,等大家都完成,再携手共进
Semaphore(信号量)
信号量,限制某块代码的并发量
Exchanger(线程间数据交换)
用于两个线程间交换数据。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据
fork join
工作窃取
指某个线程从其他队列里窃取任务来执行
线程池
线程队列
队列满了
无界队列
继续添加任务到队列中
有界队列
如果没有到达最大设定线程数,则创建线程;
如果到达最大线程数,则执行拒绝策略
如果到达最大线程数,则执行拒绝策略
线程池存在的状态
Running
正常状态,接收新任务,处理等待队列中的任务
Shutdown
不接受新任务,会继续处理任务队列中的任务
Stop
终止当前执行任务,不接受新任务,不执行等待队列中的任务
Tidying
当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。
当线程池变为TIDYING状态时,会执行钩子函数terminated()。
terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;
可以通过重载terminated()函数来实现。
当线程池变为TIDYING状态时,会执行钩子函数terminated()。
terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;
可以通过重载terminated()函数来实现。
Terminated
terminated()方法执行之后的状态
创建方式
Executors
newSingleThreadExecutor(单线程,异常等原因结束之后会创建新线程代替)
newFixedThreadPool(线程池大小固定,有线程挂掉之后会有新线程来代替)
newCachedThreadPool(有线程空闲60秒以上会回收,需要的时候再创建,最大数由操作系统决定)
newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
ThreaPoolExecutor
构造参数
corePoolSize
核心线程数(最少线程数)
maximumPoolSize最大线程数
workQueue
等待队列
新任务来先判断是否使用完了所有核心线程
如果使用完了就把任务放在队列中
如果使用完了就把任务放在队列中
ArrayBlockingQueue
PriorityBlockingQueue
支持优先级的无界阻塞队列,使用较少
LinkedBlockingQueue
Integer.MAX_VALUE
吞吐量高于ArrayBlockingQueue
Executors.newFixedThreadPool()使用该队列
SynchronousQueue
不储存元素(无容量)的阻塞队列;
每个put操作必须等待一个take操作
每个put操作必须等待一个take操作
吞吐量高于LinkedBlockingQueue
Executors.newCachedThreadPool使用该队列
keepAliveTime
空闲线程存活时间
线程数量大于 corePoolSize 的时候,
如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,
直到等待的时间超过了 keepAliveTime 才会被回收销毁;
如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,
直到等待的时间超过了 keepAliveTime 才会被回收销毁;
unit
keepAliveTime 参数的时间单位
threadFactory
为线程池提供创建新线程的线程工厂
handler
submit()/execute()
submit()
可以执行Runnable、Callable类型任务
可以返回持有异步执行结果的Future对象
execute()
只能执行Runnable()类型任务
只能执行Runnable()类型任务
饱和策略
中止策略-AbortPolicy(默认使用方式)
抛出 RejectedExecutionException 来拒绝新任务的处理。
调用者运行-CallerRunsPolicy
由调用线程处理该任务
抛弃策略-DiscardPolicy
不处理新任务,直接丢弃掉
抛弃旧任务策略-DiscardOldestPolicy
此策略将丢弃最早的未处理的任务请求。
如何配置参数
分析角度
1.任务性质:计算密集型/IO密集型/混合型;
计算密集型:尽可能少的线程执行,例如cpu+1
IO密集型:尽可能多的线程执行,例如CPU*2
混合型:可以考虑是否可以拆分为IO密集型和计算密集型
2.任务优先级:高、中、低
3.任务执行时间:长、中、短
4.任务的依赖性:是否依赖别的资源(数据库,网络等资源)
如果依赖其他耗时的资源,则应设置尽可能多的线程
最佳线程数=[(线程执行时间+线程等待时间)/线程执行时间] * CPU数
线程中断
中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其它线程进行了中断操作,
但是通过中断并不能直接终止另一个线程,而是需要中断的线程自己处理。
但是通过中断并不能直接终止另一个线程,而是需要中断的线程自己处理。
线程的中断不会立马影响线程的状态,
线程中断前默认标志位为false,中断后标志位被修改true,标志位被修改后,子线程并没有马上执行中断,而是在主线程继续执行一段时间后才执行中断
线程中断前默认标志位为false,中断后标志位被修改true,标志位被修改后,子线程并没有马上执行中断,而是在主线程继续执行一段时间后才执行中断
相关方法
interrupted()
测试当前线程是否被中断,该方法可以消除线程的中断状态,如果连续调用该方法,第二次的调用会返回false。
isInterrupted()
测试线程是否已经中断,中断的线程返回true,中断的状态不受该方法的影响
interrupt()
中断线程,将中断状态设置为true
总结:线程中断不会使得线程立马退出,而是会给线程发送一个通知,告诉目标线程你需要退出了,具体的退出操作是由目标线程来执行
同步容器
并发容器
并发容器
同步容器
简单的在需要同步的方法上加上关键字 synchronized
Vector
Hashtable
Collections.synchronizedSet,synchronizedList 等方法返回的容器
并发容器
(效率更高)
(效率更高)
采用细粒度的分段锁和内部分区等技术
ConcurrentHashMap
另外:使用了一种不同的迭代方式。在这种迭代方式中,当iterator 被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时 new 新的数据从而不影响原有的数据,iterator 完成后再将头指针替换为新的数据 ,这样 iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。
CopyOnWriteArrayList
写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。
ThreadLocal
程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。
内存泄漏问题
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而 value 不会被清理掉。
这样一来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露
所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而 value 不会被清理掉。
这样一来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露
内存泄漏解决方法
在调用 set() 、get() 、remove() 方法的时候,会清理掉 key 为 null 的记录。
使用完 ThreadLocal 方法后 最好手动调用remove() 方法
使用完 ThreadLocal 方法后 最好手动调用remove() 方法
BlockingQueue
(主要作为线程同步的工具)
(主要作为线程同步的工具)
在队列为空时,获取元素的线程会等待队列变为非空。
当队列满时,存储元素的线程会等待队列可用。
当队列满时,存储元素的线程会等待队列可用。
ArrayBlockingQueue
LinkedBlockingQueue
PryorityBlockingQueue
DelayQueue
SynchronousQueue
LinkedTransferQueue
LinkedBlockingDeque
AQS(AbstractQueueSychronizer)
(构建锁和同步器的框架)
(构建锁和同步器的框架)
核心思想
如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
实现工具
Unsafe(提供CAS操作)
LockSupport(提供park/unpark操作)
资源共享方式
独占
公平锁:线程在队列中排队顺序,先到先得
非公平锁:无视队列所有线程去抢
共享
多个线程同时进行
semaphore
countDownLatch
SyclicBarrier
readWriteLock
0 条评论
下一页