死磕 java同步系列之synchronized解析

问题

(1)synchronized的特性?

(2)synchronized的实现原理?

(3)synchronized是否可重入?

(4)synchronized是否是公平锁?

(5)synchronized的优化?

(6)synchronized的五种使用方式?

简介

synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成 monitorenter 和 monitorexit 字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。

实现原理

在学习Java内存模型的时候,我们介绍过两个指令:lock 和 unlock。

lock,锁定,作用于主内存的变量,它把主内存中的变量标识为一条线程独占状态。

unlock,解锁,作用于主内存的变量,它把锁定的变量释放出来,释放出来的变量才可以被其它线程锁定。

但是这两个指令并没有直接提供给用户使用,而是提供了两个更高层次的指令 monitorenter 和 monitorexit 来隐式地使用 lock 和 unlock 指令。

而 synchronized 就是使用 monitorenter 和 monitorexit 这两个指令来实现的。

根据JVM规范的要求,在执行monitorenter指令的时候,首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,就把锁的计数器加1,相应地,在执行monitorexit的时候会把计数器减1,当计数器减小为0时,锁就释放了。

我们还是来上一段代码,看看编译后的字节码长啥样来学习:

public class SynchronizedTest{
public static void sync(){
    synchronized(SynchronizedTest.class){
        synchronized(SynchronizedTest.class){
        }
    }
}
public static void main(String[] args){
      }
 }

我们这段代码很简单,只是简单地对SynchronizedTest.class对象加了两次synchronized,除此之外,啥也没干。

编译后的sync()方法的字节码指令如下,为了便于阅读,彤哥特意加上了注释:

// 加载常量池中的SynchronizedTest类对象到操作数栈中
0 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest>
// 复制栈顶元素
2 dup// 存储一个引用到本地变量0中,后面的0表示第几个变量
3 astore_0
// 调用monitorenter,它的参数变量0,也就是上面的SynchronizedTest类对象
4 monitorenter
// 再次加载常量池中的SynchronizedTest类对象到操作数栈中
5 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest>
// 复制栈顶元素
7 dup
// 存储一个引用到本地变量1中
8 astore_1
// 再次调用monitorenter,它的参数是变量1,也还是SynchronizedTest类对象
9 monitorenter
// 从本地变量表中加载第1个变量
10 aload_1
// 调用monitorexit解锁,它的参数是上面加载的变量1
11 monitorexit
// 跳到第20行
12 goto 20 (+8)
15 astore_2
16 aload_1
17 monitorexit
18 aload_2
19 athrow
// 从本地变量表中加载第0个变量
20 aload_0
// 调用monitorexit解锁,它的参数是上面加载的变量0
21 monitorexit
// 跳到第30行
22 goto 30 (+8)
25 astore_326 aload_0
27 monitorexit28 aload_329 athrow
// 方法返回,结束
30 return

按照彤哥的注释读起来,字节码比较简单,我们的synchronized锁定的是SynchronizedTest类对象,可以看到它从常量池中加载了两次SynchronizedTest类对象,分别存储在本地变量0和本地变量1中,解锁的时候正好是相反的顺序,先解锁变量1,再解锁变量0,实际上变量0和变量1指向的是同一个对象,所以synchronized是可重入的。

至于,被加锁的对象具体在对象头中是怎么存储的,彤哥这里就不细讲了,有兴趣的可以看看《Java并发编程的艺术》这本书。

原子性、可见性、有序性

前面讲解Java内存模型的时候我们说过内存模型主要就是用来解决缓存一致性的问题的,而缓存一致性主要包括原子性、可见性、有序性。

那么,synchronized关键字能否保证这三个特性呢?

还是回到Java内存模型上来,synchronized关键字底层是通过monitorenter和monitorexit实现的,而这两个指令又是通过lock和unlock来实现的。

而lock和unlock在Java内存模型中是必须满足下面四条规则的:

(1)一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一个线程执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能被解锁。

(2)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值;

(3)如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其它线程锁定的变量;

(4)对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作;

通过规则(1),我们知道对于lock和unlock之间的代码,同一时刻只允许一个线程访问,所以,synchronized是具有原子性的。

通过规则(1)(2)和(4),我们知道每次lock和unlock时都会从主内存加载变量或把变量刷新回主内存,而lock和unlock之间的变量(这里是指锁定的变量)是不会被其它线程修改的,所以,synchronized是具有可见性的。

通过规则(1)和(3),我们知道所有对变量的加锁都要排队进行,且其它线程不允许解锁当前线程锁定的对象,所以,synchronized是具有有序性的。

综上所述,synchronized是可以保证原子性、可见性和有序性的。

公平锁 VS 非公平锁

通过上面的学习,我们知道了synchronized的实现原理,并且它是可重入的,那么,它是否是公平锁呢?

直接上菜:

