前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >synchronized

synchronized

原创
作者头像
hhss
修改2021-02-14 15:27:45
4780
修改2021-02-14 15:27:45
举报

synchronized与reentrantlock区别、底层原理?

volatile原理?

ThreadLocal?

以下内容来自马士兵老师的B站教学视频:https://www.bilibili.com/video/BV1tz411q7c2

synchronized底层原理

1、需要了解的基础知识

① CAS

cas是一种无锁算法。

CAS的语义:我们认为内存N值应该是E,如果是,那么将N的值更新为V,否则不修改并告诉N的值实际是多少。也就是:CAS有三个操作数,内存值N,旧的预期值E,要修改的新值V,当且仅当预期值E和内存值N相同时,将内存值N修改为V,否则什么都不做。    CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其他线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,可以再次尝试。 CAS的C语言实现(没有Java实现):

代码语言:javascript
复制
int compare_and_swap(int reg,int oldval,int newval){
   ATOMIC();
  int old_reg_val = reg;
      if(old_reg_val == oldval)
       rag = newval;
       END_ATOMIC();
     return old_reg_val;
}

` 代码意思就是当两者进行比较时,如果相等,证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。

java.util.atomic包里面的原子类AtomicInteger等就是利用CAS来实现线程安全的,不需要加重量级的锁,从而提高效率。

② CAS原理

CAS是利用Unsafe类的native方法。Java不可以直接操作内存,但可以利用Unsafe类的native方法compareAndSwapInt来操作。

native方法是cpp的代码,hotspot的实现。hotspot是jvm的一种实现,现在的jvm基本都是hotspot。

最终实现是CPU指令:lock cmpxchg指令 cmpxchg是比较并交换,由比较、写入等多个操作组成,不是原子操作,所以要加上lock命令来保证原子性。

从硬件上来说lock锁的就是内存总线,或者说是锁的是一个北桥信号。

③ 对象内存布局

第一,markword占8个字节

第二,指向类的Class T.class,默认4个字节(第一+第二是对象头)

第三,对象的属性,占4个字节

第四,64位虚拟机,对象需要是8的倍数,所以用来对齐的。

要使用代码查看java对象的内存布局,需要借助jol工具类

代码语言:javascript
复制
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
代码语言:javascript
复制
import org.openjdk.jol.info.ClassLayout;

public class TestObject {

    public static void main(String[] args) {
        Object object = new Object();
        System.out.println(ClassLayout.parseInstance(object).toPrintable());

        synchronized (object){
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }
}

④ markword的细节

锁状态:是由最后的1bit+2bit组成(https://blog.csdn.net/weixin_44627989/article/details/88866450

也就是对象头的最后两位,是作为锁的状态标志

01—偏向锁/无锁 00—轻量锁 10—重量锁 11—GC标记(要回收的) 偏向锁状态和无锁状态通过区分对象头倒数第三位来确定,0代表无锁,1代表偏向锁

2、synchronized的底层原理

参考这篇文章的第二章:https://www.cnblogs.com/aspirant/p/11470858.html

2.1 底层原理:

每一个锁都对应一个monitor对象,在HotSpot虚拟机中它是由ObjectMonitor实现的(C++实现)。每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。

ObjectMonitor中有两个队列WaitSet和EntryList,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装ObjectWaiter对象),owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入EntryList 集合,当线程获取到对象的monitor 后进入 Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1。

若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因。

2.2 同步代码块与同步方法

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

2.2.1 同步代码块:

  1. monitorenter:每个对象都有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
    1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
    2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
    3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;
  2. monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。 monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;

通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

2.2.2 同步方法:

从编译的结果来看,方法的同步并没有通过指令 monitorentermonitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

3、JVM对synchronized的优化

参考这篇文章的第五章:https://cloud.tencent.com/developer/article/1465413

① 锁升级

偏向锁:大部分情况下同步方法都是只有一个线程再执行,这样如果加锁会很浪费资源。故在只有一个线程使用时,采用偏向锁即可,偏向锁不是真的加锁,而是用户态的实现。把当前线程的指针JavaThread*写入markword里面54位,这就是偏向锁的实现。jvm启动后4s,偏向锁才会启动

轻量级锁:如果此时多了一个线程来竞争,就升级为轻量级锁(通过cas来竞争的,所以也叫自旋锁)。(偏向锁和自旋锁都是用户空间完成的,重量级锁需要向内核申请)

偏向锁的效率一定比自旋锁效率高吗??不一定。如果明确知道资源有多个线程竞争,不如直接采用自旋锁,加偏向锁还要经过撕除的过程。

重量级锁:如果竞争很激烈(一个线程自旋超过10次或者超过cpu核数一半的线程再竞争),就升级为重量级锁,由内核态来加重量级锁。(synchronized锁叫重量级锁,因为申请所资源必须通过kernel,系统调用)。等待队列waitSet 竞争队列

Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到内核态(https://blog.csdn.net/zzt4326/article/details/89786855),这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

② 锁消除

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是检测到不可能发生数据竞争的锁进行消除。

③ 锁粗化

如果虚拟机检测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • synchronized底层原理
    • 1、需要了解的基础知识
      • ① CAS
      • ② CAS原理
      • ③ 对象内存布局
      • ④ markword的细节
    • 2、synchronized的底层原理
      • 2.1 底层原理:
      • 2.2 同步代码块与同步方法
    • 3、JVM对synchronized的优化
      • ① 锁升级
      • ② 锁消除
      • ③ 锁粗化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档