java并发编程2-synchronised

155 阅读3分钟

1. sync 实现原理

核心的话,就是 monitor ;

monitor 是c 的结构体,对应到java 可以理解为一个类;

monitor 属性如下:

ObjectMonitor() {
    _count        = 0;
    _owner        = NULL;//指向获得ObjectMonitor对象的线程或基础锁
    _EntryList    = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
    _WaitSet      = NULL;//处于wait状态的线程,会被加入到wait set;
    _WaitSetLock  = 0 ;
    
    _header       = NULL;//markOop对象头
    _waiters      = 0,//等待线程数
    _recursions   = 0;//重入次数
    _object       = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
    _previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
  }

多线程并发时, monitor 会先将线程加入等待队列,获取锁的线程 会把 monitor 的线程属性设置成自己的;

然后获取锁失败的线程 就会被加载到 monitor 的等待队列里面;

持有锁的线程释放锁后,随机唤醒一个线程;

2. sync使用

作用范围:静态代码块、方法

写法如下:

Object lock = new Object();
synchronised(lock){
	// 业务代码
}

多线程对锁的竞争,就是对共享资源lock 的竞争; 抢夺lock 对象monintor修改锁标识;

对象的内存布局

3. 锁升级

偏向锁

锁竞争中,同一个线程获得锁的概率大,为了让获取锁的代价更低,引入了偏向锁;

当一个线程获取到锁,会在对象头中存储线程id;后续这个线程进入和退出代码块时,不需要再次加锁和释放锁;

偏向锁的获取和撤销

1)获取锁的对象,判断是否处于可偏向的状态 ( lock_1, 且 threadId 为 null )

2)可以偏向,通过cas ,把当前线程id 写入 Markword;

  • 写入成功,锁对象升级为偏向锁
  • 写失败,说明其他线程已经获取到锁,锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字节码)

3)如果是已偏向状态,需要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID

    • 如果相等,不需要再次获得锁,可直接执行同步代码块
    • 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

轻量级锁

轻量级锁设置失败,会升级为轻量级锁;

相比于偏向锁,轻量级锁会再当线程 创建 锁记录, 将对象头中的锁记录指向当前线程;

也就是说偏向锁比较的是  线程id是否与当前线程一致; 轻量级锁比较的是,锁对象的指针是否指向自己;

重量级锁

轻量级锁升级为重量级锁后,就只能被挂起阻塞来等待唤醒了;

锁的转化

注意点:

  • 1)锁的升级是不可逆向的

  • 2)偏向锁cas替换threadId 失败后情况?

  • 3)轻量级锁到重量级锁的转化?

  • 4)轻量级锁和偏向锁都有CAS操作,两者区别?

4. 参考

Java Synchronised机制

cpu 上下文切换,用户态、内核态

segmentfault.com/a/119000002…