Fork me on GitHub

synchronized、ReentrantLock、volatile

名词解释

  • synchronized
    是Java中的关键字,是一种同步锁,可以修饰代码块,方法,静态的方法。synchronized(Object) 不能用String常量、Integer、 Long。
  • ReentrantLock
    是一种同步锁,可以实现公平锁机制,获取锁和释放锁都需要手动操作。
  • volatile
    是Java中的关键字,保障可见性,有序性,并不能保证原子性。
    • 可见性
      当线程t1开始运行的时候,会把running值从内存中读到t1线程的工作区,在运行过程中直接使用这个copy,并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行,使用volatile,将会强制所有线程都去堆内存中读取running的值。
    • 有序性
      volatile关键字能禁止指令重排序,所以volatile能在一定程度上保证有序性。
    • 原子性
      不可分割,最小执行单位。

synchronized和ReentrantLock区别

  • synchronized是独占锁,加锁和解锁的过程自动进行,易于操作,但不够灵活;
    ReentrantLock也是独占锁,加锁和解锁的过程需要手动进行,不易操作,但非常灵活。

  • synchronized可重入,因为加锁和解锁自动进行,不必担心最后是否释放锁;
    ReentrantLock也可重入,但加锁和解锁需要手动进行,且次数需一样,否则其他线程无法获得锁。

  • synchronized不可响应中断,一个线程获取不到锁就一直等着;ReentrantLock可以响应中断;

  • ReentrantLock可以实现公平锁机制,synchronized不行;

  • ReentrantLock 底层调用的是Unsafe的park方法加锁,synchronized 操作的应该是对象头中 mark word。

synchronized和volatile区别

  • volatile保证线程的可见性,有序性,但是不能保证其原子性;synchronized保证线程的原子性、可见性,但是不能保证其有序性。

synchronized源码解析

Java对象组成及Synchronized锁存放位置

对象是放在堆内存中的,对象大致可以分为三个部分,分别是对象头,实例变量和填充字节。
image

  • 对象头,主要包括两部分1. Mark Word (标记字段),2.Klass Pointer(类型指针)。Klass Point 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例(即指向方法区类的模版信息)。Mark Word用于存储对象自身的运行时数据。
  • 实例变量,存放类的属性数据信息,包括父类的属性信息,这部分内存按4字节对齐。
  • 填充字节,由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

假如有如下的类,a=100这个信息就存储在实例变量中

public class Test {
    int a = 100;
}

填充数据主要是为了方便内存管理,如你想要10字节的内存,但是会给你分配16字节的内存,多出来的字节就是填充数据

synchronized不论是修饰方法还是代码块,都是通过持有修饰对象的锁来实现同步,那么synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头Mark Word,来看一下Mark Word存储了哪些内容?

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,变化状态如下 (32位虚拟机):
image

锁升级过程

在JDK1.6之前,synchronized都是重量级锁,1.6版本之后,进行了锁的升级;锁可以升级但不能降级,但是偏向锁状态可以被重置为无锁状态。

  • 无锁
    没有线程运行时,此时处于无锁状态。

  • 偏向锁
    当线程1访问代码块并获取锁对象时,会在java对象头(即锁对象头)中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址(即monitor的起始地址),monitor中的Owner指向线程1,把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;如果不一致(其他线程,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。

  • 自旋锁(轻量级锁)(CAS)(Comapre And Swap)
    线程1获取轻量级锁时会先把锁对象的对象头MarkWord复制一份到线程1的栈帧中创建的用于存储锁记录的空间(称为DisplacedMarkWord),然后使用CAS把锁对象头中的内容替换为线程1存储的锁记录(DisplacedMarkWord)的地址。
    如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。 自旋锁简单来说就是让线程2在循环中不断CAS。
    但是如果自旋的时间太长也不行,因为自旋是要消耗CPU的,因此自旋的次数是有限制的,比如10次或者100次,如果自旋次数到了线程1还没有释放锁,或者线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象,那么这个时候轻量级锁就会膨胀为重量级锁。重量级锁把除了拥有锁的线程都阻塞,防止CPU空转。

  • 重量级锁
    线程1持有对象锁时,线程2到达竞争该对象锁,但是线程1还没有释放锁,线程2将自旋,如果线程2自旋次数达到10次,将升级为重量级锁,将正在运行的线程除外都加入队列中,防止空旋,消耗CPU资源。重量级锁调用的是内核,轻量级锁和偏向锁不需要调用内核,是在内存中进行处理。

几种锁的优缺点

image

参考:https://blog.csdn.net/zzti_erlie/article/details/103997713

可重入锁

  • 简介
    可重入锁的意义之一在于防止死锁,实现原理实现是通过为每个锁关联一个请求计数器和一个占有它的线程。当计数为0时,认为锁是未被占有的;线程请求一个未被占有的锁时,JVM将记录锁的占有者,并且将请求计数器置为1 。如果同一个线程再次请求这个锁,计数器将递增;每次占用线程退出同步块,计数器值将递减。直到计数器为0,锁被释放。
    关于父类和子类的锁的重入:子类覆写了父类的synchonized方法,然后调用父类中的方法,此时如果没有可重入的锁,那么这段代码将产生死锁。
  • 释放锁条件
    • 计数器为0,正常结束;
    • 抛出异常,计数器直接归零。

AbstractQueuedSynchronizer(AQS)和Synchronized区别

Synchronized
  • Synchronized关键字在底层的C++实现中,存在两个重要的数据结构(集合):WaitSet和EntryList。
  • WaitSet中存放的是调用了Object的Wait方法的线程对象(被封装成了C++的Node对象)。
  • EntryList中存放的是陷入阻塞状态,需要获取moniter的那些个线程对象。
  • 当一个线程被notify后,它就会从WaitSet中移动到EntryList中去。
  • 当进入到EntryList后,该线程依然需要与其他的线程竞争moniter对象。
  • 如果争抢到了,就表示该线程获取到了对象的锁,它可以以排他的方式对应的执行同步代码。
AQS
  • AQS中存在两个队列,分别是condition对象上的条件队列,以及AQS本身的阻塞队列。
  • 这两个队列中的每一个对象都是Node实例(里面封装了线程对象)。
  • 当condition条件队列中的线程被signal后,该线程就会从条件队列中被转移到AQS阻塞队列中去。
  • 位于AQS阻塞队列中的Node对象本质上都是由一个双向链表去构成的(CLH)
  • 在获取AQS锁的时候,这些进入到阻塞队列中的线程会按照在队列中的排序先后尝试的去获取锁(公平锁)。
  • 当阻塞队列中的线程获取锁后,就表示改线程可以正常执行了
  • 陷入到阻塞状态的线程,依然需要进入到操作系统内核态,进去阻塞(park方法)。

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制,AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

参考

https://blog.csdn.net/Ypopstar/article/details/106898129;
https://www.cnblogs.com/waterystone/p/4920797.html。
https://www.jianshu.com/p/19f861ab749e
https://blog.csdn.net/m0_58559010/article/details/118423485

posted @ 2022-01-13 19:57  晨度  阅读(261)  评论(0编辑  收藏  举报