[Java并发系列] Java并发机制的底层实现

在Java并发实现的机制中,大部分的容器和框架都是依赖于volatile/synchronized/原子操作实现的,了解底层的并发机制,对于并发编程会带来很多帮助

1. synchronized的应用

synchronized在多线程并发编程中已经是一个元老级的存在,通常被称作是重量级锁。既然是常用的一种锁,那么就需要对它的底层实现有深入的了解。

1. synchronized的实现原理

当一个线程在访问同步代码块时,就必须要先获取该代码块中对象的锁,退出或者抛出异常时,就必须要释放锁。synchronized的同步实现,是JVM基于进入和退出同步对象的Monitor对象来实现方法同步和代码块同步的。 每个Monitor对象都有与之关联的monitor,当且仅当monitor被持有后,它才会处于锁定状态。同步代码是使用monitorenter和monitorexit指令实现的。monitorenter指令时在代码编译后插入到同步代码块的开始位置,monitorexit指令则是插入到方法的结束和异常处。当线程执行到monitorenter时,会尝试去获取对象的monitor的所有权,即尝试获取对象的锁。

2. synchronized的使用形式及意义
  • 修饰普通方法:锁是当前实例对象
  • 修饰静态方法:锁是当前类的Class对象
  • 修饰代码块:锁是synchronized括号里面配置的对象
3. 锁的升级

锁共有四种状态:无锁状态->偏向锁状态->轻量级锁状态->重量级锁状态

偏向锁:当线程访问同步块并获取锁时,会在对象头和栈针中的锁记录中存储锁偏向的ID,以后该线程在进入和退出同步块时就不需要在使用CAS操作来进行加锁和解锁操作,而仅仅需要测试一下对象头中的Mark Word字段中存储的线程ID是否是当前线程即可。如果是,那么表示获取锁成功;如果不是,则需要检查对象头中偏向锁的字段是否设置为了1(表示当前锁是偏向锁),如果没有设置,则需要使用CAS来竞争获取锁,如果已经设置了,则尝试使用CAS将对象头的偏向锁ID指向当前线程。 轻量级锁;

  • 轻量级锁加锁:线程在执行同步块之前,首先会在自己的线程栈中创建一个用于存储锁记录的空间,并将对象中的Mark Word 赋值到锁记录中。然后线程尝试使用CAS操作将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则表示当前线程获取锁,如果失败,则表示其他线程也在竞争锁,当前线程便使用自循的方式来获取锁。
  • 轻量级锁解锁:轻量级锁解锁时,会使用原子CAS操作将锁记录替换会对象头,如果成功,则表示没有竞争发生。如果失败,则表示当前锁存在竞争,锁就会膨胀为重量级锁。
4. 锁的对比

锁类型

优点

缺点

应用场景

重量级锁

线程竞争不使用自旋,不会浪费CPU

线程阻塞,响应时间慢

追求吞吐量,同步块执行的时间长的场景

轻量级锁

竞争的线程不会阻塞,提高程序的响应速度

如果线程长时间得不到锁,那么自旋就会浪费CPU

追求响应时间,同步块执行速度快

偏向锁

加锁和解锁不需要额外的资源消耗

如果线程间存在竞争,会带来额外的锁撤销的消耗

适用于只有一个线程访问同步场景

2. volatile的应用

volatile是轻量级的synchronized,volatile在多线程开发中保证了共享变量的可见性,所谓可见性,指的是当一个线程修改了共享变量之后,对于其他线程来说,能够读到这个修改的值。

1. volatile的定义及实现原理

volatile定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁获得这个变量。 实现原理:当对声明了volatile的变量进行写操作时,JVM就会向处理器发送一条带有lock前缀的指令,将这个变量所在的缓存行的数据写回到系统内存中。同时,在多处理器的情况下,需要执行缓存一致性协议,即每个处理器都需要通过嗅探总线上传播的数据来检查自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效,当处理器需要对这个数据进行操作时,再从系统内存中把数据读取到缓存行中。

2. 实现volatile的两条原则
  • 带有Lock前缀的指令会引起处理器缓存写回到内存;
  • 一个处理器的缓存写回到内存,会导致其他处理器的缓存无效。

3. 原子操作的原理

见文章[并发编程系列]Java中的原子操作类

原文发布于微信公众号 - 瞎说开发那些事(jsj201501)

原文发表时间:2017-11-01

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Goroutine(协程)为何能处理大并发?

简单来说:协程十分轻量,可以在一个进程中执行有数以十万计的协程,依旧保持高性能。 进程、线程、协程的关系和区别: 进程拥有自己独立的堆和栈,既不共享堆,亦不共享...

2705
来自专栏程序员叨叨叨

【转】使用 Spring HATEOAS 开发 REST 服务原文

绝大多数开发人员对于 REST 这个词都并不陌生。自从 2000 年 Roy Fielding 在其博士论文中创造出来这个词之后,REST 架构风格就很快地流行...

801
来自专栏coding

vue.js过滤器的基本使用

1065
来自专栏java一日一条

正确使用Java事件通知

让我们从一个最简单的 Java Bean 开始,它叫StateHolder,里面封装了一个私有的 int 型属性state 和常见的访问方法:

591
来自专栏云霄雨霁

概念题知识点总结

1640
来自专栏自动化测试实战

接口测试基础——第8篇 requests模块

3136
来自专栏从流域到海域

如何在Mule 4 Beta中实现自动流式传输

原文地址:https://dzone.com/articles/how-automatic-streaming-in-mule-4-beta-works

1745
来自专栏用户画像

2.5.1 进程与程序的区别和联系

(1)进程是程序及其数据在计算机上的一次运行活动,是一个动态的概念。进程的运行实体是程序,离开程序的进程没有存在的意义。从静态角度看,进程是由程序,数据和进程控...

652
来自专栏偏前端工程师的驿站

asp.net 解码gb2312下urlencode后的字符串

公司网站前期的网页用了gb2312保存用户数据,而我负责的部分用的是utf8,今天恰好要获取前期录入的数据于是毫无悬念地出现乱码问题,经过一番网上的搜索还是找不...

1845
来自专栏IT技术精选文摘

从Java视角理解系统结构(二)CPU缓存

众所周知, CPU是计算机的大脑, 它负责执行程序的指令; 内存负责存数据, 包括程序自身数据. 同样大家都知道, 内存比CPU慢很多. 其实在30年前, CP...

2189

扫码关注云+社区