synchronized关键字的一些自我理解+JUCAPI的理解

synchronized 关键字最主要的三种使用方式:

1.修饰实例方法

作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
同一个类里的两个synchronized 方法,在单例模式或者同一个对象的时候也会冲突,所以springboot 自动注入的时候默认是单利模式,在Service层的时候尽量一个service层只出现一个synchronized关键字修饰的方法
未被synchronized修饰的方法则不会被冲突

synchronized void method() {
  //业务代码
}

2.修饰静态方法

也就是给当前类加锁,会作用于类的对象实例 ,进入同步代码前要获得 当前 class 的锁。因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管 new 了多少个对象,只有一份)。所以,如果一个线程 A 调用一个实例对象的非静态 synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

synchronized void staic method() {
  //业务代码
}

3.修饰代码块

指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁。synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁

synchronized(this) {
  //业务代码
}

在一个类里面增删改查最细的颗粒度,不能直接在方法上加锁,这样四个会竞争一个锁

public class UserServiceImpl {
    private final Object addLock = new Object();
    private final Object deleteLock = new Object();
    private final Object updateLock = new Object();
    private final Object queryLock = new Object();

    public void add() {
        synchronized (addLock) {
            // 添加操作
        }
    }

    public void delete() {
        synchronized (deleteLock) {
            // 删除操作
        }
    }

    public void update() {
        synchronized (updateLock) {
            // 更新操作
        }
    }

    public void query() {
        synchronized (queryLock) {
            // 查询操作
        }
    }
}

总结:
synchronized 关键字加到 static 静态方法和 synchronized(XX.class) 代码块上都是是给 Class 类上锁。

synchronized 关键字加到实例方法上是给对象实例上锁。

synchronized 关键字修饰代码块需要判断里面修饰的是对象,还是类

尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

在JDK1.6之前,synchronized直接加重量级锁,JDK1.6之后得到了很好的优化。

synchronized锁升级:偏向锁 → 轻量级锁 → 重量级锁(锁膨胀)

对象一共16个字节,对象头占12个字节在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

  • 1.A线程初次执行到synchronized代码块的时候,锁对象变成偏向锁(通过CAS修改对象头里的线程id:0->A线程id,在线程的栈中生成锁记录(1.锁记录的地址2.对象指针(如果为重入则为null))),字面意思是“偏向于第一个获得它的线程”的锁。
  • 此时对象从无锁状态变为偏向锁状态,执行完同步代码块后,线程并不会主动释放偏向锁。当第二次到达同步代码块时,线程会验证对象头中的线程id是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。
  • 如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

  • 2.线程A还在继续持有锁并且未死亡,此时有第二个线程B加入锁竞争。
  • CAS修改对象markword中的threadId为B线程Id,因为此时线程id已经不为期望值0所以修改失败。
  • 进入偏向锁撤销的流程,等待全局安全点,此时判断持有锁的线程是否死亡。
  • 如果已经死亡或者没有持有锁,则将对象头设置成无锁不可偏向状态,(标志位为001),然后CAS修改锁标志位01->00来获取轻量级锁。
  • 未死亡则进入升级轻量级锁的过程。
  • 持有锁的线程A的栈中生成锁记录,锁记录包含锁记录地址和对象指针,将对象头中Mark Word存储的信息与锁记录地址互换,将对象头中锁标志位改为00
  • 偏向锁此时升级为轻量级锁(自旋锁)
  • 在轻量级锁状态下继续锁竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取。获取锁的操作,其实就是通过CAS修改对象头里的锁标志位 01->00 并将对象头中Mark Word存储的信息与锁记录地址互换。如果成功就获得轻量级锁

