multiThreadTheory

Java多线程原理

线程安全的主要原因

  • 存在共享数据
  • 存在多条线程共同操作这些共享数据
  • 因此需要同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后在对共享数据进行操作。

互斥锁的特性(原子性、可见性)——synchronized能保证这两点

互斥性:在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块进行访问。互斥性也称为操作的原子性。

可见性(volatile):必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本继续操作,从而引起不一致。

synchronized锁的不是代码,时对象

锁的分类:获取对象锁和获取类锁

获取对象锁的两种方法

  1. 同步代码块锁住小括号中的实例对象。
    • synchronized(this)
    • synchronized(类实例对象,例如this)
  2. 同步非静态方法
    • synchronized method
    • 锁住的是当前对象(this)的实例对象

获取类锁的两种方法

  1. 同步代码块
    • synchronized(类.class)
  2. 同步静态方法
    • synchronized static method

总结:

对象锁:

  • 有线程访问对象的同步代码块/同步方法时,另外的线程可以访问该对象的非同步代码块/非同步方法
  • 若锁住同一个对象,一个线程访问同步代码块/同步方法时,另一线程访问同步代码块/同步方法会被阻塞
  • 不同对象的对象锁互不干扰

类锁:

  • 类锁表现上与对象锁类似,由于一个类只有一把对象锁,所以同一个类的所有使用类锁的不同对象将会是同步的
  • 类锁和对象锁互不干扰

synchronized底层实现原理

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

Monitor锁的竞争、获取与释放

Monitor竞争

  • 其中EntryList是之前提到到过的锁池
  • WaitSet是等待池

重入

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态。

但当一个线程再次请求自己持有对象锁的临界资源时,这种情况叫做重入。

换句话说,synchronized是可以再次上锁的(synchronized代码块中可以再写synchronized)。

死锁

所谓死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。死锁产生的4个必要条件:

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。

自旋锁与自适应自旋锁

自旋锁

  • 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
  • 通过让线程执行忙循环等待锁的释放,不让出CPU(while(true))
  • 不再挂起/恢复(开销比较大)
  • 缺点:若锁被其他线程长时间占用,则自旋锁将带来许多性能上的开销(PreBlockSpin)

自适应自旋锁

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

Java中的四种锁

膨胀方向:无锁->偏向锁->轻量级锁->重量级锁(该过程不可逆)

偏向锁:减少同一线程获取锁的代价(mark word 01)

https://blog.csdn.net/zqz_zqz/article/details/70233767)

  • 大多数情况下,锁虽然存在,但不存在多线程竞争,总是由同一线程多次获得
  • 不适用于锁竞争比较激烈的多线程场合

核心思想:

  • 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为了偏向锁结构。当该线程再次请求锁时,无需再做任何同步操作,即获取所得过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
  • 因此单线程下对代码进行synchronized,其效率也不低

偏向锁的获取过程

  1. 访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。

  2. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。

  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。

  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the world)

  5. 执行同步代码。

偏向锁的释放

  • 偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或者偏向其他线程的偏向锁状态。

轻量级锁(mark word 00)

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

轻量级锁的加锁过程

  • JVM在线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark word 存储在该空间,然后线程尝试将mark word替换为指向锁记录的指针(锁标记位为00),成功:线程获取锁,失败:线程通过自旋(CAS)来获取锁,若CAS失败依然失败,则将锁膨胀为重量级锁。

轻量级锁的释放

线程通过CAS将栈帧存储锁记录的空间Mark word替换回来,成功,锁释放,失败:存在线程线程竞争锁,锁膨胀为重量级锁。

重量级锁

此状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的锁争夺。

锁优化

锁消除

JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁,提高效率

锁粗化

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

synchronized的四种状态

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

锁膨胀方向:无锁 -> 偏向锁 - >轻量级锁 - >重量级锁

四种锁的对比:

优点 缺点 适用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 适用于只有一个线程访问同步块场景。
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度。 如果始终得不到锁竞争的线程使用自旋会消耗CPU。 追求响应时间。同步块执行速度非常快。
重量级锁 线程竞争不使用自旋,不会消耗CPU。 线程阻塞,响应时间缓慢。 追求吞吐量。同步块执行速度较长。
  • 偏向锁:适用于只有一个线程的情况,存在线程竞争时,会带来锁撤销的消耗,加锁和解锁不需要额外的消耗,只需要判断对象头中是否存在该线程ID。如果没有指向该线程ID,则需要CAS操作
  • 轻量级锁:竞争的线程不会阻塞,通过自旋来获得锁,消耗cpu资源
  • 重量级锁:线程阻塞,需要用户态到系统态的切换,追求吞吐量

volatile变量为何立即可见

  • 当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中;
  • 当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效。

volatile如何禁止重排优化

通过插入内存屏障指令禁止在内存屏障前后的指令执行重排序优化

强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本

内存屏障(Memory Barrier)

  • 保证特定操作的执行顺序
  • 保证某些变量的内存可见性

volatile和synchronized的区别

  1. volatile本质是在告诉JVM当前变量在寄存器(工作内存中)的值时不确定的,需要从主内存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住知道该线程完成变量操作为止;
  2. volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别;
  3. volatile仅能实现变量的修改可见性,不能保证原子性;synchronized则可以保证变量修改的可见性和原子性;
  4. volatile不会造成线程阻塞;synchronized可能会造成线程阻塞;
  5. volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

补充:CAS(Compare and Swap)——乐观锁

一种高效实现线程安全性的方法

  • 支持原子更新操作,适用于计数器,序列发生器等场景
  • 属于乐观锁机制(lock-free)
  • CAS失败时由开发者决定操作还是挂起

思想

  • CAS–Compare And Swap 比较并交换–通过比较传入的旧值和原内存位置中的值比较,来决定是不是要更新数据。
  • CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项 乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS的使用多数情况下是对开发者透明的

  • J.U.C的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具,是很多线程安全程序的首选

缺点:

  • 如果循环时间长,则开销很大
  • 只能保证一个共享变量的原子操作(多个只能用锁)
  • ABA问题(读取和检查时,变量值都为A,期间可能变为B,但是却不能被感知)
    • 解决: AtomicStampedReference 会维护版本
Ty.Wings wechat
欢迎您订阅我的公众号,并在GitHub上为我Star!