当前位置:首页 > 开发教程 > 软件工程 >

Java 锁机机制——浅析 Synchronized

时间:2016-08-12 15:53 来源: 作者: 收藏

首先,楼主想说,当你接手一个项目,刚想要真机爽一把,突然给你报错,你当时心里那真是一万头草泥马飞奔啊就如同上面这个错误,楼主的心,是一颗强大的心,一看这种情况,楼主想都不想,管他什么鬼,,管他说什么,先点fix再说,哈哈哈哈!结果点完fix,还是

剖析 Synchronized


Synchronized 介绍

Synchronized 方面的文章网上有很多了。它主要是用来进行同步操作。也被称为重量级的锁,它的同步包括:

  • 对于普通方法同步,锁是当前实例对象
  • 对于静态方法同步,锁是当前类的 Class 对象
  • 对于方法块同步,锁是 Synchronized 括号里的对象

那么如何理解锁是“对象”

Java 编程语言中号称一切皆对象。当我们 new 一个对象的时候 JVM 会给 heap 中分配对象。如下图:

Java 锁机机制——浅析 Synchronized

对象头 这个头包括两个部分,第一部分用于存储自身运行时的数据例如GC标志位、哈希码、锁状态 等信息。第二部分存放指向方法区类静态数据的指针。锁状态 就是用来同步操作的 bit 位。因为锁信息是存储在对象上的,所以就不难理解 锁是对象 这句话了。

那么 Java 为什么要将 锁 内置到对象中呢?

这要从 monitor Object 设计模式说起:


monitor Object 设计模式

问题描述:
我们在开发并发的应用时,经常需要设计这样的对象,该对象的方法会在多线程的环境下被调用,而这些方法的执行都会改变该对象本身的状态。为了防止竞争条件 (race condition) 的出现,对于这类对象的设计,需要考虑解决以下问题:

  • 在任一时间内,只有唯一的公共的成员方法,被唯一的线程所执行。
  • 对于对象的调用者来说,如果总是需要在调用方法之前进行拿锁,而在调用方法之后进行放锁,这将会使并发应用编程变得更加困难。
  • 如果一个对象的方法执行过程中,由于某些条件不能满足而阻塞,应该允许其它的客户端线程的方法调用可以访问该对象。

我们使用 Monitor Object 设计模式来解决这类问题:将被客户线程并发访问的对象定义为一个 monitor 对象。客户线程仅仅通过 monitor 对象的同步方法才能使用 monitor 对象定义的服务。为了防止陷入竞争条件,在任一时刻只能有一个同步方法被执行。每一个 monitor 对象包含一个 monitor 锁,被同步方法用于串行访问对象的行为和状态。此外,同步方法可以根据一个或多个与 monitor 对象相关的 monitor conditions 来决定在何种情况下挂起或恢复他们的执行。

来看看 monitor object 设计模式执行时序图:

Java 锁机机制——浅析 Synchronized

其实, monitor object 设计模式执行时序图中的红线部分 Monitor Object、Monitor Lock、Monitor Condition 三者就是 Java Object!! Java 将该模式内置到语言层面,对象加 Synchronized 关键字,就能确保任何对它的方法请求的同步被透明的进行,而不需要调用者的介入。

这也就是为什么 Java 所有对象的基类 Object 中会有 wait()、notify()、notifyAll() 方法了。

详情可参考这篇文章


Synchronized 实现

在第一部分我们说到了 Java 对象头,大致包含如下:

Java 锁机机制——浅析 Synchronized

其中用 2bit 来标记锁。

锁种类如下(不同 bit 值代表不同):

Java 锁机机制——浅析 Synchronized

按照锁的重量从小到达来排序分别是:偏向锁 -> 轻量锁 ->重量锁。

其中重量锁就是操作系统的互斥锁来实现的,轻量锁和偏向锁是 JDK 1.6 引入的,为什么引入这么多种类的锁,原因是为了某些情况下没有必要加重量级别的锁,如没有多线程竞争,减少传统的重量级锁产生的性能消耗。这几种锁的区别可以参考 这里

当多线程访问时,就是通过对象头中的锁来同步的。访问过程如下图:

