一、synchronized介绍
线程安全问题的主要诱因
存在共享数据(也称临界资源)
存在多条线程共同操作这些数据
解决问题的根本方法:
同一时刻有且只有一个线程操作共享数据,其它线程必须等待该线程处理完数据后再对共享数据进行操作。
互斥锁的特性
互斥性: 即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块(复合操作)进行访问。互斥性也称操作的原子性。
可见性:必须确保在锁被释放之前,对共享变量所做的修改,对应随后获得该锁的另一个线程是可见的(即获得锁的同时应获得最新共享变量的值),否则另一个线程可能是在本地缓存某个副本上继续操作,从而引起不一致。
synchronized锁的不是代码,锁的都是对象。
根据获取锁的分类: 获取对象锁和获取类锁
获取对象锁的两种用法
1、同步代码块( synchronized(this), synchronized(类实例对象) ),锁是括号() 中的实例对象。
2、同步非静态方法( synchronized method) ,锁是当前对象的实例对象
获取类锁的两种用法
1、同步代码块(synchronized(类.class)), 锁的是小括号()中的类对象(Class对象).
2、同步静态方法 (synchronized static method ) , 锁是当前对象的类对象 (Class对象)
二、synchronized底层实现原理
实现synchronized基础
Java对象头
Monitor
对象在内存中的布局
对象头
实例数据
对齐填充
对象头的结构
Mark Word
Monitor: 每个Java对象天生自带了一把看不见的锁
进入源码: http://hg.openjdk.java.net/jdk8u/hs-dev/hotspot/file/ae5624088d86/src/share/vm/runtime/objectMonitor.hpp
可以发现里面有两个队列 _WaitSet 和 _EntryList
_owner 指向持有objectMonitor的线程
下面从字节码的角度查看synchronize
创建类
public class SyncBlockAndMethod { public void syncsTask(){ synchronized (this){ System.out.println("Hello"); } } public synchronized void syncTask(){ System.out.println("Hello again"); } }
然后编译成class文件
javac SyncBlockAndMethod.java
查看字节码
javap -verbose SyncBlockAndMethod.class
Classfile /xxx/src/thread/SyncBlockAndMethod.class Last modified 2019-12-29; size 613 bytes MD5 checksum 9aa751fc8ed2cf7d372724572edfb1a8 Compiled from "SyncBlockAndMethod.java" public class thread.SyncBlockAndMethod SourceFile: "SyncBlockAndMethod.java" minor version: 0 major version: 51 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #7.#20 // java/lang/Object."<init>":()V #2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #23 // Hello #4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = String #26 // Hello again #6 = Class #27 // thread/SyncBlockAndMethod #7 = Class #28 // java/lang/Object #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 syncsTask #13 = Utf8 StackMapTable #14 = Class #27 // thread/SyncBlockAndMethod #15 = Class #28 // java/lang/Object #16 = Class #29 // java/lang/Throwable #17 = Utf8 syncTask #18 = Utf8 SourceFile #19 = Utf8 SyncBlockAndMethod.java #20 = NameAndType #8:#9 // "<init>":()V #21 = Class #30 // java/lang/System #22 = NameAndType #31:#32 // out:Ljava/io/PrintStream; #23 = Utf8 Hello #24 = Class #33 // java/io/PrintStream #25 = NameAndType #34:#35 // println:(Ljava/lang/String;)V #26 = Utf8 Hello again #27 = Utf8 thread/SyncBlockAndMethod #28 = Utf8 java/lang/Object #29 = Utf8 java/lang/Throwable #30 = Utf8 java/lang/System #31 = Utf8 out #32 = Utf8 Ljava/io/PrintStream; #33 = Utf8 java/io/PrintStream #34 = Utf8 println #35 = Utf8 (Ljava/lang/String;)V { public thread.SyncBlockAndMethod(); flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 6: 0 public void syncsTask(); flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #3 // String Hello 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_1 13: monitorexit 14: goto 22 17: astore_2 18: aload_1 19: monitorexit 20: aload_2 21: athrow 22: return Exception table: from to target type 4 14 17 any 17 20 17 any LineNumberTable: line 9: 0 line 10: 4 line 11: 12 line 12: 22 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 17 locals = [ class thread/SyncBlockAndMethod, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 public synchronized void syncTask(); flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String Hello again 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 15: 0 line 16: 8 }
可以发现有monitorenter 和monitorexit 两条指令
另外一个方法,没有monitorenter 和monitorexit 两条指令。可以看到ACC_SYNCHRONIZED的访问标志,用来区分一个方法是否是同步方法。当发现有同步标志,执行线程将持有Monitor。
为什么会对synchronized嗤之以鼻?
早期版本中,synchronized属于重量级锁,依赖于Mutex Lock实现
线程之间的切换需要从用户态转换为核心态,开销较大
Java6以后,synchronized性能得到了很大的提升
自旋锁和自适应自旋锁
自旋锁:
许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
通过让线程执行忙循环等待锁的释放,不让出CPU
缺点: 若锁被其它线程长时间占用,会带来许多性能上的开销
自适应自旋锁
自旋的次数不再固定
由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。
synchronized的四种状态
无锁,偏向锁,轻量级锁, 重量级锁
锁膨胀方向: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
偏向锁: 减少同一线程获取锁的代价
大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
核心思想:
如果一个线程获得了锁,那么锁就进入了偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadId即可,这样就省去了大量有关锁申请的操作。
不适用于锁竞争比较激烈的多线程场合。
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就好升级为轻量级锁。
适用的场景: 线程交替执行同步块
若存在同一时间访问同一锁的情况,就好导致轻量级锁膨胀为重量级锁。
偏向锁、轻量级锁、重量级锁的汇总