synchronized 的超多干货!(一)

简介: synchronized 这个关键字的重要性不言而喻,几乎可以说是并发、多线程必须会问到的关键字了。synchronized 会涉及到锁、升级降级操作、锁的撤销、对象头等。所以理解 synchronized 非常重要,本篇文章就带你从 synchronized 的基本用法、再到 synchronized 的深入理解,对象头等,为你揭开 synchronized 的面纱。

synchronized 这个关键字的重要性不言而喻,几乎可以说是并发、多线程必须会问到的关键字了。synchronized 会涉及到锁、升级降级操作、锁的撤销、对象头等。所以理解 synchronized 非常重要,本篇文章就带你从 synchronized 的基本用法、再到 synchronized 的深入理解,对象头等,为你揭开 synchronized 的面纱。

浅析 synchronized

synchronized 是 Java 并发模块 非常重要的关键字,它是 Java 内建的一种同步机制,代表了某种内在锁定的概念,当一个线程对某个共享资源加锁后,其他想要获取共享资源的线程必须进行等待,synchronized 也具有互斥和排他的语义。

什么是互斥?我们想必小时候都玩儿过磁铁,磁铁会有正负极的概念,同性相斥异性相吸,相斥相当于就是一种互斥的概念,也就是两者互不相容。

synchronized 也是一种独占的关键字,但是它这种独占的语义更多的是为了增加线程安全性,通过独占某个资源以达到互斥、排他的目的。

在了解了排他和互斥的语义后,我们先来看一下 synchronized 的用法,先来了解用法,再来了解底层实现。

synchronized 的使用

关于 synchronized 想必你应该都大致了解过

  • synchronized 修饰实例方法,相当于是对类的实例进行加锁,进入同步代码前需要获得当前实例的锁
  • synchronized 修饰静态方法,相当于是对类对象进行加锁
  • synchronized 修饰代码块,相当于是给对象进行加锁,在进入代码块前需要先获得对象的锁

下面我们针对每个用法进行解释

synchronized 修饰实例方法

synchronized 修饰实例方法,实例方法是属于类的实例。synchronized 修饰的实例方法相当于是对象锁。下面是一个 synchronized 修饰实例方法的例子。

public synchronized void method()
{
   // ...
}

像如上述 synchronized 修饰的方法就是实例方法,下面我们通过一个完整的例子来认识一下 synchronized 修饰实例方法

public class TSynchronized implements Runnable{
    static int i = 0;
    public synchronized void increase(){
        i++;
        System.out.println(Thread.currentThread().getName());
    }
    @Override
    public void run() {
        for(int i = 0;i < 1000;i++) {
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        TSynchronized tSynchronized = new TSynchronized();
        Thread aThread = new Thread(tSynchronized);
        Thread bThread = new Thread(tSynchronized);
        aThread.start();
        bThread.start();
        aThread.join();
        bThread.join();
        System.out.println("i = " + i);
    }
}

上面输出的结果 i = 2000 ,并且每次都会打印当前现成的名字

来解释一下上面代码,代码中的 i 是一个静态变量,静态变量也是全局变量,静态变量存储在方法区中。increase 方法由 synchronized 关键字修饰,但是没有使用 static 关键字修饰,表示 increase 方法是一个实例方法,每次创建一个 TSynchronized 类的同时都会创建一个 increase 方法,increase 方法中只是打印出来了当前访问的线程名称。Synchronized 类实现了 Runnable 接口,重写了 run 方法,run 方法里面就是一个 0 - 1000 的计数器,这个没什么好说的。在 main 方法中,new 出了两个线程,分别是 aThread 和 bThread,Thread.join 表示等待这个线程处理结束。这段代码主要的作用就是判断 synchronized 修饰的方法能够具有独占性。

synchronized 修饰静态方法

synchronized 修饰静态方法就是 synchronized 和 static 关键字一起使用

public static synchronized void increase(){}

当 synchronized 作用于静态方法时,表示的就是当前类的锁,因为静态方法是属于类的,它不属于任何一个实例成员,因此可以通过 class 对象控制并发访问。

这里需要注意一点,因为 synchronized 修饰的实例方法是属于实例对象,而 synchronized 修饰的静态方法是属于类对象,所以调用 synchronized 的实例方法并不会阻止访问 synchronized 的静态方法。

synchronized 修饰代码块

synchronized 除了修饰实例方法和静态方法外,synchronized 还可用于修饰代码块,代码块可以嵌套在方法体的内部使用。

public void run() {
  synchronized(obj){
    for(int j = 0;j < 1000;j++){
      i++;
    }
  }
}

上面代码中将 obj 作为锁对象对其加锁,每次当线程进入 synchronized 修饰的代码块时就会要求当前线程持有obj 实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待。

synchronized 修饰的代码块,除了可以锁定对象之外,也可以对当前实例对象锁、class 对象锁进行锁定

// 实例对象锁
synchronized(this){
    for(int j = 0;j < 1000;j++){
        i++;
    }
}
//class对象锁
synchronized(TSynchronized.class){
    for(int j = 0;j < 1000;j++){
        i++;
    }
}

synchronized 底层原理

在简单介绍完 synchronized 之后,我们就来聊一下 synchronized 的底层原理了。

我们或许都有所了解(下文会细致分析),synchronized 的代码块是由一组 monitorenter/monitorexit 指令实现的。而Monitor 对象是实现同步的基本单元。

啥是 Monitor 对象呢?

Monitor 对象

任何对象都关联了一个管程,管程就是控制对象并发访问的一种机制管程 是一种同步原语,在 Java 中指的就是 synchronized,可以理解为 synchronized 就是 Java 中对管程的实现。

管程提供了一种排他访问机制,这种机制也就是 互斥。互斥保证了在每个时间点上,最多只有一个线程会执行同步方法。

所以你理解了 Monitor 对象其实就是使用管程控制同步访问的一种对象。

对象内存布局

hotspot 虚拟机中,对象在内存中的布局分为三块区域:

