【JUC并发编程】Synchronized深度分析

目录

一、Java JUC简介

二、CPU相关知识

1、CPU时间片

2、Cpu密集型/IO密集型

3、CPU上下文切换

 3.1、程序计数器

4、用户态与内核态区别(也叫用户空间、内核空间)!!

 4.1、线程安全同步的方式

4.2、传统锁有哪些缺点

5、发生CPU上下文切换的原因

6、如何避免上下文切换

二、synchronized前置知识

1、重入锁

2、偏向锁>>轻量级锁>>重量级锁应用场景!!!

2.1、偏向锁与重入锁之间有什么区别?

三、synchronized原理分析

1、JVM对象头

1.1、想个问题:偏向锁/轻量锁/重量锁 锁状态存放在我们对象什么地方中?

1.2、对象在内存中的布局?

1.3、New 一个对象占用多少字节!!

2、对象头详解

 2.1、对象的5种状态

2.2、synchronized底层实现原理总结

3、小结


一、Java JUC简介

在Java5.0提供了java.util.concurrent包,简称JUC包,在此包中增加了在并发编程中很常用的实用工具类,用于定义类似于县城的自定义子系统,包括线程池、异步 IO 和轻量级任务框架。提供可调的、灵活的线程池。还提供了设计用于多线程上下文中的Collection实现等。

二、CPU相关知识

1、CPU时间片

CPU时间片:CPU分配给各个程序的使用时间,每个线程被分配一个时间段,称作它的时间片;即该进程允许运行的时间,使各个程序从表面上看是同时进行的。如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结束,则CPU当即进行切换。

  1. 单核的cpu上每次只能够执行一次线程,如果在单核的cpu上开启了多线程,则会发生对每个线程轮流执行 。
  2. cpu每次单个计算的时间成为一个cpu时间片,实际只有几十毫秒人为感觉好像是在多线程。
  3. 如果在单核的cpu之上开启了多线程,底层执行并不是真正意义上的多线程。

说明:由于只有一个CPU,一次只能处理程序要求的一部分,如何处理公平,一种方法就是引入时间片,每个程序轮流执行。

上下文切换耗时3us微妙左右(最低值是200ns纳秒),根据机器不同略有差异。

2、Cpu密集型/IO密集型

Cpu密集型:长时间占用cpu;例如: 视频剪辑

IO密集型 :cpu计算时间短,访问外接设备时间长,如读取数据量、rpc远程调用

3、CPU上下文切换

Linux 是一个多任务的操作系统,它支持远大于CPU数量的任务同时运行,当然并不是真正的同时运行,是每个任务轮流执行CPU分给他们的时间片,让人感觉是同时在运行。

每一个任务运行前,CPU都需要知道任务从哪里加载,又从哪里运行,也就是说,需要系统事先设置好CPU寄存器。

CPU寄存器包含指令寄存器(IR)和程序计数器(PC)。他们用来暂存指令,数据和地址,程序运行的下一条指令地址,这些都是任务运行时的必要环境。因此也被称作CPU上下文

上下文切换就是把前一个任务的CPU上下文保存起来,然后加载新任务的上下文到寄存器中;

这些被保存下来的上下文会存储在操作系统的内核中,等再次加载时,这样就能保证任务的原来状态不受影响,让任务看起来是连续运行的。

 3.1、程序计数器

程序计数器是用于存放线程下一步所该操作地址的地方。

4、用户态与内核态区别(也叫用户空间、内核空间)!!

内核态:运行操作系统程序,操作硬件  (内核空间)

用户态:运行用户程序(用户空间)

为了安全应用程序无法直接调用的硬件的功能,而是将这些功能封装成特定的函数。当应用程序需要硬件功能时(例如读写文件),就需要进行系统调用。当进程进行系统调用时就从用户态装换为内核态。

4.1、线程安全同步的方式

阻塞式:synchronized/ lock(aqs) 当前线程如果没有获取到锁,当前的线程就会被阻塞等待。

非阻塞式:CAS 当前线程如果没有获取到锁,不会阻塞等待,而是一直不断重试。

堵塞式 -》重量级锁      ----------- 内存态

非堵塞式 -》轻量级锁  ------------ 用户态

Synchronized 重量级锁获取锁的流程:

需要经历用户态与内核态切换:首先从内核态阻塞状态进行唤醒,然后切换用户态从新cpu调度。

4.2、传统锁有哪些缺点

在使用synchronized1.0版本 如果没有获取到锁的情况下则当前线程直接会变为阻塞的状态----升级为重量级锁,在后期唤醒的成本会非常高(需要在内核态唤醒再切换到用户态)。

CAS锁运行用户态,缺点:cpu飙高。

synchronized现在版本:偏向锁 →轻量级锁(cas)→重量级锁。

轻量级都是在用户态完成;

重量级需要用户态与内核态切换;

内核空间、用户空间

5、发生CPU上下文切换的原因

通过调用下列方法会导致自发性上下文切换:

Thread.sleep()

Object.wait()

Thread.join()

Thread.yeild()

LockSupport.park()

6、如何避免上下文切换

1. 多线程竞争锁时,加锁、释放锁会导致比较多的上下文切换;

2. 使用最少线程。避免创建不需要的线程;

3.CAS算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁,可能会消耗cpu资源。

CAS 优点:  避免用户态到内核态切换

缺点: 非常消耗cpu的资源

二、synchronized前置知识

1、重入锁

重入锁:当前线程如果已经获取到锁,则不会重复的获取锁,而是直接复用。

synchronized/lock

2、偏向锁>>轻量级锁>>重量级锁应用场景!!!

1.偏向锁:加锁和解锁不需要额外的开销,只适合于同一个线程访问同步代码块,无需额外的开销,如果多个线程同时竞争的时候,会撤销该锁

