Synchronized关键字算是Java的元老级锁了,一开始它撑起了Java的同步任务,其用法简单粗暴容易上手。但是有些与它相关的知识点还是需要我们开发者去深入掌握的。比如,我们都知道通过Synchronized锁来实现互斥功能,可以用在方法或者代码块上,那么不同用法都是怎么实现的,以及都经历了了哪些优化等等问题都需要我们扎实的理解。

1.基本用法

通常我们可以把Synchronized用在一个方法或者代码块里,方法又有普通方法或者静态方法。

对于普通同步方法,锁是当前实例对象,也就是this

public class TestSyn{
  private int i=0;
  public synchronized void incr(){
    i++;
  }
}

对于静态同步方法,锁是Class对象

public class TestSyn{
  private static int i=0;
  public static synchronized void incr(){
    i++;
  }
}  

对于同步代码块,锁是同步代码块里的对象

public class TestSyn{
  private  int i=0;
  Object o = new Object();
  public  void incr(){
    synchronized(o){
        i++;
    }
  }
}

2.实现原理

在JVM规范中介绍了synchronized的实现原理,JVM基于进入和退出Monitor对
象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,通过一个方法标志(flag) ACC_SYNCHRONIZED来实现的。

2.1 同步代码块的实现

monitorenter 和 monitorexit

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.monitorenter (参考来源)

下面看下JVM规范里对moniterenter 和 monitorexit的介绍

Each object has a monitor associated with it. The thread that executes monitorenter gains ownership of the monitor associated with objectref. If another thread already owns the monitor associated with objectref, the current thread waits until the object is unlocked,

每个对象都有一个监视器(Moniter)与它相关联,执行moniterenter指令的线程将获得与objectref关联的监视器的所有权,如果另一个线程已经拥有与objectref关联的监视器,则当前线程将等待直到对象被解锁为止。

A monitorenter instruction may be used with one or more monitorexit instructions to implement a synchronized statement in the Java programming language. The monitorenter and monitorexit instructions are not used in the implementation of synchronized methods

重点来了,上面这段介绍了两点:

  • 通过monitorenter和monitorexit指令来实现Java语言的同步代码块(后面有代码示例)
  • monitorenter和monitorexit指令没有被用在同步方法上!!!

2.2 同步方法的实现

先看下JVM规范里怎么说的
https://docs.oracle.com/javase/specs/jvms/se6/html/Compiling.doc.html#6530 (参考来源)

A synchronized method is not normally implemented using monitorenter and monitorexit. Rather, it is simply distinguished in the runtime constant pool by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the current thread acquires a monitor, invokes the method itself, and releases the monitor whether the method invocation completes normally or abruptly.

上面这段话主要讲了几点:

  • 同步方法的实现不是基于monitorenter和monitorexit指令来实现的
  • 在运行时常量池里通过ACC_SYNCHRONIZED来区分是否是同步方法,方法执行时会检查该标志
  • 当一个方法有这个标志的时候,进入的线程首先需要获得监视器才能执行该方法
  • 方法结束或者抛异常时会释放监视器
public class TestSyn {

    private int i=0;
    // 同步方法
    public synchronized void incer(){
        i++;
    }
    // 同步代码块
    public  void decr(){
        synchronized (this) {
            i--;
        }
    }
}

可以通过反编译字节码来查看底层是怎么实现的

// 得到字节码
javac TestSyn.java
// 反编译字节码
javap -v TestSyn.class

同步代码块的反编译结果如下:

同步方法的反编译结果如下:

3.锁升级

3.1 Java对象头介绍

对象的内存布局

在我们常见的HotSpot虚拟机中对象由三部分组成,分别是对象头,实例数据,以及对齐填充位。其中对象头是跟锁信息相关的部分,在对象头里会存储该对象运行时数据,包括哈希吗,GC分代年龄,锁状态(无锁,偏向锁,轻量级锁,重量级锁),是否偏向锁,偏向线程ID等信息。
存储上述这些的区域叫做Mark Word(标记词),除了这部分对象头还有一部分区域用来存储类型指针,可以通过该类型指针来定位对象的元数据信息。下面重点看下,对象头的内存布局,因为这部分是跟我们这次相关的。

