synchronized
简介
synchronized修饰符是Java提供的多线程访问临界资源的同步器,一般我们有三种使用方式:
- 对类的静态方法加锁
- 对类的一般方法加锁
- 对代码块加锁
静态方法加锁
静态方法是属于类的,所以这种加锁实际上对类进行加锁,加锁对象为class。
如果一个类有多个静态方法加锁,如下面代码,test和test1加锁,一个线程访问test,其他线程想要访问test1或者test都会被阻塞。
public class Juc_LockOnClass{
public static synchronized void test(){
System.out.println("静态方法加锁");
}
public static synchronized void test1(){
System.out.println("静态方法加锁1");
}
}
一般方法加锁
一般方法加锁实际是对this加锁。当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
public class Juc_LockOnThisObject {
public synchronized void test(){
System.out.println(ClassLayout.parseInstance(this).toPrintable());
}
}
实现原理
同步方法,方法访问标志上添加ACC_SYNCHRONIZED 标志。
代码块加锁
对代码块的加锁需要单独创建一个对象充当锁。
如下代码,method1和method2使用不同的对象加锁,method1和method3使用相同的对象加锁。
如果有一个线程1执行method1,其他线程可以执行method2,在尝试执行method3时会阻塞。
public class Juc_LockAppend {
Object object = new Object();
Object object1 = new Object();
private void method1(){
synchronized (object){
System.out.println("代码块加锁1");
}
}
private void method2(){
synchronized (object1){
System.out.println("代码块加锁2");
}
}
private void method3(){
synchronized (object){
System.out.println("代码块加锁3");
}
}
}
实现原理
同步代码块的前面添加monitorenter,之后添加monitorexit指令。
Monitor对象
Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的。
对象内存布局
1. 对象头(Header)
比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象)等。
Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
2. 实例数据(Instance Data)
存放类的属性数据信息,包括父类的属性信息。
3. 对齐填充(Padding)
由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
锁的升级过程
偏向锁
线程获取到锁,就转为偏向锁,会把线程ID CAS设置为当前线程id,下次该线程重入的时候,就可以直接判断id。
如果有其他线程竞争锁,就会进入轻量级锁。
轻量级锁
线程在获取到轻量级锁以后,会在线程栈建立一个锁记录的空间,将对象头的markword拷贝到该空间(CAS操作),如果失败,就会膨胀。
自旋锁
如果轻量级锁失败,线程会先做几个空循环(自旋),如果在自旋时间内获取到了锁,就开始执行,如果没有获取到锁,就进入到重量级锁。
重量级锁
重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。
锁消除
如果虚拟机开启了逃逸分析,发现加锁代码不会被一个以上线程访问,会消除这种没有必要的锁。
逃逸分析优化
- 同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
- 将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
- 分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。