Java并发
2024-11-17 08:49:19 0 举报
AI智能生成
Java并发是指在Java语言中实现多个任务同时执行的能力。在多线程编程中,一个线程的执行不会干扰另一个线程的执行,这有助于提高程序的运行速度和性能。Java提供了一系列并发编程的工具,如线程、线程池、锁、信号量等,方便开发者进行并发编程。然而,并发编程也可能带来一些挑战,如线程安全问题、死锁、资源争用等,开发者需要掌握并发编程的相关知识,以确保程序的正确性和稳定性。
作者其他创作
大纲/内容
单个任务总时间=CPU运算时间+IO等待时间,当任务A在等待IO时,CPU可以切到任务B进行CPU运算。如果你想拉满CPU利用率,那理想线程数 = ((CPU运算时间+IO等待时间) / CPU运算时间) * CPU核心数 ;当你认为CPU和IO时间相等时,这时就是2CPU核心数;但是在常规的Web场景下,IO时间总是远大于CPU时间,比如一个简单的数据库查询,计算可能只有0.1ms,IO则可能达到2ms,这样一算,得21CPU核心数;Tomcat和Dubbo默认核心线程数都是200,而不是简单的2*核心数。真实场景下,每个接口/任务可能时间占比和执行量都不一样,应该通过压测来确定。
IO密集型线程池具体计算公式
L1/L2/L3时钟访问所需周期1.L1:4个是时钟周期2.L2:10个时钟周期3.L3:52个时钟周期Linux OS的上下文切换需要花费5000-20000个时钟周期
时钟访问周期
什么是虚假唤醒?当一定的条件触发时会唤醒很多在阻塞态的线程,但只有部分的线程唤醒是有用的,其他线程的唤醒是多余的。比如说卖货,如果本来没有货物,突然进了一间货物,这时所有的顾客都被通知了,但是只能一个人买,所以其他人都是无用的通知if (某个条件不成立) {wait()}// 业务操作。。。。。为什么if会出现虚假唤醒?1.因为if只会执行一次,执行完会接着向下执行if(){}后边的逻辑2.而while不会,直到条件满足才会向下执行while(){}后边的逻辑如何避免虚假唤醒使用while循环去循环判断一个体哦啊金,而不是使用if只判断一次体哦阿健,即wait()要在循环中
什么是虚假唤醒
一个或多个线程因为种种原因无法获得锁需要的资源,导致线程一直无法执行的状态。一直有线程级别高的占用资源,线程级别低的一直处在饥饿状态。比如ReentrantLock显示锁里提供的不公平锁机制,不公平锁能够提高吞吐量但不可避免的会造成某些线程的饥饿。线程饥饿是指线程一直无法获得所需要的资源导致任务一直无法执行的一种活性故障。我们知道多线程执行中有线程优先级这个东西,优先级高的线程能够插队并优先执行,这样如果优先级高的线程一直抢占优先级低线程的资源,导致低优先级无法得到执行,与死锁不同的是饥饿在以后一段时间内还是能够得到执行的,如果那个占用资源的线程结束了并释放了资源。线程饥饿的一个典型例子就是在高争用环境中使用非公平模式的读写锁,读写锁默认情况下采用非公平调度模式,如果这些线程对锁的争用程度比较高,有可能会出现读锁总是抢先执行,而写锁始终无法得到的情况,导致一直无法更新数据,非公平锁可以支持更高的吞吐率,也可能导致某些线程始终无法获得资源锁。在高争用环境中,由于线程优先级设置不当,可能会导致优先级低的线程一直无法获得CPU执行权,出现了线程饥饿的情况饥饿与死锁的区别:线程处于饥饿时因为不断有优先级高的线程占用资源,当不再有高优先级的线程争抢资源时,饥饿状态将会自动解除产生饥饿的原因:1.高优先级线程抢占资源2.线程在等待一个本身也处于永久等待完成的对象3.线程被永久阻塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问
什么是线程饥饿
任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试、失败、尝试、失败。在这期间线程状态会不停的改变活锁与死锁的区别:1.死锁会阻塞,一直等待对方释放资源,一直处于阻塞状态;活锁会不停的改变线程状态尝试获得资源。活锁有可能自行解开,死锁则不行活锁具有两个特点:1.第一个是线程没有阻塞,始终在运行中,所以叫做活锁,线程是获得,运行中的2.第二个是程序却得不到进展,因为线程始终重复同样的无效事情
什么是活锁
什么是死锁?死锁可以这样理解,就是互相不让步不放弃,同时需要对方的资源。造成互相不满足资源需求,也不放弃自身已有资源。死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如果外力作用,它们都将无法推进下去,如果系统资源重组,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺优先的资源而陷入死锁。产生死锁的主要原因是:1.因为系统资源不足2.进程运行推进的顺序不合理3.资源分配不当等死锁产生的4个必要条件:1.互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待2.不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)3.请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不释放4.循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁死锁的四个条件:1.互斥 2.保持锁并请求锁 3.不可抢夺 4.循环等待
什么是死锁
对象内存布局
开启指针压缩的情况下的内存布局。Markword 占8B开启指针压缩:类型指针占4B变量a占4B不需要对齐总共对象大小占16B
关闭指针压缩的情况下的内存布局MarkWord:8B关闭指针压缩:类型指针占8B变量a占4B需要对齐,补充4B总共对象大小占24B
内存编织
我们可以通过Test的内存首地址加上0x1b8计算得出一个内存地址,
可以看到,Test类当中也存在着5个方法位于虚表当中
接着我们再来查看Object的方法内存地址,finalize、equals、toString、hashCode、clone也存在于虚表中
C++ vtableklass contentJVM vtable我们随便定义的一个类,它有没有JVM虚表呢?其实是有的。那是哪些方法的内存地址呢?回答这个问题前先得搞明白:什么样的方法回存入虚表,只有public、protected类型的,且不被static、final修饰的方法才能被多态调用,才会进入虚表。因为Java中所有的类都是Object的子类,所以Object中满足这个条件的方法都会在每个类的虚表中.Object中的finalize、equals、toString、hashCode、clone方法也会存入虚表
对应的HotSpot中的klass.hpp
虚表
Oop三大机制为什么就是封装、继承、多态。没增加也没减少。在HotSpot中,多态需要动态绑定才能得以实现,而绑定通俗一点讲就是让不同的对象对同一个函数进行调用,或者反过来讲,就是让同一个函数与不同的对象绑定起来,所以多态得以实现的一个大前提就是,编程语言必须是面向对象的。同时,函数与对象相互绑定,意味着函数也是属于对象的一部分,这便具备了封装的特性。因为有了封装,才有了对象。同时,一个函数能够绑定多个对象,意味着对各不同的对象具有相同的行为,这是继承的含义。因此,面向对象的三大特性缺一不可。封装与继承其实是为了多态准备的,或者说,封装与继承成全了多态,多态让封装与继承的意义最大化
C++是如何实现多态的?多态的实现,现在几乎所有的编程语言都是基于虚表实现的,英文vtable。C++的虚表位于new创建的对象的头部。虚表里面存储的是什么呢?是虚函数。
Java是如何实现虚表分发的JVM实现虚表分发,对应的字节码指令有两个:invokevirtual、invokeinterface。虽然invokeinterface后面的操作数是接口方法信息。但是真正的对象会作为this传过来。所以在调用的时候,从操作数栈拿到真正的对象,然后通过对象头中的类型指针拿到TestDuotai(自己手写的一个类)的C++类对象,即Klass模型。虚表就在这个对象的头部。然后通过函数名+内涵参数信息及返回值信息的签名去虚表中找。因为是从前往后找,所以如果子类重写了父类的方法,会调用子类的方法。这就是JVM虚表分发的底层原理
从HotSpot源码层main剖析Java的多态实现原理
PV操作的意义我们用信号量及PV操作来实现进程的同步和互斥。PV操作是属于进程的低级通信。进程的同步、互斥:同步:与其说同步,称为\"协作\"更好点,目标只有一个,奔着同一个目标去的,都是在大家的努力下共同完成这么一件事情。互斥:只能有一个线程在操作共享资源
MESA模型(AQS近似于)。管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。举个例子,多个线程对一个共享队列进行操作。假设线程T1要执行出队操作,但是这个操作要执行成功的前提是队列不能为空。这个队列不能为空就是管程里的条件变量。若是线程T1进入管程后发现队列是空的,那它就需要在\"队列不空\"这个条件变量的等待队列中等待。通过调用wait()实现。若是用对象A代表\"队列不空\
wait()的正确使用对于MESA管程来说,有一个编程范式:```javawhile(条件不满足) { wait();}```这个范式可以解决\"条件曾经满足过\"这个问题。唤醒的时间和获取到锁继续执行的时间是不一致的,被唤醒的线程再次执行时可能条件又不满足了,所以循环检验条件。MESA模型的wait()方法还有一个超时参数,为了避免线程进入等待队列永久阻塞
notify()和notifyAll()何时使用满足以下三个条件时,可以使用notify(),其余情况尽量使用notifyAll():1.所有等待线程拥有相同的等待条件2.所有等待线程被唤醒后,执行相同的操作3.只需要唤醒一个线程
Java语言的内置管程synchronizedJava参考了MESA模型,语言内置的管程(synchronized)对MESA模型进行了精简。MESA模型中,条件变量可以有多个,Java语言内置的管程里只有一个条件变量。Java内置的管程方案(synchronized)使用简单,synchronized关键字修饰的代码块,在编译器会自动生成相关加锁和解锁的代码,但是仅支持一个条件变量;而Java SDK并发包AQS实现的管程支持u东哥条件变量,不过JUC包里的锁,需要我们自己进行加锁和解锁操作
管程模型
自旋锁模型
线程状态转换图
JVM的虚拟机栈大小初始是1M是如何实现的,就是通过这两个API实现的。若成功,返回0,否则,返回错误编号。可以使用函数pthread_attr_getstack和pthread_attr_setstack对线程栈属性进行管理。
线程中断机制Linux多线程是没有中断机制的.JVM中的中断机制是自己上线的,实现逻辑很简单,在线程运行函数中或线程运行这条路上有一处或多处这样的判断。意思就是通过改变中断标志告诉线程,你的任务已经执行完了,你可以销毁了。当然,这个标志可以被清理掉,所以看到说设置了有可能会被忽略掉。其实就是其他逻辑发现这个线程不能销毁,把这个状态位改掉了
等待线程运行结束。若成功,返回0,否则,返回错误编号。参数可以用来获取线程执行结果
加锁/解锁。若成功返回0,否则返回错误编号
互斥锁的初始化和销毁逻辑。若成功返回0,否则,返回错误编号
读写锁的上锁/解锁。若成功返回0,否则返回错误编号
读写锁的初始化和销毁逻辑。若成功返回0,否则返回错误编号
条件变量的等待,若成功返回0,否则返回错误编号
条件变量的初始化和销毁逻辑。若成功返回0,否则返回错误编号
Linux中的原生线程操作API
Linux应用态的线程相关点多线程pthread_createpthread_joinpthread_exit互斥同步线程与信号锁 mutexpthread_mutex_initpthread_mutex_destroypthread_mutex_lockpthread_mutex_unlock条件变量pthread_cond_initpthread_cond_destroypthread_cond_waitpthread_cond_signal 公平锁,逐个唤醒队列中的线程pthread_cond_broadcast 非公平锁,唤醒队列里的所有线程Linux的线程机制是面向过程式的API,而JVM则是在Linux的基础之上开发出了一种面向对象的线程机制
OS内核层的线程相关点用户态和内核态的切换线程切换线程在内核中到底是什么
Java中的main线程属性是JOINABLE
代码测试JOINABLE
代码测试DETACHED
joinable
detached解决方案:1.借助全局访问变量。加上sleep()等待全局变量去被赋值。但HotSpot肯定不会这么去做这件事。JVM是通过vm_result vm_result_2,一个是基本数据类型,另一个是引用类型。那么线程的先后顺序如何控制的呢?使用mutex+条件变量控制的
获取线程返回值。joinable线程:detached线程:
线程中断机制中断机制是Java自己实现的。理解有个前提,线程是自己杀死的,设置它的中断标志位,然后等线程唤醒它之后,发现自己的中断标志位已经被设置了,于是它就中断退出。这就是中断的本质。中断的意义精准杀死你想要杀死的线程中断机制是JVM自己实现的,Linux没有线程中断这一说
ParkEvent#park()
ParkEvent#unpark()
执行结果
线程并行执行遍历线程去加锁,如果子线程执行结束,则唤醒主控线程
把线程封装成面向对象机制会出现的问题1.通过分离线程解决串行执行2.知晓线程合适运行结束。得让主控线程阻塞,所有子线程运行结束,主控线程退出HotSpot是借助锁来实现的,mutex互斥锁,以及条件变量达到的效果1.面向过程的线程API封装成面向对象的线程机制2.主控线程要阻塞住,知道子线程运行完
1.线程机制
Java中的Thread对象和操作系统的线程的关系|Java的Thread对象是一个oop对象JVM的JavaThread是一个C++对象JVM的OSThread是一个C++对象操作系统线程主要牵涉到四个层面Java层面JVM层面OS(应用层面)-----------------OS(内核层)
JVM中的start0方法对应的方法名叫做JVM_StartThread方法,该方法是以JVM开头的方法,我们需要到jvm.cpp中去找这个方法
再比如说想要找Object的native方法,我们就要到Object.c中去找方法对应的JVM方法,找到JVM方法之后,再去jvm.cpp里面去找
Object.c中的HashCode方法在jvm.cpp中所对应的
如何找到native方法的JVM源码。以Thread#start()方法为例。在HotSpot源码中会有一个Thread.c文件
new JavaThread(C++对象)在哪里创建
获取run函数的执行逻辑
Thread()方法当中做了一些线程初始化的工作,关键的代码在于这个地方创建了ParkEvent对象,用来控制线程的阻塞与唤醒
set_entry_point(entry_point)是把我们定义的run函数里面的逻辑设置到线程的entry_point当中
os::create_thread()创建OSThread的核心逻辑,这里会涉及到其他系统平台,每个操作系统的线程机制又不一样,所以,我们以Linux为主,点进去看os_linux.cpp文件即可。其中thread->set_osthread(osthread);这行代码会将JavaThread和OSThread进行一个绑定.并且还将OSThread的属性设置为了DETACHED
这行代码执行完,程序中就会有一个子线程、一个main线程
线程的操作都是靠pthread_create的传出参数tid进行识别的,在这里osthread->set_pthread_id(tid)。又将OSThread和Linux线程做了绑定关系。但是new Thread创建的oop对象还没有进行关联
java_start分析。接着我们再来看子线程的java_start方法逻辑。主要做了以下几件事情1.初始化子线程环境2.唤醒父线程3.把自己的状态改为INITIALIZED4.自己阻塞,等待调用os::start_thread()5.执行run方法子线程为什么要唤醒父线程再把自己设置成阻塞状态?猜想:其实也可以在这里直接让子线程去运行run方法,但是hotspot的开发人员却是将子线程的环境初始化工作和执行逻辑分成两个阶段执行的,具体出于什么原因不得而知,但其实子线程和主线程等来等去都是为了在一个合适的时机让运行环境准备好再去执行,而不是直接一下全部执行完
因为它是通过操作偏移量进行设置oop对象中的JavaThread
在创建线程的时候,还将创建的子线程加入到了一个线程单链表当中,因为OS并没有提供这样的一个API来获取这个进程的所有线程。也是为了后面的STW做准备
如果线程创建失败了,那么就抛出异常。否则调用Thread::start(native_thread);启动线程,现在子线程还是处于阻塞状态,这里是主线程在执行。有个现象,如果唤醒完子线程,主线程在这里已经执行完了,但是没有发现主线程阻塞等待子线程执行完的逻辑。DETACHED还是有一个机制去等待子线程运行结束。核心代码还没找到
接着设置线程状态为RUNNABLE,调用pd_start_thread方法,
sync_with_child->notify()去唤醒子线程,子线程被唤醒后,就会执行run方法,这是一个多态方法,需要到JavaThread::run方法中去找,
这里设置了entry_point.run方法跑了之后,会执行exit动作
JavaThread的构造方法当中的逻辑主要做了以下三件事1.执行了Thread()2.set_entry_point(entry_point)3.os::create_thread();
Java线程实现整体结构main方法/程序代码的执行是由main线程来执行的new Thread(() -> System.out.println(\"ss\")).start();核心逻辑是在start()方法里面,new Thread只是创建了一个Java对象,Thread对象oop.new出来的Thread对象oop和JavaThread(C++对象)、OSThread(C++对象)、Linux线程这四层的对象创建、绑定逻辑也是在start()方法里面,
为什么join方法能阻塞直到线程执行结束?因为它底层是一个wait方法,当线程的run方法执行结束之后,会对其进行唤醒
Java世界 | (CallStub) | JVM世界generate_call_stub方法内部也是一种执行流核心节点:1.父线程与子线程的交替执行顺序2.四层关系的建立3.run方法是怎么跑起来的(线程模块与执行流模块是如何建立联系的的)
调试HotSpot源码的代码,添加如图所示的代码,就可以在我们自己定义的线程处下断点
代码部分
HotSpot源码中的位置
最终结论是: JVM捕获了段异常信号并做了处理,处理方式是栈帧回溯。
JVM设置的1024栈大小,就是MaxJavaStackTraceDepth的值。JVM默认支持的最大深度就是1024,为什么是1024?因为触发异常的时候需要遍历栈,导出栈信息,如果栈的深度很深,很肥时间费性能,就取了一个有象征意义的值。1024也是一个临界点,当栈深度达到1024,栈帧开始回溯。你可以理解成程序跑起来把栈深度冲到1024,开始回溯,回溯到初始调用时的栈,然后栈深度又开始冲1024.循环往复
最后,针对回调做优化,目前主流的优化方式有:尾递归优化、内联优化。JVM没有用尾递归优化,而是用的内联优化,专业名词叫递归内联
线程无限执行的现象演示。如果catch Error会永远执行,catch Exception的话,会抛出StackOverflowError错误。栈的深度大小默认为1024.既然这个Java程序能够无限执行,这个能力是操作系统自带的还是JVM开发出来的?
2.Java线程底层实现
线程API创建线程:pthread_create阻塞线程:pthread_join互斥锁:pthread_mutex_init、pthread_mutex_destroy、pthread_mutex_tryLock、pthread_mutex_lock、pthread_mutex_unlock结束线程:pthread_exit
线程池实现管理者线程:负责检测任务池及线程池来动态调节线程数量,如扩容、缩容,还有在一定条件下的阻塞与唤醒工作线程:抢任务、执行任务、生成统计数据
任务池实现一般采用环形队列实现。结合锁实现阻塞唤醒。环形队列有两种实现思路:数组、链表式
pthread_cond_wait()这个API做了什么?1.执行了unlock2.线程进入阻塞,阻塞在条件变量上3.唤醒之后又会lock
3.手写线程池
机制1.状态位 state 0 12.队列机制1.如果我们实现:普通队列2.AQS式的队列3.waitStatus 闹钟-3 同步相关-2 同步相关-1 可唤醒的线程0 默认1 cancel
接着分析当前线程不是第一个线程过来抢锁,从源码中我们可以看到如果抢锁失败,则先调用addWaiter()将当前线程封装成node节点,然后入队,此时还没有设置waitStatus,如果当前线程是第一个等待获取锁的线程,则该线程还要承担队列初始化的任务
接着调用acquireQueued方法,判断前面将当前线程封装成的Node的前驱节点是否为头节点,如果为头节点则再次尝试加锁
lock方法的逻辑1.获取当前锁状态位2.如果为0,则表示当前锁没有被其他人持有当锁状态为0,判断是否有形成队列,如果此时队列还没有,则说明自己是第一个线程来加锁,接着会进行CAS设置锁状态位,设置独占锁线程属性。如果说队列已经形成了,说明自己不是第一个抢锁的线程,那就要入队如果不为0,则判断是否重入,如果不是,则加锁失败
如果释放锁成功,则调用unparkSuccessor唤醒后面队列中的线程
unlock方法的逻辑1.如果解锁方法不是当前持有锁的线程,则直接抛出异常2.如果重入解锁的次数小于加锁的次数,则正常将重入锁的次数减13.如果说当前锁状态位已经置为0,恢复到无锁的状态了,则释放锁成功,将当前独占线程置为空
公平锁
tryAcquire()的非公平锁加锁逻辑nonfairTryAcquire()进来之后,1.如果当前锁状态位为无锁状态,则直接进行CAS抢锁,如果抢到锁了,则会将锁的独占线程设置为自己本身。这里也是体现了非公平抢锁的特点,在公平锁当中,如果锁状态位为无锁,则会判断是否有队列形成以及排队逻辑。这里呢就暴力,抢到锁为目的,2.重入锁
lock方法的逻辑可以看到,ReentrantLock上来就直接抢锁,CAS的方式,抢不到锁再走acquire()方法
unlock方法的逻辑,解锁逻辑,两种模式都是一致的,像重入解锁,非重入解锁
非公平锁
公平锁和非公平锁的差异在于锁释放之后,下一个节点还没开始抢锁,此时信赖了一个线程找到了这个线程缝隙于是抢锁成功
4.AQS
并发编程三大特性:1.可见性2.有序性3.原子性
volatile为何而生?Java中其实也不一定非得叫这个关键字,也可以叫别的名称,只要它能实现volatile应该干的事情。语言的特性实现是编译系统和运行系统搭配起来的结果。volatile贯穿了计算机的每一层。Java层 内核层 硬件层volatile的出现是解决JMM(Java Memory Model)模型和CPU乱序执行存在的问题volatile触发了MESI协议(谬论),MESI协议不需要触发,这是CPU本身内部就会触发这个协议,因为CPU数据刷新是有两种策略,一种是同步,一种是异步写(目前的主流机制)。所以MESI协议确实可以通过内存屏障触发,目的是在异步机制下保证同步刷新,不是说你不加就不触发了,而是硬件本身就会触发MESI协议的,只不过会有延时.
原生线程操作共享变量
Java线程去操作共享变量,不加static修饰
Java线程去操作共享变量,加static修饰
Java线程去操作共享变量,加上volatile修饰
开启了编译优化,则直接对全局变量进行赋值
编译优化。其实在这里是不需要开辟堆栈的,因为没有局部变量需要去存储到栈帧操作,只有一个全局变量。模板解释器、JIT也有编译优化,内部使用的是O2编译级别编译优化对线程可见性是没有影响的。 volatile其实就是编译屏障。在开启优化的情况下,程序本来编译出来的就是精简的版本。加上volatile修饰,就是告诉编译器,跟这个变量相关的程序不要做优化
CPU乱序执行/ CPU顺序执行目的:为了保证执行的高效乱序有两层1.指令的乱序2.读写的乱序这些都是CPU内部执行的,需要用CPU机制去解决,进而引入了内存屏障volatile也可以保证了CPU的顺序执行。
对属性或静态属性的操作,对应的bytecodeInterpreter中会有是否有volatile修饰的判断
核心代码.在不同架构下,屏障指令是不一样的,CPU内核有对称架构和非对称架构。__asm__ volatile (\
JVM是怎么知道这个变量被volatile修饰的呢?字段的访问标志中会有volatile修饰?
可见性volatile是如何实现可见性的?1.回写2.实时触发MESI协议,实时回写内存volatile跟原子性关系不大__asm__ volatile (\
有序性volatile能保证程序的有序性,其实是在告诉我们CPU会乱序执行,当然还有可能是编译优化造成的。认为乱序执行效率更高这是编译优化造成的。研究编译优化有三个层面1.Java层面(加/不加volatile,生成的字节码是不是一样的,结果是一样的)2.编译层面OpenJDK在本身在编译阶段是开启了-O2优化的。可能存在指令重排,为了执行效率.对于部分代码,在编译的时候不希望被优化,就插入编译屏障3.运行层面CPU乱序执行有可能带来的问题,单核情况跟多核情况执行结果不一样。所以有了as-if-serial语义(CPU的约束,不是Java层面)。需要保证单核和多核执行结果都一样int a = 10;int sum = a + 20;a = 20
内存屏障1.保证了数据已回写内存2.隔离了写与读触发实时回写。CPU写内存,内存数据刷硬盘1.同步写 write through2.异步写(主流实现),正常情况下,效率比同步要高。会存在延时,但是这个延时是纳秒级别的。write back正常写程序,一般不需要加volatile,在并发超高的情况下要求数据的实时性,才会加,接受不了纳秒级别的并发可以通过屏障来让CPU实现同步写,有两种方案:1.lock (SMP 对称架构下)2.fence: mfence(读写)、lfence(读)、sfence(写) (NUMA架构下)HotSpot中有个注解// always use locked addl since mfence is sometimes expensivelock是锁总线fence族本身就是序列化读写,读写操作都放入到一个队列中,是硬件层面的一个CPU竞争,通过硬件信号解决的。这个指令操作比较昂贵
DCL为什么要加volatile?关键指令new 创建内存dup 把创建的对象在栈中复制一份,因为要充当this指针invokespecial 执行构造函数putstatic 把指针赋值给静态属性在超高并发情况下,有乱序执行的情况,创建完内存,还没执行构造函数,就把这个不完全对象指针返回了,本质是解决CPU的乱序执行带来不完全对象的问题。gcc优化越高,跨平台性越差DCL为什么要二次判空?因为第一次判断instance是否为null的时候参与竞争进入同步代码块的线程可能不止一个,但是一次只有一个能够进入同步代码块,其他的线程会进入阻塞状态等待锁的释放。当第一个进入同步代码块的线程创建好实例后退出同步代码块释放锁,这个时候处于阻塞状态线程立马被通知激活重新参与竞争,当获得锁的线程进入同步代码块时其实已经有一个实例了,所以为了保证单例要进行二次检测。也就是说,第一次判空是判断堆中有没有,第二次的判空是为了防止被阻塞的线程唤醒后再次创建新的对象,当一个线程创建完之后,会立马刷新到主内存当中,这样,后面的线程再进来时,第一个判空条件则不再满足,唤醒后的线程们也会由于第二个判空条件不成立退出,
5.volatile
markword的内存布局
对象的MarkWord组成
为什么能够用MarkWord内存地址的尾三位存储锁类型?8字节对齐
无锁
偏向锁
轻量级锁
重量级锁
锁对应的HotSpot中的实体类是什么?偏向锁的实体: 对象头 markOop 在线程栈中是没有记录的 工具类是BiasedLockking轻量级锁的实体是: BasicLock / BasicObjectLock的_lock属性是BasicLock记录在栈区, 是怎么放到栈上去的,重写操作符重量级锁的实体是:ObjectMonitor 记录在堆区整个synchronized的工具类: ObjectSynchronizer:fast_enter() 整个锁机制都是从在这个类里面
synchronized和ReentrantLock对比都是一种线程锁,保证互斥,用来保证同步synchronized C++写的 重量级锁 mutex 队列比较多,为什么没有直接用mutex去实现?因为是操作系统的代码, 1.不方便调试 2.实现起来没那么复杂 3. Qmode 希望定制化AQS ReentrantLock Java写的 CLH队列其实就是EntryList
锁的用法1.代码块2.修饰方法---------------------------都是遵循锁膨胀逻辑3.Unsafe类 # monitorenter 只有mutex的重量级锁,没有膨胀逻辑
研究synchronized市面上的资料都是从bytecodeInterpreter字节码解释器的_monitorenter指令已经不再用了改用模板解释器的ObjectSyncrhonizer::fast_enter(),怎么证明:Java层面:JOL-core
怎么找native方法1.常识 Unsafe在JVM中有一个.c的文件2.找到JVM_xxxx开头的方法,3.再去Jvm.cpp中去找
基础知识
为什么要延迟4s呢?答案在HotSpot的源码注释当中翻译如下:如果启用了偏向锁,则调度一个任务在运行时启动几秒钟这将为当前加载的所有类以及未来加载的类开启偏向锁。这是一种解决启动时间倒退的方法因为在VM启动期间,为了取消偏差而采取大量安全点。理想情况下,我们将有一个更低的成本,以消除个人偏见,而不需要这样的机制
为什么新创建的对象是无锁?1.Java对象,在JVM中就是一个oop2.对象头就是markOop.C++里面的一个对象private: volatile markOop _mark;
创建一个对象的时候通过allocate_instance(),Java的对象是内存编织的。申请内存和属性赋值是分开的,这个方法是申请内存,new Object(); 对象占16字节。
调用obj_allocate()申请内存返回一个HeapWord指针,但是还没有进行属性赋值
接着我们再来看post_allocate_setup_obj()方法,这个里面主要看post_allocate_setup_common()方法,
如果使用了偏向锁就将对象的markword设置为klass->prototype_header();反之则设置为markOopDEsc::prototype.
对象头会存在两个地方klass.hpp中有个属性_prototype_header对象头oop_mark 是从klass_prototype_header中copy过来的。当开启了偏向锁的时候取决于klass的prototype_header是无锁还是偏向锁当偏向锁还没开启的时候,比如说JVM刚启动的时候,此时就是无锁,markOopDesc::prototype(),所以新创建的对象是无锁
延迟偏向之前:无锁.
延迟偏向开启之后,为什么是偏向锁?(klass._prototype_header是如何变成偏向锁的)拆分成两个问题1.klass在创建的时候,是什么? 看klass的构造函数,里面有句代码set_prototype_header(markOopDesc::prototype());
2.何时变成偏向锁的? JVM肯定是有一个时机把klass._prototype_header改成了偏向锁。init()方法
biasedLocking.cpp#doit()SystemDictionary::clases_do(enable_biased_locking()).把延迟偏向之前加载的类,遍历,把它们设置为偏向锁
enable_biased_locking()方法内开启偏向锁k->set_prototype_header(markOOpDesc::biased_locking_prototype())
dictionary.cpp#classes_do()开始遍历。f(k)
加载的Test2创建的对象是偏向锁
systemDictionary#update_dictionary
klass和oop都有一个markWord
延迟偏向之后: 可偏向的偏向锁
新创建的对象的锁是如何被延迟偏向影响的?
延迟偏向之前加载的类的初始锁是什么锁?
轻量级锁实体: BasicObjectLock的一个_lock属性是BasicLock
延迟偏向之后加载的类是无锁还是偏向锁
延迟偏向是什么?有个调优参数,JVM启动的时候偏向锁是不开启的,是有延迟的,java -XX:+PrintFlagsFinal -version | grep DealyBiasedLockingStartupDelay 4s
BiasedLocking不能理解成是偏向锁实体,它就是一个工具类,类似SytemDirectoryBiasedLocking::revoke_and_rebias撤销和重偏向fast_enter()如果还未置为偏向锁,就走slow_enter判断是否为无锁,如果是,则加轻量级锁,否则就上重量级锁
ObjectLocker加锁和解锁
ObjectLocker:类的初始化要上锁,上的就是这把锁,轻量级锁
轻量级锁重入生成LockRecord,displaced_header为null。其实这种说法有点问题,C++的析构函数会解锁的。在JVM层面轻量级锁重入是不管的,为什么?因为轻量级锁其实是不需要处理的,它是一种CAS操作,不像是mutex_lock有明显的加锁重入,需要解锁
什么是执行流?__ movptr__ 是一种宏定义,代码生成器的意思movptr 汇编指令1.HotSpot源码中的执行流是什么意思生成执行流JVM初始化启动时,申请一块内存用来存储执行流,JVM会编译生成的汇编代码所对应的机器码,然后声明一个指针指向这块代码,这就叫硬编码。下断点的话,需要在内存中去调试执行流如何存储执行流如何运行void *(*lock_method_type)(void*);lock_method_type lock_method = 0x7ff324234;lock_method();2.synchronized相关的执行流有哪些monitorenter:字节码解释器 bytecodeInterpreter.cpp模板解释器 TemplateTable::monitorenter()_x86_64.cpp (主流)方法的修饰符call_stsub->entry_point->InterpreterGenerator::lock_method
synchronized修饰方法,多次调用则创建多个栈帧synchronized修饰代码块,多次调用就在一个栈帧里面生成多个lock record
为什么要生成lock_record?1.兼容轻量级锁2.为了批量撤销与批量重偏向display_header是一个随机数,这块是用不到的,偏向锁用不到oop当前的锁对象内存地址写进去
批量重偏向逻辑
遍历线程
批量撤销:当一个偏向锁如果撤销次数达到40的时候就认为这个对象设计的有问题,那么JVM会把这个对象锁对应的类所有对象撤销偏向锁,并且新实例化的对象也是不可偏向的
运行结果分析:1.t1执行完后,0-59的对象偏向t12.t2执行完后,0-18的对象为无锁,19-59偏向t23.t3执行时由于之前执行过批量重偏向了,所以这里会升级为轻量级锁4.t4休眠前对象65为匿名偏向状态,t4休眠后,由于触发了批量撤销,所以锁状态变为轻量级锁,所以批量撤销会把正在执行同步的对象的锁状态由偏向锁变为轻量级锁,而不再执行同步的对象的锁不会改变,如对象66批量重偏向和批量撤销是针对类的优化,和对象无关,偏向锁重偏向一次之后不可再次重偏向。当某个类已经已经触发批量撤销机制后,JVM会默认当前的类产生了严重的问题,剥夺该类的新实例对象使用偏向锁的权利
见草稿图
AQS的CLH队列其实就是EntryList如果偏向锁恢复到无锁是锁降级那就存在降级这个概念但是轻量级锁不i变成偏向锁,重量级锁不会变成轻量级锁
什么是线程饥饿问题?线程进入了队列,迟迟得不到机会,
封装线程节点
入队,然后调用exit
wait方法是重量级锁才有的synchronizer.cpp#wait()直接膨胀成重量级锁,释放锁,加入队列把当前线程包装成ObjectWaiter加入到_WaitSet队列中去WaitSet 它是一个回环队列,尾插法,头出队
从WaitSet的头节点开始,取出WaitSet中的节点,然后加入到_cxq的前面
notifyObjectMonitor:notify()不是直接唤醒,而是加入到调度队列中,还会有三个队列中拷贝的操作会有Policy机制去决定怎么唤醒
CAS抢锁,初始化队列
自适应自旋
如果自旋失败,封装节点
CAS入队每失败一次,就尝试一次加锁
tryLock CAS操作
入队/出队操作:EnterI():初始化队列、自旋。如果一旦入队成功,那么就会直接跳出去,等待唤醒,反之,如果没有入队成功,那么还会tryLock再抢一次锁
QMode == 0什么顺序进去,就什么顺序取出来,先进入cxq的后获取调度如果当前调度的是最后一个线程,那么会做下面几件事儿:1.release_store_ptr(): 释放锁2.OrderAccess:storeload(); 立马更新到内存中去3.判断_EntryList和_cxq队列是否为空,如果为空,那么也就不需要做唤醒动作了,直接退出
QMode == 1将cxq队列中的节点进行翻转,先进入队列先调度唤醒
QMode == 2唤醒_cxq队列中第一个节点线程
QMode == 3_EntryList->_cxq把_cxq容器中的数据移动到EntryList的后面,顺序不变,没有做唤醒动作
QMode == 4_cxq->_EntryList不做唤醒动作
最后唤醒_EntryList
调度./ exit方法QMode参数解释:0(默认):什么顺序进去,就什么顺序取出来,先进入cxq的后获取调度1: 入队顺序是反的,将cxq队列中的节点进行反转,先进入队列先唤醒2:唤醒cxq中的第一个节点3:把cxq容器中的数据移到EntryList的后面,顺序不变,没有做唤醒工作4:cxq加入到EntryList的前面,不做唤醒工作有待考究? 这个是没错的如果3和4的情况,走到后面如果为3,唤醒cxq的第一个线程如果为4,EntryList的第一个线程被唤醒
AQS就是六大操作1.抢锁 自旋抢锁(可忽略) 加入队列 阻塞2.释放锁 抢锁 出队线程抢锁之间会存在一些缝隙,通过抢锁的方式去弥补这些缝隙 几大机制可能会略有区别state AQS中用state标识锁状态位ObjectMonitor使用_owner判断当前哪个线程拿到了锁currentThread:
6.synchrnoized
重量级锁其实底层也是AQS.本质上synchronized也是一种AQS
syncrhonized可以做互斥也可以做同步线程的阻塞与唤醒mutex 互斥锁 Linuxcond 条件锁ParkEvent其实是对这两个变量抽象化的一个实体
enter()// 1.CAS抢锁// 2.重入// 3.加入队列// 4.线程阻塞exit()// 1.处理重入// 2.CAS释放锁// 3. 唤醒队列中的节点
轻量级锁 偏向锁都是在先有了重量级锁之后才被开发出来的
在synchronized中,轻量级锁是最简单,偏向锁是最复杂的。偏向锁解决的是单个线程的线程同步问题,轻量级锁的CAS成本过高
1.如果marword对象头中已经膨胀成重量级锁了,直接返回
2.如果说markword对象头正在膨胀,那么会进行park阻塞,等待唤醒,并非自旋一直获取重量级锁
3.如果当前markword中是轻量级锁,会构造ObjectMonitor
上轻量级锁必须要是在恢复成无锁的情况才能膨胀,偏向锁需要在STW进行安全点撤销,要把无锁的markword设置到displaced_header中
它的目的主要是为了节省CAS的开销,认为其开销很大。如果没有偏向锁,只有轻量级锁,CAS抢锁失败,判断是否重入,不是就会发生锁膨胀。如果说一个线程,只用一个对象这样的场景,在JVM中,这样的场景居多,相当于每用一次都要CAS.HotSpot的开发人员认为开销很大,在偏向锁中,如果这个对象是匿名可偏向,当前线程加锁,只需要判断当前持有锁是否等于当前线程即可,无需CASowner == current_threadId
偏向锁的使用场景1.加载一个类偏向锁的逻辑1.模板解释器 lock_object biased_locking_enter2.fast_enter
偏向锁哪些地方用到了?它最开始是为了节省CAS的开销,但是随着版本的迭代,其他地方的升级,偏向锁也要兼容,牵扯的地方很多包括类加载的地方。它本身的开销已经超过了CAS的开销类加载的时候,初始化会加锁,会使用ObjectLocker fast_enter上锁
实现偏向锁锁对象oop对象头 markOop1.上锁CAS2.重入不需要考虑重入问题,不像mutex_lock,mutex_unlock成对的出现,它是借助了锁对象3.解锁撤销逻辑膨胀成轻量级锁,需要恢复到无锁,在JVM里面是通过VMThread实现的4.批量在HotSpot中,批量的操作是通过VMThread异步执行任务池实现的,手写版本暂时没有实现
调试HotSpot源码技巧需要使用ResourceMark,否则会内存泄漏找到自己调试的类,
1.抢锁膨胀成轻量级锁,拿到偏向锁的线程不会阻塞,而是会继续执行,在HotSpot中,是要线程互斥的,同一时刻只能有一个线程执行2.没有实现批量撤销 批量重偏向的功能
场景模拟t1拿到了偏向锁t2拿到了轻量级锁,应该对t1做处理,使其阻塞t3t2拿到了轻量级锁,t3只能去膨胀成重量级锁
7.手写synchronized
Java并发
0 条评论
下一页