1.MarkWord和Synchronized的源码锁升级机制详解(JDK8)
2.初始synchronized关键字的偏向锁、轻量锁、源码重量锁
3.synchronized关键字
4.线ç¨å®å
¨çlistä¹synchronizedListåCopyOnWriteArrayList
5.Redis 源码实现分布式锁 +Redisson 源码解析
6.synchronized和ReentrantLock的5个区别!
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关键字的偏向锁、轻量锁、重量锁
作为一名Java程序员,synchronized关键字在日常编码中不可或缺。然而,是否真正理解了synchronized背后的工作原理呢?从性能角度来看,synchronized关键字在早期版本(JDK 1.6之前)只支持重量锁,这意味着线程在加锁时会由用户态切换到内核态,导致性能下降。为了解决这一问题,Doug Lea引入了ReentrantLock类库,其采用纯Java代码实现加锁逻辑,避免了用户态与内核态的切换,从而在多线程竞争同一把锁时,性能显著提高。
那么,synchronized关键字是如何实现这三种锁类型的?它们分别是偏向锁、轻量锁和重量锁。在JDK 1.6及之后版本中,synchronized引入了这些锁类型,以适应不同场景下的juc系列源码并发需求。从左到右,这三种锁的性能逐渐降低,但它们之间可以相互转换。JVM在特定条件下,如在无锁竞争时使用偏向锁,有锁竞争时转换为轻量锁或重量锁。
让我们深入探讨每种锁类型的特点。偏向锁在第一次加锁时偏向特定线程,后续加锁操作无需额外判断,性能最高,但若存在其他线程竞争锁,偏向锁会转换为轻量锁或重量锁。轻量锁在多个线程交替执行时使用,同样避免了用户态与内核态的切换。重量锁则支持所有并发场景,当偏向锁或轻量锁无法满足需求时,重量锁会取代它们,导致线程切换。
随着synchronized关键字引入偏向锁和轻量锁,其性能已经与ReentrantLock相当,甚至在某些情况下,JVM开发者更推荐使用synchronized。除非业务场景需要ReentrantLock的特性,如可打断、条件锁等,通常使用synchronized已经足够。
接下来,让我们探索JVM是如何判断synchronized给对象加的是什么锁。对象头中的“Mark Word”区域记录了锁的信息。通过“Mark Word”的值,可以判断对象当前所处的锁状态。例如,无锁或偏向锁时,最低几位表示锁状态;轻量锁时,前几位存储指向锁记录的对象;重量锁时,后几位标识重量锁。通过分析“Mark Word”,可以确定对象当前的锁类型。
动手验证代码,可以实现在不同锁状态下的对象布局信息,如偏向锁、轻量锁和重量锁。通过观察打印结果,可以直观地理解每种锁类型在对象头中的表示方式。在验证代码中,配置了关闭偏向延迟的JVM参数,确保初始对象布局为无锁或偏向锁状态。spark编程源码通过加锁和释放锁的操作,可以观察到“Mark Word”值的变化,从而了解不同锁状态的特性。
综上所述,synchronized关键字通过引入偏向锁、轻量锁和重量锁,显著优化了并发场景下的性能。理解这些锁类型及其在对象布局中的表示方式,对于深入掌握Java并发编程至关重要。探索JVM底层源码,可以更全面地了解synchronized加锁逻辑的实现细节,为高级并发编程奠定基础。
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对象以及变量。admin网站源码
在开发中最常用的是用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中的指针指向自己。
今天就学到这里了!收工!
线ç¨å®å ¨ç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æçè¾é«ï¼éå读å¤åå°çåºæ¯ï¼å 为å¨è¯»çæ¶å读çæ¯æ§éåï¼æ以å®çå®æ¶æ§ä¸é«ã
Redis 实现分布式锁 +Redisson 源码解析
在一些场景中,多个进程需要以互斥的方式独占共享资源,这时分布式锁成为了一个非常有用的工具。
随着互联网技术的快速发展,数据规模在不断扩大,分布式系统变得越来越普遍。一个应用往往会部署在多台机器上(多节点),在某些情况下,为了保证数据不重复,同一任务在同一时刻只能在一个节点上运行,即确保某一方法在同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,仅需通过Java提供的 volatile、ReentrantLock、synchronized 及 concurrent 并发包下的线程安全类等来保证线程安全性。而在多机部署环境中,不同机器不同进程,需要在多进程下保证线程的安全性,因此分布式锁应运而生。
实现分布式锁的三种主要方式包括:zookeeper、Redis和Redisson。这三种方式都可以实现分布式锁,但基于Redis实现的性能通常会更好,具体选择取决于业务需求。
本文主要探讨基于Redis实现分布式锁的方案,以及分析对比Redisson的RedissonLock、RedissonRedLock源码。
为了确保分布式锁的可用性,实现至少需要满足以下四个条件:互斥性、过期自动解锁、请求标识和正确解锁。实现方式通过Redis的set命令加上nx、px参数实现加锁,以及使用Lua脚本进行解锁。实现代码包括加锁和解锁流程,核心实现命令和Lua脚本。这种实现方式的主要优点是能够确保互斥性和自动解锁,但存在单点风险,即如果Redis存储锁对应key的节点挂掉,可能会导致锁丢失,导致多个客户端持有锁的情况。
Redisson提供了一种更高级的实现方式,实现了分布式可重入锁,包括RedLock算法。Redisson不仅支持单点模式、主从模式、哨兵模式和集群模式,还提供了一系列分布式的Java常用对象和锁实现,如可重入锁、公平锁、联锁、读写锁等。Redisson的使用方法简单,旨在分离对Redis的关注,让开发者更专注于业务逻辑。
通过Redisson实现分布式锁,相比于纯Redis实现,有更完善的特性,如可重入锁、失败重试、最大等待时间设置等。同时,RedissonLock同样面临节点挂掉时可能丢失锁的风险。为了解决这个问题,Redisson提供了实现了RedLock算法的RedissonRedLock,能够真正解决单点故障的问题,但需要额外为RedissonRedLock搭建Redis环境。
如果业务场景可以容忍这种小概率的错误,推荐使用RedissonLock。如果无法容忍,推荐使用RedissonRedLock。此外,RedLock算法假设存在N个独立的Redis master节点,并确保在N个实例上获取和释放锁,以提高分布式系统中的可靠性。
在实现分布式锁时,还需要注意到实现RedLock算法所需的Redission节点的搭建,这些节点既可以是单机模式、主从模式、哨兵模式或集群模式,以确保在任一节点挂掉时仍能保持分布式锁的可用性。
在使用Redisson实现分布式锁时,通过RedissonMultiLock尝试获取和释放锁的核心代码,为实现RedLock算法提供了支持。
synchronized和ReentrantLock的5个区别!
在Java中,尽管synchronized(内置锁)和ReentrantLock(可重入锁)都具备锁定和同步的功能,但它们在使用方式、操作细节和特性上存在显著差异。以下是对两者五个主要区别的简要阐述:区别1:用法
synchronized适用于方法、静态方法和代码块,而ReentrantLock仅限于代码块。synchronized的锁操作更为简洁,而ReentrantLock则需要手动创建和管理锁。区别2:加锁与释放
synchronized自动进行加锁和释放,而ReentrantLock则需要显式调用lock和unlock方法。ReentrantLock的解锁操作需确保在finally块中执行,以防止资源泄漏。区别3:锁类型
synchronized是公平锁,ReentrantLock则可选择公平或非公平。默认ReentrantLock是非公平锁,通过new ReentrantLock(true)可创建公平锁。区别4:中断响应
ReentrantLock支持lockInterruptibly,允许中断并释放锁,解决死锁问题。synchronized则不会响应中断,可能导致线程阻塞。区别5:底层实现
synchronized基于JVM的监视器(Monitor),字节码中表现为monitorenter和monitorexit。ReentrantLock则通过AQS(AbstractQueuedSynchronizer)API实现,源码更易理解和定制。 总结来说,synchronized和ReentrantLock在使用时需根据具体需求选择,它们的差异体现在控制的灵活性、中断响应和底层实现等方面。源码分析: 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关键字、原子类以及线程安全的集合类等,需要根据具体场景逐步掌握。