并发编程-03.synchronized详解

1.设计同步器的意义

       临界资源: 多线程中的共享可变资源。这种资源可能是: 对象、变量、文件等。所以需要采用同步机制来协同对对象可变状态的访问。

共享:资源可以由多个线程同时访问
可变:资源可以在其生命周期内被修改

1.1.如何解决线程并发安全问题?

解决方案: 序列化访问临界资源,即:同一时刻只能有一个线程访问临界资源(同步互斥访问)。
Java 提供的两种方式:synchronized Lock

synchronized : 在JDK < 1.6 以前,性能极低,JDK 1.6以后做了一些列优化,使其效率与ReentrantLock相当,是一种内置锁(隐式锁)。
ReentrantLock: 由于以前 synchronized 性能低下,Doug Lea 使用Java语言编写了基于AQS的 ReentrantLock 可重入锁,它具有可重入公平性,是一种显示锁。

2.synchronized原理详解

       synchronized 内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可重入的。

       加锁的方式:具体可以查看此链接: 线程间的通信与线程安全问题.

1、同步实例方法,锁是当前实例对象
2、同步类方法,锁是当前类对象
3、同步代码块,锁是括号里面的对象

2.1.synchronized底层原理

       synchronized 是基于 JVM 内置锁实现,通过内部对象 Monitor(监视器锁) 实现,基于进入与退出 Monitor 对象实现方法与代码块同步,监视器锁的实现依赖底层操作系统的 Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。JVM内置锁在JDK 1.5 以后版本做了重大的优化,如使用 锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、
偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)
等技术来减少锁操作的开销,内置锁的并发性能已经基本与 Lock持平。
       synchronized 关键字被编译成字节码后会被翻译成 monitorenter monitorexit 两条指令分别在同步块逻辑代码起始位置结束位置
在这里插入图片描述
每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:
在这里插入图片描述

2.1.1.Monitor(监视器锁)

什么是 monitor ?
       Monitor 被翻译为管程监视器锁。可以把它理解为一个同步工具,也可以描述为一种同步机制,它通常被描述为一个对象。所有的 Java 对象是天生的Monitor,每一个Java对象都有成为 Monitor 的潜质,因为在Java的设计中 ,每一个 Java 对象都自带了一把看不见的锁,它叫做内部锁或者Monitor锁(即Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。也就是通常说 synchronized 的对象锁,MarkWord 锁标识位为10,其中指针指向的是 Monitor 对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于 HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):
在这里插入图片描述
Minitor结构:
在这里插入图片描述

  • 刚开始时 Moinitor 为NULL
  • 当 Thread 执行到 synchronized(obj) 就会将 Monitor 的所有者Owner置为Thread-2,Moinitor 只能有一个所有者
  • 在 Thread 上锁过程中,如果 Thread-3、Thread-4、Thread-5 也执行到 synchronized(obj),就会进入到 EntryList BLOCKED
  • Thread-2 同步代码块执行完毕,就会唤醒 EntryList 中等待线程来竞争锁,竞争时是非公平的
  • 图中WaitSet中的Thread-0、Thread-1是之前获得过锁,但条件不满足而放弃锁进入 WAITING 等待状态的线程(在下方解释)

wait/notify原理
在这里插入图片描述

  • Owner线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU
  • BLOCKED 线程会在 Owener 线程释放锁时被唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后进入 EntryLis t重新竞争

注意:
synchronized 必须是进入同一个对象的 Monitor 才会有以上效果
不加 sychronized 对象不会关联 Monitor,不尊遵从以上规则

字节码角度理解synchronized加锁原理:

static final Object lock = new Object();
static int counter = 0;

public static void main(String[] args) {
	synchronized (lock) {
		counter++;
	}
}

对应字节码为:
在这里插入图片描述
       方法的同步没有 monitorenter 和 monitorexit 指令,但相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的: 当方法调用时,调用指令将会检查方法是否有 ACC_SYNCHRONIZED 标识符,如果有,执行线程将先获取 monitor 。

public class SynchronizedMethod {
	public synchronized void method() {
		System.out.println("Hello World!");
	}
}

反编译结果:
在这里插入图片描述
        两个指令的执行是 JVM 通过调用操作系统的 互斥原语 mutex 来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

3.对象内存布局

       HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(Header)实例数据(Instance Data)对齐填充 (Padding)
在这里插入图片描述

       实例数据: 存放类的属性数据信息,包括父类的属性信息;
       对齐填充: 虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐;
       对象头: Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节(32bit),在64位虚拟机中,1个机器码是8个字节(64bit)),但是如果对象是数组类型,则需要3个机器码,因为 JVM 虚拟机可以通过 Java 对象的元数据信息确定 Java 对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