public class SynchronizedTest {
      public static void sync(String tips) {
          synchronized (SynchronizedTest.class) {
              System.out.println(tips);
              try {
          Thread.sleep(1000);
          } catch (InterruptedException e) {
              e.printStackTrace();
     }
   }
}
public static void main(String[] args) throws InterruptedException {
   new Thread(()->sync("线程1")).start();
   Thread.sleep(100);
   new Thread(()->sync("线程2")).start();
   Thread.sleep(100);
   new Thread(()->sync("线程3")).start();
   Thread.sleep(100);
   new Thread(()->sync("线程4")).start();
     }
}

在这段程序中,我们起了四个线程,且分别间隔100ms启动,每个线程里面打印一句话后等待1000ms,如果synchronized是公平锁,那么打印的结果应该依次是 线程1、2、3、4。

但是,实际运行的结果几乎不会出现上面的样子,所以,synchronized是一个非公平锁。

锁优化

Java在不断进化,同样地,Java中像synchronized这种古老的东西也在不断进化,比如ConcurrentHashMap在jdk7的时候还是使用ReentrantLock加锁的,在jdk8的时候已经换成了原生的synchronized了,可见synchronized有原生的支持,它的进化空间还是很大的。

那么,synchronized有哪些进化中的状态呢?

我们这里稍做一些简单地介绍:

(1)偏向锁,是指一段同步代码一直被一个线程访问,那么这个线程会自动获取锁,降低获取锁的代价。

(2)轻量级锁,是指当锁是偏向锁时,被另一个线程所访问,偏向锁会升级为轻量级锁,这个线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。

(3)重量级锁,是指当锁是轻量级锁时,当自旋的线程自旋了一定的次数后,还没有获取到锁,就会进入阻塞状态,该锁升级为重量级锁,重量级锁会使其他线程阻塞,性能降低。

总结

(1)synchronized在编译时会在同步块前后生成monitorenter和monitorexit字节码指令;

(2)monitorenter和monitorexit字节码指令需要一个引用类型的参数,基本类型不可以哦;

(3)monitorenter和monitorexit字节码指令更底层是使用Java内存模型的lock和unlock指令;

(4)synchronized是可重入锁;

(5)synchronized是非公平锁;

(6)synchronized可以同时保证原子性、可见性、有序性;

(7)synchronized有三种状态:偏向锁、轻量级锁、重量级锁;

彩蛋——synchronized的五种使用方式

通过上面的分析,我们知道synchronized是需要一个引用类型的参数的,而这个引用类型的参数在Java中其实可以分成三大类:类对象、实例对象、普通引用,使用方式分别如下:

public class SynchronizedTest2 {
    public static final Object lock = new Object();
    // 锁的是SynchronizedTest.class对象
    public static synchronized void sync1() {
    }
    public static void sync2() {
        // 锁的是SynchronizedTest.class对象
        synchronized (SynchronizedTest.class) {
     }
 }
    // 锁的是当前实例this
    public synchronized void sync3() {
 }
    public void sync4() {
      // 锁的是当前实例this
      synchronized (this){
     }
 }
     public void sync5() {
       // 锁的是指定对象lock
       synchronized (lock) {
         }
     }
}

总结

在方法上使用synchronized的时候要注意,会隐式传参,分为静态方法和非静态方法,静态方法上的隐式参数为当前类对象,非静态方法上的隐式参数为当前实例this。

另外,多个synchronized只有锁的是同一个对象,它们之间的代码才是同步的,这一点在使用synchronized的时候一定要注意。

这篇文章就到这里,希望可以给你带来帮助,也希望您可以多多关注我们的更多内容!

(0)