Java 锁机机制——浅析 Synchronized

上图简单描述了这个过程,当多个线程同时访问一段同步代码时,首先会进入 Entry Set 这个集合中,当线程获取到对象的监视锁时,进入 The Owner 运行代码,若调用 wait() 方法则让出监视锁进入 Wait Set 集合中。可再次获取锁进入执行区,执行完毕释放锁交给其它线程后退出。


上图其实是 Java 线程运行状态的一个简单版本,看下线程执行状态图:

Java 锁机机制——浅析 Synchronized

一个常见的问题是 wait()、sleep()、yield() 方法的区别是什么?wait() 和 sleep()、yield() 最大的不同在于 wait() 会释放对象锁,而 sleep()、yield() 不会,sleep() 是让当前线程休眠,而 yield() 是让出当前 CPU。


那么 Synchronized 如何实现一系列同步操作的。代码:

public class LockTest {
    //对普通方法同步
    public synchronized void sayGoodbye() {
        System.out.println("say good bye");
    }
    //对静态方法同步
    public synchronized static void sayHi() {
        System.out.println("say hi");
    }
    //对方法块同步
    public void sayHello() {
        synchronized (LockTest.class) {
            System.out.println("say hello");
        }
    }

    public static void main(String[] args) {
        LockTest lockTest = new LockTest();
        lockTest.sayGoodbye();
        lockTest.sayHello();
        LockTest.sayHi();
    }
}

将这段代码通过 javap -c 反编译一下

Compiled from "LockTest.java"
public class Lock.LockTest {
  public Lock.LockTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void sayHello();
    Code:
       0: ldc           #2                  // class Lock/LockTest
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #4                  // String say hello
      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

  public synchronized void sayGoodbye();
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #6                  // String say good bye
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static synchronized void sayHi();
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #7                  // String say hi
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class Lock/LockTest
       3: dup
       4: invokespecial #8                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: invokevirtual #9                  // Method sayGoodbye:()V
      12: aload_1
      13: invokevirtual #10                 // Method sayHello:()V
      16: invokestatic  #11                 // Method sayHi:()V
      19: return
}

方法块同步

反编译出来的指令比较长,但比较清晰,首先看同步普通方法,重点关注 4、14 的指令

 public void sayHello();
      ...
      4: monitorenter
      ...
      14: monitorexit
      ...

从上面可以看出对方法块同步是通过 monitorentermonitorexit 两个比较重要的指令来实现的。来看下 Java 虚拟机规范是如何说的。

monitorenter:任何对象都有一个 monitor(这里 monitor 指的就是锁) 与之关联(规范上说,对象与其 monitor 之间的关系有很多实现,如 monitor 可以和对象一起创建销毁,也可以线程尝试获取对象的所有权时自动生成)。当且仅当一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取 objectref 所对应的 monitor 的所有权,那么:如果 objectref 的 monitor 的进入计数器为 0,那线程可以成功进入 monitor,以及将计数器值设置为 1。当前线程就是 monitor 的所有者。如果当前线程已经拥有 objectref 的 monitor 的所有权,那它可以重入这个 monitor,重入时需将进入计数器的值加 1。如果其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到 monitor 的进入计数器值变为 0 时,重新尝试获取 monitor 的所有权。

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

静态方法同步和方法块同步

public synchronized void sayGoodbye();
       ...
       5: invokevirtual #5                  // Method

public static synchronized void sayHi();
       ...
       5: invokevirtual #5                  // Method 

对静态方法同步和方法块同步并没有 monitor 相关指令,而是多了 invokevirtual 指令。invokevirtual 指令是用来调用实例方法,依据实例的类型进行分派。

Java 虚拟机规范上描述该指令:如果调用的是同步方法,那么与 objectref 相关的同步锁将会进入或者重入,就如同当前线程中执行了 monitorenter 指令一般。

代码块同步是通过 monitorenter 和 monitorexit 指令显示实现的,而方法级别的同步是隐式的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构(method_info structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否是同步方法。当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。


本文完,如有错误还望指出 :)


软件工程阅读排行

最新文章