2.轻量级锁:竞争的线程不会阻塞,提高了程序响应速度,如果始终得不到锁的竞争线程,则使用自旋的形式,消耗cpu资源,适合于同步代码块执行非常快的情况下,自旋(jdk1.7以后智能自转)

3.重量级锁: 线程的竞争不会使用自旋,不会消耗cpu资源,适合于同步代码执行比较长的时间

2.1、偏向锁与重入锁之间有什么区别?

偏向锁与重入锁实现的思路基本上相同

偏向锁---当前只有一个线程的情况下,没有其他的线程竞争锁,该锁一直会被我们的该线程持有

----不会立即释放锁

偏向锁:需要有另外的一个线程竞争该锁,就会撤销我们的偏向锁,根据线程id判断

重入锁:当前线程如果已经获取到了锁,当前线程中 方法如果有需要获取锁的话,则直接复用。

三、synchronized原理分析

Synchronized 底层是在虚拟机实现好的  源码属于C++编写

1、JVM对象头

New 一个对象 占用多少字节呢?

一个对象如何组成的?

对象头 Mark Word  Klass Pointer

实例数据-----成员属性

对齐填充----

Class User{

   Int i=1;

}

1.1、想个问题:偏向锁/轻量锁/重量锁 锁状态存放在我们对象什么地方中?

1.2、对象在内存中的布局?

在JVM中,对象在内存中的布局分为三个部分:对象头、实例属性和对齐填充

1. HotSpot虚拟机的对象头(Object Header)包括两部分信息:

第一部分"Mark Word":用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等.

第二部分"Klass Pointer":对象指向它的类的元数据的指针虚拟机通过这个指针来确定这个对象是哪个类的实例。(数组,对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。 )

markword : 32位 占4字节 ,64位 占 8字节

klasspoint : 开启压缩占4字节,未开启压缩 占 8字节。

64位对象头占用8+8=16字节。 开启压缩指针后对象头占12字节(默认开启压缩)。

2. 实例属性

 就是定义类中的成员属性

3. 对齐填充

对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用

由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

1.3、New 一个对象占用多少字节!!

1Byte=8bit  (1B=8bit)   

1KB=1024Byte

1MB=1024KB

1GB=1024MB

byte     8bit   1字节

char     16bit   2字节

short   16bit   2字节

int     32bit   4字节

long     64bit   8字节

float   32bit

double   64bit

boolean 1bit

// 查看Java对象布局
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

public class Test03 {
    public static void main(String[] args) {
        MayiktLockObject mayiktLockObject = new MayiktLockObject();
        System.out.println(ClassLayout.parseInstance(mayiktLockObject).toPrintable());
    }

    static class MayiktLockObject {
        int j = 4;
        long i = 1;
        boolean m = false;
    }
}

 执行结果:

 在开启了指针压缩的情况下:MayiktLockObject对象占用大小

对象头:12个字节

实例数据: int  j=4 4个字节     long i=1 8个字节     boolean m=false 1个字节

对齐补充: 7个字节。(需要被8整除)

总共32个字节。

 未开启指针压缩情况下:

对象头:16个字节

实例数据: int  j=4 4个字节     long i=1 8个字节     boolean m=false 1个字节

对齐补充: 3个字节。(需要被8整除)

总共32个字节。

2、对象头详解

① 哈希值:它是一个地址,用于栈对堆空间中对象的引用指向,不然栈是无法找到堆中对象的。

②GC分代年龄(占4位):记录幸存者区对象被GC之后的年龄age,一般age为15(阈值为15的原因是因为age只有4位最大就可以将阈值设置15)之后下一次GC就会直接进入老年代,要是还没有等到年龄为15,幸存者区就满了怎么办,那就下一次GC就将大对象或者年龄大者直接进入老年代。

③ 锁状态标志:记录一些加锁的信息(我们都是使用加锁的话,在底层是锁的对象,而不是锁的代码,锁对象的话,那会改变什么信息来表示这个对象被改变了呢?也就是怎么才算加锁了呢?

 2.1、对象的5种状态

1.无锁

2.偏向锁

3.轻量锁

4.重量锁

5.GC标记

2.2、synchronized底层实现原理总结

1. Synchronized 偏向锁(101)、轻量锁(000)、重量级(010)

2. Synchronized 锁的升级状态存放在 java对象头中markword中

3. 偏向锁: 当前线程从对象头中markword获取是否是为偏向锁,如果是为偏向锁,则判断线程的id===当前线程id

   3.1. 如果等于当前的线程id,则不会重复的执行CAS操作,而是直接进入到我们的同步代码快

   3.2. 如果不等于当前的线程id,如果是为无锁的情况,没有其他的线程与我竞争的话,直接使用CAS修改对象头中锁的标识位状态为101,同时也存放当前线程的id在对象头中。

4. 其他的线程与偏向锁线程开始竞争达到20次后会升级为轻量级锁CAS,修改对象头锁的状态为00,(CAS锁对CPU资源消耗比较大,但是可以减少CPU上下文切换)

5. 当前我们的线程重试了多次还是没有获取到锁,则当前锁会升级为重量级锁

        重量级锁:减轻对CPU消耗资源,但是后期唤醒成本高,需要从内核态唤醒。

3、小结

偏向锁、轻量级锁、重量级锁适用于不同的并发场景:

  • 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。
  • 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。
  • 重量级锁:有实际竞争,且锁竞争时间长。

重量级锁依赖于操作系统的互斥锁实现,互斥锁操作会导致进程从用户态与内核态之间的切换、进程的上下文切换,是一个开销较大的操作。   

猜你喜欢

转载自blog.csdn.net/qq_36881887/article/details/127406247