java并发编程
2022-11-10 15:25:38 1 举报
AI智能生成
java并发编程
作者其他创作
大纲/内容
抽象队列同步器AQS
框架
实现原理
AQS在内部定义了一个volatile int state 变量,表示同步状态;当线程调用lock方法时,如果state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,
AQS通过Node内部类构成一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就会被添加到队列末尾。
Node类时对要访问同步代码的线程的封装,包含了线程本身以及其状态交waitStatus(有五种取值,分别表示是否被阻塞,是否等待唤醒,是否已经被取消等),每个Node节点关联其prev节点和next节点,方便线程释放后唤醒下一个在等待的线程,是一个FIFO的过程。
Node类有两个常量,SHARED和EXCLUSIVE,分别表示共享模式和独占模式。所谓共享模式是一个锁允许多个线程同时操作(信号量Semaphore就是基于AQS共享模式实现的),独占模式是同一时间段只能有一个线程资源对共享资源进行操作,其余的线程需要排队等待(ReentranLock)。
AQS通过内部类ConditionObject构建等待队列(可有多个),当Condition调用wait()方法后,线程加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移到同步队列中进行锁竞争。
AQS和Condition各自维护了不同的队列,在使用Lock和Condition的时候,其实就是两个队列的相互移动。
acquire()
release()
ReentrantLock
流程图
Sync
tryRelease()
非公平锁(默认)
NonfairSync
lock()
tryAcquire()
nonfairTryAcquire()
公平锁 new ReentrantLock(true)
FairSync
lock()
tryAcquire()
首先看队列里是否有人排队
没有人排队则尝试获取锁
未获取到锁,看当前拿到锁的线程是否是自己,是则自己state+1重入
线程池
为什么使用线程池
单个线程的创建和销毁的开销太大
有效的控制线程数量,避免创建过多线程
核心参数
(线程池核心线程数)corePoolSize
(线程池里允许的最大线程数量)maximunPoolSize
(等待时间,coolPoolSize外的线程等待时间大于这个值,则会被清理掉)keepAliveTime
(keepAliveTime的单位)unit
(工作队列,当前运行的线程数 > corePoolSize时,多出来的线程进入queue等待)workQueue
(如果有新的线程需要创建时,就是由这个线程池来进行创建)threadFactory
(线程数超过maximumPoolSize并且queue满了的时候,仍有线程进来所执行的策略(默认直接报错))handle
常见线程池
(单线程池队列)SingleThreadExecutor
里面就一个线程,然后慢慢去消费
如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务执行顺序按照任务的提交顺序执行
corePoolSize:1,只有一个核心线程;maximumPoolSize:1;keeoAlive 0l, workQueue:new LinkBlockingQueue<Runnable>(),其缓冲队列是无界的
(固定数量线程池)FixedThreadExecutor
适用于负载比较均衡的情况,比如我们系统每小时运行一次生成统计生成报表任务
根据你设定的线程数量执行,多出来的进入排队等待队列
(自动回收线程池)CacheThreadEcxecutor
ScheduleThreadEcecutor
并发基础
并发编程的优缺点
优点
充分利用多核CPU的运算能力
便于业务才分
缺点
频繁的上下文切换
线程安全问题
线程基本操作
线程的创建
继承Thread类
实现Runnable接口
通过Callable和FutureTask创建线程
通过线程池创建线程
线程的状态以及切换
线程的基本操作
interrupt()
设置线程的中断标志不能终止线程运行,而是被中断的线程根据中断状态自行进行处理
static boolean interrupted()和 void interrupt()区分
interrupt()是调用线程的标志位。
isInterrupted()是当前线程的标志位(并非调用线程)。
isInterrupted()是当前线程的标志位(并非调用线程)。
join()
当前线程 等待 调用join()的线程 任务执行完成
yield()
线程请求调度器让出自己的 CPU 使用,让出成功则处于就绪态
sleep()
调用线程暂时让出指定时间的执行权,不参与 CPU 调度,但是该线程拥有的锁资源不让出
sleep和wait的区别
所属类不同
(1)sleep方法是定义在Thread上
(2)wait方法是定义在Object上
(2)wait方法是定义在Object上
使用语法不同
(1)sleep可以使用在任何代码块
(2)wait必须在同步方法或同步代码块执行
(2)wait必须在同步方法或同步代码块执行
唤醒的方式不同
(1)sleep睡眠的时间到了之后,会自动唤醒
(2)wait需要通过notify或notifyAll方法来唤醒
(2)wait需要通过notify或notifyAll方法来唤醒
对于锁资源的处理方式不同
(1)sleep不会释放锁
(2)wait会释放锁
(2)wait会释放锁
线程的状态不同
(1)当线程调用wait(),线程都会进入到waiting状态
(2)当线程调用sleep(time),或者wait(time)时,进入timed waiting状态
(2)当线程调用sleep(time),或者wait(time)时,进入timed waiting状态
Java内存模型(JMM)
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。
八种内存交互操作
lock(锁定),作用于主内存中的变量,把变量标识为线程独占的状态。
read(读取),作用于主内存的变量,把变量的值从主内存传输到线程的工作内存中,以便下一步的load操作使用
load(加载),作用于工作内存的变量,把read操作主存的变量放入到工作内存的变量副本中。
use(使用),作用于工作内存的变量,把工作内存中的变量传输到执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
assign(赋值),作用于工作内存的变量,它把一个从执行引擎中接受到的值赋值给工作内存的变量副本中,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作。
store(存储),作用于工作内存的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用。
write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
重排序
为了提高执行性能,编译器和处理器会对指令进行重排序
编译器重排序
CPU指令重排序
CPU内存重排序
as-if-serial语义
所有的动作(Action)5都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。
happens-before规则
程序次序法则:线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。
监视器锁法则:对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。
volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。
线程启动法则:在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。
线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。
中断法则:一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。
终结法则:一个对象的构造函数的结束happens-before于这个对象finalizer的开始。
传递性:如果A happens-before于B,且B happens-before于C,则A happens-before于C
内存屏障
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
并发关键字
volatile
volatile如何保证可见性
lock前缀指令+MESI缓存一致性协议
对于volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU计算完之后会立即将这个值协会主内存,同时因为MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改,如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取这个变量的时候,就会从主内存重新加载最新的数据
有序性
volatile变量在写操作之后会插入一个store屏障,在读之前会插入一个load屏障
synchronized
对象头
对象头包含三个字段
Mark Word(标记字)
用于存储自身运行时的数据,例如GC状态码,哈希码,锁状态等
Class Point(类对象指针)
用于存放方法区Class对象的地址,虚拟机通过这个指针来确定这个对象是哪个类的实例。
Array Length
如果对象是一个Java数组,那么此字段必须有,用于记录数组长度的数据;如果对象不是一个Java数组,那么此字段不存在,所以这是一个可选字段。
Mark Word(标记字)32位
字段详情
lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。
biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。
identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。
thread:持有偏向锁的线程ID。
epoch:偏向时间戳。
ptr_to_lock_record:指向栈中锁记录的指针。
ptr_to_heavyweight_monitor:指向管程Monitor的指针。
锁状态
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间
无锁状态
java对象刚创建时,没有任何线程来竞争,说明该对象处于无锁状态,此时这时偏向锁标识位是0,锁状态是01
偏向锁
偏向锁是指一段同步代码一直被同一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。如果内置锁处于偏向状态,当有一个线程来竞争锁时,先用偏向锁,表示内置锁偏爱这个线程,这个线程要执行该锁关联的同步代码时,不需要再做任何检查和切换。偏向锁在竞争不激烈的情况下效率非常高。偏向锁状态的Mark Word会记录内置锁自己偏爱的线程ID。
轻量级锁
当有两个线程开始竞争这个锁对象时,情况就发生变化了,不再是偏向(独占)锁了,锁会升级为轻量级锁,两个线程公平竞争,哪个线程先通过CAS操作占有锁对象,锁对象的Mark Word就指向哪个线程的栈帧中的锁记录。当锁处于偏向锁,又被另一个线程企图抢占时,偏向锁就会升级为轻量级锁。企图抢占的线程会通过自旋的形式尝试获取锁,不会阻塞抢锁线程,以便提高性能。
重量级锁
如果持有锁的线程执行的时间超过自旋等待的最大时间仍没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,自旋不会一直持续下去,这时争用线程会停止自旋进入阻塞状态,该锁膨胀为重量级锁。重量级锁会让其他申请的线程之间进入阻塞,性能降低。重量级锁也叫同步锁,这个锁对象Mark Word再次发生变化,会指向一个监视器对象,该监视器对象用集合的形式来登记和管理排队的线程。
底层原理
知识点
Monitor监视器
在HotSpot虚拟机中,monitor是由ObjectMonitor实现的
monitor竞争
通过CAS尝试把monitor的owner字段设置为当前线程。
如果设置之前的owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行 recursions ++ ,记录重入的次数。
如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
如果获取锁失败,则等待锁的释放。使用EnterI()方法
monitor等待
当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node
节点push到cxq列表中。
节点push到cxq列表中。
node节点push到cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当
前线程挂起,等待被唤醒。
前线程挂起,等待被唤醒。
当该线程被唤醒时,会从挂起的点继续执行,通过 ObjectMonitor::TryLock 尝试获取锁
monitor释放
当某个持有锁的线程执行完同步代码块时,会释放锁并 unpark 后续线程
QMode = 2的操作最特殊:取_cxq队列首元素唤醒;
QMode = 3,把_cxq队列的首元素放入_EntryList尾部
QMode = 4,把_cxq队列的首元素放入_EntryList头部
QMode = 0,如果_EntryList非空,就取首元素唤醒,否则取_cxq的首元素唤醒
锁升级过程
可以保证原子性、可见性、有序性
原子性
加锁和释放锁,ObjectMonitor
可见性
加锁,在进入synchronized代码块时的读操作,都会强制执行refresh
Load内存屏障
释放锁,在出代码块时,代码块内所有的写操作,都会强制执行flush操作
Store内存屏障
有序性
通过加各种内存屏障,保证有序性
代码块内部不保证有序性
但是同步代码块内部的指令和外部的指令,是不能重排的
1.6以后的锁优化
适应性自旋锁
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
锁消除
JIT对锁的一种优化,利用逃逸分析技术判断锁对象是否只被一个线程访问,如果是的话就取消上锁的过程
锁粗化
比如 for循环整个过程都在频繁加锁和释放锁,非常耗费性能,因此JIT将会把加锁的过程优化到for循环外面,这就是锁粗化的过程。
0 条评论
下一页