1.深入探秘高性能并发:C++如何在Linux巧妙应用Futex实现线程锁同步(ob_latch.cpp篇)大篇幅(3万字)
2.ReentrantLock源码详细解析
3.Vue3之事件循环、线程线程nextTick与源码解析
4.信号量(Semaphore)从入门到源码精通
5.synchronize底层原理
6.GPU编程9:共享内存3→线程同步和数据布局
深入探秘高性能并发:C++如何在Linux巧妙应用Futex实现线程锁同步(ob_latch.cpp篇)大篇幅(3万字)
通过实例学习C++的同步同步Futex应用,理解线程锁同步在OceanBase 4.0源码中的源码源码巧妙使用
这篇文章详细介绍了如何在Linux环境下,利用C++的线程线程Futex实现线程锁同步,以开源项目ob_latch.cpp为例,同步同步探讨了自旋锁、源码源码怎么使用指标源码互斥锁和等待队列的线程线程实现和优缺点。 1. 自旋锁分析:通过low_try_lockA,同步同步自旋次数由max_spin_cnt控制,源码源码避免CPU资源浪费。线程线程 2. 互斥锁-ObLatchMutex:提供try_lock,同步同步 lock, wait三种加锁方式,分别对应不同的源码源码场景和策略。 3. ObLatchWaitQueue:管理等待队列,线程线程确保公平调度,同步同步如wait阻塞锁的源码源码使用和唤醒机制。 4. 锁的解锁逻辑:如ObLatchMutex的unlock,通过原子操作移除或减少锁的持有计数,必要时唤醒等待队列。 5. 高级锁封装:如ObLatchWGuard等RAII类,自动管理锁的生命周期,确保资源安全。 通过以上组件的组合,开发者可以灵活设计线程同步机制,保证多线程环境下资源访问的正确性和效率。 如果你在项目中设计线程锁,可以根据这些原理和实例进行调整和优化。ReentrantLock源码详细解析
在深入解析ReentrantLock源码之前,我们先了解ReentrantLock与同步机制的关系。ReentrantLock作为Java中引入的并发工具类,由Doug Lea编写,相较于synchronized关键字,它提供了更为灵活的锁管理策略,支持公平与非公平锁两种模式。AQS(AbstractQueuedSynchronizer)作为实现锁和同步器的核心框架,由AQS类的独占线程、同步状态state、FIFO等待队列和UnSafe对象组成。AQS类的内部结构图显示了其组件的构成。在AQS框架下,等待队列采用双向链表实现,头结点存在但无线程,T1和T2节点中的线程可能在自旋获取锁后进入阻塞状态。
Node节点作为等待队列的基本单元,分为共享模式和独占模式,安卓screenrecorder源码值得关注的是waitStatus成员变量,它包含五种状态:-3、-2、-1、0、1。本文重点讨论-1、0、1状态,-3状态将不涉及。非公平锁与公平锁的差异在于,非公平锁模式下新线程可直接尝试获取锁,而公平锁模式下新线程需排队等待。
ReentrantLock内部采用非公平同步器作为其同步器实现,构造函数中根据需要选择非公平同步器或公平同步器。ReentrantLock默认采用非公平锁策略。非公平锁与公平锁的区别在于获取锁的顺序,非公平锁允许新线程跳过等待队列,而公平锁严格遵循队列顺序。
在非公平同步器的实例中,我们以T1线程首次获取锁为例。T1成功获取锁后,将exclusiveOwnerThread设置为自身,state设置为1。紧接着,T2线程尝试获取锁,但由于state为1,获取失败。调用acquire方法尝试获得锁,尝试通过tryAcquire方法实现,非公平同步器的实现调用具体逻辑。
在非公平锁获取逻辑中,通过CAS操作尝试交换状态。交换成功后,设置独占线程。当当前线程为自身时,执行重入操作,叠加state状态。若获取锁失败,则T2和T3线程进入等待队列,调用addWaiter方法。队列初始化通过enq方法实现,enq方法中的王者荣耀重名源码循环逻辑确保线程被正确加入队尾。新线程T3调用addWaiter方法入队,队列初始化完成。
在此过程中,T2和T3线程开始自旋尝试获取锁。若失败,则调用parkAndCheckInterrupt()方法进入阻塞状态。在shouldParkAfterFailedAcquire方法中,当前驱节点等待状态为CANCELLED时,方法会找到第一个非取消状态的节点,并断开取消状态的前驱节点与该节点的连接。若T5线程加入等待队列,T3和T4线程因为自旋获取锁失败进入finally块调用取消方法,找到等待状态不为1的节点(即T2),断开连接。
理解了shouldParkAfterFailedAcquire方法后,我们关注acquireQueued方法的实现。该方法确保线程在队列中正确释放,如果队列的节点前驱为head节点,成功获取锁后,调用setHead方法释放线程。setHead方法通过CAS操作更新head节点,释放线程。acquire方法中的阻塞是为防止线程在唤醒后重新尝试获取锁而进行的额外阻断。
锁的释放过程相对简单,将state减至0,将exclusiveOwnerThread设置为null,完成锁的释放。通过上述解析,我们深入理解了ReentrantLock的锁获取、等待、释放等核心机制,为并发编程提供了强大的工具支持。
Vue3之事件循环、nextTick与源码解析
事件循环是JavaScript单线程执行的核心机制,确保了同步任务与异步任务能有序执行。同步任务按顺序执行,而异步任务则分为宏任务和微任务。宏任务包括setTimeout、setInterval、整体代码、ajax、postMessage、交互事件等,app源码级调试微任务则包括Promise.then、catch、finally、MutationObserver、process.nextTick(Node环境下)。
事件循环机制确保了同步任务先执行,宏任务和微任务则交替执行,形成事件循环的周期。此过程确保了JavaScript代码的流畅执行,避免了因耗时任务阻塞主线程导致的卡顿。
在Vue3中,nextTick功能用于处理异步更新DOM问题。它允许开发者在DOM更新之前执行异步代码,确保DOM的正确渲染。有以下两种使用方式:一种是直接传入回调函数,另一种是通过async和await实现。当对数据进行操作后,如果观察到DOM没有更新,原因在于Vue3中数据响应式是同步的,而DOM更新是异步的。
为解决此问题,可以使用nextTick将同步代码转化为异步代码,确保在浏览器的下一次事件循环中执行DOM更新。在Vue3源代码中,nextTick通过将同步代码包装为Promise,从而转化为异步任务来实现这一功能。
Vue3将DOM更新设置为异步,旨在优化性能。考虑到大量数据变化时,频繁的DOM更新可能导致性能开销过大,异步更新策略降低了这种浪费,提高了应用的响应性和性能效率。
信号量(Semaphore)从入门到源码精通
Semaphore是一个用于同步的工具类,在并发编程中扮演重要角色。PV操作是Semaphore的核心操作,P代表获取许可,V代表释放许可。P操作会检查许可是否可用,若可用则获取并返回,否则阻塞直到许可可用;V操作则释放一个许可。
Semaphore的使用场景主要涉及线程间的同步,比如在资源有限的情况下控制多个线程对资源的访问。例如,互联主机网源码当一个队列只允许一定数量的线程同时访问时,可以使用Semaphore来限制队列访问的线程数量。
使用Semaphore的方式是通过构造方法创建实例,然后使用acquire和release方法来控制许可的获取和释放。acquire方法接受一个参数,表示需要获取的许可数量,默认为1,如果当前可用许可数不足,线程将阻塞直到许可可用。release方法则释放指定数量的许可,通常为1。
要深入理解Semaphore源码,建议从AQS(AbstractQueuedSynchronizer)的基础开始。AQS提供了对同步器的基本抽象,Semaphore正是基于AQS实现的一种同步工具。
对于具体源码分析,可以重点关注Semaphore的构造方法、获取锁方法(acquire)以及释放锁方法(release)。在源码中,acquire方法通过调用tryAcquireShared方法尝试获取许可,如果成功则返回true,否则阻塞直到许可可用。release方法则简单地调用releaseShared方法释放一个许可。
深入学习Semaphore和并发编程,可以参考内核技术中文网的相关资源。该网站提供了一些学习资料和交流社区,包括Linux内核源码学习路线、视频教程、电子书以及实战项目和代码等。此外,网站还定期更新内核技术资料包,供学习者免费获取。
synchronize底层原理
synchronize底层原理是什么?我们先通过反编译下面的代码来看看Synchronized是如何实现对代码块进行同步的:
1 package com.paddx.test.concurrent;
2
3 public class SynchronizedDemo {
4 public void method() {
5 synchronized (this) {
6 System.out.println(Method 1 start);
7 }
8 }
9 }
反编译结果:
关于这两条指令的作用,我们直接参考JVM规范中描述:
monitorenter :
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitors entry count is zero, then tries again to gain ownership.
这段话的大概意思为:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
monitorexit:
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
这段话的大概意思为:
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。
通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。
我们再来看一下同步方法的反编译结果:
源代码:
1 package com.paddx.test.concurrent;
2
3 public class SynchronizedMethod {
4 public synchronized void method() {
5 System.out.println(Hello World!);
6 }
7 }
反编译结果:
从反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。 其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。
GPU编程9:共享内存3→线程同步和数据布局
并行线程间的同步是所有并行计算语言的重要机制,确保数据一致性与程序顺序执行。共享内存可以同时被线程块中的多个线程访问,当不同步的多个线程修改同一个共享内存地址时,将导致线程内的冲突。CUDA提供障碍(barrier)和内存栅栏(memory fences)来实现块内同步。
在弱排序内存模型下,GPU线程在不同内存写入数据的顺序不一定和这些数据在源码中的顺序相同,且一个线程的写入顺序对其他线程可见时,可能与写操作被执行的实际顺序不一致。为了显式地强制程序确切顺序执行,必须在代码中插入内存栅栏和障碍。
同步方法包括显式障碍和内存栅栏。显式障碍只能在同一线程块的线程间执行,通过调用void __syncthreads()函数来指定一个barrier点。__syncthreads作为barrier点要求块中的线程必须等待直到所有线程都到达该点。内存栅栏功能可确保栅栏前的任何内存写操作,对栅栏后的其他线程都是可见的,包括块、网格或系统级的内存栅栏。
Volatile修饰符用于防止编译器优化,避免数据在寄存器或本地内存中被缓存。GPU全局内存常驻在设备内存(DRAM),访问粒度可以是个字节或个字节,共享内存的访问粒度为4字节或8字节存储体宽。
数据布局通过选择共享内存的形状和访问方式来优化全局内存加载。方形共享内存块可以通过相邻线程访问邻近元素来优化,最佳实现方式是按行主序写、按行主序读。对于行列不等长的矩阵转置,可以使用共享内存进行并行归约或展开并行归约,以减少全局内存的访问。
通过全局内存进行矩阵转置时,读取行、存储列或读取列、存储行都会有一次读写的交叉访问。使用共享内存作为中转可以提高效率,因为共享内存相比全局内存有更好的带宽。共享内存中的交叉访问效率也高于全局内存。
性能上下限在不同硬件下表现可能不同,具体原因尚不明确。在实际编码中需要注意这个问题。
synchronized关键字
并发编程中的关键点在于数据同步、线程安全和锁。编写线程安全的代码,核心在于管理对共享和可变状态的访问。
共享意味着变量可以被多个线程访问,而可变则意味着变量的值在其生命周期内可以变化。
当多个线程访问某个状态变量,且有一个线程执行写入操作时,必须使用同步机制来协调对这些线程的访问。
Java中的主要同步机制是关键字synchronized,它提供了一种独占的加锁方式。
以下是关于synchronized关键字的几个方面:
关键字synchronized的特性:
不可中断:synchronized关键字提供了独占的加锁方式,一旦一个线程持有了锁对象,其他线程将进入阻塞状态或等待状态,直到前一个线程释放锁,中间过程不可中断。
原子性:synchronized关键字的不可中断性保证了它的原子性。
可见性:synchronized关键字包含了两个JVM指令:monitor enter和monitor exit,它能够保证在任何时候任何线程执行到monitor enter时都必须从主内存中获取数据,而不是从线程工作内存获取数据,在monitor exit之后,工作内存被更新后的值必须存入主内存,从而保证了数据可见性。
有序性:synchronized关键字修改的同步方法是串行执行的,但其所修饰的代码块中的指令顺序还是会发生改变的,这种改变遵守java happens-before规则。
可重入性:如果一个拥有锁持有权的线程再次获取锁,则monitor的计数器会累加1,当线程释放锁的时候也会减1,直到计数器为0表示线程释放了锁的持有权,在计数器不为0之前,其他线程都处于阻塞状态。
关键字synchronized的用法:
synchronized关键字锁的是对象,修饰的可以是代码块和方法,但不能修饰class对象以及变量。
在开发中最常用的是用synchronized关键字修饰对象,可以控制锁的粒度,所以针对最常用的场景,先来看看它的字节码文件。
TIPS:在使用synchronized关键字时注意事项
锁膨胀:
在jdk1.6之前,线程在获取锁时,如果锁对象已经被其他线程持有,此线程将挂起进入阻塞状态,唤醒阻塞线程的过程涉及到了用户态和内核态的切换,性能损耗比较大。
synchronized作为亲儿子,混的太差肯定不行,在jdk1.6对其进行了优化,将锁状态分为了无锁状态、偏向锁、轻量级锁、重量级锁。
锁的升级过程既是:
在了解锁的升级过程之前,重点理解了monitor和对象头。
每一个对象都与一个monitor相关联,monitor对象与实例对象一同创建并销毁,monitor是C++支持的一个监视器。锁对象的争夺即是争夺monitor的持有权。
在OpenJdk源码中找到了ObjectMonitor的源码:
owner:指向线程的指针。即锁对象关联的monitor中的owner指向了哪个线程表示此线程持有了锁对象。
waitSet:进入阻塞等待的线程队列。当线程调用wait方法之后,就会进入waitset队列,可以等待其他线程唤醒。
entryList:当多个线程进入同步代码块之后,处于阻塞状态的线程就会被放入entryList中。
那什么是对象头呢?它与synchronized又有什么关系呢?
在JVM中,对象在内存中分为3块区域:
我们先通过一张图了解下在锁升级的过程中对象头的变化:
接下来我们分析锁升级的过程:
第一个分支锁标志为:
当线程运行到同步代码块时,首先会判断锁标志位,如果锁标志位为,则继续判断偏向标志。
如果偏向标志为0,则表示锁对象未被其他线程持有,可以获取锁。此时当前线程通过CAS的方法修改线程ID,如果修改成功,此时锁升级为偏向锁。
如果偏向标志为1,则表示锁对象已经被占有。
进一步判断线程id是否相等,相等则表示当前线程持有的锁对象,可以重入。
如果线程id不相等,则表示锁被其他线程占有。
需进一步判断持有偏向锁的线程的活动状态,如果原持有偏向锁线程已经不活动或者已经退出同步代码块,则表示原持有偏向锁的线程可以释放偏向锁。释放后偏向锁回到无锁状态,线程再次尝试获取锁。主要是因为偏向锁不会主动释放,只有其他线程竞争偏向锁的时候才会释放。
如果原持有偏向锁的线程没有退出同步代码块,则锁升级为轻量级锁。
偏向锁的流程图如下:
第二个分支锁标志为:
在第一个分支中我们了解到在如果偏向锁已经被其他线程占有,则锁会被升级为轻量级锁。
此时原持有偏向锁的线程的栈帧中分配锁记录Lock Record,将对象头中的Mark Word信息拷贝到锁记录中,Mark Word的指针指向了原持有偏向锁线程中的锁记录,此时原持有偏向锁的线程获取轻量级锁,继续执行同步块代码。
如果线程在运行同步块时发现锁的标志位为,则在当前线程的栈帧中分配锁记录,拷贝对象头中的Mark Word到锁记录中。通过CAS操作将Mark Word中的指针指向自己的锁记录,如果成功,则当前线程获取轻量锁。
如果修改失败,则进入自旋,不断通过CAS的方式修改Mark Word中的指针指向自己的锁记录。
当自旋超过一定次数(默认次),则升级为重量锁。
轻量级流程图如下图:
第三个分支锁标志位为:
锁标志为时,此时锁已经为重量锁,线程会先判断monitor中的owner指针指向是否为自己,是则获取重量锁,不是则会挂起。
整个锁升级过程中的流程图如下,如果看懂了一定要自己画一遍。
总结:
synchronized关键字是一种独占的加锁方式,不可中断,保证了原子性、可见性和有序性。
synchronized关键字可用于修饰方法和代码块,但不能用于修饰变量和类。
多线程在执行同步代码块时获取锁的过程在不同的锁状态下不一样,偏向锁是修改Mark Word中的线程ID,轻量锁是修改Mark Word的指针指向自己的锁记录,重量锁是修改monitor中的指针指向自己。
今天就学到这里了!收工!
2024-12-22 21:05
2024-12-22 20:46
2024-12-22 20:35
2024-12-22 20:02
2024-12-22 19:23
2024-12-22 18:41