`
IXHONG
  • 浏览: 450092 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

全面剖析synchronized

阅读更多

剖析 Synchronized


Synchronized 介绍

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

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

上述都是对象级别的锁,当一个线程访问对象中的同步方法时,会获取到对象级别的锁,由于 Synchronized 内部是可重入的互斥锁,所以线程可再次重入用 Synchronized 修饰的方法,但当其它线程执行同一个对象的带有 Synchronized 的方法时,会被阻塞,即使和以持有对象锁的线程执行的相同对象的不同 Synchronized 方法。因为锁是对象级别的。比如线程 A、B。对象 Foo 有同步方法 M、N。线程 A 首先执行同步方法 M 时就会获取对象锁,此时 B 不能执行同一把对象锁修饰的方法 M、N。除非 A 释放锁。又因为锁是可重入的,所以 A 可以继续执行 M,N 方法。可重入锁一定程度上避免了死锁的问题,内部是关联一个计数器,加一次锁计数器值加一,为零时释放锁。

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

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

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

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

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


monitor Object 设计模式

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

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

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

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

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

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

详情可参考这篇文章

Monitor Lock 和 Monitor Condition 其实就是我们常说的互斥锁加条件变量实现同步操作,Java 内置 wait() 和 notify()/notifyAll() 也是这个原理。可参考 
Java中的synchronized、Object.wait()、Object.notify()/notifyAll()原理

下面这篇是使用 C++11 的互斥锁加条件变量,可能会看到的更清晰 
c++11 实现半同步半异步线程池


Synchronized 实现

在第一部分我们说到了 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 如何实现一系列同步操作的。代码:

 

 

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
      ...

 

从上面可以看出对方法块同步是通过 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的所有权。

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

 
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 访问标志是否设置,如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法完成时释放锁。


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

参考:

探索 Java 同步机制

synchronized、锁、多线程同步的原理是咋样的

 

深入JVM锁机制1-synchronized http://wely.iteye.com/blog/2331899

0
1
分享到:
评论
2 楼 IXHONG 2017-03-03  
synchronized里面的都会执行
herman_liu76 写道
研究的很细

notify()后面如果有代码会执行吗?

1 楼 herman_liu76 2017-03-01  
研究的很细

notify()后面如果有代码会执行吗?

相关推荐

    Synchronized Phasor Measurements and Their Applications

    同步相量测量技术(Synchronized Phasor Measurements,简称SPM)是电力系统监测、分析与控制领域的一项关键技术。该技术通过高精度的时间同步机制(如全球定位系统GPS)实现了电网中各点电压和电流相量的精确测量,...

    Java并发编程:Synchronized关键字深度解析

    本文深入探讨了Java中用于解决并发...通过对synchronized关键字的深入分析,本文为Java开发者提供了对并发编程中关键同步工具的全面理解,特别是在高并发场景下如何有效使用synchronized以确保线程安全和提高程序性能。

    深析Synchronized关键字(小白慎入,深入jvm源码,两万字长文)

    【描述】:本文将全面剖析Java中的Synchronized关键字,从基础概念到高级机制,包括其在JVM中的实现原理和锁升级过程。我们将探讨它的使用方式,特点以及如何在并发编程中发挥作用。 ### 一、Synchronized基础 1.1...

    今天会是有Offer的一天么:面试时不要再问我CAS和Synchronized的区别了

    2. **安全性**:synchronized提供更全面的锁保护,可以防止死锁,并确保线程安全,而CAS只保证单个变量的原子性。 3. **使用难度**:synchronized使用简单,而CAS需要手动处理并发冲突,可能需要额外的版本控制。 ...

    IBM thread dump文件分析工具

    5. **监视**:线程进入synchronized方法或同步块,等待获取监视器锁。 6. **终止**:线程执行完毕或异常结束。 TMDA可以帮助我们识别这些状态,找出可能的死锁、线程阻塞或其他性能瓶颈。 其次,TMDA提供了丰富的...

    Java线程Dump分析工具jstack解析及使用场景

    使用jstack进行线程分析时,还需要结合其他工具,如jconsole、jvisualvm等,以便全面理解应用程序的性能状况。同时,分析结果应与代码逻辑相结合,找出可能的性能瓶颈或异常情况,进而优化代码或调整系统配置。 总...

    数据结构与算法分析(java版内含源代码)

    数据结构与算法分析是计算机科学中的核心课程,它关乎到程序设计的效率和问题解决的...总之,《数据结构与算法分析(Java版)》是一本全面且实用的教材,无论对于初学者还是经验丰富的开发者,都是不可多得的学习资料。

    MF00904-Java行为分析系统源码.zip

    6. **代码覆盖率工具**: 行为分析系统可能会集成代码覆盖率工具,用于评估测试的全面性,确保所有代码路径都被充分测试。 7. **性能基准测试**: 为了对比优化前后的性能,系统可能包含基准测试框架,通过模拟真实...

    java超全面的面试总结

    - 线程同步:synchronized关键字,volatile,Lock接口及其子类。 - 死锁与活锁:理解死锁的概念,避免死锁的策略。 5. **IO/NIO**: - 字节流与字符流:InputStream、OutputStream、Reader、Writer的使用。 - ...

    JVM-GC全面知识系统详解

    GC通过可达性分析算法判断对象是否存活,然后进行垃圾清理。常见的垃圾收集器有Serial、Parallel、CMS、G1等,每种收集器有其特定的优缺点和适用场景。 类加载与字节码技术是理解JVM运行原理的关键。类加载器负责将...

    Java理论与实践: 一个有缺陷的微基准的剖析

    为了全面评估,应该记录并分析整个系统的资源使用情况。 最后,基准测试的结果解读也至关重要。在 `SyncLockTest` 中,结果被简单地以运行时间比例呈现,但忽略了可能的误差来源和统计显著性。性能测试应该有足够的...

    Java中高级核心知识全面解析

    理解线程同步机制,如synchronized、volatile、Lock接口以及原子类(AtomicInteger、AtomicReference等)的应用至关重要。此外,学习如何避免死锁、活锁和饥饿现象也是多线程编程的关键。 2. **集合框架**:深入...

    java教程(易理解全面)

    以上内容只是Java教程(全面详细)的一部分概述,完整的教程将通过详细的示例代码和案例分析,帮助学习者逐步掌握这些知识点,并通过实践提升编程能力。在学习过程中,建议结合实际项目或小实验,加深对Java语言的...

    比较全面的各公司JAVA面试题

    4. **多线程**:Java并发编程是重要考点,包括线程同步(synchronized、Lock)、线程池、死锁、活锁、线程安全的数据结构等。 5. **IO/NIO/BIO**:了解Java的输入输出系统,对比BIO、NIO和AIO的不同,并掌握文件...

    JAVA面试问题全面集合

    这份"JAVA面试问题全面集合"的资源包含三个PDF文档,分别命名为1.670道.pdf、2.630道.pdf和3.358道.pdf,它们很可能包含了广泛的Java编程、设计模式、并发处理、框架应用等方面的问题,旨在帮助求职者提升面试技巧和...

    java面试宝典 吐血推荐,很全面

    - **知识点**: 需要具体代码示例来分析,此处缺少具体的代码示例。 **17. 请说出作用域public,private,protected,以及不写时的区别** - **知识点**: `public`表示公有,`private`表示私有,`protected`表示受...

    java面试 初中高级适用 最详细全面 私人珍藏

    2. **进阶特性**:随着面试级别的提升,会涉及到更深入的知识,例如反射、动态代理、枚举、注解、IO流、NIO、多线程、线程池、并发工具类(如synchronized、volatile、Lock、Semaphore、CountDownLatch等)、垃圾...

    java基础全程剖析

    本教程将全面覆盖Java的基础知识,使学习者能够轻松掌握这一强大的编程工具。 一、Java简介 Java是由Sun Microsystems(现已被Oracle公司收购)于1995年推出的面向对象的编程语言。它的设计目标是具有平台独立性、...

    JAVA 笔试面试 宝典 十分全面

    总的来说,"JAVA笔试面试宝典"是一个全面的学习资源,它涵盖了从基础到高级的Java知识,以及针对实际工作场景的面试技巧。通过深入学习和实践,不仅可以提高Java编程技能,还能提升在竞争激烈的就业市场中的竞争力。...

Global site tag (gtag.js) - Google Analytics