设为首页 加入收藏

TOP

15000字、6个代码案例、5个原理图让你彻底搞懂Synchronized(一)
2023-09-09 10:25:50 】 浏览:82
Tags:15000 Synchronized

Synchronized

本篇文章将围绕synchronized关键字,使用大量图片、案例深入浅出的描述CAS、synchronized Java层面和C++层面的实现、锁升级的原理、源码等

大概观看时间17分钟

可以带着几个问题去查看本文,如果认真看完,问题都会迎刃而解:

1、synchronized是怎么使用的?在Java层面是如何实现?

2、CAS是什么?能带来什么好处?又有什么缺点?

3、mark word是什么?跟synchronized有啥关系?

4、synchronized的锁升级优化是什么?在C++层面如何实现?

5、JDK 8 中轻量级锁CAS失败到底会不会自旋?

6、什么是object monitor?wait/notify方法是如何实现的?使用synchronized时,线程阻塞后是如何在阻塞队列中排序的?

...

synchronized Java层面实现

synchronized作用在代码块或方法上,用于保证并发环境下的同步机制

任何线程遇到synchronized都要先获取到锁才能执行代码块或方法中的操作

在Java中每个对象有一个对应的monitor对象(监视器),当获取到A对象的锁时,A对象的监视器对象中有个字段会指向当前线程,表示这个线程获取到A对象的锁(详细原理后文描述)

synchronized可以作用于普通对象和静态对象,当作用于静态对象、静态方法时,都是去获取其对应的Class对象的锁

synchronized作用在代码块上时,会使用monitorentry和monitorexit字节码指令来标识加锁、解锁

synchronized作用在方法上时,会在访问标识上加上synchronized

指令中可能出现两个monitorexit指令是因为当发生异常时,会自动执行monitorexit进行解锁

正常流程是PC 12-14,如果在此期间出现异常就会跳转到PC 17,最终在19执行monitorexit进行解锁

        Object obj = new Object();
        synchronized (obj) {

        }

image.png

上篇文章中我们说过原子性、可见性以及有序性

synchronized加锁解锁的字节码指令使用屏障,加锁时共享内存从主内存中重新读取,解锁前把工作内存数据写回主内存以此来保证可见性

由于获取到锁才能执行相当于串行执行,也就保证原子性和有序性,需要注意的是加锁与解锁之间的指令还是可以重排序的

CAS

为了更好的说明synchronized原理和锁升级,我们先来聊聊CAS

上篇文章中我们说过,volatile不能保证复合操作的原子性,使用synchronized方法或者CAS能够保证复合操作原子性

那什么是CAS呢?

CAS全称 Compare And Swap 比较并交换,读取数据后要修改时用读取的数据和地址上的值进行比较,如果相等那就将地址上的值替换为目标值,如果不相等,通常会重新读取数据再进行CAS操作,也就是失败重试

synchronized加锁是一种悲观策略,每次遇到时都认为会有并发问题,要先获取锁才操作

而CAS是一种乐观策略,每次先大胆的去操作,操作失败(CAS失败)再使用补偿措施(失败重试)

CAS与失败重试(循环)的组合构成乐观锁或者说自旋锁(循环尝试很像在自我旋转)

并发包下的原子类,依靠Unsafe大量使用CAS操作,比如AtomicInteger的自增

    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

    //var1是调用方法的对象,var2是需要读取/修改的值在这个对象上的偏移量,var4是自增1
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            //var5是通过对象和字段偏移量获取到字段最新值
            var5 = this.getIntVolatile(var1, var2);
            //cas:var1,var2找到字段的值 与 var5比较,相等就替换为 var5+var4 
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }

CAS只能对一个变量进行操作,如果要对多个变量进行操作,那么只能对外封装一层(将多个变量封装为新对象的字段),再使用原子类中的AtomicReference

不知各位同学有没有发现,CAS的流程有个bug,就是在读数据与比较数据之间,如果数据从A被改变到B,再改变到A,那么CAS也能执行成功

这种场景有的业务能够接受,有的业务无法接受,这就是所谓的ABA问题

而解决ABA问题的方式比较简单,可以再比较时附加一个自增的版本号,JDK也提供解决ABA问题的原子类AtomicStampedReference

CAS能够避免线程阻塞,但如果一直失败就会一直循环,增加CPU的开销,CAS失败后重试的次数/时长不好评估

因此CAS操作适用于竞争小的场景,用CPU空转的开销来换取线程阻塞挂起/恢复的开销

锁升级

早期版本的synchronized会将获取不到锁的线程直接挂起,性能不好

JDK 6 时对synchronized的实现进行优化,也就是锁升级

锁的状态可以分为无锁、偏向锁、轻量级锁、重量级锁

可以暂时把重量级锁理解为早期获取不到锁就让线程挂起,新的优化也就是轻量级锁和偏向锁

mark word

为了更好的说明锁升级,我们先来聊聊Java对象头中的mark word

我们下面的探究都是围绕64位的虚拟机

Java对象的内存由mark word、klass word、如果是数组还要记录长度、实例数据(字段)、对其填充(填充到8字节倍数)组成

mark word会记录锁状态,在不同锁状态的情况下记录的数据也不同

下面这个表格是从无锁到重量级锁mark word记录的内容

|----------------------------------------------------------------------|--------|--------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1  | lock:2 | 无锁   
|----------------------------------------------------------------------|--------|--------|
|  thread:54 |         epoch:2      | unused:1 | age:4 | biased_lock:1 | lock:2 | 偏向锁
|----------------------------------------------------------------------|--------|--------|
|                     ptr_to_lock_record:62                            | lock:2 | 轻量级锁
|----------------------------------------------------------------------|-
首页 上一页 1 2 3 4 5 6 7 下一页 尾页 1/7/7
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇不好意思,list.contain 去重该换.. 下一篇精选版:用Java扩展Nginx(nginx-..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目