java的线程是内核映射的,如果获取不到锁,那么就必然会发生内核态与用户态的转换,成本很高,所以效率比较低.
Java中每一个对象都可以作为锁。具体有如下三种形式:
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步代码块,锁是synchronized括号里配置的对象。
public class SynchronizedTest {
/**
* 同步修饰普通方法
*/
public synchronized void test01() {
// 同步修饰代码块
synchronized (this) {
System.out.println("hello synchronized");
}
}
/**
* 同步修饰静态方法
*/
public synchronized static void test02() {
}
}
使用javap 查看生成的class 文件
从JVM规范中可以看到Synchronized在JVM里的实现原理,JVM基于进入和退出monitor对象来实现方法同步和代码块同步的,但两者的实现细节不一样。
是使用monitorenter指令在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法的结束处和异常处,然后执行完对应操作后,在monitorexit监视器出口释放锁。
而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。在class文件中synchronized被ACC_SYNCHRONIZED标记,表明该方法为同步方法。
使用javap 可以看出synchronized被编译为普通的命令invokevirtual、areturn字节码指令。在JVM层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass作为锁对象。(引用(详细介绍了1.6后锁的各种优化))
JVM要保证每个monitorenter必须都有对应的monitorexit与之对应。任何对象都有一个monitor对象与之关联,并且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。
public class VolatileTest {
int i = 0;
boolean flag = false;
public synchronized void write(){
i = 2;
flag = true;
}
public synchronized void read(){
if(flag){
System.out.println("---i = " + i);
}
}}
假设线程A执行write()方法后,B线程执行 read()方法,这是一个正确同步的多线程程序。根据JMM规范,该程序的运行结果将与改程序在顺序一致性模型中的执行结果是一样的,具体对比图如下:
在JMM中,允许临界区的代码可以重排序(但是JMM不允许临界区的代码“逸出”到临界区之外,那样会破坏监视器的语义)
在现在的版本中,锁的状态总共有四种:
很显然锁的“重量”从左到右,依次递增
无锁状态<偏向锁<轻量级锁<重量级锁
所有的优化,其实都是为了将原来的重量级锁的“重量”变轻。无锁状态很好理解,新增加的偏向锁与轻量级锁,其实就是尽可能的将重量级锁往“无锁”的方向靠拢,尽可能的减少重量
减少重量的思路就是,通过一定的逻辑处理与判断,如果不需要加锁,那么我就少加一点锁。
继续之前先介绍两个概念,java对象头,Monitor Record和CAS
synchronized用的锁是存在Java对象头里的。所以这里对Java对象头做详细介绍
在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:
对象头(Header)
实例数据(Instance Data)
对齐填充(Padding)
HotSpot虚拟机的对象头包括两部分信息
- 哈希码(HashCode)
- GC分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳等
这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,官方称它为“MarkWord”(标记字段)。对象需要存储的运行时数据很多,其实已经超出了定义的位数。
并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身
虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的原数组中却无法确定数组的大小
Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,为了节省空间,并不是每个字段都有空间,不同的锁状态,有不同的字段含义,它会根据对象的状态复用自己的存储空间。
例如在32位HotSpot虚拟机中,如果对象处于被锁定状态下,那么Mark Word的32bit空间中的25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定位0,而在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下对象的存储是不一样的,在运行期间,Mark Word标记字段里存储的数据会随着锁的标志位的变化而变化;
无锁状态: 对象的hashCode+对象分代年龄+(是否位偏向锁)0+(所标志位)01
轻量级锁时会记录:指向栈中锁记录的指针
重量级锁时会记录:指向互斥量(重量级锁)的指针
偏向锁时会记录:线程ID
从字面意义上理解为监控、监视的意思。在Java中可以把它看作为一个同步工具,相当于操作系统中的互斥量,即值为1的信号量,它内置与每一个对象。在java世界里,每一个对象天生都拥有一把内置锁(Monitor)。这相当于一个许可证,只有你拿到许可证之后才可以进行操作,没有拿到则需要进行阻塞等待。
从字面意义上理解为:监视器记录。Monitor Record是线程私有的数据结构,每一个线程都有一个可用Monitor Record列表,同时还有一个全局的可用列表。
每一个被锁住的对象都会和一个monitor record关联(对象头的MarkWord中的LockWord指向monitor record的起始地址),同时monitor record中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。
CAS即compareAndSwap,比较并替换,是一种实现并发算法时常用到的技术。
CAS需要有3个操作数:
比如你要操作一个变量,他的值为A,你希望将他修改为B,这期间不会进行加锁,当你在修改的时候,你发现值仍旧是A,然后将它修改为B,如果此时值被其他线程修改了,变成了C,那么将不会进行值B的写入操作,这就是CAS的核心理论,通过这样的操作可以实现逻辑上的一种“加锁”,避免了真正去加锁
内置锁是JVM提供的最便捷的线程同步工具,在代码块或方法声明上添加synchronized关键字即可使用内置锁。使用内置锁能够简化并发模型。内置锁在Java中被抽象为监视器锁(monitor)。在JDK1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”
如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。 感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
原创不易,欢迎转发,关注公众号“码农进阶之路”,获取更多面试题,源码解读资料!