synchronized详解

1、并发编程会出现原子性、可见性、有序性问题。

原子性:在一次或多次操作中,要么所有的操作都执行并且不会受其它因素干扰而中断,要么所有的操作都不执行。在多线程环境下,线程对共享变量的操作,要么成功,要么失败,不会受其它线程的干扰。

可见性:在多线程环境下,某个线程对共享变量的修改,其它的线程可以知道并获取最新修改的值。

有序性:指程序中代码的执行顺序,java在编译时和运行时对代码进行优化,会导致程序的最终执行顺序不一定是我们Java代码编写时的顺序。

2、JVM内存模型

Java内存模型,是Java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别。

Java内存模型是一套规范,描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节,具体如下。

  • 主内存

主内存是所有线程都共享的,都能访问的。所有的共享变量都存储于主内存。

  • 工作内存

每一个线程有自己的工作内存,工作内存只存储该线程对共享变量的副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问对方工作内存中的变量。
在这里插入图片描述
Java内存模型的作用:Java内存模型是一套在多线程读写共享数据时,对共享数据的可见性、有序性、和原子性的规则和保障。主要就是synchronized和volatile。

3、主内存与工作内存的交互

Java内存模型中定义了以下8种操作来完成,主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
在这里插入图片描述
只有使用synchronized才会有Lock和unlock操作。

  1. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值
  2. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中

4、synchronized如何保证可见性、原子性、有序性?

保证可见性:

由于使用synchronized加锁时,会清空工作内存中的变量的值,导致线程重新读取主存中的最新的值。

从而保证了线程的可见性。使用volatile也可以保证可见性。

保证原子性:

使用synchronized加锁,线程需要获取对象的锁,由于对象的锁,只有一把,同一时间只有一个线程能操作加锁的代码,从而保证了原子性。使用volatile无法保证原子性。

保证有序性:

加了synchronized依然会发生指令重排序,但是我们的synchronized会保证同步块在同一时间只有一个线程访问,就是单线程访问同步代码块,那么在单线程情况下,管你指令怎么重排,指令执行的结果都是一样的,如果指令重排导致在单线程下执行结果不一致,那么也就不会发生指令重排了。从而保证了有序性。使用volatile也可以保证有序性。


为什么要发生指令重排?

为了提高程序的执行效率,编译器和CPU会对程序中的代码进行重排序。

什么情况下可以发生指令重排序?

我们要满足as-if-serial语义:即不管编译器和CPU如何进行重排序,必须保证在单线程环境下,程序的结果是正确的。

5、synchronized的特性

5.1 可重入

一个线程可以重复获取某个对象的锁,即重复获取同一把锁。

可重入原理:

synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,获取锁计数器+1,释放锁计数器-1。

可重入的好处:

  1. 可以避免死锁
  2. 可以使我们更好的封装代码

5.2 不可中断

不可中断是指一个线程在获取锁之后,另一个线程想要获取锁,获取不到处于阻塞或等待状态,如果第一个线程不释放锁,那么阻塞或等待的线程就一直等,并且不可被中断。

synchronized:是不可中断的

Lock:

  • lock()方法:不可中断
  • tryLock()方法:可中断

6、synchronized的原理(jdk1.6以前)

synchronized是JVM内置锁,基于Monitor机制实现。依赖底层操作系统的互斥原语Mutex(互斥量)。
Monitor,直译为“监视器”,而操作系统领域一般翻译为“管程”。管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发。

Java(HotSpot)中的Monitor是基于C++实现的,由ObjectMonitor实现的。

// 初始化monitor,除了semaphore,其他字段都是简单的int或者指针类型
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

ObjectMonitor 的主要参数都在里面,从名字上慢慢分析。

  • _owner:指向持有ObjectMonitor对象的线程
  • _WaitSet:存放处于wait状态的队列
  • _EntryList:存放处于等待锁block状态的队列
  • _recursions:锁的重入次数
  • _count:用来记录该线程获取锁的次数

_WaitSet和 _EntryList有什么区别呢?