这里要明确一下什么是锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。


  • 3.长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)。如果多个线程用一个锁,但是没有发生锁竞争,或者发生了很轻微的锁竞争,那么synchronized就用轻量级锁,允许短时间的忙等现象。这是一种折衷的想法,短时间的忙等,换取线程在用户态和内核态之间切换的开销。
  • 显然,此忙等是有限度的(有个计数器记录自旋次数,默认允许循环10次,可以通过虚拟机参数更改,1.6之后改为JVM自己控制)。
  • 如果锁竞争情况严重,C线程达到最大自旋次数的线程,会将轻量级锁升级为重量级锁。
  • C线程CAS修改mark word 中锁的标志位为10,并为对象申请Monitor锁,将Owner设置为持有锁的线程,然后把对象头中Mark Word的锁记录地址替换为Monitor锁的地址。
  • C线程完成这些操作后自己进入Monitor的EntryList BLOCKED。
  • 当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则跟C线程一样直接将自己挂起(而不是忙等),等待将来被唤醒。
  • 当轻量级锁持有的线程进入解锁流程,CAS将Mark Word值还给对象头,此时因为锁标志位已经被修改为10,锁记录已经被替换为Monitor记录,所以解锁失败,进入重量级锁解锁流程。
  • 按照Monitor地址找到Monitor对象,并将原对象头存入Monitor,设置Owner为null,唤醒EntryList中BLOCKED的首节点线程进行非公平竞争。

  • 4.当线程D尝试获取对象锁时先根据对象头中的Monitor地址找到Monitor,然后查找owner是否为Null。
  • 如果为Null就直接获取锁并把Owner改为自己。
  • 如果不为Null就进入cxq队列中,在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。
  • 当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程唤醒。
  • 因为在cxq队列中的线程会自旋抢夺锁,所以唤醒的线程不一定能抢到锁,因此重量级锁是不公平锁。
  • cxq(竞争列表)(头插尾取)
    cxq是一个单向链表。被挂起线程等待重新竞争锁的链表, monitor 通过CAS将竞争锁的线程包装成ObjectWaiter写入到列表的头部。为了避免插入和取出元素的竞争,所以Owner会从列表尾部取元素。
  • EntryList(候选者列表)(尾插头取)
    EntryList是一个双向链表。当EntryList为空,cxq不为空,Owener会在unlock时,将cxq中的数据移动到EntryList。并指定EntryList列表头的第一个线程为OnDeck线程。
  • EntryList跟cxq的区别
    在cxq中的队列可以继续自旋等待锁,若达到自旋的阈值仍未获取到锁则会调用park方法挂起。而EntryList中的线程都是被挂起的线程。

偏向锁

  • 第一个线程占有的锁,如果两个线程竞争一定不是偏向锁

  • 当一个对象已经计算过identity hash code,它就无法进入偏向锁状态

  • hash code 是懒加载,不用到就不会计算

  • 当一个对象当前正处于偏向锁状态,并且需要计算其identity hash code的话,则它的偏向锁会被撤销,并且锁会膨胀为重量锁;

  • 偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    (此处线程id为操作系统给线程定义的id,与java中线程id不是一一对应的,不要尝试去理解)

101可偏向的对象,调用对象的hashcode()方法后会撤销偏向状态变成001,清除对象头里线程id等信息,并且把hashcode存入对象头,因为对象hashcode没有存储空间,轻量级锁会把hashcode存在线程栈帧的锁记录里,重量级锁会把hashcode存在monitor里,后期会还原回去,所以这两个锁不会受到影响
在这里插入图片描述

轻量级锁

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

重量级锁

在这里插入图片描述
在这里插入图片描述

Monitor监听器

  ObjectMonitor() {
    _header       = NULL;   //储存monitor关联对象的对象头
    _count        = 0;		// 记录个数
    _waiters      = 0,
    _recursions   = 0;		// 重入次数
    _object       = NULL;	// 储存monitor关联对象
    _owner        = NULL;	// 储存当前持有锁的线程ID
    _WaitSet      = NULL;	// 等待池:通过调用 wait(),将当前线程变为阻塞状态放到等待池
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;	// 多线程竞争锁时的单向链表
    FreeNext      = NULL ;
    _EntryList    = NULL ;	// 锁池:处于等待锁block状态的线程,会被加入到该列表,如果被调用了notify(),则会将当前线程从 _WaitSet 移到 _EntryList
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

在这里插入图片描述

批量重偏向,批量撤销

1、当某个线程持有偏向锁,另一个线程想要获取锁时需要撤销锁。
2、撤销先尝试在不安全点使用CAS修改Mark Word为无锁状态,若还是无法撤销则考虑在安全点撤销,等安全点是比较低效的操作。

针对的是所有类的实例对象来说的,一个实例对象撤销偏向锁就算一次

  • 批量重偏向:当撤销A对象偏向锁阈值超过20次,就会把所有classA的实例对象全部置位可偏向状态101
  • 批量撤销:当撤销A对象偏向锁阈值超过40次,就会把所有classA的实例对象全部置位不可偏向状态(包括新创建的对象)001

在这里插入图片描述

wait()和notify()

https://blog.csdn.net/qq_43578385/article/details/124688200

锁消除 lock eliminate

有开关可以控制
有开关可以控制

例子1:

JIT即时编译器发现一段代码属于热点代码就会进行代码优化,如果方法里的局部变量不会逃离方法的作用范围,此时synchronized代码就会被优化掉

例子2:

public void add (String str1,String str2){
		StringBuffer sb = new StringBuffer();
		sb.append(str1).append(str2);
}

在这里插入图片描述

锁粗化 lock coarsening

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值