多线程之:偏向锁,轻量级锁,重量级锁

简介: 一:java多线程互斥,和java多线程引入偏向锁和轻量级锁的原因? --->synchronized的重量级别的锁,就是在线程运行到该代码块的时候,让程序的运行级别从用户态切换到内核态,把所有的线程挂起,让cpu通过操作系统指令,去调度多线程之间,谁执行代码块,谁进入阻塞状态。
一:java多线程互斥,和java多线程引入偏向锁和轻量级锁的原因?
--->synchronized的重量级别的锁,就是在线程运行到该代码块的时候,让程序的运行级别从用户态切换到内核态,把所有的线程挂起,让cpu通过操作系统指令,去调度多线程之间,谁执行代码块,谁进入阻塞状态。这样会频繁出现程序运行状态的切换,线程的挂起和唤醒,这样就会大量消耗资源,程序运行的效率低下。为了提高效率,jvm的开发人员,引入了偏向锁,和轻量级锁,尽量让多线程访问公共资源的时候,不进行程序运行状态的切换,由用户态进入内核态,借助操作系统进行互斥。

--->jvm规范中可以看到synchronized在jvm里实现原理,jvm基于进入和退出Monitor对象来实现方法同步和代码块同的。在代码同步的开始位置织入monitorenter,在结束同步的位置(正常结束和异常结束处)织入monitorexit指令实现。线程执行到monitorenter处,讲会获取锁对象锁对应的monitor的所有权,即尝试获得对象的锁。(任意对象都又一个monitor与之关联,当且一个monitor被持有后,他处于锁定状态)

--->java的多线程安全是基于lock机制实现的,而lock的性能往往不如人意。原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是jvm依赖操作系统互斥(mutex)来实现的。

--->互斥是一种会导致线程挂起,并在较短时间内又需要重新调度回原线程的,较为消耗资源的操作。

--->为了优化java的Lock机制,从java6开始引入轻量级锁的概念。轻量级锁本意是为了减少多线程进入互斥的几率,并不是要替代互斥。它利用了cpu原语Compare-And-Swap(cas,汇编指令CMPXCHG),尝试进入互斥前,进行补救。

二:为什么要自旋或者自适应自旋?
--->前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能 带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如 果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程“稍等一会”,但不放弃处理器的执行时间,看看持有 锁的线程是否很快就会释放锁。为了让线程等待,我们只须让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 

--->自旋锁在JDK 1.4.2中就已经引入,只不过默认是关闭的,可以使用-XX:+UseSpinning参数来开启,在JDK 1.6中就已经改为默认开启了。自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的, 所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作, 反而会带来性能的浪费。因此自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次 数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改。 

--->在JDK 1.6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象 上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间, 比如100个循环。另一方面,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自 旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。 


三:锁削除
--->锁削除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行削除。锁削除的主要判定依据来源于逃逸分析的数 据支持(第11章已经讲解过逃逸分析技术),如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待, 认为它们是线程私有的,同步加锁自然就无须进行。 

--->也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的 情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中的普遍程度也许超过了大部分读者的想象。比如:(只是说明概念,但实际情况并不一定如例子)在线程安全的环境中使用stringBuffer进行字符串拼加。则会在java文件编译的时候,进行锁销除。



四:锁粗化
--->原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快地拿到锁。
--->大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
--->如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(锁粗化)到整个操作序列的外部。

五:偏向锁,轻量级锁,重量级锁对比
优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到索竞争的线程,使用自旋会消耗CPU 追求响应速度,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长

 

对象头的存储内容(monitor)

 

长度 内容 说明
32/64bit Mark Word 存储对象的hashcode或锁信息
32/64bit 类对象的地址 存储到对象类型数据的指针
32/64bit Array length 数组的长度(如果当前对象是数组)
Mark Word存储内容(monitor)的状态变化

 

锁状态 25bit,4bit 1bit(是否是偏向锁) 2bit(锁标示位)
轻量级锁 指向栈中锁记录的指针
00
重量级锁 指向互斥量(重量级锁)的指针
10
 GC
11
偏向锁 线程id,对象hashcode,对象分代年龄 1 01


六:锁的状态

--->锁一共有四种状态(由低到高的次序):无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态
--->锁的等级只可以升级,不可以降级。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。


七:偏向锁
--->a线程获得锁,会在a线程的的栈帧里创建lock record(锁记录变量),则在锁对象的对象头里和lock record里存储a线程的线程id.以后该线程的进入,就不需要cas操作,只需要判断是否是当前线程。
--->a线程获取锁,不会释放锁。直到b线程也要竞争该锁时,a线程才会释放锁。
--->偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的所记录。栈帧中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。
--->关闭偏向锁,通过jvm的参数-XX:UseBiasedLocking=false,则默认会进入轻量级锁。

八:轻量级锁
--->a线程获得锁,会在a线程的栈帧里创建lock record(锁记录变量),让lock record的指针指向锁对象的对象头中的mark word.再让mark word 指向lock record.这就是获取了锁。
--->轻量级锁,b线程在锁竞争时,发现锁已经被a线程占用,则b线程不进入内核态,让b线程自旋,执行空循环,等待a线程释放锁。如果,完成自旋策略还是发现a线程没有释放锁,或者让c线程占用了。则b线程试图将轻量级锁升级为重量级锁。

--->锁记录(lockrecord)和对象头(mark word)的进行指针交换的示意图
 


十:重量级锁
--->重量级锁,就是让争抢锁的线程从用户态转换成内核态。让cpu借助操作系统进行线程协调。
 
相关文章
|
21天前
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
33 6
|
2月前
|
安全 Go 调度
goroutine:轻量级线程
【10月更文挑战第15天】
26 3
|
2月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
43 0
|
2月前
|
安全 调度 数据安全/隐私保护
iOS线程锁
iOS线程锁
31 0
|
2月前
|
Java API
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
41 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
56 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
26 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
22 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
35 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
41 1