Java Synchronized(一)原理

538 阅读4分钟

Synchronized 的特点

  1. 内存可见性: 当线程释放锁时,JMM(Java Memory Model) 会把该线程对应的本地内存中的共享变量刷新到主内存中;当线程获取锁时,JMM 会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量(这个过程实质上是线程 A 通过主内存向线程 B 发送消息)

  2. 操作原子性: JMM 提供保证了访问基本数据类型的原子性(在写一个工作内存变量到主内存主要分两步:store、write),但是实际业务处理场景往往是需要更大的范围的原子性保证,所以模型也提供了 synchronized 块来保证

  3. 有序性: 这个概念是相对而言的,如果在本线程内,所有的操作都是有序的,如果在一个线程观察另一个线程,所有的操作都是无序的,前句是 "线程内表现为串行行为",后句是 "指令的重排序" 和 "工作内存和主内存同步延迟" 现象,JMM 提供了 volatilesynchronized 来保证线程之间操作的有序性

Synchronized 底层原理

JVM 是基于进入和退出 Monitor(监视器锁) 对象来实现方法同步和代码块同步的

  • synchronized 修饰的方法在字节码中添加了一个 ACC_SYNCHRONIZEDflag,当线程执行到某个方法时,JVM 会去检查该方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了那线程会去获取这个对象所对应的 monitor 对象(每一个对象都有且仅有一个与之对应的 monitor 对象),获取成功后才执行方法体,方法执行完再释放 monitor 对象,在这一期间,任何其他线程都无法获得这个 monitor 对象
  • 同步代码块则是在同步代码块前插入 monitorenter,在同步代码块结束后插入 monitorexit。根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁(monitor),如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加 1;相应地,在执行 monitorexit 指令时会将锁计数器减 1,当计数器被减到 0 时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止

注意:

  1. synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题
  2. 同步块在已进入的线程执行完之前,会阻塞后面其他线程的进入

Mutex Lock

监视器锁(Monitor)本质是依赖于底层操作系统的 Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为 "互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象

互斥锁: 用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁;在完成了对共享资源的访问后,要对互斥量进行解锁

Mutex 的工作方式:

  1. 申请 Mutex
  2. 如果成功,则持有该 Mutex
  3. 如果失败,则进行 spin 自旋。spin 的过程就是在线等待 Mutex,不断发起 Mutex gets,直到获得 Mutex 或者达到 spin_count 限制为止
  4. 依据工作模式的不同选择 yiled 或者 sleep
  5. 若达到 sleep 限制或者被主动唤醒或者完成 yield, 则重复 1~4 步,直到获得为止

由于 Java 的线程是映射到操作系统的原生线程之上的,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态中,因此状态转换需要耗费很多的处理器时间。所以 synchronized 是 Java 语言中的一个重量级操作

JDK 1.6 改进: synchronizedjava.util.concurrent 包中的 ReentrantLock 相比,由于 JDK1.6 中加入了针对锁的优化措施(引入 "偏向锁" 和 "轻量级锁"),使得 synchronizedReentrantLock 的性能基本持平。ReentrantLock 只是提供了比 synchronized 更丰富的功能,而不一定有更优的性能,所以在 synchronized 能实现需求的情况下,优先考虑使用 synchronized 来进行同步

锁优化具体见 Java Synchronized(二)锁的优化

小结

  • synchronized 的特点: 保证内存可见性、操作原子性、代码执行有序性
  • synchronized 影响性能的原因:
    1. 加锁解锁操作需要额外操作
    2. 互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的线程挂起和恢复操作都需要转入内核态中完成(用户态与内核态的切换的性能代价是比较大的)


参考资料:

~~~~~~~~知乎 - Java synchronized原理总结

~~~~~~~~简书 - java中synchronized的底层实现