  • 对象头(Header)
  • 实例数据(Instance Data)
  • 对齐填充(Padding)

这三块区域的内存分布如下图所示

微信图片_20220416175148.png

我们来详细介绍一下上面对象中的内容。

对象头 Header

对象头 Header 主要包含 MarkWord 和对象指针 Klass Pointer,如果是数组的话,还要包含数组的长度。

微信图片_20220416175152.png

在 32 位的虚拟机中 MarkWord ,Klass Pointer 和数组长度分别占用 32 位,也就是 4 字节。

如果是 64 位虚拟机的话,MarkWord ,Klass Pointer 和数组长度分别占用 64 位,也就是 8 字节。

在 32 位虚拟机和 64 位虚拟机的 Mark Word 所占用的字节大小不一样,32 位虚拟机的 Mark Word 和 Klass Pointer 分别占用 32 bits 的字节,而 64 位虚拟机的 Mark Word 和 Klass Pointer 占用了64 bits 的字节,下面我们以 32 位虚拟机为例,来看一下其 Mark Word 的字节具体是如何分配的。

微信图片_20220416175157.png

微信图片_20220416175250.png

用中文翻译过来就是

微信图片_20220416175200.png

  • 无状态也就是无锁的时候,对象头开辟 25 bit 的空间用来存储对象的 hashcode ,4 bit 用于存放分代年龄,1 bit 用来存放是否偏向锁的标识位,2 bit 用来存放锁标识位为 01。
  • 偏向锁 中划分更细,还是开辟 25 bit 的空间,其中 23 bit 用来存放线程ID,2bit 用来存放 epoch,4bit 存放分代年龄,1 bit 存放是否偏向锁标识, 0 表示无锁,1 表示偏向锁,锁的标识位还是 01。
  • 轻量级锁中直接开辟 30 bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为 00。
  • 重量级锁中和轻量级锁一样,30 bit 的空间用来存放指向重量级锁的指针,2 bit 存放锁的标识位,为 11
  • GC标记开辟 30 bit 的内存空间却没有占用,2 bit 空间存放锁标志位为 11。

其中无锁和偏向锁的锁标志位都是 01,只是在前面的 1 bit 区分了这是无锁状态还是偏向锁状态。

关于为什么这么分配的内存,我们可以从 OpenJDK 中的markOop.hpp类中的枚举窥出端倪

微信图片_20220416175204.png

来解释一下

