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