多线程与并发

线程安全问题的主要诱因

  • 存在共享数据(临界资源)
  • 存在多条线程共同操作这些共享数据

解决问题的根本办法:

同一时刻有且只有一个线程再操作共享数据,发其他线程必须等到该线程处理完数据后再对共享数据进行操作。

互斥锁的特性

  • 互斥性:即在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块进行访问。互斥性也称为操作的原子性。
  • 可见性:必须确保在锁被释放之前,对共享变量所作的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本上继续操作,从而引起不一致。

synchronized

synchronized锁的不是代码,是对象

根据获取的锁的分类:

  • 获取对象锁
    • 主要有两种用法:
      1. 同步代码块(synchronized(this) , synchronized(类实例对象)),锁是小括号()中的实例对象。
      2. 同步非静态方法(synchronized method),锁是当前对象的实例对象。
  • 获取类锁
    • 两种方法:
      1. 同步代码块(synchronized(类.class)),锁是小括号()中的类对象(Class对象)
      2. 同步静态方法(synchronized static method),锁是当前对象的类对象(Class对象)

synchronized底层实现原理

实现synchronized的基础

  • Java对象对
  • Monitor

对象在内存中的布局

  • 对象头
  • 实例数据
  • 对齐填充

对象头的结构:

image-20210121213718004 image-20210121213848642

Monitor:每个Java对象天生自带了一把看不见的锁

image-20210121214226607

什么是重入:

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入。

早期synchronized效率低下

  • 早期版本中,synchronized属于重量级锁,依赖于Mutex Lock实现
    • 线程之间的切换需要从用户态转移到核心态,开销较大
  • Java6以后,synchronized性能得到很大的提升
    • Adaptive Spinning
    • Lock Eliminate
    • Lock Coarsening
    • Lightweight Locking
    • Biased Locking

自旋锁与自适应自旋锁

自旋锁:

  • 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
  • 通过让线程执行忙循环等待锁的释放,不让出cpu时间
  • 缺点:若锁被其他线程长时间占用,会带来许多性能上的开销
  • 用户可以使用PreBlockSpin参数来修改自旋次数

自适应自旋锁

资源锁定时间不确定,PreBlockSpin不太智能

  • 自旋的次数不再固定
  • 由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定

锁消除

更彻底的优化

  • JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
  • 如果能确认某个加锁的对象不会逃逸出局部作用域,就可以进行锁删除。

锁粗化

是锁清除的另一种极端,如果存在一连串操作被同一个线程反复加锁释放锁(例如再循环中加锁),则可以进行锁粗化

  • 通过扩大加锁的范围,避免反复加锁和解锁

synchronized的四种状态

  • 无锁、偏向锁、轻量级锁、重量级锁

锁膨胀的方向:

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

img
  • 无锁:没有任何锁

  • 重量级锁:上面讲到的synchronized方法,常规锁

  • 偏向锁:减少统一线程获取锁的代价

    • 大多数情况下,锁不存在多线程竞争,总是由同一线程多次获得
    • 核心思想:如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查Mark Word的锁标记位为偏向锁及当前线程ID等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
    • 不适用于锁竞争比较激烈的多线程场合
    • 偏向锁失败后不会立即膨胀为重量级锁,而是升级为轻量级锁
  • 轻量级锁:

    • 轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁
    • 适用的场景:线程交替执行同步块
    • 若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
    • image-20210121231328102
  • 重量级锁:

    • 如上轻量级锁的加锁过程步骤(5),轻量级锁所适应的场景是线程近乎交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。Mark Word的锁标记位更新为10,Mark Word指向互斥量(重量级锁)
  • Synchronized的重量级锁是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。

锁的内存语义

当线程释放锁时,Java内存模型会把该线程对应的本地内存中的共享变量率刷新到主内存中;

而当线程获取锁时,Java内存模型会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

image-20210121232211318

锁的汇总

image-20210121232253804

synchronized和ReentrantLock的区别

ReentrantLock(再入锁)

  • 位于java.util.concurrent.locks包
  • 和CountDownLatch、FutureTask、Semaphore一样基于AQS实现
  • 能够实现比synchronized更细粒度的控制,如控制fairness
  • 调用lock()之后,必须调用unlock()释放锁
  • 性能未必比synchronized高,并且也是可重入的

ReentrantLock公平性的设置

  • ReentrantLock fairLock = new ReentrantLock(true);
  • 参数为true时,倾向于将锁赋予等待时间最久的线程(公平锁)
  • 公平锁:获取锁的顺序按先后调用lock方法的顺序(慎用)
  • 非公平锁:抢占的顺序不一定,看运气
  • synchronized是非公平锁

ReentrantLock将锁对象化

  • 判断是否由线程,或者某个特定线程,在排队等待获取锁
  • 带超时的获取锁尝试
  • 感知有没有成功获取锁

是否能将wait、notify、notifyAll对象化

  • java.util.concurrent.locks.Condition

synchronized和ReentrantLock的区别

  • synchronized是关键字,ReentrantLock是类
  • ReentrantLock可以对获取锁的等待时间进行设置,避免死锁
  • ReentrantLock可以获取各种锁的信息
  • ReentrantLock可以灵活地实现多路通知
  • 锁机制:sync操作Mark Word,lock调用Unsafe类的park()方法