JVM中的锁(上):对象头和锁

目录

Java对象头

Mark Word

JVM中的锁

偏向锁

轻量级锁


      以前的日志里总结过Java中锁的应用,在多线程编程专栏里,JVM内部对锁进行了一些优化,所以在Java程序中使用锁很方便,接下来的日志就总结下锁在虚拟机里的实现和优化。锁的作用简单来说是为了保护多线程访问同一内容,也就是临界区内容时,因多个线程同时读,写入操作导致数据一致性出现问题,为了保证事务执行前和执行后数据的完整性没有遭到破坏,可以通过给对象加锁来让多线程按顺序逐一访问临界区,这样大家访问到的资源总是正确的。

 

Java对象头

      首先来看对象头,JVM中有关锁的信息都存放在Java对象的对象头里面,除了锁的状态外,还包括一些对象的年龄,对象哈希值,如果对象是数组的话还有数组的长度等,每一个Java对象都有对象头,组成有三部分:
1、Mark Word,存储了对象的锁信息,锁状态,对象年龄,哈希值和线程拥有者等。
2、Class Pointer:存储了对象的一些指针信息,如该对象的类元数据(),JVM可以通过其来确定对象是属于哪一个类。
3、Array Length:如果对象是一个数组对象,那么对象头中就会有这一部分,用来存储数组的长度,它的长度和JVM一个字的大小(注意是一个字的大小,不是一个字节的大小)一致,如果在32位的JVM环境下就占32位,在64位下大小就占64位。
      这三部分组成中最重要的是Mark Word,因为对象占用了什么锁,锁的状态信息都存放在了这里面。

Mark Word

      Mark Word中用lock标记来记录锁的状态,大小占两位, biased_lock记录对象是否使用了偏向锁(偏向锁下面会总结),大小占一位。lock和biased_lock两者配合一起表示锁的状态,例如biased_lock值为0,lock值为01时表示对象处于无锁状态:

      还有一些标记例如age,大小占4位,表示对象的年龄,每当对象经历了一次GC后,年龄就会+1,由于age大小4位最大值1111等于15,所以对象的年龄最大到15就会转为老年代。还有thread标记,这个很简单,就是标记线程id。前面说到Mark Word中还存储了对象的哈希值,用的是identity_hashcode标记为,举些例子,如果对象头的格式是这样的:

thread:12 | age:3 | biased_lock:1 | 01

表示线程id为12,对象年龄等于3,持有偏向锁。

identity_hashcode:25 | age:4 | biased_lock:0 | 00

表示有25位标识的哈希值,对象年龄等于4,持有轻量级锁。

 

JVM中的锁

      看完对象头,往下看虚拟机中对锁的一些实现,Java程序中对于多线程的同步操作并不是全部交由操作系统来完成的,JVM也会 参与进来,例如遇到线程访问临界区资源但没能获得锁而等待时,JVM不会立刻让操作系统挂起线程,而是做一些例如自旋操作,让线程尽可能拿到锁,这样做的目的是尽可能提高程序运行速度,因为线程拿不到锁后进入阻塞态,等待下一次获取锁后进入就绪态再到运行态,这一系列动作需要耗费较大的性能和时间。

偏向锁

      首先来看偏向锁,它的做法是,如果一个线程A获取了锁对象,那么这个锁对象就进入了偏向状态,Mark Word中标记为biased_lock:1 | 01,同时经过CAS无锁交换比较(compare and swap)操作后记录线程id,线程执行完释放锁之后,如果想要再次获得该锁对象,则不需要再进行加锁,cas等同步操作。当然,如果有其他线程试图去获取该锁对象时,这个偏向状态就会失效。来看一个例子:

public class BiasedLockDemo {

	public static List<Integer> integerList = new Vector<Integer>();
	
	public static void main(String[] args) {
		int num = 0;
		
		long beginTime = System.currentTimeMillis();
		for(int i=0; i<9000000; i++) {
			integerList.add(num);
			num++;
		}
		
		long endTime = System.currentTimeMillis();
		System.out.println("耗时: " + (endTime - beginTime));
	}

}

      例子中使用动态数组Vector类,因为这个类的操作是同步操作,对它进行写入时内部会用到锁,我们不断地向数组里添加数据,运行时程序每一次add()操作都需要先获得锁,浪费不少时间:

      如果我们使用偏向锁,那么从第一次获得该锁对象后,下一次程序再次访问该资源时,就不需要同步操作,可以直接写入,来试下用参数-XX:UseBiasedLocking –XX:BiasedLockingStartupDelay=0 –client,启用偏向锁,并且让JVM在启动后就立刻使用偏向锁,执行程序:

      可以看到,程序执行时间有了提升,因为少了很多同步操作。使用偏向锁的目的就是为了在一些多线程访问同一临界资源较少,对锁的竞争不大的的情境下,可以提升程序效率,不足就是,在多线程竞争锁很频繁的情境下,偏向锁的作用就不大了,因为多线程不停申请同一把锁对象,偏向锁的状态几乎得不到保持,锁状态会不停被切换,带来的性能提升微乎其微。

 

轻量级锁

      轻量级锁在JVM内部实现用的是BasicObjectLock类,类里面有轻量锁对象BasicLock,线程如果获取偏向锁失败,就会转去申请轻量级锁,轻量级锁的就是先让线程进行CAS操作来同步,期间还会将原来对象的Mark Word进行备份,然后复制BasicLock的地址到Mark Word中,如果BasicLock地址复制成功,表示线程处于轻量级锁状态,Mark Word中标记lock为00。如果地址复制失败,先会去判断线程是否之前就已经获得了锁对象,如果是就直接进入同步块,如果没有,则表明有多个线程在获取同一对象,此时轻量级锁就会膨胀成重量级锁。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值