掘金 后端 ( ) • 2024-04-07 16:20

synchronized中的类锁和对象锁

synchronized的锁是基于对象实现的

synchronized一般修饰同步代码块或者同步方法

修饰同步方法时:

如果修饰的是静态方法,那么使用的就是类锁

如果修饰的是普通方法,那么使用的就是对象锁

修饰同步代码块时:

同步代码块中的()中的对象就是锁对象

synchronized的优化

优化背景

在Jdk1.5,有大牛发明了ReentrantLock,性能比synchronized好很多,所以Jdk团队在Jdk1.6对synchronized做了优化

优化方式

锁消除:当同步代码中没有对共享变量进行操作时,说明加锁和没加锁效果相同。此时即使使用了synchronized,效果和没加锁一样

锁膨胀:当锁资源被频繁的获取和释放时,触发锁膨胀,扩大锁的范围

public class test {

    public static void main(String[] args) {

        for(int i=0; i<999999; i++){
            synchronized (test.class) {

            }
        }
    }
}

//上述代码效果在锁膨胀作用下,与下述代码相同
public class test {

    public static void main(String[] args) {

        synchronized (test.class){
            for(int i=0; i<999999; i++){
            }
        }
    }
}

锁升级:线程在获取ReentrantLock锁时,先基于乐观锁的CAS尝试获取锁,获取不到才会挂起。但是synchronized是获取不到锁资源就立即挂起线程

synchronized在Jdk1.6做了锁升级优化

  • 无锁:还没有线程来获取锁资源
  • 偏向锁:只有一个线程在频繁获取和释放锁资源,此时线程过来,只需要判断锁指向的线程是否是此线程即可
    • 如果是,继续执行
    • 如果不是,基于CAS的方式,尝试将锁指向当前线程。如果失败,触发锁升级,升级到轻量级锁
  • 轻量级锁:采用自旋的方式基于CAS获取锁资源,自旋时间是由JVM自己决定的
    • 如果成功获取,继续执行
    • 如果自旋一定时间还是没有获取到,触发锁升级,升级到重量级锁
  • 重量级锁:最传统的synchronized方式,线程没有获取到锁就直接挂起

synchronized的实现原理

synchronized是基于对象实现的

对象在堆内存存储的信息包括:对象头、属性数据、类对象指针、填充对齐。synchronized锁信息就存储在对象头中

对象头(MarkWork)展开后如下:

image.png

synchronized锁升级

java进程启动后,所有对象头中存储的是无锁信息,过了5秒后,所有对象头中的无锁信息会变成匿名偏向锁信息。这个过程叫做偏向锁延迟。并且开启偏向锁了,就不会再出现无锁状态。

匿名偏向锁:对象头中存储的信息和偏向锁相同,只是指向的线程信息是空的

偏向锁撤销:偏向锁在升级成轻量级锁时,需要撤销偏向锁,撤销动作需要等到安全点。在明知道会有并发情况,就选择不开启偏向锁,或者设置偏向锁延迟开启

偏向锁延迟现象背景:在java进程启动时,需要加载大量的类,在类加载过程中使用了synchronized,为了避免偏向锁撤销现象频繁发生,所以java进程启动后的5秒钟后,才会使用偏向锁

image.png

  • 在偏向锁和轻量级锁时,会在线程栈开辟空间存储Lock Record
  • Lock Record中存储了对象的地址信息和Mark Work信息
  • 偏向锁的对象头指向栈空间的Lock Record,指向哪个线程的栈,锁就被哪个线程占有
  • 在重量级锁时,由C++实现的ObjectMonitor存储了MarkWork信息和排队的线程信息

image.png

重量级锁的ObjectMonitor

重量级锁的ObjectMonitor是C语言实现的,可以在以下网址中找到其实现

https://hg.openjdk.org/jdk8u/jdk8u-dev/hotspot/file/69087d08d473/src/share/vm/runtime

ObjectMonitor中的重要结构体:

ObjectMonitor:

ObjectMonitor() {
	_header       = NULL;        // header存储着MarkWork
	_count        = 0;           // 竞争锁的线程个数
	_waiters      = 0,           // wait状态的线程个数
	_recursions   = 0;           // 表示当前synchronized锁重入的次数
	_object       = NULL;
	_owner        = NULL;        // 持有锁的线程
	_WaitSet      = NULL;        // 保存wait状态的线程信息,双向链表结构
	_WaitSetLock  = 0 ;
	_Responsible  = NULL ;
	_succ         = NULL ;
	_cxq          = NULL ;       // 获取锁资源失败后,线程存放的地方,单向链表结构
	FreeNext      = NULL ;
	_EntryList    = NULL ;       // _cxq以及被唤醒的_WaitSet中的线程,在一定机制下,会放到_EntryList中
	_SpinFreq     = 0 ;
	_SpinClock    = 0 ;
	OwnerIsThread = 0 ;
	_previous_owner_tid = 0;
}

ObjectMonitor中的重要方法:

TryLock:

int ObjectMonitor::TryLock (Thread * Self) {
   for (;;) {
      // 拿到持有锁的线程
      void * own = _owner ;
      // 如果有线程持有锁,返回
      if (own != NULL) return 0 ;
      // 说明没有线程持有锁,own是null,cmpxchg指令是底层的CAS实现
      if (Atomic::cmpxchg_ptr (Self, &_owner, NULL) == NULL) {
   	 // 成功获取锁资源
         return 1 ;
      }
      // 这里其实重试操作没有什么意义,直接返回-1
      if (true) return -1 ;
   }
}

try_enter:

bool ObjectMonitor::try_enter(Thread* THREAD) {
  // 在判断_owner是否是当前线程
  if (THREAD != _owner) {
    // 判断当前持有锁的线程是否是当前线程
    if (THREAD->is_lock_owned ((address)_owner)) {
       assert(_recursions == 0, "internal state error");
       _owner = THREAD ;
       _recursions = 1 ;
       OwnerIsThread = 1 ;
       return true;
    }
    // CAS操作,尝试获取锁资源
    if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
      // 没有拿到锁资源,告辞
      return false;
    }
    // 拿到锁资源
    return true;
  } else {
    // 将_recursions+1,代表锁重入操作
    _recursions++;
    return true;
  }
}