对象在内存中的表示如下图:

对象头的结构表示如下图:

mark word的表示如下图:

3.2 什么是锁升级

下面举个抢茅坑的例子来解释一下锁升级过程。

当只有一个线程访问时叫做偏向锁

假设我们每个厕所都有一把钥匙,要想使用厕所首先必须得获得锁。某天上午员工甲急急忙忙的打完卡上厕所了,并在厕所门上贴了 “工号007使用中”的标签,说明目前被工号007(相当于线程id)的员工占用呢,他再次向进入的时候只要上面的标签还显示工号007,他自己可以随便进入,不需要再次上锁了,有点偏向工号007员工的意思,所以这叫偏向锁。

发生竞争的时候升级成轻量级锁 (自旋等待)

员工甲正在使用厕所的时候,又来了两个人想用厕所,但发现厕所被人使用着呢,无法获得锁。所以只能在外面等着甲出来,他们等的过程叫做“自旋”,这个叫做轻量级锁。那么又有一个问题,当甲出来之后正等着的那两个人谁活得锁呢?有两种方式,按到达的顺序来排队或者不排队,这两种都可以实现,前者叫做公平锁,后者叫做非公平锁。

自旋等待没结果的时候升级成重量级锁

但那两个人自旋一段时间之后发现甲还没出来(JDK1.6规定为10次),一直这么等也不是个法子啊,所以打算向上升级,找厕所管理员(操作系统)反馈,升级成了重量级锁了

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。


锁升级过程中mark word的变化如下:

偏向锁
偏向锁也是JDK 1.6中引入的一项锁优化, 引入它是为了优化在没有锁竞争场景下的锁消除。比如一段同步代码一直是由单个线程调用,在这种场景下就没必要使用同步锁了,这里指的同步锁不是指synchronized,而是说没不要到操作系统层面的互斥量了。
偏向锁的偏向是指该同步代码会一直偏向第一个调用它的线程,直到有别的线程过来竞争这把锁,在第一次调用同步代码并获得锁时会在对象头和栈帧锁记录行(Lock Record)里存储偏向线程Id,该线程在此进入的时候就不需要重新申请锁了。只需检测对象头的Mark Word里是否存储着指向该线程的ID即可。

直到又有线程来竞争这把锁的时候偏向锁会撤销偏向。

轻量级锁

轻量级锁是JDK 1.6之中加入的新型锁机制, 它名字中的“轻量级”是相对于使用操作系统
互斥量来实现的传统锁而言的, 因此传统的锁机制就称为“重量级”锁。 它并不是用来代替重量级锁的, 它的本意是在统的重量级锁使用操作系统互斥量产生的性能消耗。

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁.一直原地自旋,如果自旋数达到10次了则升级为重量级锁。

重量级锁
竞争的线程自旋一段时间未能获取锁之后会升级为重量级锁,这个时候锁的获取与释放都会由操作系统来分配了,如果持有锁的线程释放锁之后操作系统会唤醒所有阻塞的哪些线程,并进入新一轮的争抢模式,需要注意的是这些阻塞的线程没有获得锁的优先级,也就是说synchronized锁是非公平的。除此之外synchronized对中断操作也是无感的,不会因为被中断而放弃阻塞等待,它要么得到锁要么一直阻塞。

