要学习java中锁的机制,首先要了解java对象头。因为锁机制的实现依赖于java的对象头。 那什么是java对象头呢? 当你创建一个对象时,该对象在内存中的存储布局为: 其中Mark Word和类型指针就被称为对象头。MarkWord中存放的东西下面详细介绍,类型指针里存放着一个地址,指向该对象是哪个类创建的对象。 Mark Word的大小为8个字节,类型指针的大小为4个字节。实例数据和对齐位的大小不一定。对齐位主要是要让该对象大小要被8整除,如果不能被整除就补位。 如何查看对象各个区域所占的大小呢?导入如下依赖并使用ClassLayer类来输出。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
输出语句:
System.out.println(ClassLayout.parseInstance(对象名).toPrintable());
举个栗子:
public class LayoutTest {
public static class T{
int i = 0;
}
public static void main(String[] args) {
T t = new T();
System.out.println(ClassLayout.parseInstance(t).toPrintable());
}
}
输出如下: 表示该对象大小为16个字节。为什么是16bytes呢? 首先Mark Word占8个字节,class pointer占4个字节,数据类型为int占4个字节,总共16个字节,可以被8整除所以对齐位没有补位。 第二个栗子:
public class LayoutTest {
public static class T{
String s = "abcdefghijklmn";
}
public static void main(String[] args) {
T t = new T();
System.out.println(ClassLayout.parseInstance(t).toPrintable());
}
}
嗯?这字符串s中有这么长一串怎么还是16个字节呢? 因为此处的s其实是一个指针,该指针指向字符串常量池的这个字符串,这个字符串不在这个对象中创建,所以还是12+4+4=16bytes。因为s是指针class pointer也是指针所以都是4个字节。 然后大的要来了。
Mark Word
Mark Word里默认存储对象的HashCode、分代年龄、是否偏向锁以及锁标记位。32位JVM的Mark Word的默认存储结构如下: Synchronized锁的状态被分为4种:无锁,偏向锁,轻量级锁,重量级锁 注意锁标志汇总没有出现自旋锁,自旋锁仅仅是锁可能存在的一种状态,是暂时性的没有官方的标志。 无锁和偏向锁标志位都是01,只是偏向锁时偏向模式会被置为1,。锁可以升级但是不可以降级,意味偏向锁升级成轻量级锁后不能降级称为偏向锁。
无锁
无锁是001状态,偏向标志0,锁标志01; 更准确来说,001状态应该是无锁不可偏,因为还有一种101状态是无锁可偏(又叫匿名偏向)。无锁不可偏状态下遇到同步会直接升级成轻量级锁,而不会变成偏向锁。只有在无锁可偏101状态下才能变成偏向锁。101状态下线程ID部分全部为0,以为着没有线程实际获得偏向锁。
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。 这个锁永远会偏向于获得它的线程,线程第二次到达同步代码块时,会判断此时持有锁的线程是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。偏向锁通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁) 。升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致STW(stop the word)操作;
锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候都很顺利,没有发生阻塞,那么就不存在锁竞争。
只有当某线程尝试获取锁的时候,发现该锁已经被占用,只能等待其释放,这才发生了锁竞争。
优点:在只有单一线程访问对象的时候,偏向锁机会没有影响。只有第一次需要cas操作替换,随后的只要比较线程ID即可,比较方便。 缺点:多个线程访问时会出现竞争,一系列分析比较耗费时间。而且偏向锁存放线程ID和Epoch后,对象头中不存在hash值,如果程序需要hash值会导致偏向锁退出。 如果对象需要调用Object方法,会启用对象的minoter内置锁,此时会直接有偏向锁升级为重量级锁。
轻量级锁(自旋锁)
自旋锁:自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。 设计初衷是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统的互斥量产生的性能消耗。
轻量级锁的获取获取
|