Java synchronized之类锁/对象锁

来源:互联网 发布:java 二进制上传文件 编辑:程序博客网 时间:2024/06/06 19:31

  • 前言
  • synchronized的锁粒度介绍
    • synchronized 的用法举例
      • 类锁的场景举例
      • 对象锁的场景举例
    • 类锁和对象锁在使用方法上的区别
    • 类锁和对象锁的QA
  • synchronized对于类锁和对象锁的解析
    • 对象锁互斥原理
    • 类锁原理及为何类锁完全互斥
  • synchronized 的内存可见性

前言

    之前看到过一篇文章,写的就是Java关键字synchronized的类锁和对象锁,今天想重温一下无奈发现文章已经搜索不到,百度之大部分都是重复的那么几篇文章,甚至也仅仅讲了对象锁没有讲解类锁。于是重写一篇博客介绍 synchronized 类锁 对象锁。 

Java原生提供了 synchronized 关键字用于多线程编程,但往往入门使用者在发现使用情况与预期有差别,可阅读此文章。

Java的 synchronized 锁的是对象,也只锁对象: 对象锁是基于对堆内存内对象的头部加锁信息; 类锁是基于对类对应的 java.lang.Class对象加锁信息; 特别的, synchronized(this) 是对this所对应的对象加锁。

Java 提供 synchronized 关键字,在语言层面上做出支持。JDK实现上还有很多其它的实现,例如: ReentrantLock

synchronized的锁粒度介绍

synchronized 的用法举例

synchronized 的锁可作用于Java方法,或者是一个代码块。无论何种用法,所起到的作用仅限于 类锁/对象锁

类锁的场景举例

① 当synchronized 修饰一个使用了 static 关键字修饰的方法时:

public static synchronized void staticA();

② 当synchronized 修饰一个 class 的方法块时:

