1.面试突击46:公平锁和非公平锁有什么区别?
2.源码分析: Java中锁的锁源d锁种类与特性详解
3.08.从源码揭秘偏向锁的升级
4.初始synchronized关键字的偏向锁、轻量锁、代码重量锁
5.线ç¨å®å
¨çlistä¹synchronizedListåCopyOnWriteArrayList
6.synchronized关键字
面试突击46:公平锁和非公平锁有什么区别?
从公平的锁源d锁角度来说,Java 中的代码锁总共可分为两类:公平锁和非公平锁。但公平锁和非公平锁有哪些区别?孰优孰劣呢?在 Java 中的锁源d锁应用场景又有哪些呢?接下来我们一起来看。正文公平锁:每个线程获取锁的代码cms 源码下载顺序是按照线程访问锁的先后顺序获取的,最前面的锁源d锁线程总是最先获取到锁。 非公平锁:每个线程获取锁的代码顺序是随机的,并不会遵循先来先得的锁源d锁规则,所有线程会竞争获取锁。代码 举个例子,锁源d锁公平锁就像开车经过收费站一样,代码所有的锁源d锁车都会排队等待通过,先来的代码车先通过,如下图所示:
通过收费站的锁源d锁顺序也是先来先到,分别是张三、李四、王五,这种情况就是公平锁。 而非公平锁相当于,来了一个强行加塞的老司机,它不会准守排队规则,来了之后就会试图强行加塞,如果加塞成功就顺利通过,当然也有可能加塞失败,如果失败就乖乖去后面排队,这种情况就是非公平锁。
应用场景在 Java 语言中,锁 synchronized 和 ReentrantLock 默认都是非公平锁,当然我们在创建 ReentrantLock 时,可以手动指定其为公平锁,但 synchronized 只能为非公平锁。 ReentrantLock 默认为非公平锁可以在它的源码实现中得到验证,如下源码所示:当使用 new ReentrantLock(true) 时,可以创建公平锁,如下源码所示:
公平和非公平锁代码演示接下来我们使用 ReentrantLock 来演示一下公平锁和非公平锁的执行差异,首先定义一个公平锁,开启 3 个线程,每个线程执行两次加锁和释放锁并打印线程名的操作,如下代码所示:
import?java.util.concurrent.locks.Lock;import?java.util.concurrent.locks.ReentrantLock;public?class?ReentrantLockFairTest?{ static?Lock?lock?=?new?ReentrantLock(true);public?static?void?main(String[]?args)?throws?InterruptedException?{ for?(int?i?=?0;?i?<?3;?i++)?{ new?Thread(()?->?{ for?(int?j?=?0;?j?<?2;?j++)?{ lock.lock();System.out.println("当前线程:"?+?Thread.currentThread().getName());lock.unlock();}}).start();}}}以上程序的执行结果如下图所示:接下来我们使用非公平锁来执行上面的代码,具体实现如下:
import?java.util.concurrent.locks.Lock;import?java.util.concurrent.locks.ReentrantLock;public?class?ReentrantLockFairTest?{ static?Lock?lock?=?new?ReentrantLock();public?static?void?main(String[]?args)?throws?InterruptedException?{ for?(int?i?=?0;?i?<?3;?i++)?{ new?Thread(()?->?{ for?(int?j?=?0;?j?<?2;?j++)?{ lock.lock();System.out.println("当前线程:"?+?Thread.currentThread().getName());lock.unlock();}}).start();}}}以上程序的执行结果如下图所示:从上述结果可以看出,使用公平锁线程获取锁的顺序是:A -> B -> C -> A -> B -> C,也就是按顺序获取锁。而非公平锁,获取锁的顺序是 A -> A -> B -> B -> C -> C,原因是所有线程都争抢锁时,因为当前执行线程处于活跃状态,其他线程属于等待状态(还需要被唤醒),所以当前线程总是会先获取到锁,所以最终获取锁的顺序是:A -> A -> B -> B -> C -> C。
执行流程分析公平锁执行流程获取锁时,先将线程自己添加到等待队列的队尾并休眠,当某线程用完锁之后,会去唤醒等待队列中队首的线程尝试去获取锁,锁的使用顺序也就是队列中的先后顺序,在整个过程中,箭头朝上源码线程会从运行状态切换到休眠状态,再从休眠状态恢复成运行状态,但线程每次休眠和恢复都需要从用户态转换成内核态,而这个状态的转换是比较慢的,所以公平锁的执行速度会比较慢。
非公平锁执行流程当线程获取锁时,会先通过 CAS 尝试获取锁,如果获取成功就直接拥有锁,如果获取锁失败才会进入等待队列,等待下次尝试获取锁。这样做的好处是,获取锁不用遵循先到先得的规则,从而避免了线程休眠和恢复的操作,这样就加速了程序的执行效率。 公平锁和非公平锁的性能测试结果如下,以下测试数据来自于《Java并发编程实战》:
从上述结果可以看出,使用非公平锁的吞吐率(单位时间内成功获取锁的平均速率)要比公平锁高很多。
优缺点分析公平锁的优点是按序平均分配锁资源,不会出现线程饿死的情况,它的缺点是按序唤醒线程的开销大,执行性能不高。 非公平锁的优点是执行效率高,谁先获取到锁,锁就属于谁,不会“按资排辈”以及顺序唤醒,但缺点是资源分配随机性强,可能会出现线程饿死的情况。
总结在 Java 语言中,锁的默认实现都是非公平锁,原因是非公平锁的效率更高,使用 ReentrantLock 可以手动指定其为公平锁。非公平锁注重的是性能,而公平锁注重的是锁资源的平均分配,所以我们要选择合适的场景来应用二者。
是非审之于己,毁誉听之于人,得失安之于数。
公众号:Java面试真题解析
面试合集:/post/
源码分析: Java中锁的种类与特性详解
在Java中存在多种锁,包括ReentrantLock、Synchronized等,它们根据特性与使用场景可划分为多种类型,如乐观锁与悲观锁、可重入锁与不可重入锁等。本文将结合源码深入分析这些锁的设计思想与应用场景。
锁存在的意义在于保护资源,防止多线程访问同步资源时出现预期之外的错误。举例来说,当张三操作同一张银行卡进行转账,如果银行不锁定账户余额,可能会导致两笔转账同时成功,违背用户意图。因此,在多线程环境下,锁机制是必要的。
乐观锁认为访问资源时不会立即加锁,齿轮测量源码仅在获取失败时重试,通常适用于竞争频率不高的场景。乐观锁可能影响系统性能,故在竞争激烈的场景下不建议使用。Java中的乐观锁实现方式多基于CAS(比较并交换)操作,如AQS的锁、ReentrantLock、CountDownLatch、Semaphore等。CAS类实现不能完全保证线程安全,使用时需注意版本号管理等潜在问题。
悲观锁则始终在访问同步资源前加锁,确保无其他线程干预。ReentrantLock、Synchronized等都是典型的悲观锁实现。
自旋锁与自适应自旋锁是另一种锁机制。自旋锁在获取锁失败时采用循环等待策略,避免阻塞线程。自适应自旋锁则根据前一次自旋结果动态调整等待时间,提高效率。
无锁、偏向锁、轻量级锁与重量级锁是Synchronized的锁状态,从无锁到重量级锁,锁的竞争程度与性能逐渐增加。Java对象头包含了Mark Word与Klass Pointer,Mark Word存储对象状态信息,而Klass Pointer指向类元数据。
Monitor是实现线程同步的关键,与底层操作系统的Mutex Lock相互依赖。Synchronized通过Monitor实现,其效率在JDK 6前较低,但JDK 6引入了偏向锁与轻量级锁优化性能。
公平锁与非公平锁决定了锁的分配顺序。公平锁遵循申请顺序,非公平锁则允许插队,提高锁获取效率。
可重入锁允许线程在获取锁的同一节点多次获取锁,而不可重入锁不允许。共享锁与独占锁是另一种锁分类,前者允许多个线程共享资源,后者则确保资源的独占性。
本文通过源码分析,详细介绍了Java锁的种类与特性,以及它们在不同场景下的应用。了解这些机制对于多线程编程至关重要。此外,还有多种机制如volatile关键字、原子类以及线程安全的集合类等,需要根据具体场景逐步掌握。
.从源码揭秘偏向锁的升级
深入探讨偏向锁的升级至轻量级锁的过程,主要涉及HotSpot虚拟机的源码分析。在学习synchronized机制时,将通过本篇文章解答关于synchronized功能的相关问题。首先,sevlte源码分析进行一些准备工作,了解在分析synchronized源码前的必要步骤。然后,通过示例代码的编译结果,揭示synchronized修饰代码块后生成的字节码指令,以及这些指令对应的操作。进一步地,使用jol工具跟踪对象状态,提供更直观的数据支持。
接下来,重点解析monitorenter指令的执行过程,包括其与templateTable_x和interp_masm_x方法之间的关联。通过分析注释中的参数设置,可以理解偏向锁升级为重量级锁的逻辑,以及epoch在偏向锁有效性判断中的作用。进一步,详细介绍对象头(markOop)的结构和其在偏向锁实现中的具体功能,包括epoch的含义及其在更新过程中的角色。
在理解了偏向锁的原理后,将分析其在不同条件下的执行流程,包括是否可偏向、是否重入偏向、是否依旧可偏向、epoch是否过期以及重新偏向等分支逻辑。接着,介绍偏向锁撤销和重偏向的过程,以及在获取偏向锁失败后的操作,即执行轻量级锁加锁的过程。最后,讨论偏向锁与轻量级锁的区别,总结它们的关键技术和性能特点,并简述偏向锁的争议与现状。
在偏向锁的实现中,关键点在于CAS操作的使用,以及在CAS竞争失败时导致的锁升级。偏向锁适用于单线程执行的场景,但在线程交替持有执行时,撤销和重偏向逻辑的复杂性导致性能下降,因此引入轻量级锁以保证“轻微”竞争情况的安全性。尽管偏向锁在Java 中已被弃用,但在当前广泛应用的Java 8环境下,了解偏向锁的原理仍然具有重要意义。
总结而言,偏向锁与轻量级锁分别针对不同场景进行了优化,它们的核心逻辑基于CAS操作,但在处理线程竞争时的表现有所不同。通过深入学习这两种锁的升级过程,可以更好地理解synchronized机制在Java并发编程中的应用。
初始synchronized关键字的偏向锁、轻量锁、重量锁
作为一名Java程序员,synchronized关键字在日常编码中不可或缺。然而,是否真正理解了synchronized背后的工作原理呢?从性能角度来看,synchronized关键字在早期版本(JDK 1.6之前)只支持重量锁,罗盘源码大全这意味着线程在加锁时会由用户态切换到内核态,导致性能下降。为了解决这一问题,Doug Lea引入了ReentrantLock类库,其采用纯Java代码实现加锁逻辑,避免了用户态与内核态的切换,从而在多线程竞争同一把锁时,性能显著提高。
那么,synchronized关键字是如何实现这三种锁类型的?它们分别是偏向锁、轻量锁和重量锁。在JDK 1.6及之后版本中,synchronized引入了这些锁类型,以适应不同场景下的并发需求。从左到右,这三种锁的性能逐渐降低,但它们之间可以相互转换。JVM在特定条件下,如在无锁竞争时使用偏向锁,有锁竞争时转换为轻量锁或重量锁。
让我们深入探讨每种锁类型的特点。偏向锁在第一次加锁时偏向特定线程,后续加锁操作无需额外判断,性能最高,但若存在其他线程竞争锁,偏向锁会转换为轻量锁或重量锁。轻量锁在多个线程交替执行时使用,同样避免了用户态与内核态的切换。重量锁则支持所有并发场景,当偏向锁或轻量锁无法满足需求时,重量锁会取代它们,导致线程切换。
随着synchronized关键字引入偏向锁和轻量锁,其性能已经与ReentrantLock相当,甚至在某些情况下,JVM开发者更推荐使用synchronized。除非业务场景需要ReentrantLock的特性,如可打断、条件锁等,通常使用synchronized已经足够。
接下来,让我们探索JVM是如何判断synchronized给对象加的是什么锁。对象头中的“Mark Word”区域记录了锁的信息。通过“Mark Word”的值,可以判断对象当前所处的锁状态。例如,无锁或偏向锁时,最低几位表示锁状态;轻量锁时,前几位存储指向锁记录的对象;重量锁时,后几位标识重量锁。通过分析“Mark Word”,可以确定对象当前的锁类型。
动手验证代码,可以实现在不同锁状态下的对象布局信息,如偏向锁、轻量锁和重量锁。通过观察打印结果,可以直观地理解每种锁类型在对象头中的表示方式。在验证代码中,配置了关闭偏向延迟的JVM参数,确保初始对象布局为无锁或偏向锁状态。通过加锁和释放锁的操作,可以观察到“Mark Word”值的变化,从而了解不同锁状态的特性。
综上所述,synchronized关键字通过引入偏向锁、轻量锁和重量锁,显著优化了并发场景下的性能。理解这些锁类型及其在对象布局中的表示方式,对于深入掌握Java并发编程至关重要。探索JVM底层源码,可以更全面地了解synchronized加锁逻辑的实现细节,为高级并发编程奠定基础。
线ç¨å®å ¨çlistä¹synchronizedListåCopyOnWriteArrayList
å¨ä¸ç¯æç« ä¸æ们已ç»ä»ç»äºå ¶ä»çä¸äºlistéåï¼å¦ArrayListãlinkedlistçãä¸æ¸ æ¥çå¯ä»¥çä¸ä¸ç¯æç« /p/ab5bf7ä½æ¯åArrayListè¿äºä¼åºç°çº¿ç¨ä¸å®å ¨çé®é¢ï¼æ们该ææ ·è§£å³å¢ï¼æ¥ä¸æ¥å°±æ¯è¦ä»ç»æ们线ç¨å®å ¨çlistéåsynchronizedListåCopyOnWriteArrayListã
synchronizedListç使ç¨æ¹å¼ï¼
ä»ä¸é¢ç使ç¨æ¹å¼ä¸æ们å¯ä»¥çåºï¼synchronizedListæ¯å°Listéåä½ä¸ºåæ°æ¥å建çsynchronizedListéåã
synchronizedList为ä»ä¹æ¯çº¿ç¨å®å ¨çå¢ï¼
æ们å æ¥çä¸ä¸ä»çæºç ï¼
æ们大æ¦è´´äºä¸äºå¸¸ç¨æ¹æ³çæºç ï¼ä»ä¸é¢çæºç ä¸æ们å¯ä»¥çåºï¼å ¶å®synchronizedList线ç¨å®å ¨çåå æ¯å 为å®å ä¹å¨æ¯ä¸ªæ¹æ³ä¸é½ä½¿ç¨äºsynchronizedåæ¥éã
synchronizedListå®æ¹ææ¡£ä¸ç»åºç使ç¨æ¹å¼æ¯ä»¥ä¸æ¹å¼ï¼
å¨ä»¥ä¸æºç ä¸æ们å¯ä»¥çåºï¼å®æ¹ææ¡£æ¯å»ºè®®æ们å¨éåçæ¶åå éå¤ççãä½æ¯æ¢ç¶å é¨æ¹æ³ä»¥åå äºéï¼ä¸ºä»ä¹å¨éåçæ¶åè¿éè¦å éå¢ï¼æ们æ¥çä¸ä¸å®çéåæ¹æ³ï¼
ä»ä»¥ä¸æºç å¯ä»¥çåºï¼è½ç¶å é¨æ¹æ³ä¸å¤§é¨åé½å·²ç»å äºéï¼ä½æ¯iteratoræ¹æ³å´æ²¡æå éå¤çãé£ä¹å¦ææ们å¨éåçæ¶åä¸å éä¼å¯¼è´ä»ä¹é®é¢å¢ï¼
è¯æ³æ们å¨éåçæ¶åï¼ä¸å éçæ åµä¸ï¼å¦ææ¤æ¶æå ¶ä»çº¿ç¨å¯¹æ¤éåè¿è¡addæè removeæä½ï¼é£ä¹è¿ä¸ªæ¶åå°±ä¼å¯¼è´æ°æ®ä¸¢å¤±æè æ¯èæ°æ®çé®é¢ï¼æ以å¦ææ们对æ°æ®çè¦æ±è¾é«ï¼æ³è¦é¿å è¿æ¹é¢é®é¢çè¯ï¼å¨éåçæ¶åä¹éè¦å éè¿è¡å¤çã
ä½æ¯æ¢ç¶æ¯ä½¿ç¨synchronizedå éè¿è¡å¤ççï¼é£è¯å®é¿å ä¸äºä¸äºéå¼éãæ没ææçæ´å¥½çæ¹å¼å¢ï¼é£å°±æ¯æ们å¦ä¸ä¸ªä¸»è¦ç并åéåCopyOnWriteArrayListã
CopyOnWriteArrayListæ¯å¨æ§è¡ä¿®æ¹æä½æ¶ï¼copyä¸ä»½æ°çæ°ç»è¿è¡ç¸å ³çæä½ï¼å¨æ§è¡å®ä¿®æ¹æä½åå°åæ¥éåæåæ°çéåæ¥å®æä¿®æ¹æä½ãå ·ä½æºç å¦ä¸ï¼
ä»ä»¥ä¸æºç æ们å¯ä»¥çåºï¼å®å¨æ§è¡addæ¹æ³åremoveæ¹æ³çæ¶åï¼åå«å建äºä¸ä¸ªå½åæ°ç»é¿åº¦+1å-1çæ°ç»ï¼å°æ°æ®copyå°æ°æ°ç»ä¸ï¼ç¶åæ§è¡ä¿®æ¹æä½ãä¿®æ¹å®ä¹åè°ç¨setArrayæ¹æ³æ¥æåæ°çæ°ç»ãå¨æ´ä¸ªè¿ç¨ä¸æ¯ä½¿ç¨ReentrantLockå¯éå ¥éæ¥ä¿è¯ä¸ä¼æå¤ä¸ªçº¿ç¨åæ¶copyä¸ä¸ªæ°çæ°ç»ï¼ä»èé æçæ··ä¹±ã并ä¸ä½¿ç¨volatile修饰æ°ç»æ¥ä¿è¯ä¿®æ¹åçå¯è§æ§ã读åæä½äºä¸å½±åï¼æ以å¨æ´ä¸ªè¿ç¨ä¸æ´ä¸ªæçæ¯é常é«çã
synchronizedListéå对æ°æ®è¦æ±è¾é«çæ åµï¼ä½æ¯å 为读åå ¨é½å éï¼æææçè¾ä½ã
CopyOnWriteArrayListæçè¾é«ï¼éå读å¤åå°çåºæ¯ï¼å 为å¨è¯»çæ¶å读çæ¯æ§éåï¼æ以å®çå®æ¶æ§ä¸é«ã
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中的指针指向自己。
今天就学到这里了!收工!
MarkWord和Synchronized的锁升级机制详解(JDK8)
锁升级机制在JDK 后已经废弃,本文所述仅为面试中常问的低版本synchronized的锁升级机制,具体新机制需查阅最新JDK源码。
在Java并发编程中,synchronized是最常用的关键字,用于保护代码块和方法在多线程场景下的并发安全问题。synchronized锁基于对象实现,通常用于修饰同步方法和同步代码块。
下面给出一段简单的Java代码,包含三种synchronized的使用方法,通过反编译查看字节码,了解synchronized的实现原理。
修饰方法时,synchronized关键字会在方法的字节码中添加ACC_SYNCHRONIZED标志,确保只有一个线程可以同时执行该方法。synchronized修饰静态方法同样添加此标志。
修饰代码块时,synchronized关键字会在相应的指令区间添加monitorenter和monitorexit指令,JVM通过这两个指令保证多线程状态下的同步。
ACC_SYNCHRONIZED、monitorenter、monitorexit的解释,来源于官网介绍和chatgpt翻译。
方法级的synchronized隐式执行,通过ACC_SYNCHRONIZED标志区分,方法调用指令会检查此标志。调用设置ACC_SYNCHRONIZED的方法时,线程进入monitor,执行方法,并在方法调用正常完成或异常中断时退出monitor。
monitorenter指令尝试获取与对象相关联的monitor的所有权,monitorexit指令执行时,对象相关联的monitor的进入计数减1。
Monitor是Java中用于实现线程同步和互斥的机制,每个Java对象都与一个Monitor相关联,主要目的是确保在任何给定时间,只有一个线程能够执行与特定对象相关联的临界区代码。
ObjectMonitor是JDK 的HotSpot源码中定义的Monitor,其核心参数包括EntrySet、WaitSet和一个线程的owner。
Java对象与monitor关联,需要了解Java对象布局和对象头的相关知识。
在JDK 1.6之前,synchronized需要依赖于底层操作系统的Mutex Lock实现,导致效率低下。在JDK 1.6之后,引入了偏向锁与轻量锁来减小获取和释放锁的性能消耗。
锁升级分为四种状态:无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁,锁会随着线程的竞争情况逐渐升级,但锁升级是不可逆的。
偏向锁在没有其他线程竞争时,持有偏向锁的线程不会主动释放,偏向锁的释放时机是在其他线程竞争该锁时。
轻量级锁使用CAS操作,尝试将对象头部的锁记录指针替换为指向线程栈上的锁记录。轻量级锁的撤销意味着不再通过自旋的方式等待获取锁,而是直接阻塞线程。
重量级锁状态下,对象的头部会指向一个Monitor对象,该Monitor对象负责管理锁的获取和释放。
JDK 1.6及之后版本引入了自适应自旋锁、锁消除和锁粗化等锁优化策略,以进一步提升synchronized的性能。
自适应自旋锁根据前一次在相同锁上的自旋时间以及锁的持有者状态来动态决定自旋的上限次数。
锁消除是JVM在JIT编译期间进行的优化,通过逃逸分析来消除不可能存在共享资源竞争的锁。
锁粗化是通过将加锁范围扩展到整个操作序列的外部,降低加锁解锁的频率来减少性能损耗。
本文总结了JDK8中synchronized的锁升级机制,介绍了无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁的升级流程,以提升并发效率。
关于@synchronized,你所不知道的事情
使用 Objective-C 编写并发程序时,可能会遇到 @synchronized 的使用。它的作用类似锁(lock),防止不同线程同时执行同一段代码。相较于使用 NSLock 创建锁对象、加锁和解锁,@synchronized 更方便、可读性更高。下面通过一个例子来说明它的使用方法。
假设我们需要实现一个线程安全的队列,通过 @synchronized 结构简化代码实现。在初始阶段,我们可能直接使用 NSLock 实现,但在使用 @synchronized 结构后,代码会更加简洁。
在前面的例子中,@synchronized 结构与锁操作的效果相同,可以视为锁定 self,确保代码在特定对象上只执行一次。通过左括号 { 和右括号 } 控制锁的获取与释放,省去了手动管理锁的步骤。
@synchronized 结构可以应用到任何 Objective-C 对象上,使用 @synchronized(_elements) 相当于锁定 self。这种实现方式简化了锁的操作,并保证了线程安全。
研究 @synchronized 的实现细节时,我们发现它在对象上暗中添加了异常处理。当同步对象时抛出异常,锁会被自动释放。同时,@synchronized 结构在工作时为传入对象分配了一个递归锁。在代码中,我们观察到它如何实现锁的分配、释放以及处理 nil 的情况。
通过阅读源码,我们了解到 @synchronized 结构如何将锁与对象关联,并在同步过程中处理内存地址哈希、链表操作、锁的加锁与解锁等关键步骤。它通过递归锁机制确保同一线程多次获取锁时不会造成死锁。
在实际应用中,@synchronized 结构通过函数 objc_sync_enter 和 objc_sync_exit 实现锁的管理。当对象在 @synchronized block 中被释放或设为 nil 时,系统能够正确处理并避免潜在的竞态条件(race conditions),确保线程安全。
总结来说,@synchronized 结构通过简化锁的操作、分配递归锁以及处理内存管理细节,为 Objective-C 程序提供了高效的线程安全机制。研究其实现有助于深入理解并发编程中锁的概念与应用,进一步提升程序的可靠性和性能。