synchronized
- java语言级的支持,1.6之后性能极大提高
- 字节码层面的实现: monitorenter/monitorexit
//代码
private void increment(){
synchronized(this){
counter++;
}
}
//字节码
private increment()V
TRYCATCHBLOCK L0 L1 L2 null
TRYCATCHBLOCK L2 L3 L2 null
L4
LINENUMBER 22 L4
ALOAD 0
DUP
ASTORE 1
MONITORENTER //占有锁(监视器)
L0
LINENUMBER 23 L0
ALOAD 0
GETFIELD com/Coordinator$MyThread.i : Ljava/lang/Integer;
ASTORE 2
ALOAD 0
ALOAD 0
GETFIELD com/Coordinator$MyThread.i : Ljava/lang/Integer;
INVOKEVIRTUAL java/lang/Integer.intValue ()I
ICONST_1
IADD
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
DUP_X1
PUTFIELD com/Coordinator$MyThread.i : Ljava/lang/Integer;
ASTORE 3
ALOAD 2
POP
L5
LINENUMBER 24 L5
ALOAD 1
MONITOREXIT // 退出锁(监视器)
L1
GOTO L6
L2
FRAME FULL [com/Coordinator$MyThread java/lang/Object] [java/lang/Throwable]
ASTORE 4
ALOAD 1
MONITOREXIT
L3
ALOAD 4
ATHROW
L6
LINENUMBER 25 L6
FRAME CHOP 1
RETURN
L7
LOCALVARIABLE this Lcom/Coordinator$MyThread; L4 L7 0
MAXSTACK = 3
MAXLOCALS = 5
//方法2.synchronized 在方法上
private synchronized static void doSomething(){
i++;
}
- 锁住的是什么?
- 在方法中时,锁住的是其中的对象 ;
- 在方法上时,1. static方法锁住的是当前的class对象;2. 非static方法,锁住的是当前对象的实例;
// 反例:
static class MyThread extends Thread{
private synchronized void doSomething(){
}
@Override
public void run(){
doSomething();
System.out.println(123);
}
}
public static void main(String[] args) {
// 开启了四个线程,但synchronized 方法锁住的是每个实例自己的doSomething();所以synchronized 锁并没有用;
new MyThread().start();
new MyThread().start();
new MyThread().start();
new MyThread().start();
}
-
底层实现
- 对象头
- 无锁 -> 偏向锁(biased lock) -(锁膨胀)> 轻量级锁 -(锁膨胀)> 重量级锁
无锁状态: 没有线程访问,是个游离在JVM中的对象;
偏向锁状态: 一直以来都是同一个线程访问,该对象头会变成偏向锁,下次该线程访问,可以直接使用;此时没有锁发生,所以即使线程不再调用,标志位还会一直存在在对象头中;
当第二个线程,尝试获取该锁,偏向锁标志位会撤销,会恢复成无锁的状态;
轻量级锁状态: 有线程竞争的情况发生,但是不严重(同一时刻只有一个线程获取锁),假如能抢到我的话,就不需要获取monitor.
重量级锁状态: 有多个线程在竞争锁,所以必须获取monitor;所以进入monitorenter时,或退出monitorexit时,不一定需要获取monitorenter或释放monitorexit; 对象头定义: markOop.hpp 锁膨胀定义: synchronizer.cpp
对象头.png
锁粗化与锁消除(编译器层面)
- 锁粗化: 同一个锁要连续频繁加锁解锁,粗化为更大范围的锁;
public static void main(String[] args) {
increment();
}
private static void increment(){
// monitorenter
for (int i = 0 ;i <10; i++) {
foo();
}
// monitorexit
}
// foo()频繁的加锁解锁,编译器就会把他优化成更大范围的锁
private static synchronized void foo() {
System.out.println("");
}
- 锁消除: 通过逃逸分析,发现一个对象不可能被其他线程所竞争,那么就不需要上锁;
private static void foo() {
Object object = new Object();
// 锁住一个局部变量是没有意义的
synchronized (object){
}
}
private static Object foo2() {
Object object = new Object();
// 此时,synchronized 有用,因为object 已经逃逸出去,存在被其他线程引用的可能
synchronized (object){
}
return object ;
}
附:
逃逸分析
Java的逃逸分析只发在JIT的即时编译中,(因为Java的分离编译和动态加载使得前期的静态编译的逃逸分析比较困难或收益较少,所以目前Java的逃逸分析只发在JIT的即时编译中) ;
JVM判断新创建的对象是否逃逸的依据有:
一、对象被赋值给堆中对象的字段和类的静态变量。
二、对象被传进了不确定的代码中去运行。如果满足了以上情况的任意一种,那这个对象JVM就会判定为逃逸。
基于逃逸分析的优化:
- 将堆分配转化为栈分配。如果某个对象在子程序中被分配,并且指向该对象的指针永远不会逃逸,该对象就可以在分配在栈上,而不是在堆上。在有垃圾收集的语言中,这种优化可以降低垃圾收集器运行的频率。
- 同步消除。如果发现某个对象只能从一个线程可访问,那么在这个对象上的操作可以不需要同步。
- 分离对象或标量替换。如果某个对象的访问方式不要求该对象是一个连续的内存结构,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。 好处: 减少堆内存的占用, 因为一旦不需要创建对象了, 那么就不需要再分配堆内存了.
// 例: 标量替换
public class Point{
public int x;
public int y;
}
// 上面的Point对象,没有在方法外部被使用,就会被标量替换成:
int x = 1;
int y = 2;