Java多线程原理
线程安全的主要原因
- 存在共享数据
- 存在多条线程共同操作这些共享数据
- 因此需要同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后在对共享数据进行操作。
互斥锁的特性(原子性、可见性)——synchronized能保证这两点
互斥性:在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块进行访问。互斥性也称为操作的原子性。
可见性(volatile):必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁时应获得最新共享变量的值),否则另一个线程可能是在本地缓存的某个副本继续操作,从而引起不一致。
synchronized锁的不是代码,时对象
锁的分类:获取对象锁和获取类锁
获取对象锁的两种方法
- 同步代码块锁住小括号中的实例对象。
- synchronized(this)
- synchronized(类实例对象,例如this)
- 同步非静态方法
- synchronized method
- 锁住的是当前对象(this)的实例对象
获取类锁的两种方法
- 同步代码块
- synchronized(类.class)
- 同步静态方法
- synchronized static method
总结:
对象锁:
- 有线程访问对象的同步代码块/同步方法时,另外的线程可以访问该对象的非同步代码块/非同步方法
- 若锁住同一个对象,一个线程访问同步代码块/同步方法时,另一线程访问同步代码块/同步方法会被阻塞
- 不同对象的对象锁互不干扰
类锁:
- 类锁表现上与对象锁类似,由于一个类只有一把对象锁,所以同一个类的所有使用类锁的不同对象将会是同步的
- 类锁和对象锁互不干扰
synchronized底层实现原理
Monitor:每个Java对象天生自带了一把看不见的锁
Monitor锁的竞争、获取与释放
- 其中EntryList是之前提到到过的锁池
- WaitSet是等待池
重入
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态。
但当一个线程再次请求自己持有对象锁的临界资源时,这种情况叫做重入。
换句话说,synchronized是可以再次上锁的(synchronized代码块中可以再写synchronized)。
死锁
所谓死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。死锁产生的4个必要条件:
- 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。
自旋锁与自适应自旋锁
自旋锁
- 许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得
- 通过让线程执行忙循环等待锁的释放,不让出CPU(while(true))
- 不再挂起/恢复(开销比较大)
- 缺点:若锁被其他线程长时间占用,则自旋锁将带来许多性能上的开销(PreBlockSpin)
自适应自旋锁
- 自选次数不再固定
- 由前一次在同一个锁上的自旋时间及锁的拥有着的状态来决定
Java中的四种锁
膨胀方向:无锁->偏向锁->轻量级锁->重量级锁(该过程不可逆)
偏向锁:减少同一线程获取锁的代价(mark word 01)
- 大多数情况下,锁虽然存在,但不存在多线程竞争,总是由同一线程多次获得
- 不适用于锁竞争比较激烈的多线程场合
核心思想:
- 如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word的结构也变为了偏向锁结构。当该线程再次请求锁时,无需再做任何同步操作,即获取所得过程只需要检查Mark Word的锁标记位为偏向锁以及当前线程Id等于Mark Word的ThreadID即可,这样就省去了大量有关锁申请的操作。
- 因此单线程下对代码进行synchronized,其效率也不低
偏向锁的获取过程
访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3。
如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4。
如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the world)
- 执行同步代码。
偏向锁的释放
- 偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“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的区别
- volatile本质是在告诉JVM当前变量在寄存器(工作内存中)的值时不确定的,需要从主内存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住知道该线程完成变量操作为止;
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法和类级别;
- volatile仅能实现变量的修改可见性,不能保证原子性;synchronized则可以保证变量修改的可见性和原子性;
- volatile不会造成线程阻塞;synchronized可能会造成线程阻塞;
- 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 会维护版本