Synchronized实现原理及锁升级
Synchronized是Java内置的机制,是JVM层面的,而Lock则是接口,JDK层面的。最初的Synchronized的性能效率比较差,但是随着版本的升级,Synchronized已经越来越强大。
-
修饰普通方法,使用类的实例加锁,进入方法前需要获取当前类的实例锁;
注意反编译后的内容,flags那一行多了ACC_SYNCHRONIZED标识,表示这是同步方法。
public synchronized void demo1() { System.out.println("demo1"); }
public synchronized void demo1(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #7 // String demo1 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return
-
修饰静态方法,使用当前类加锁,进入方法前需要获取当前类对象锁;
注意反编译后的内容,flags那一行多了ACC_SYNCHRONIZED标识,表示这是同步方法。
public synchronized static void demo(){ System.out.println("demo"); }
public static synchronized void demo(); descriptor: ()V flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED Code: stack=2, locals=0, args_size=0 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String demo 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return
-
修饰代码块,使用对括号里面配置的锁;
在开启锁和结束锁的位置分别有monitorenter、monitorexit两个指令,也就是说Synchronized同步代码块是基于monitorenter加锁和monitorexit解锁来操作的。
方法上加入Synchronized关键字,编译后没有使用上面这两个指令,而是通过ACC_SYNCHRONIZED标识这是同步方法。
public static void main(String[] args) { synchronized (Demo.class) { System.out.println("main"); } }
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=1 0: ldc #2 // class com/Demo 2: dup 3: astore_1 4: monitorenter 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 8: ldc #4 // String main 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 13: aload_1 14: monitorexit 15: goto 23 18: astore_2 19: aload_1 20: monitorexit 21: aload_2 22: athrow 23: return
注意Synchronized是可重入的锁,同一个线程可多次获取锁,如果当前线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1,如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
Java对象头
Synchronized用的锁存在Java对象头中,Java对象头里的Mark Word默认存储对象的HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。
锁一共有四种状态,级别从低到高分别为:无锁
–>偏向锁
–>轻量级锁
–>重量级锁
,这几个状态随着竞争情况逐渐升级。为提高获取锁和释放锁的效率,锁可以升级但不能降级。
-
无锁
没有对资源进行锁定,所有线程都能够访问并修改同一资源,但同时修改成功的只有一个线程,其他修改失败的线程会不断重试知道修改成功。
-
偏向锁
是指偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁时才会被释放。
偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态,如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;如果线程处于活动状态,则升级成为轻量级锁状态。
-
轻量级锁
是指当锁是偏向锁的时候,被第二个线程B所访问,此时偏向锁就会升级为轻量级锁,线程B会通过自旋的形式尝试获取锁,线程不会阻塞,从而节省线程切换的性能。
当只有一个等待线程,则该线程将通过自旋进行等待,但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。
JDK采用的是适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋次数会更多,如果自旋失败了,则下次自旋次数就会减少。
-
重量级锁
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监视器(monitor)实现,而其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。