并发编程——synchronized

文章目录

原子性、有序性、可见性

原子性

数据库事务的原子性:是一个最小的执行的单位,一次事务的多次操作要么都成功,要么都失败。

并发编程的原子性:一个或多个指令在CPU执行过程中不允许中断。

i++;操作是原子性?

肯定不是:i++操作一共有三个指令

getfield:从主内存拉取数据到CPU寄存器

iadd:在寄存器内部对数据进行+1

putfield:将CPU寄存器中的结果更新搭配主内存中

如何保证i++是原子性?

使用synchronized、lock、Atomic(CAS)来保证

使用lock锁也会有类似的概念,也就是在操作i++的三个指令前,先基于AQS成功修改state后才可以操作

使用synchronized和lock锁时,可能会触发将线程挂起的操作,而这种操作会触发内核态和用户态的切换,从而导致消耗资源。

CAS方式就相对synchronized和lock锁的效率更高,因为CAS不会触发线程挂起操作!

CAS:compare and swap

线程基于CAS修改数据的方式:先获取主内存数据,在修改之前,先比较数据是否一致,如果一致修改主内存数据,如果不一致,放弃这次修改

CAS就是比较和交换,而比较和交换是一个原子操作

CAS在Java层面就是Unsafe类中提供的一个native方法,这个方法只提供了CAS成功返回true,失败返回false,如果需要重试策略需要自己实现

CAS问题:

  • CAS只能对一个变量的修改实现原子性。
  • CAS存在ABA问题。
    • A线程修改主内存数据从1~2,卡在了获取1之后。
    • B线程修改主内存数据从1~2,完成。
    • C线程修改主内存数据从2~1,完成。
    • A线程执行CAS操作,发现主内存是1,没问题,直接修改
    • 解决方案:加版本号
  • 在CAS执行次数过多,但是依旧无法实现对数据的修改,CPU会一直调度这个线程,造成对CPU的性能损耗
    • synchronized的实现方式:CAS自旋一定次数后,如果还不成,挂起线程
    • LongAdder的实现方式:当CAS失败后,将操作的值,存储起来,后续一起添加

有序性

指令在CPU调度执行时,CPU会为了提升执行效率,在不影响结果的前提下,对CPU指令进行重新排序。但是这样可能会造成数据的不一致。

如果不希望CPU对指定进行重排序,怎么办?

可以对属性追加volatile修饰,就不会对当前属性的操作进行指令重排序。

可见性

CPU在处理时,需要将主内存数据拿到寄存机中再执行指令,执行完指令后,需要将寄存器数据扔回到主内存中。但是寄存器数据同步到主内存是遵循MESI协议的,简单来说就是:不是每次操作结束就将CPU缓存数据同步到主内存,这样就会造成多个线程看到的数据不一样。

所以通常需要synchronized和volatile配合解决这一问题:

  • volatile每次操作后,立即同步数据到主内存。
  • synchronized,只有一个线程操作这个数据。

synchronized使用

使用方法:声明方法时使用synchronized或者在代码块中使用synchronized。

锁类型:

  • 类锁:基于当前类的Class加锁
  • 对象锁:基于this对象加锁

synchronized是互斥锁,每个线程获取synchronized时,基于synchronized绑定的对象去获取锁!

synchronized是如何基于对象实现的互斥锁,先了解对象再内存中是如何存储的。

在Java中查看对象的存储:

导入依赖:

xml 复制代码
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

查看对象信息

synchronized锁升级

synchronized在jdk1.6之前,一直是重量级锁:只要线程获取锁资源失败,直接挂起线程。

jdk1.6之前synchronized效率贼低,再加上Doug Lea推出了ReentrantLock,效率比synchronized快多了,导致JDK团队不得不在jdk1.6将synchronized做优化。