相关推荐

  • java synchronized同步静态方法和同步非静态方法的异同

    java synchronized 详解 synchronized关键字有两种用法,一种是只用于方法的定义中,另外一种是synchronized块,我们不仅可以使用synchronized来同步一个对象变量,你也可以通synchronizedl来同步类中的静态方法和非静态方法. synchronized块的语法如下: public void method() { synchronized(表达式) { } } public void method() { synchronized(表达式) {

  • 详细解读java同步之synchronized解析

    问题 (1)synchronized的特性? (2)synchronized的实现原理? (3)synchronized是否可重入? (4)synchronized是否是公平锁? (5)synchronized的优化? (6)synchronized的五种使用方式? 简介 synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成 monitorenter 和 monitorexit 字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解

  • Java 同步锁(synchronized)详解及实例

    Java 同步锁(synchronized)详解及实例 Java中cpu分给每个线程的时间片是随机的并且在Java中好多都是多个线程共用一个资源,比如火车卖票,火车票是一定的,但卖火车票的窗口到处都有,每个窗口就相当于一个线程,这么多的线程共用所有的火车票这个资源.如果在一个时间点上,两个线程同时使用这个资源,那他们取出的火车票是一样的(座位号一样),这样就会给乘客造成麻烦.比如下面程序: package com.pakage.ThreadAndRunnable; public class Ru

  • Java 多线程同步 锁机制与synchronized深入解析

    打个比方:一个object就像一个大房子,大门永远打开.房子里有很多房间(也就是方法).这些房间有上锁的(synchronized方法), 和不上锁之分(普通方法).房门口放着一把钥匙(key),这把钥匙可以打开所有上锁的房间.另外我把所有想调用该对象方法的线程比喻成想进入这房子某个 房间的人.所有的东西就这么多了,下面我们看看这些东西之间如何作用的. 在此我们先来明确一下我们的前提条件.该对象至少有一个synchronized方法,否则这个key还有啥意义.当然也就不会有我们的这个主题了. 一

  • java中synchronized(同步代码块和同步方法)详解及区别

     java中synchronized(同步代码块和同步方法)详解及区别 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问题,当有多条线程同时访问共享数据时,如果进行同步,就会发生错误,Java提供的解决方案是:只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他

  • 死磕 java同步系列之synchronized解析

    问题 (1)synchronized的特性? (2)synchronized的实现原理? (3)synchronized是否可重入? (4)synchronized是否是公平锁? (5)synchronized的优化? (6)synchronized的五种使用方式? 简介 synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成 monitorenter 和 monitorexit 字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解

  • Java并发系列之JUC中的Lock锁与synchronized同步代码块问题

    目录 一.Lock锁 二.锁的底层 三.案例 案例一:传统的synchronized实现 案例二:Lock锁的实现 四.Lock锁和synchronized的区别 写在前边: 在Java服务端中,会常常遇到并发的场景,以下我使用两个售票的案例实现传统的Lock锁与synchronized加锁解决线程安全问题. 本章代码:Gitee: juc.demo 一.Lock锁 ReentrantLock类: 可重用锁(公平锁|非公平锁) ReentrantReadWriteLock.ReadLock:读锁

  • Java lock同步锁使用实例解析

    这篇文章主要介绍了Java lock同步锁使用实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就

  • Java同步关键字synchronize底层实现原理解析

    目录 1 字节码层实现 1.1 InterpreterRuntime::monitorenter 1.1.1 函数参数 JavaThread *thread 1.1.2 函数体 2 偏向锁 2.1 偏向锁的意义 2.2 偏向锁的获取 2.2.1 markOop mark = obj->mark() 2.2.2 判断mark是否为可偏向状态 2.2.3 判断mark中JavaThread的状态 2.2.4 通过CAS原子指令 2.2.5 如果执行CAS失败 2.3 偏向锁的撤销 2.4 轻量级锁

  • Java多线程编程中synchronized线程同步的教程

    0.关于线程同步 (1)为什么需要同步多线程? 线程的同步是指让多个运行的线程在一起良好地协作,达到让多线程按要求合理地占用释放资源.我们采用Java中的同步代码块和同步方法达到这样的目的.比如这样的解决多线程无固定序执行的问题: public class TwoThreadTest { public static void main(String[] args) { Thread th1= new MyThread1(); Thread th2= new MyThread2(); th1.st

  • java同步之volatile解析

    问题 (1)volatile是如何保证可见性的? (2)volatile是如何禁止重排序的? (3)volatile的实现原理? (4)volatile的缺陷? 简介 volatile可以说是Java虚拟机提供的最轻量级的同步机制了,但是它并不容易被正确地理解,以至于很多人不习惯使用它,遇到多线程问题一律使用synchronized或其它锁来解决. 了解volatile的语义对理解多线程的特性具有很重要的意义,所以彤哥专门写了一篇文章来解释volatile的语义到底是什么. 语义一:可见性 前面

  • Java多线程之synchronized同步代码块详解

    目录 1.同步方法和同步块,哪种更好? 2.synchronized同步代码块 3.如果同步块内的线程抛出异常会发生什么? 总结 面试题: 1同步方法和同步块,哪种更好? 2.如果同步块内的线程抛出异常会发生什么? 1. 同步方法和同步块,哪种更好? 同步块更好,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率.请知道一条原则:同步的范围越小越好. 对于小的临界区,我们直接在方法声明中设置synchronized同步关键字,可以避免竞态条件的问题.但是对于较大的临界区代码段

  • Java同步锁Synchronized底层源码和原理剖析(推荐)

    目录 1 synchronized场景回顾 2 反汇编寻找锁实现原理 3 synchronized虚拟机源码 3.1 HotSpot源码Monitor生成 3.2 HotSpot源码之Monitor竞争 3.3 HotSpot源码之Monitor等待 3.4 HotSpot源码之Monitor释放 1 synchronized场景回顾 目标:synchronized回顾(锁分类–>多线程)概念synchronized:是Java中的关键字,是一种同步锁.Java中锁分为以下几种:乐观锁.悲观锁(

随机推荐