【JVM并发编程专题】——多线程安全——synchronized应用与原理

多线程安全——起源

数据的实效性问题(数据时效过期):我们知道即使多个线程在同一个进程内可以共享内存数据,但内存共分为工作内存和主内存;工作内存就是,线程的在cpu三级缓存中的内存数据;主内存,就是内存条中分配的内存数据,每一次线程执行对会将主内存的数据复制到工作内存中便于快速访问提供支持,但是如果复制了之后,主内存的数据发生了改变,此时如果你的工作内存中还是以前的数据,那显然业务上是不被允许的,一定要实时同步才可以满足业务严格要求,在java中我们利用volatile关键字实现数据的实时同步,这个关键字会利用cpu缓存一致性协议MESI和内存屏障,强制我们的每次在用到这个变量时读取主内存的最新值,就避免了这个问题;
业务的原子性问题(代码判断过期):如果你写代码经验够多,就知道;我们if判断和我们的更新操作,这两个步骤是两行代码,在高并发环境中,很有可能判断完之后还没执行更新的时候,cpu调度到了其他线程去了,这个时候他们执行了减库存的操作,然后执行回来的时候,这个时候你的更新操作的前提条件,可能是不满足的,但是你的判断代码已经过了,即使你用volatie实现了数据时效,但判断已经过时,导致了线程安全问题;所以java层面使用unsafe方法的比较更新来判断,这个方法是cpu级别的原子性,就是每次更新前调用cpu底层的原子性方法,输入一个期望值、要更新的值,如果发现实际的期望值不是(被改了),代表那就不断进行重试直到成功了;这个在后续章节我们详细叙说
线程安全分类
JVM内存的线程安全问题,用java机制解决;
Redis线程安全问题,用lua解决;
数据库线程安全问题,用行锁+事务或者版本号重试机制解决;
分布式线程安全问题,用redis的lua作为分布式锁解决;

多线程安全——synchronized锁应用

对象监视器(锁信息保存在对象头中(可以是this或者任意一个对象的引用)):
public void save1(int money) {
       synchronized (this) {
             account += money;
       }
 }
类监视器(锁信息保存在类中):
synchronized (User.class){
            
}

Ps:我们只需要记得一点,那就是锁的执行是需要判断当前锁属于哪个线程,这些是需要将持有信息保存起来的,因此就需要使用一个监视器去保存,jvm虚拟机实现中,可以将对象作为这些信息的数据地(对象头),或者类里面也可以

在这里插入图片描述

多线程安全——死锁现象

一般常见于使用了嵌套锁以及多个对象监视器,导致发生了相互等待的情形

 public class ThreadResource{
    public static Object resource1 = new Object();
    public static Object resource2 = new Object();
}
public void run() {
         synchronized (ThreadResource.resource1){
             System.out.println("Thread1 lock resource1");
             Thread.sleep(2000);//休眠2s等待线程2锁定资源2
             synchronized (ThreadResource.resource2){
                 System.out.println("Thread1 lock resource2");
             }
         }
}
 public void run() {
     synchronized (ThreadResource.resource2){
         System.out.println("Thread1 lock resource2");
         Thread.sleep(2000);//休眠2s等待线程2锁定资源2
         synchronized (ThreadResource.resource1){
             System.out.println("Thread1 lock resource1");
         }
     }
 }
 Ps:以上两个线程,死锁激活的条件就是,两段代码的外层锁都执行成功了,但还未释放锁;
 这个时候,两个线程的内层锁在竞争时都在互相等待对方的外层锁释放;显然外层锁这个时候是不可能释放的

多线程安全——synchronized锁升级

jdk1.6版本优化: 在jdk1.6以前,当一个线程遇到synchronized时就直接会去竞争锁,如果失败直接进入阻塞,根本没有任何的重试,很显然在竞争度不大的情况下,我们可以利用一些重试提高性能;
JDK1.6对锁的实现引入了大量的优化,如适应性自旋,锁消除,锁粗化,偏向锁,轻量级锁等锁的技术来减少锁操作的开销。
锁消除:锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能 存在共享数据竞争的锁进行消除,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。锁消除的主要判定依据来源于逃逸分析的数据支持,
锁粗化:频繁的加锁、解锁会损耗性能,jvm虚拟机层面会分析所有锁的位置,多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁,避免频繁的加锁解锁
偏向锁:HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁,利用对象的监视器数据的偏向id是否是当前线程,如果是直接放行,不是而且当前锁已经被占用就将锁膨胀轻量级锁,标志位:01-00;如果偏向id不是当前线程,但是锁未被占用只需要更新偏向id即可;
轻量级锁:当多个线程竞争锁时,为避免直接阻塞导致上下文切换,其他竞争线程会利用自旋重试的方式尝试获取锁,当重试达到一定次数还是失败直接进入阻塞;标志位 00-10,至于重试的次数可以通过虚拟机参数手动调整;
重量级锁:线程直接进入阻塞
偏向锁——轻量级锁升级过程
比较当前线程的threadID和Java对象头中的threadID是否一致
(1.1)如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;
(1.2)如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活
(2.1)如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;
(2.2)如果存活,那么立刻查找该线程(持锁线程)的栈帧信息,暂停该线程(为了避免锁还未升级为轻量级时,就代码执行完解锁导致,升级操作更新markwork排在了解锁更新markword后面,导致冲突)
(3.0)撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程
轻量级锁——重量级锁升级过程:不断使用cas竞争锁,实际上比较维护的是一个数字代码有锁无锁,更新的是锁状态的(0是无锁,其他就是有锁或者重入锁)以及对象头指向线程栈的指针,在jvm参数中可以设定重试的数量,如果达到数量就失败让出cpu进入阻塞状态;避免空转还占用cpu!
-XX:+UseSpining 开启自旋锁
-XX:+PreBlockSpin 20 次选锁数量参数

多线程安全——synchronized锁信息

在这里插入图片描述

轻量级锁的存储:指向当前持锁线程的锁记录,无论是持锁线程还是竞争线程,都将锁对象的对象头MarkWord复制一份到线程的栈顶帧中创建的用于存储锁记录的空间,然后使用CAS把对象头中的内容替换为这个线程的锁记录的地址;(锁记录的栈帧称为DisplacedMarkWord指向的目的是线程主动沦陷你指针是不是自己;拷贝的目的,主要考虑重入锁时,存在多条锁记录栈帧,为之前锁记录是做一个副本,内层锁解锁时,外部代码是否能继续执行,就是用外部的锁栈帧去获取继续执行的权限,所有复制一份副本过来)如果是重入轻量级锁,那就会先cas更新第一份外层锁栈帧,然后更新内层锁栈帧;解锁时,要将栈帧指向外层锁,外层才能获得执行权限;
重量级锁的存储:指向一个监视器对象,其数据结构如下:


ObjectMonitor() {
    _count        = 0; //重入锁
    _waiters      = 0;
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持锁线程
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //放入阻塞队列
  }

注意,可以升级不可以降级,但是偏向锁状态可以被重置为无锁状态

JVM并发编程专题章节:
多线程安全——cas
多线程安全——aqs
多线程协作
多线程管理
多线程框架
多线程测试

GodSchool
致力于简洁的知识工程,输出高质量的知识产出,我们一起努力
博主私人微信:supperlzf

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值