当多个线程同时访问同步代码时,首先进入的就是_EntryList 。当获得对象的monitor时,_owner 指向当前线程,_count进行加1。
​若持有monitor的线程调用wait() ,则释放持有的monitor ,_owner 变为null ,_count 减1。
​同时该线程进入_WaitSet 等待被唤醒。如果执行完毕,也释放monitor。

6.1 synchronized代码块:

将synchronized代码块使用javap对字节码进行反编译时,会发现在代码块的前后插入一个monitorenetr和monitorexit的指令。
在这里插入图片描述

JVM规范对monitorenter的描述:

每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获

取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应

的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为
    monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直
    到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

小结:

synchronized的锁对象会关联一个monitor,这个monitor对象不是我们创建的,是JVM的线程在执行到这个同步代码块时,如果发现没有monitor与锁对象关联,则会创建该对象(C++的对象),这个对象有两个成员变量:owner:记录拥有该monitor的线程,recursions:记录线程拥有锁的次数,当一个线程拥有monitor后,其他线程只能等待。

JVM规范对monitorexit的描述:

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出
    monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个
    monitor的所有权。

小结:
monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

6.2 synchronized方法:

同步方法在通过反汇编可以看到会增加ACC_SYNCHRONIZED修饰,会隐式调用monitorenetr和monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit。本质还是monitor。
在这里插入图片描述

6.3 总结:

通过反汇编可以看到在synchronized同步代码是由monitorenter指令开始,monitorexit指令结束,每个锁对象会关联一个monitor(它才是真正的锁对象),这个monitor对象是由JVM创建的,里面有两个成员变量:owner:拥有该锁对象的线程,recursions:该线程获取锁的次数,当JVM执行到某个线程的某个方法内的monitorenter指令,jvm会判断当前锁对象是否有monitor关联,没有就创建,并将owner设置为当前线程,recursions+1,该线程每重入一次,recursions就加一,当执行到monitorexit指令时,recursions-1,直到减为0,这个线程就会释放锁,其他被monitor对象阻塞的线程就可以竞争该锁。


7、synchronized的优化(jdk1.6及以后)

重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cpu。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。所以说synchronized是一个重量级锁,重量级的操作。

针对synchorized的重量级操作及RetrantLock轻量级锁的出现,JDK在1.6版本对synchronized进行了优化,主要有三方面:

7.1 锁升级

无锁 —> 偏向锁 —> 轻量级锁 —> 自旋锁 —> 重量级锁

jdk1.6对synchronized进行优化,减低了直接升级为重量级锁而耗费大量的资源,降低线程获取锁的代价,synchronized的优化会涉及对象头。它是将锁保存在对象头中。
在这里插入图片描述
对象头组成:

  • Mark World:存储对象运行时一些信息,如:hashCode()、分代年龄、锁标志位、偏向锁标志、ThreadID,它的大小和虚拟机的位长一致,如果虚拟机是32位的,它就是32位,虚拟机是64位,他就是64位。
  • Klass pointer:类型指针,这个指针确定这个对象是哪个类的实例。大小和虚拟机的位长也是一致,虚拟机还会对其进行指针压缩,减少该指针占用的空间。

64位虚拟机Mark World的存储结构:
在这里插入图片描述
对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)

  • 在32位系统中,Mark Word = 4 bytes,类型指针 = 4bytes,对象头 = 8 bytes = 64 bits;
  • 在64位系统中,Mark Word = 8 bytes,类型指针 = 8bytes,对象头 = 16 bytes = 128bits;

实例数据:类中定义的成员变量

字节填充:不一定有,Hotspot虚拟机对象的大小是8字节整数倍,不是8字节整数倍会进行字节填充。


偏向锁延迟及匿名偏向锁:

虚拟机默认在4秒后开启偏向锁功能:这叫做偏向锁延迟

  1. 在4s内创建对象,此时对象的对象头的锁标志位:01,偏向标志位:0
  2. 在4s后创建对象,此时对象的对象头的锁标志位:01,偏向标志位:1 (匿名偏向锁,没有存储线程id)

