本文共 6631 字,大约阅读时间需要 22 分钟。
Synchronized 方面的文章网上有很多了。它主要是用来进行同步操作。也被称为重量级的锁,它的同步包括:
上述都是对象级别的锁,当一个线程访问对象中的同步方法时,会获取到对象级别的锁,由于 Synchronized 内部是可重入的互斥锁,所以线程可再次重入用 Synchronized 修饰的方法,但当其它线程执行同一个对象的带有 Synchronized 的方法时,会被阻塞,即使和以持有对象锁的线程执行的相同对象的不同 Synchronized 方法。因为锁是对象级别的。比如线程 A、B。对象 Foo 有同步方法 M、N。线程 A 首先执行同步方法 M 时就会获取对象锁,此时 B 不能执行同一把对象锁修饰的方法 M、N。除非 A 释放锁。又因为锁是可重入的,所以 A 可以继续执行 M,N 方法。可重入锁一定程度上避免了死锁的问题,内部是关联一个计数器,加一次锁计数器值加一,为零时释放锁。
那么如何理解锁是“对象”。
编程语言中号称一切皆对象。当我们 new 一个对象的时候 JVM 会给 heap 中分配对象。如下图:
对象头 这个头包括两个部分,第一部分用于存储自身运行时的数据例如GC标志位、哈希码、锁状态 等信息。第二部分存放指向方法区类静态数据的指针。锁状态 就是用来同步操作的 bit 位。因为锁信息是存储在对象上的,所以就不难理解 锁是对象 这句话了。
那么 Java 为什么要将 锁 内置到对象中呢?
这要从 monitor Object 设计模式说起:
问题描述:
我们在开发并发的应用时,经常需要设计这样的对象,该对象的方法会在多线程的环境下被调用,而这些方法的执行都会改变该对象本身的状态。为了防止竞争条件 (race condition) 的出现,对于这类对象的设计,需要考虑解决以下问题:我们使用 Monitor Object 设计模式来解决这类问题:将被客户线程并发访问的对象定义为一个 monitor 对象。客户线程仅仅通过 monitor 对象的同步方法才能使用 monitor 对象定义的服务。为了防止陷入竞争条件,在任一时刻只能有一个同步方法被执行。每一个 monitor 对象包含一个 monitor 锁,被同步方法用于串行访问对象的行为和状态。此外,同步方法可以根据一个或多个与 monitor 对象相关的 monitor conditions 来决定在何种情况下挂起或恢复他们的执行。
来看看 monitor object 设计模式执行时序图:
其实, monitor object 设计模式执行时序图中的红线部分 Monitor Object、Monitor Lock、Monitor Condition 三者就是 Object!! Java 将该模式内置到语言层面,对象加 Synchronized 关键字,就能确保任何对它的方法请求的同步被透明的进行,而不需要调用者的介入。
这也就是为什么 Java 所有对象的基类 Object 中会有 wait()、notify()、notifyAll() 方法了。
详情可参考这篇
Monitor Lock 和 Monitor Condition 其实就是我们常说的互斥锁加条件变量实现同步操作,Java 内置 wait() 和 notify()/notifyAll() 也是这个原理。可参考
下面这篇是使用 11 的互斥锁加条件变量,可能会看到的更清晰
在第一部分我们说到了 Java 对象头,大致包含如下:
其中用 2bit 来标记锁。
锁种类如下(不同 bit 值代表不同):
按照锁的重量从小到达来排序分别是:偏向锁 -> 轻量锁 ->重量锁。
其中重量锁就是的互斥锁来实现的,轻量锁和偏向锁是 JDK 1.6 引入的,为什么引入这么多种类的锁,原因是为了某些情况下没有必要加重量级别的锁,如没有多线程竞争,减少传统的重量级锁产生的性能消耗。这几种锁的区别可以参考
当多线程访问时,就是通过对象头中的锁来同步的。访问过程如下图:
上图简单描述了这个过程,当多个线程同时访问一段同步代码时,首先会进入 Entry Set 这个集合中,当线程获取到对象的监视锁时,进入 The Owner 运行代码,若调用 wait() 方法则让出监视锁进入 Wait Set 集合中。可再次获取锁进入执行区,执行完毕释放锁交给其它线程后退出。
上图其实是 Java 线程运行状态的一个简单版本,看下线程执行状态图:
一个常见的问题是 wait()、sleep()、yield() 方法的区别是什么?wait() 和 sleep()、yield() 最大的不同在于 wait() 会释放对象锁,而 sleep()、yield() 不会,sleep() 是让当前线程休眠,而 yield() 是让出当前 CPU。
那么 Synchronized 如何实现一系列同步操作的。代码:
将这段代码通过 javap -c 反编译一下
反编译出来的指令比较长,但比较清晰,首先看同步普通方法,重点关注 4、14 的指令
从上面可以看出对方法块同步是通过 monitorenter 和 monitorexit 两个比较重要的指令来实现的。来看下 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的所有权。
对静态方法同步和方法块同步并没有 monitor 相关指令,而是多了 invokevirtual 指令。invokevirtual 指令是用来调用实例方法,依据实例的类型进行分派。
Java 虚拟机规范上描述该指令:如果调用的是同步方法,那么与 objectref 相关的同步锁将会进入或者重入,就如同当前线程中执行了 monitorenter 指令一般。
代码块同步是通过 monitorenter 和 monitorexit 指令显示实现的,而方法级别的同步是隐式的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构(method_info structure)中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否是同步方法。当调用方法时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。
本文完,如有错误还望指出 :)
参考:
原文链接:[http://wely.iteye.com/blog/2359593]