锁升级:

  • 无锁状态、匿名偏向状态:没有线程拿锁。
  • 偏向锁状态 :没有线程的竞争,只有一个线程在获取锁资源。
    线程竞争锁资源时,发现当前synchronized没有线程占用锁资源,并且锁是偏向锁,使用CAS的方式,设置线程ID为当前线程,获取到锁资源,下次当前线程再次获取时,只需要判断是偏向锁,并且线程ID是当前线程ID即可,直接获得到锁资源。
  • 轻量级锁 :偏向锁出现竞争时,会升级到轻量级锁。
    轻量级锁的状态下,线程会基于CAS的方式,尝试获取锁资源,CAS的次数是基于自适应自旋锁实现的,JVM会自动的基于上一次获取锁是否成功,来决定这次获取锁资源要CAS多少次。
  • 重量级锁:轻量级锁CAS一段次数后,没有拿到锁资源,升级为重量级锁(其实CAS操作是在重量级锁时执行的)。重量级锁就是线程拿不到锁,就挂起。

偏向锁是延迟开启的,并且在开启偏向锁之后,默认不存在无锁状态,只存在匿名偏向synchronized因为不存在从重量级锁降级到偏向或者是轻量。

synchronized在偏向锁升级到轻量锁时,会涉及到偏向锁撤销,需要等到一个安全点,stw,才可以撤销,并发偏向锁撤销比较消耗资源。在程序启动时,偏向锁有一个延迟开启的操作,因为项目启动时,ClassLoader会加载.class文件,这里会涉及到synchronized操作。为了避免启动时涉及到偏向锁撤销,导致启动效率变慢,所以程序启动时,默认不是开启偏向锁的。

编译器优化的结果,出现了下列效果

  • 锁消除:线程在执行一段synchronized代码块时,发现没有共享数据的操作,自动帮你把synchronized去掉。

  • 锁粗化:在一个多次循环的操作中频繁的获取和释放锁资源,synchronized在编译时,可能会优化到循环外部。

synchronized-ObjectMonitor

ObjectMonitor一般是到达了重量级锁才会涉及到。在到达重量级锁之后,重量级锁的指针会指向ObjectMonitor对象。

hpp 复制代码
  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;     // 抢占锁资源的线程个数
    _waiters      = 0,     // 调用wait的线程个数。
    _recursions   = 0;     // 可重入锁标记,
    _object       = NULL; 
    _owner        = NULL;  // 持有锁的线程
    _WaitSet      = NULL;  // wait的线程  (双向链表)
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;  // 假定的继承人(锁释放后,被唤醒的线程,有可能拿到锁资源)
    _cxq          = NULL ;  // 挂起线程存放的位置。(单向链表)
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // _cxq会在一定的机制下,将_cxq里的等待线程扔到当前_EntryList里。  (双向链表)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
相关推荐
小筱在线几秒前
SpringCloud微服务实现服务熔断的实践指南
java·spring cloud·微服务
luoluoal5 分钟前
java项目之基于Spring Boot智能无人仓库管理源码(springboot+vue)
java·vue.js·spring boot
ChinaRainbowSea10 分钟前
十三,Spring Boot 中注入 Servlet,Filter,Listener
java·spring boot·spring·servlet·web
小游鱼KF13 分钟前
Spring学习前置知识
java·学习·spring
扎克begod17 分钟前
JAVA并发编程系列(9)CyclicBarrier循环屏障原理分析
java·开发语言·python
青灯文案119 分钟前
SpringBoot 项目统一 API 响应结果封装示例
java·spring boot·后端
我就是程序猿29 分钟前
tomcat的配置
java·tomcat
阳光阿盖尔35 分钟前
EasyExcel的基本使用——Java导入Excel数据
java·开发语言·excel
二十雨辰36 分钟前
[苍穹外卖]-12Apache POI入门与实战
java·spring boot·mybatis
程序员皮皮林36 分钟前
开源PDF工具 Apache PDFBox 认识及使用(知识点+案例)
java·pdf·开源·apache