偏向锁在升级为轻量级锁时,会涉及到偏向锁撤销,需要等到一个安全点(STW),才可以做偏向锁撤销,在明知道有并发情况,就可以选择不开启偏向锁,或者是设置偏向锁延迟开启。

因为JVM在启动时,需要加载大量的.class文件到内存中,这个操作会涉及到synchronized的使用,为了避免出现偏向锁撤销操作,JVM启动初期,有一个延迟4s开启偏向锁的操作

偏向锁的加锁和撤销流程:

  • ①创建锁对象(对象头的锁标志位:01,偏向标志位:1)
  • ②当线程访问同步代码块时:
    • ③如果是无锁状态或匿名偏向锁(锁标志位01,偏向标志位0 或 锁标志位01,偏向标志位1):(对象没有存储线程id)
      • 设置锁标志位:01
      • 偏向标志位:1
      • 使用cas将当前线程id记录在对象头mark word的前54位中
    • ④如果是匿名偏向锁:(对象有存储线程id)
      • ⑤判断当前线程的线程id是否与锁对象对象头的线程id是否一致:
        • ⑥一致:当前线程直接进入代码块执行
          • ⑦执行过程中没有其他线程要执行同步代码块:
            • 代码块执行完毕,结束,锁对象对象头还是偏向锁标志
          • ⑧执行过程中存在其他线程要执行同步代码块:
            • 回到⑤
        • ⑨不一致:
          • ⑩撤销偏向锁为无锁状态(需等待全局安全点)
            • 如果目标偏向锁没有正在使用,则恢复成无锁状态
          • ⑪撤销偏向锁升级为轻量级锁
            • 如果偏向锁正在使用,则膨胀成轻量级锁,走轻量级锁加锁流程

轻量级锁的加锁流程:

  1. 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝
  2. 使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针
    1. 更新成功:
      • 当前线程加轻量级锁成功,锁对象的锁标志位变为“00”
    2. 更新失败:
      • 说明至少存在一条线程与当前线程竞争获取该对象的锁
        1. 检查对象的Mark Word是否指向当前线程的栈帧
          1. 是:说明当前线程拥有这个对象的锁(可重入),直接进入同步块执行
          2. 否:说明这个锁对象已经被其他线程抢占,抢锁线程自旋,自旋一定次数后轻量级锁膨胀为重量级锁

轻量级锁解锁流程:

  1. 如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来
    1. 替换成功:说明同步过程就顺利完成,锁对象转为无锁状态
    2. 替换失败:说明有其他线程尝试过获取该锁,并且将锁升级为重量级锁,在释放锁的同时,唤醒被挂起的线程

当对象处于偏向锁状态时,收到计算hashCode的请求

  1. 正在执行同步代码块中的方法:偏向锁会被撤销,并膨胀为重量级锁
  2. 同步代码块中的方法已经执行完:偏向锁会被撤销,恢复成无锁状态

7.2 锁消除:

锁消除是指虚拟机即时编译器(JIT)在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。

public class Demo01 {
    public static void main(String[] args) {
        contactString("aa", "bb", "cc");
    }
    public static String contactString(String s1, String s2, String s3) {
        return new StringBuffer().append(s1).append(s2).append(s3).toString();
    }
}

7.3 锁粗化:

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

public class Demo01 {
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
        sb.append("aa");
        }
        System.out.println(sb.toString());
    }
}	

8、平时代码中对synchronized进行优化的建议:

  • 减少synchronized代码块的范围,减少同步代码块的执行时间,减少锁的竞争。

  • 降低锁的粒度:

    • ConcurrentHashMap
    • LinkedBlockingQueue入队和出队使用不同的锁,相对于读写只有一个锁效率要高。
  • 读写分离
    读取时不加锁,写入和删除时加锁,如:ConcurrentHashMap,CopyOnWriteArrayList和CopyOnWriteSet


引用:
https://blog.csdn.net/v123411739/article/details/117401299?spm=1001.2014.3001.5501
https://blog.csdn.net/qq_40788718/article/details/106450724
https://www.bilibili.com/read/cv14772871/
https://juejin.cn/post/6844903640197513230
https://xiaohuang.blog.csdn.net/article/details/129848342

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值