注意:图中每一行都代表 32bit

       下面是 32 为虚拟机的对象头: 64位的对象头浪费空间,JVM默认会开启指针压缩,所以基本上也是按 32 位的形式记录对象头的,什么是指针压缩参考链接: JVM-03.对象创建与内存分配.
在这里插入图片描述
对象头分析工具
       运行时对象头锁状态分析工具JOL,OpenJDK开源工具包,引入下方 maven 依赖,通过对象头分析,我们可以具体的分析出当前锁对象的状态。可以分析锁升级膨胀的过程、HashCode、线程ID等信息。

<dependency>
	<groupId>org.openjdk.jol</groupId> 
	<artifactId>jol‐core</artifactId>
	<version>0.10</version> 
</dependency>

打印markword

// object是锁对象
System.out.println(ClassLayout.parseInstance(object).toPrintable());

注意:操作系统又分为大小端,我们常用的 Linux 和 Windows 都是小端模式,它们的 markword 打印出来会地址颠倒。如(0000001 00000000 00000000 00000000 就是 0000000 00000000 00000000 00000001)

4.synchronized锁的升级膨胀过程

       JDK1.6 以前,都是重量级锁。JDK6 时逐步对 sychronized进行了一系列的优化,在某些场景甚至比CAS更快。

锁膨胀升级过程(简易):
在这里插入图片描述
请添加图片描述

4.1.偏向锁

       在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些 CAS 操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是:如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时, 无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。
       注意: JVM 会延迟启动偏向锁(4秒),因为 JVM 内部也有部分线程带有 synchronized,延迟启动可以避免无谓的升级为轻量级锁。

4.2.轻量级锁

       轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

4.3.自旋锁

       轻量级锁失败后,JVM 不会立即将其升级为重量级锁,在大多数情况下,线程持有锁的时间都不会太长,所以 JVM 会让等待锁的线程自旋(会让当前想要获取锁的线程做几个空循环)等待锁释放。如果自旋一定次数仍然未等待到锁释放,就会升级为重量级锁。
       JDK1.6后自旋所锁自适应的,比如说对象刚刚的一次自旋操作成功过,JVM 会认为自旋成功的可能性就高一些,从而多自旋几次。反之,就少自旋甚至不自旋
       重量级锁竞争的时候,也可以使用自旋来进行优化,如果当前线程自旋成功,当前线程就可以避免阻塞。
       自旋会占用CPU时间,单核CPU自旋就是浪费,只有多核CPU自旋才能优化

4.4.重量级锁

       通过操作系统 Metux 元语实现,性能极差。竞争激烈情况下才会出现。

4.5.锁粗化

       如果虚拟机探测到有一串零碎操作都是对同一对象加锁,将会把加锁同步的范围扩展到整个操作序列的外部。
       例如: 多次循环进入同步块不如同步块内多次循环

// StringBuffer是线程安全的,内部被synchronized修饰
//JVM可能会优化把多次append的加锁操作粗化为一次
//因为都是对用一个对象加锁,没有必要重入多次
new StringBuffer().appen("a").appen("b").appen("c");

4.6.锁消除

       Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。锁消除的依据是逃逸分析的数据支持
       例如:

// 优化前
synchronized (obj) {
    逻辑1
}
synchronized (obj) {
    逻辑2
}
//锁消除后
synchronized (obj) {
	逻辑1
	逻辑2
}

逃逸分析
使用逃逸分析,编译器可以对代码做如下优化:

一、同步省略。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
二、将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配(通过标量替换,将对象(聚合量)转化为标量(对象里面的成员))的候选,而不是堆分配。
三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存, 而是存储在CPU寄存器中。

4.7.其他优化

  • 减少上锁时间
    同步代码块尽量短
  • 减少锁的粒度
    将一个锁拆分为多个锁,提高并发度。例如:
    A.ConcurrentHashMap
    B.LongAdder(累加工具类)分为 base 和 cell 两部分。没有并发竞争的时候或者cells数组正在初始化的时候,会使用 CAS 来累加值到 base,有并发争用是,会初始化cells数组,数组有多个cell,就运行有多少线程并行修改,最后将数组中每个cell累加,在加上base就是最终值。
    C.LinkedBlockingQueue(基于链表的阻塞队列)入队和出队使用不同的锁,相对于 LinkedBlockingArray 只有一个锁效率要高
  • 读写分离
    读读的是原始数据内容,写会复制一份,在一个新数组上写,读操作不需要同步,仅需要写加锁
    CopyOnWriteArrayList
    CopyOnWriteSet
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值