Synchronize 关键字原理

简介: 众所周知 Synchronize 关键字是解决并发问题常用解决方案,有以下三种使用方式:同步普通方法,锁的是当前对象。同步静态方法,锁的是当前 Class 对象。同步块,锁的是 {} 中的对象。

众所周知 Synchronize 关键字是解决并发问题常用解决方案,有以下三种使用方式:


  • 同步普通方法,锁的是当前对象。


  • 同步静态方法,锁的是当前 Class 对象。


  • 同步块,锁的是 {} 中的对象。


实现原理:JVM 是通过进入、退出对象监视器( Monitor )来实现对方法、同步块的同步的。


具体实现是在编译之后在同步方法调用前加入一个 monitor.enter 指令,在退出方法和异常处插入 monitor.exit 的指令。


其本质就是对一个对象监视器( Monitor )进行获取,而这个获取过程具有排他性从而达到了同一时刻只能一个线程访问的目的。


而对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程 monitor.exit 之后才能尝试继续获取锁。


流程图如下:



通过一段代码来演示:


public static void main(String[] args) {
        synchronized (Synchronize.class){
            System.out.println("Synchronize");
        }
    }


使用 javap -c Synchronize 可以查看编译之后的具体信息。


public class com.crossoverjie.synchronize.Synchronize {
  public com.crossoverjie.synchronize.Synchronize();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #2                  // class com/crossoverjie/synchronize/Synchronize
       2: dup
       3: astore_1
       **4: monitorenter**
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String Synchronize
      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
    Exception table:
       from    to  target type
           5    15    18   any
          18    21    18   any
}


可以看到在同步块的入口和出口分别有 monitorenter,monitorexit指令。


锁优化


synchronize  很多都称之为重量锁,JDK1.6 中对 synchronize 进行了各种优化,为了能减少获取和释放锁带来的消耗引入了偏向锁轻量锁


轻量锁


当代码进入同步块时,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record)区域,同时将锁对象的对象头中 Mark Word 拷贝到锁记录中,再尝试使用 CASMark Word 更新为指向锁记录的指针。


如果更新成功,当前线程就获得了锁。


如果更新失败JVM 会先检查锁对象的 Mark Word 是否指向当前线程的锁记录。


如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。


不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁


解锁


轻量锁的解锁过程也是利用 CAS 来实现的,会尝试锁记录替换回锁对象的 Mark Word 。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁)


轻量锁能提升性能的原因是:认为大多数锁在整个同步周期都不存在竞争,所以使用 CAS 比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有 CAS 的开销,甚至比重量锁更慢。


偏向锁


为了进一步的降低获取锁的代价,JDK1.6 之后还引入了偏向锁。


偏向锁的特征是:锁不存在多线程竞争,并且应由一个线程多次获得锁。


当线程访问同步块时,会使用 CAS 将线程 ID 更新到锁对象的 Mark Word 中,如果更新成功则获得偏向锁,并且之后每次进入这个对象锁相关的同步块时都不需要再次获取锁了。


释放锁


当有另外一个线程获取这个锁时,持有偏向锁的线程就会释放锁,释放时会等待全局安全点(这一时刻没有字节码运行),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁来判定将对象头中的 Mark Word 设置为无锁或者是轻量锁状态。


轻量锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。可以使用 -XX:-userBiasedLocking=false 来关闭偏向锁,并默认进入轻量锁。


其他优化


适应性自旋


在使用 CAS 时,如果操作失败,CAS 会自旋再次尝试。由于自旋是需要消耗 CPU 资源的,所以如果长期自旋就白白浪费了 CPUJDK1.6加入了适应性自旋:


如果某个锁自旋很少成功获得,那么下一次就会减少自旋。


相关文章
|
6月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法
|
2月前
|
存储 缓存 Java
volatile关键字最全原理剖析
【9月更文挑战第27天】`volatile` 是一个用于修饰变量的关键字,告知编译器被修饰的变量可能在程序控制流之外被改变。在多线程环境下,`volatile` 确保变量的值每次从内存中读取,保持最新状态,但不能解决所有同步问题。在硬件交互中,`volatile` 用于内存映射的 I/O 操作,确保读取最新值。不同编程语言如 C/C++ 和 Java 中的 `volatile` 用法略有差异,但都是处理易变变量的重要工具。
|
6月前
|
存储 安全 Java
【JavaEE初阶】 volatile关键字 与 wait()方法和notify()方法详解
【JavaEE初阶】 volatile关键字 与 wait()方法和notify()方法详解
|
Java
synchronize 三大作用、三大用法
synchronize 三大作用、三大用法
119 0
|
6月前
|
设计模式 安全 编译器
线程学习(3)-volatile关键字,wait/notify的使用
线程学习(3)-volatile关键字,wait/notify的使用
56 0
|
安全 Java 编译器
Volatile关键字的作用和实现原理
Volatile关键字的作用和实现原理
188 0
|
存储 缓存 Java
Java并发编程之Volatile关键字解析
在java的并发编程中我们经常会使用到Volatile关键字。而关于Volatile关键字的使用以及Volatile关键字的特性和实现原理也是在笔面试中经常会遇到的问题了。
112 0
Java并发编程之Volatile关键字解析
|
缓存 Java 编译器
Java并发编程:volatile关键字解析
Java并发编程:volatile关键字解析
139 0
Java并发编程:volatile关键字解析
|
缓存 Java
多线程--Volatile关键字
多线程--Volatile关键字
471 0
多线程--Volatile关键字
synchronized关键字的用法?
synchronized关键字可以将对象或者方法标记为同步,以实现对对象和方法的互斥访问,可以用synchronized(对象) { … }定义同步代码块,或者在声明方法时将synchronized作为方法的修饰符。
959 0