EchoDemo's Blogs

深入理解多线程之Synchronized的实现原理

伫倚危楼风细细,望极春愁,黯黯生天际。草色烟光残照里,无言谁会凭阑意。

拟把疏狂图一醉,对酒当歌,强乐还无味。衣带渐宽终不悔,为伊消得人憔悴。

—-柳永《蝶恋花·伫倚危楼风细细》

在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能)。在了解synchronized的实现原理之前,我们先来看看synchronized关键字的三种应用方式。

synchronized的三种应用方式

1、修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。

2、修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。

3、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

没有加synchronized关键字的情况

public class SynchronizedTest {

      public void method1(){
          System.out.println("Method 1 start");
          try {
              System.out.println("Method 1 execute");
              Thread.sleep(3000);
          } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 1 end");
     }

     public void method2(){
         System.out.println("Method 2 start");
         try {
             System.out.println("Method 2 execute");
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 2 end");
     }

     public static void main(String[] args) {
         final SynchronizedTest test = new SynchronizedTest();

         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method1();
             }
         }).start();

         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method2();
             }
         }).start();
     }
 }

执行结果如下,线程1先执行,但由于线程1在中途sleep了3秒钟时间,所以导致了线程2先执行完成。

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end

synchronized作用于实例方法

所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法。

public class SynchronizedTest {

  public synchronized void method1(){
      System.out.println("Method 1 start");
      try {
          System.out.println("Method 1 execute");
          Thread.sleep(3000);
      } catch (InterruptedException e) {
         e.printStackTrace();
     }
     System.out.println("Method 1 end");
 }

 public synchronized void method2(){
     System.out.println("Method 2 start");
     try {
         System.out.println("Method 2 execute");
         Thread.sleep(1000);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     System.out.println("Method 2 end");
 }

 public static void main(String[] args) {
     final SynchronizedTest test = new SynchronizedTest();

     new Thread(new Runnable() {
         @Override
         public void run() {
             test.method1();
         }
     }).start();

     new Thread(new Runnable() {
         @Override
         public void run() {
             test.method2();
         }
     }).start();
 }
}

执行结果如下,跟代码段一比较,可以很明显的看出,线程2需要等待线程1的method1执行完成才能开始执行method2方法。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

synchronized作用于静态方法

当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

public class SynchronizedTest {

 public static synchronized void method1(){
     System.out.println("Method 1 start");
     try {
         System.out.println("Method 1 execute");
         Thread.sleep(3000);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     System.out.println("Method 1 end");
 }

 public static synchronized void method2(){
     System.out.println("Method 2 start");
     try {
         System.out.println("Method 2 execute");
         Thread.sleep(1000);
     } catch (InterruptedException e) {
         e.printStackTrace();
     }
     System.out.println("Method 2 end");
 }

 public static void main(String[] args) {
     final SynchronizedTest test = new SynchronizedTest();
     final SynchronizedTest test2 = new SynchronizedTest();

     new Thread(new Runnable() {
         @Override
         public void run() {
             test.method1();
         }
     }).start();

     new Thread(new Runnable() {
         @Override
         public void run() {
             test2.method2();
         }
     }).start();
 }
}

执行结果如下,对静态方法的同步本质上是对类的同步(静态方法本质上是属于类的方法,而不是对象上的方法),所以即使test和test2属于不同的对象,但是它们都属于SynchronizedTest类的实例,所以也只能顺序的执行method1和method2,不能并发执行。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

synchronized同步代码块

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了。

public class SynchronizedTest {

    public void method1(){
        System.out.println("Method 1 start");
        try {
            synchronized (this) {
                System.out.println("Method 1 execute");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            synchronized (this) {
                System.out.println("Method 2 execute");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

执行结果如下,虽然线程1和线程2都进入了对应的方法开始执行,但是线程2在进入同步块之前,需要等待线程1中同步块执行完成。

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end

Synchronized的实现原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由monitorenter和monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED 标志来隐式实现的。

反编译

我们先来看一下下面这段synchronized同步方法和同步代码块两种使用形式的代码:

public class SynchronizedTest {

    public synchronized void doSth(){
        System.out.println("Hello World");
    }

    public void doSth1(){
        synchronized (SynchronizedTest.class){
            System.out.println("Hello World");
        }
    }
}

使用javap来反编译以上代码,结果如下(部分无用信息过滤掉了):

public synchronized void doSth();
descriptor: ()V
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           #3                  // String Hello World
     5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return

public void doSth1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
  stack=2, locals=3, args_size=1
     0: ldc           #5                  // class com/hollis/SynchronizedTest
     2: dup
     3: astore_1
     4: monitorenter
     5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
     8: ldc           #3                  // String Hello World
    10: invokevirtual #4                  // 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

反编译后,我们可以看到Java编译器为我们生成的字节码。在对于doSth和doSth1的处理上稍有不同。也就是说。JVM对于同步方法和同步代码块的处理方式不同。

对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步。 对于同步代码块,JVM采用monitorenter、monitorexit两个指令来实现同步。关于这部分内容,在JVM规范中也可以找到相关的描述。


同步方法

The Java® Virtual Machine Specification中有关于方法级同步的介绍:

Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

主要说的是: 方法级的同步是隐式的。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当某个线程要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。


同步代码块

同步代码块使用monitorenter和monitorexit两个指令实现。 The Java® Virtual Machine Specification 中有关于这两个指令的介绍:

monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

大致内容如下: 可以把执行monitorenter指令理解为加锁,执行monitorexit理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。


总结

同步方法通过ACC_SYNCHRONIZED关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。

同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得所锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。

每个对象自身维护这一个被加锁次数的计数器,当计数器数字为0时表示可以被任意线程获得锁。当计数器不为0时,只有获得锁的线程才能再次获得锁。即可重入锁。

🐶 您的支持将鼓励我继续创作 🐶
-------------本文结束感谢您的阅读-------------