深入理解 synchronized 原理

Synchronized 是一种JVM锁,大部分人都只是会使用 Synchronized 关键子,在方法上加入 Synchronized 就能对共享变量进行线程安全的使用,确没有了解 Synchronized 大部分真正含义,如,Synchronized 锁的级别,锁的范围(使用不同的对象,锁的范围不同)。希望下文对各位有所帮助。

1. Synchronized使用

1.1. 修饰实例方法

public class SynchronizedTest {
    int i = 0;
    public synchronized void monitor(){
        i++;
    }
    public synchronized void monitor1(){
        i++;
    }
    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();
        new Thread(()->{ 
            test.monitor(); 
        }).start();
        new Thread(()->{ 
            test.monitor1(); 
        }).start();
    }
}

上面代码我们在 SynchronizedTest 类中有 monitor() 以及 monitor1() 两个方法.
分别都加了一个 synchronized 两个方法都操作了一个 int 为 i 的共享变量,看main方法比如有两个线程, 线程A访问 monitor() 线程B访问 monitor1() 都是用的 test对象进行调用的,此时我们的 锁的范围 当前 test对象。

修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。同一Class类不同的实例对象锁的范围,已对象为单位

1.2. 静态对象

public class SynchronizedTest {
    static int i = 0;
    public synchronized void monitor(){
        i++;
    }
    public static synchronized  void monitor1(){
        i++;
    }
    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();
        new Thread(()->{
            test.monitor();
        }).start();
        new Thread(()->{
            SynchronizedTest.monitor1();
        }).start();
    }
}

上面代码我们在 SynchronizedTest 类中有 monitor() 以及 monitor1() 两个方法.
分别都加了一个 synchronized 两个方法都操作了一个 static int 为 i 的共享变量,看main方法比如有两个线程, 线程A访问 test.monitor(),线程B访问 SynchronizedTest.monitor1(),main运行肯定是线程不安全的,两个方法都加了 synchronized 但是锁的对象不同,一个是实例对象,一个是静态对象

修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。静态变量为全局唯一的,所有对象在锁静态对象时间单位为Class。不同的对象使用静态变量为锁时,所有对象使用的是同一吧锁

1.3. 代码块

public class SynchronizedTest {
    Object lock = new Object();
    static Object lockStatic = new Object();
    static int i = 0;
    public void monitor(){
        synchronized (lock) {
            i++;
        }
    }
    public void monitor1(){
        synchronized (lockStatic) {
            i++;
        }
    }
    public synchronized void monitor2(){
        i++;
    }
}

修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。代码块加锁需要用实例对象或者静态对象,看自己定义锁的范围标准。

1.4. 如上代码查看同步方法的class文件

public void monitor();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: getfield      #3                  // Field lock:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter
         7: getstatic     #4                  // Field i:I
        10: iconst_1
        11: iadd
        12: putstatic     #4                  // Field i:I
        15: aload_1
        16: monitorexit
        17: goto          25
        20: astore_2
        21: aload_1
        22: monitorexit
        23: aload_2
        24: athrow
        25: return
        ...
public synchronized void monitor2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
  stack=0, locals=1, args_size=1
     0: return
  LineNumberTable:
    line 18: 0
  LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       1     0  this   Lcom/springstudy/simplespring/SynchronizedTest;
  • monitor() 方法,同步代码块的起始位置插入了 monitorenter 指令,在同步代码块结束的位置插入了 monitorexit指令。(JVM需要保证每一个monitorenter都有一个monitorexit与之相对应,但每个monitorexit不一定都有一个monitorenter)
  • monitor2() 方法,翻译成普通的方法调用和返回指令,只是在其常量池中多了 ACC_SYNCHRONIZED 标示符

JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。 monitorenter 和 monitorexit 指令是通过 monitor 对象实现的。

1.5. monitorenter

每个对象都有一个监视器锁(monitor)与之对应。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者.
  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权.

1.6. monitorexit

执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。
其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。

通过这两个指令我们应该能很清楚的看出synchronized的实现原理,synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。