Java 并发面试系列四:synchronized 隐式锁的底层原理

资深开发工程师,目前在某大型互联网公司从事电商广告相关的开发设计工作,在互联网行业从事 Java 开发工作多年,在大型系统设计、微服务架构、Java 并发编程方面有丰富的实战经验,喜欢探究技术本质,热衷技术分享。

文章正文

synchronized 是什么

synchronized 关键字是 Java 提供的一种隐式或内置锁,是最基本的互斥同步手段。以上这句 synchronized 的定义有几个含义需要清楚。

首先,synchronized 关键字是伴随着 Java 诞生就存在的元老级角色,所谓隐式或内置锁是相对于显式锁而言的。“内置”的含义是指加锁和解锁都由虚拟机帮助实现,对外只提供 synchronized 关键供开发人员使用。因此作为开发人员,无需关心锁释放等情况,只要知道其表达的含义就可以直接使用,这在一门语言诞生之初是不错的选择,因为这种方式对开发人员非常友好。

其次,互斥同步,说的就是 synchronized 的作用。所谓互斥是指一个锁在同一时刻只能被一个线程所持有,其它线程必须等待,等持有锁的线程释放锁后再进行申请锁。最后是“线程同步”,这里的“同步”与“同步调用、异步调用”的含义不太一样,它是指多线程通过特定的方法来控制执行的顺序,同步是指协同步调,与“把最新进展同步给我”这里的“同步”语境相似,因此提到同步手段可以理解为协同手段。

synchronized 能保证多个线程访问共享数据时,同一个时刻共享数据只能被一个线程访问到,因此也叫排他锁或者互斥锁,下面是 synchronized 互斥锁的示意图,多个线程并发时只有持有锁的线程才能进入执行:

在这里插入图片描述

synchronized 的基本使用方式有哪几种

synchronized 的使用只需按照语法要求使用即可,至于虚拟机是做了哪些操作我们无法干预。这不像 JDK 后期引入的显式锁 ReentrantLock,开发者可以指定一些干预手段和策略。这种屏蔽底层的细节的做法对使用者而言足够清晰和简单。

下面先看一下 synchronized 如何使用以及加锁到底锁的是什么,如果这两点都理解,作为开发者而言,使用 synchronized 就不会有问题。先抛出一个结论:在 Java 中使用 synchronized 关键字,锁住的都是一个对象。

1. 同步代码块,锁的是“()”中的对象:

public void test() {        
  synchronized (this){ 
    //执行操作逻辑   
  }    
}

如上的例子所示,这里锁的是当前对象 this,表示当同一个对象在多线程中调用 test 方法的时候,只有一个线程获取到锁并进入代码块执行,也就是要获得 synchronized(对象)中对象的锁才能执行代码块。当 synchronized 代码块中的逻辑执行完了即会释放锁,其它线程再发起竞争,最后也是只有一个线程能获取到对象的锁并进入执行“{ }”中的代码。

如果不熟悉 synchronized 锁住对象这一个原理的人,很容易造成误解,认为 synchronized 锁的是代码块中的内容,并认为是绝对的互斥,只要加上 synchronized 上锁那么代码块中的内容只会被一个线程执行,这是错误的。其实 synchronized 都是给某个对象加了锁,比如上面的例子,锁的是当前对象,当前对象也只是某个类的一个实例,当多个线程调用同一个对象的 test 方法进入到 synchronized 代码块确实会竞争锁,只有一个线程能进入,但如果不是同一个对象,是不阻塞的。这一点一定要理解清楚,它直接关系到加上 synchronized 是否能让你的程序真正的线程安全。这里同样也就得出结论,锁住的既然是一个对象,那么只有取获取同一个对象上的锁才会发生锁竞争的情况,不同对象间是不影响的。

上面这种方式是 synchronized 最常用的一种方式, synchronized (对象) { } 这种语法也是 Java 规定的,不必纠结。例子中锁的是当前对象:this,这个有点特殊,但这样的写法却是最常见。当然也可以换成任意其它对象,那样理解起来更容易。接下来进一步看另外两种用法:

2. 修饰静态方法,锁的是 Class 对象:

Class SynchronizedDemo{

  public synchronized static void test {        
    //执行操作逻辑   
    }
}

前面说过,synchronized 锁的都是对像,在上面代码中用法中也不例外,虽然是修饰方法,但并不是方法之间互斥,它锁的是当前方法所在类的类对象,即 SynchronizedDemo.class。对于 JVM 了解的话,类对象应该不陌生,每个类都有一个类对象,这个类对象并不是这个类的实例,而是代表这个类一些信息的实例,这个实例在反射的时候很有用。上面的示例表示当多个线程用该类来调用静态方法 test 时,只有一个线程能进入,其它都要等待,因此它等同于下面这种写法:

Class SynchronizedDemo{

  public static void test {        
         synchronized (SynchronizedDemo.class){ 
        //执行操作逻辑   
      }  
}

这里锁住的是类对象,如果是这个类的实例来调用的话,也是会竞争锁的,因为都要获取 SynchronizedDemo.class 这个对象的锁才能执行代码块。当然方法声明为静态的就是用类来直接调用才合理,使用类的实例调用在语法层面也是允许的,但在一些 IDE 中会提示错误。

作者正在撰写中...
隐藏内容 支付可见
内容互动
写评论
加载更多
评论文章
¥2.99 购买
× 订阅 Java 精选频道
¥ 元/月
订阅即可免费阅读所有精选内容