synchronized(Object.class) {    // do something}

对象锁的场景举例

① 当synchronized 修饰一个不使用了 static 关键字修饰的方法时:

public synchronized void noneStaticA();

② 当synchronized 修饰关键字 this 的方法块时:

synchronized(this) {    // do something}

③ 当synchronized 修饰一个对象 xxx 的方法块时:

synchronized(xxx) {    // do something}

类锁和对象锁在使用方法上的区别

当使用了对象锁之后,除了获得当前对象的对象锁的线程,其它线程对当前对象的所有使用对象锁的语句的访问受到阻塞,但是对非使用对象锁的语句的访问不受影响

当使用了类锁之后,除了当前线程外,其它线程对当前类的所有 类方法的访问受到阻塞,其它的静态方法(没有使用类锁的静态方法)的访问不受影响

特别的,可以使用 synchronized(xxx) 代码块 语法将一个无用的对象xxx作为一把锁。 这个时候的”对象锁”是针对于xxx对象的内部而言, 对于使用对象xxx作为锁的方法块来说,不管是使用的类锁还是对象锁都互不影响。

类锁和对象锁作用域不同,两者互不影响。

类锁和对象锁的Q&A

下述问题都是针对于此段伪代码片段进行:

class Sync {    public synchronized void noneStaticA();    public synchronized void noneStaticB();    public void noneStaticC();    public static synchronized void staticA();    public static synchronized void staticB();    public static void staticC();}    

Q1: 如代码片段所示,多线程环境中对象 Sync x 和 对象 Sync y 哪些语句可以同时执行:
A. x.noneStaticA() 和 x.noneStaticA();
B. x.noneStaticA() 和 x.noneStaticB();
C. x.noneStaticA() 和 y.noneStaticA();
D. x.noneStaticA() 和 x.noneStaticC();

A1: C、D.
解析: 如上皆为对象锁,单个对象内所有对象锁互互斥。而对象锁的粒度为单个对象, x对象的对象锁不影响y对象的对象锁。对象锁仅针对使用了对象锁的语句生效。


Q2: 如代码片段所示,多线程环境中对象 Sync x类 Sync 哪些语句可以同时执行:
A. x.noneStaticA() 和 Sync.staticA();
B. x.noneStaticA() 和 Sync.staticC();
C. x.noneStaticC() 和 Sync.staticA();
D. Sync.staticA() 和 Sync.staticA();
E. Sync.staticA() 和 Sync.staticB();

A2: A、B、C
解析:类锁与对象锁作用粒度不一,互不影响。对象锁与类静态方法之间无锁冲突。类锁与对象方法也没有锁冲突。类锁的作用域为这个类所有的类锁。


Q3:对于对象 Sync x 和 对象 Sync y哪些语句可以同时执行?

// 在类 Sync 里面增加如下代码:public void foo1() {    synchronized(this) {        // ...    }}// 另一个类 SyncExecute 的如此写法:public void foo2() {    // ...    synchronized(x) {         // ...    }    // ...}public void foo3() {    // ...    synchronized(Sync.class) {         // ...    }    // ...}

A3: 上述是一个复杂环境, 已知对象锁与类锁之间互不影响, 因此单独分析对象锁和类锁即可。

x.noneStaticA() 与 x.noneStaticB() 与 x.foo1() 三者使用的都是对象锁, 对同一对象 x而言是互斥的,而在SyncExecute 中的 foo2() 中也使用了对对象x的对象锁,因此 foo2() 与x里面的三个使用了对象锁的方法都是互斥的。(但是当使用的Sync的对象不是x而是其它的例如y/z的时候,他们之间完全是可以正常运行的)。

Sync.staticA() 与 Sync.staticB() 与 SyncExecute 中的 foo3() 都是使用的类锁,因此无论 new 了多少个 Sync对象他们都是互斥的,会竞争类锁。

synchronized对于类锁和对象锁的解析

对象锁互斥原理

可优先阅读该篇文章田守枝:2.1 对象的内存布局

一个被JVM创建的对象存在于JVM中,不仅仅包含了对象的实例数据,还包含对象头(Header)对齐填充(Padding)。 其中对象头(Header)里面就包含了当前对象是否有锁的信息。

如果对磁盘文件系统了解的同学就会知道,磁盘上储存的文件数据依赖于文件系统,不同的文件系统对于文件的存储数据结构可能不一样,但是大都包含如下特点:文件数据块单独储存,其它内容(如文件名、所有权信息、创建时间等)储存位置是与数据块逻辑隔离的。

因此,不管是 synchronized修饰的实例方法,还是synchronized代码块修饰的this关键字, 还是synchronized修饰的一个具体的对象,语言层面访问该对象时都会检查头部的锁信息,发现有锁了之后就会开始互斥逻辑。

同时,这也解释了为什么不同对象的对象锁之间为何互不影响: 因为对象锁的原理是基于单个对象的头部的锁信息。

synchronized 在锁的实现上相对复杂,存在着不同锁类型的切换升级。如有有兴趣可以阅读这篇文章:http://www.jianshu.com/p/5dbb07c8d5d5 。(至于synchronized在锁的抢占上目前暂未发现一篇详细介绍的文章。例如 ReentrantLock是基于Java关键字volatile和CPU的CAS机制来实现的,若有知晓可在留言区告知一二

类锁原理及为何类锁完全互斥

可优先阅读该篇文章图解Java类加载机制

想获得一个Java的对象,则需要先获得Java的一个类,这便是Java的类加载。类加载完毕之后的类代码储存在JVM的一块单独区域。一个类可能被加载或者卸载多次,但是任意一个时刻JVM里面只存在一个类的数据区域。阅读谈谈Java虚拟机——Class文件结构知晓这篇数据区域的数据结构

同时,JVM在装载完毕一个类的时候,还会给该类生成一个 java.lang.Class 的对象,由 类数据区里面的该类的this_class字段指向这个Class对象。 从而,类锁的实现原理可以转化为对象锁的原理 — 在对应的Class对象上加对象锁即可

synchronized 的内存可见性

需要特别注意的事是, 根据JMM的规范, synchronized 块里面的对象, 具有内存可见性。即:

  1. A线程在释放synchronized锁之前,会将线程内存中的共享变量回写回主存。
  2. B线程在获取synchronized锁之后,会清空线程内部涉及到的共享变量, 再由主存中读取。

参考文章: https://www.ibm.com/developerworks/library/j-jtp03304/index.html
When a thread exits a synchronized block as part of releasing the associated monitor, the JMM requires that the local processor cache be flushed to main memory.