-
乐观锁
乐观锁假设认为数据一般情况下不会产生冲突,所以在数据进行提交更新时,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让用户返回错误的信息,让用户决定如何去做。
乐观的心态看待线程安全问题,字节修改共享变量,基于某些机制,要么直接修改成功(没有冲突),要么修改失败(通过返回值可以知道,一般是Boolean)。不管成功还是失败,自行决定下一步怎么做(Java程序层面看,是非阻塞式的方式) -
悲观锁
总是假设最坏的情况,每次去拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
悲观的心态看待线程安全问题,每次都要申请加锁,如果加锁失败,则阻塞。 -
CAS机制
Compare and Swap:比较并交换,属于乐观锁的一种实现方式,从Java代码层面看,属于无锁操作。
CAS比较交换的过程可以通俗理解为CAS(V,O,N),包含三个值分别为:V内存地址存放的实际值,O预期值(旧值),N更新的新值。
CAS可能存在的ABA问题:因为CAS会检查就值有没有变化,这里存在这样的问题。比如一个就值A变为了B,然后再变成A,刚好在做CAS时检查发现就值并没有发生变化依然为A,但实际上确实发生变化了。
解决方案:可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。在JDK1.5后的atomic包中提供了AtomicStampeReference来解决问题。
解决原理:Java的CAS利用的是unsafe这个类提供的CAS操作,基于CPU的硬件指令
- 自旋锁
基于CAS操作变量时,如果出现CAS修改失败,可以使用自旋的方式,再次尝试:使用CAS再次修改自旋就是指在CAS失败时,不停的循环CAS操作。
优缺点:
自旋操作,本身还是线程的运行态执行代码,会占据一定的系统资源
使用场景:
(1)大多数场景下,在同一个时间点,常常只有一个线程对变量操作(CAS的场景)
(2)CAS的执行时间,不能太长,否则线程冲突的几率会很大,进一步导致CAS操作失败的线程浪费较多的系统资源。
- synchronized同步锁
作用:对对象头加锁的方式,保证线程安全,多线程申请同一把锁,会产生同步互斥的作用
原理:
(1)synchronized基于系统的mutex lock,就属于悲观锁
(2)该关键字会编译为字节码1个monitiorenter+多个monitorexit指令(多个的目的,是正常执行和异常执行都要释放锁)
(3)对对象头加锁,有偏向锁、轻量级锁、重量级锁(从低到高),三种状态,只能升级不能降级
重量级锁:大多数情况下,在同一时间点,常常有多个线程竞争同一把锁,悲观锁的方式,竞争失败的线程会不停的在阻塞及被唤醒态之间切换,代价比较大
轻量级锁:大多数情况下,在同一个时间点常常只有一个线程竞争对象锁,乐观锁CAS的实现
偏向锁:同一个线程,重入的方式,多次申请同一把锁。
- 死锁
产生条件:多个线程申请资源,持有锁的线程还没有释放锁的情况下,再次申请其他线程始终不释放的锁。结果就造成了死锁的线程始终处于阻塞状态,任务一直无法执行。
检测手段:
jconsole:检测死锁
jstack
- volatile
作用:
(1)禁止指令重排序
(2)建立内存屏障,禁止指令重排序
原理:
(1)如何保证可见性?
总线嗅探技术:基于volatile修饰的变量,如果某个CPU修改变量值,其他CPU就会马上得到通知
缓存一致协议:读取时,CPU缓存值置为无效,从主存读写回主存,发起通知。
(2)如何禁止指令重排序?
指令重排序,有as-if-serial的规范,规定了如何重排序(单线程下,前后关联的不能重排序),但不保证多线程下,指令之间的重排序
字节码层面看:提供主存到工作内存,读写操作的8大原子性指令,这些指令存在happens-before的原则
- 程序顺序规则:一个线程的每个操作,happens-before于该线程中的任意后序操作
- 监视器规则:对一个锁的解锁,happens-before于随后对这个锁的加锁
- volatile变量规则:对于一个volatile域的写,happens-before于任意后序对这个volatile域的读
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
-
CPU层面看: 使用volatile修改的变量操作,CPU指令(汇编)是lock前缀指令
- 变量值写回主存时,之前的操作都需要完成
- 加锁属于总线锁/高速缓存锁
- 加锁其实就类似于建立了一个屏障:写回主存,之前的都完成,之后的读操作,必须等写操作完成(读、写不能重排序)