Synchronized知道这些就可以了的更多相关文章

  1. 面试必问的Synchronized知道这些就可以了

    Synchronized关键字算是Java的元老级锁了,一开始它撑起了Java的同步任务,其用法简单粗暴容易上手.但是有些与它相关的知识点还是需要我们开发者去深入掌握的.比如,我们都知道通过Synch ...

  2. Java多线程4:synchronized锁机制

    脏读 一个常见的概念.在多线程中,难免会出现在多个线程中对同一个对象的实例变量进行并发访问的情况,如果不做正确的同步处理,那么产生的后果就是"脏读",也就是取到的数据其实是被更改过 ...

  3. Java并发编程:synchronized

    Java并发编程:synchronized 虽然多线程编程极大地提高了效率,但是也会带来一定的隐患.比如说两个线程同时往一个数据库表中插入不重复的数据,就可能会导致数据库中插入了相同的数据.今天我们就 ...

  4. Java多线程初学者指南(10):使用Synchronized关键字同步类方法

    要想解决“脏数据”的问题,最简单的方法就是使用synchronized关键字来使run方法同步,代码如下: public synchronized void run() { ... } 从上面的代码可 ...

  5. synchronized关键字简介 多线程中篇(十一)

    前面说过,Java对象都有与之关联的一个内部锁和监视器 内部锁是一种排它锁,能够保障原子性.可见性.有序性 从Java语言层面上说,内部锁使用synchronized关键字实现 synchronize ...

  6. 【转】Java并发编程:synchronized

    一.什么时候会出现线程安全问题? 在单线程中不会出现线程安全问题,而在多线程编程中,有可能会出现同时访问同一个资源的情况,这种资源可以是各种类型的资源:一个变量.一个对象.一个文件.一个数据库表等,而 ...

  7. Java并发编程(四)synchronized

    一.synchronized同步方法或者同步块 在了解synchronized关键字的使用方法之前,我们先来看一个概念:互斥锁,顾名思义:能到达到互斥访问目的的锁. 举个简单的例子:如果对临界资源加上 ...

  8. Java并发—synchronized关键字

    synchronized关键字的作用是线程同步,而线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏. synchronized用法 1. 在需要同步的方法的方法签名中加入synchro ...

  9. 使用Synchronized关键字同步类方法

    要想解决“脏数据”的问题,最简单的方法就是使用synchronized关键字来使run方法同步,代码如下: public synchronized void run() { } 从上面的代码可以看出, ...

随机推荐

  1. jquery前端问题随记

    1.图片名称不变,但要求刷新,web页面不重新加载渲染,在url地址后面加t=时间戳 js脚本 img.src=url+"?t="+Math.random() 如果是jsp页面,要 ...

  2. struts2 JSON 插件的使用

    1. 导入包: json-lib-2.3-jdk15.jar struts2-json-plugin-2.3.15.3.jar 2. 在struts.xml中修改配置如下: <package n ...

  3. filter和filter_by 的区别

  4. centos swap分区

    swap分区         通常memory是机器的物理内存,读写速度低于cpu一个量级,但是高于磁盘不止一个量级.所以,程序和数据如果在内存的话,会有非常快的读写速度.但是,内存的造价是要高于磁盘 ...

  5. shell中条件判断if中的-z到-d

    shell中条件判断if中的-z到-d的意思 [ -a FILE ] 如果 FILE 存在则为真. [ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真.[ -c FILE ] 如果 ...

  6. H5实现手写功能

    html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf- ...

  7. 'EF.Utility.CS.ttinclude' returned a null or empty string.

    需要安装https://www.microsoft.com/en-us/download/details.aspx?id=40762

  8. C# webapi

    HttpContextBase context = (HttpContextBase)Request.Properties["MS_HttpContext"];//获取传统cont ...

  9. 什么是JavaScript循环结构?

    ㈠什么是循环结构 ⑴什么是循环? 反复一遍又一遍的做着相同(相似)的事情 ⑵循环中的两大要素 ①循环条件:什么时候开始,什么时候结束 ②循环操作:循环体,循环过程中,干了什么 ㈡循环结构—while循 ...

  10. 4.JSP内置对象

    JSP内置对象,JSP提供了由容器实现和管理的内置对象,也可以称之为隐含对象,这些内置对象不需要通过 JSP页面编写来实例化,在所有的JSP页面中都可以直接使用,它起到了简化页面的作用. 在JSP中一 ...