  • age_bits 就是我们说的分代回收的标识,占用4字节
  • lock_bits 是锁的标志位,占用2个字节
  • biased_lock_bits 是是否偏向锁的标识,占用1个字节。
  • max_hash_bits 是针对无锁计算的 hashcode 占用字节数量,如果是 32 位虚拟机,就是 32 - 4 - 2 -1 = 25 byte,如果是 64 位虚拟机,64 - 4 - 2 - 1 = 57 byte,但是会有 25 字节未使用,所以 64 位的 hashcode 占用 31 byte。
  • hash_bits 是针对 64 位虚拟机来说,如果最大字节数大于 31,则取 31,否则取真实的字节数
  • cms_bits 我觉得应该是不是 64 位虚拟机就占用 0 byte,是 64 位就占用 1byte
  • epoch_bits 就是 epoch 所占用的字节大小,2 字节。

在上面的虚拟机对象头分配表中,我们可以看到有几种锁的状态:无锁(无状态),偏向锁,轻量级锁,重量级锁,其中轻量级锁和偏向锁是 JDK1.6 中对 synchronized 锁进行优化后新增加的,其目的就是为了大大优化锁的性能,所以在 JDK 1.6 中,使用 synchronized 的开销也没那么大了。其实从锁有无锁定来讲,还是只有无锁和重量级锁,偏向锁和轻量级锁的出现就是增加了锁的获取性能而已,并没有出现新的锁。

所以我们的重点放在对 synchronized 重量级锁的研究上,当 monitor 被某个线程持有后,它就会处于锁定状态。在 HotSpot 虚拟机中,monitor 的底层代码是由 ObjectMonitor 实现的,其主要数据结构如下(位于 HotSpot 虚拟机源码 ObjectMonitor.hpp 文件,C++ 实现的)

微信图片_20220416175208.png

这段 C++ 中需要注意几个属性:_WaitSet 、 _EntryList 和 _Owner,每个等待获取锁的线程都会被封装称为 ObjectWaiter 对象。

微信图片_20220416175212.png

_Owner 是指向了 ObjectMonitor 对象的线程,而 _WaitSet 和 _EntryList 就是用来保存每个线程的列表。

那么这两个列表有什么区别呢?这个问题我和你聊一下锁的获取流程你就清楚了。

相关文章
|
3月前
|
Java 开发者 C++
Java多线程同步大揭秘:synchronized与Lock的终极对决!
Java多线程同步大揭秘:synchronized与Lock的终极对决!
77 5
|
3月前
|
安全 Java 开发者
Java多线程同步:synchronized与Lock的“爱恨情仇”!
Java多线程同步:synchronized与Lock的“爱恨情仇”!
86 5
|
3月前
|
Java 程序员 开发者
深入解读:synchronized关键字背后的“黑科技”!
深入解读:synchronized关键字背后的“黑科技”!
29 4
|
4月前
|
存储 安全 Java
(三)死磕并发之深入Hotspot源码剖析Synchronized关键字实现
关于源码分析如果不是功底特别深厚的小伙伴可能需要用心的去细心咀嚼,千万不要抱着看一遍就能懂的心态学习,不然最终也没有任何作用。
|
3月前
|
Java C++
【Java 并发秘籍】synchronized vs ReentrantLock:揭秘线程同步神器的对决!
【8月更文挑战第24天】本文详细对比了Java并发编程中`synchronized`关键字与`ReentrantLock`的不同之处。`synchronized`作为内置关键字,提供自动锁管理但不支持中断或公平锁;`ReentrantLock`则通过显式调用方法控制锁,具备更多高级功能如可中断、公平锁及条件变量。文章通过两个计数器类实例展示了两种机制的具体应用,帮助读者理解其差异及适用场景。掌握这两者对于提升多线程程序设计能力至关重要。
58 0
|
5月前
|
安全 Java 程序员
惊呆了!Java多线程里的“synchronized”竟然这么神奇!
【6月更文挑战第20天】Java的`synchronized`关键字是解决线程安全的关键,它确保同一时间只有一个线程访问同步代码。在案例中,`Counter`类的`increment`方法如果不加同步,可能会导致竞态条件。通过使用`synchronized`方法或语句块,可以防止这种情况,确保线程安全。虽然同步会带来性能影响,但它是构建并发应用的重要工具,平衡同步与性能是使用时需考虑的。了解并恰当使用`synchronized`,能有效应对多线程挑战。
20 1
|
安全 Java
一文教你,synchronized和Lock的区别?
最近有多位粉丝被问到synchronized和Lock,据说还是阿里一面的面试题。在分布式开发中,锁是控制线程的重要方式。Java提供了两种锁机制synchronized 和 Lock。接下来,我给大家分享一下我对synchronized和Lock的理解。
205 0
|
存储 安全 Java
大白话讲解synchronized锁升级套路
synchronized锁是啥?锁其实就是一个对象,随便哪一个都可以,Java中所有的对象都是锁,换句话说,Java中所有对象都可以成为锁。 这次我们主要聊的是synchronized锁升级的套路
124 0
|
安全 Java 开发者
京东面试官:说说synchronized 和 ReentrantLock的区别!
前面我们介绍了很多关于多线程的内容,在多线程中有一个很重要的课题需要我们攻克,那就是线程安全问题。线程安全问题指的是在多线程中,各线程之间因为同时操作所产生的数据污染或其他非预期的程序运行结果。
128 0
|
Java
JUC系列(五)| Synchonized关键字的进一步讲解
JUC系列(五)| Synchonized关键字的进一步讲解
113 0
JUC系列(五)| Synchonized关键字的进一步讲解