《Jave并发编程的艺术》学习笔记(1-2章)
Jave并发的艺术
并发编程的挑战
上下文切换
CPU通过时间片分配算法来循环执行任务,当前时间片执行完之后会切换到下一个任务。但是,切换会保存上一个任务的状态,一遍下次切换回这个任务时,可以再次加载这个状态。所以任务从保存到再加载的过程就是一次上下文切换。
如何减少上下文切换
减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程
- 无锁并发编程:多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法避免使用锁,如将数据ID按照Hash算法取模分段,不同的线程处理不同段的数据
- CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需要加锁
- 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态
- 协程:在单线程实现多任务的调度,并在单线程里维持多个任务间的切换
死锁
死锁常见的原因是,线程之间相互等待。避免死锁的几个常见方法:
- 避免一个线程获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来代替使用内部锁机制
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况
底层实现原理
volatile
volatile的定义与实现原理
Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获取这个变量。所以java提供了volatile,在某些情况下面比锁更加的方便。如果一个字段被声明为volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
volatile如何保证可见性:通过Lock前缀指令,对volatile进行写时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存
- Lock前缀指令会引起处理器缓存行写回到内存
- 一个处理器的缓存回写会导致其他处理器的缓存无效
synchronized
synchronized的介绍
并发编程里面元老级别的存在,重量级的锁。Java里面每一个对象都可以作为锁
- 对于普通的方法,锁是当前实例的对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步方法块,锁是Synchronized括号里配置的对象
Java对象头
synchronized用的锁是存在Java对象头里面的。对象头的内容
- Mark Word:存储对象的HashCode或锁信息
- Class Metadata Address:储存到对象类型数据的指针
- Array length:数组的长度
Mark Word里默认存储对象的HashCode、分代年龄和锁标记,Mark Word里储存的数据会随着锁标志位的变化而变化。以下是四种状态的变化
- 轻量级锁:指向栈中锁记录的指针,锁标志位 00
- 重量级锁:指向互斥量(重量级锁)的指针 ,锁标志位 10
- GC标记:空,锁标志位 11
- 偏向锁:线程ID,Epoch,对象分代年龄,是否是偏向锁,锁标志位 01
锁的升级与对比
锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态,轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。注意:锁可以升级但是不能够降级。一些的设计目的只有一个。为了提高获得锁和释放锁的效率
- 偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由他同一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。如:当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再次进入和退出同步块时不需要进行CAS操作来加锁和解锁,仅仅判断一下对象头里面的Mark Word里面是否存储着指向当前线程的偏向锁。如果有,表示当前线程已经获得了锁;如果没有,则使用CAS竞争锁,再尝试使用CAS将对象头的偏向锁指向当前线程
- 轻量级锁:线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于储存记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程就会获得锁,如果失败,表示其他线程竞争锁,当前线程便可以尝试通过自旋来获取锁,自旋失败之后,锁就会膨胀成重量级锁
- 重量级锁:因为自旋会消耗CPU,为了避免无用的自旋(如:获得锁的线程被阻塞住了),一旦锁升级为重量级锁就不会恢复到轻量级锁状态。当锁出于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进入新一轮的夺锁之争
原子操作的实现原理
原子(atomic),不能被进一步分割的最小粒子,原子操作:不能被中断的一个或一系列的操作。
处理器如何实现原子操作
- 第一个机制,通过总线锁来保证原子性
- 第二个机制,通过缓存锁定来保证原子性
Java如何实现原子操作
可以通过锁,和循环CAS的方式实现原子操作
CAS
CAS(Compare And Swap):比较并且交换。一个旧值,一个新值,操作之前比较旧值有没有发生变化,如果没有发生变化,才换成新值,发生了变化则不交换
CAS存在的三大问题:
- ABA问题:CAS操作需要在操作的值的时候,检查值有没有发生变化。但是如果一个值原来是A,变成了B,又变成了A,使用CAS进行检查时会发现他的值没有发生变化,但是实际上他发生了变化。解决方案:给变量加上版本号
- 循环时间长开销大:自旋CAS如果不成功,会给CPU带来非常大的开销
- 只能保证一个共享变量的原子操作:JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作