1. 乐观锁和悲观锁
锁可以分为两类:乐观锁和悲观锁。
悲观锁:在操作数据前,总认为其他线程会修改,所以会加锁,直到处理完后释放锁。比如:synchronized关键字
乐观锁:在操作数据前,总认为其他线程不会会修改,只有在更新时候才会判断数据是否有变化。比如cas。
2.volatile
提到锁的时候不得不说volatile关键字。volatile可以保证可见性,但不能保证原子性。volatile修饰的变量,在写操作时候会加上Lock前缀的指令,作用:
- Lock前缀的指令会引起处理器缓存回写到内存
- 一个处理器缓存回写到内存会导致其他处理器的缓存无效
简单的说volatile修饰的变量,一个线程的写,其他线程可以读到。
从内存语义角度:
volatile写和锁的释放有相同的内存语义;volatile读和加锁有相同的内存语义
为实现上述内存语义:
为了实现volatile的内存语义,JMM针对编译器指定的volatile重排序规则表
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。基于保守策略的JMM内存屏障插入策略:
3.synchronized
jvm是基于monitor对象进行方法块的同步,代码编译后,在同步开始部位,加monitorenter指令,在方法结束和异常处加monitorexit指令。当一个monitor对象被持有后,处于锁定状态。线程执行到monitorenter后,将尝试获取monitor对象,即加锁。
为了减少加锁和释放锁带来的性能消耗,java在1.6对synchronized进行了优化,引入了偏向锁和轻量级锁。锁的状态从低到高分为无锁状态、偏向锁、轻量级锁、重量级锁。锁可以升级,但不能降级。不能降级目的是为了提高加锁和释放锁的效率。
- 偏向锁:偏向锁的引入是因为大多数情况下,同一把锁总是由一个线程来获得,不需要加锁和释放锁。当一个线程访问同步块时,会判断对象头和栈帧中是否记录了当前线程id,如果没有,再判断当前是否是偏向锁,没有设置,则cas竞争锁。如果设置了,cas尝试将线程标识指向自己。如果失败,升级为轻量级锁。
- 轻量级锁:线程进入同步块时,如果同步对象为无锁状态(锁标志更新为01),并且不是偏向锁。线程会在栈帧中建立一个锁记录(LockRecord)空间,并将对象的mark word拷贝到锁记录。拷贝成功后,cas尝试将对象的mark word更新为指向Lock Record的指针,并将Lock Record的owner指向object markword。如果更新成功,则代表获得锁,将锁标志更新为00。更新失败,检查对象的mark worrd是否指向当前线程栈帧,如果是,则代表已经有锁,继续执行。如果不是,代表有锁竞争,升级为重量级锁,锁标志的状态值变为10.
4.CAS
CAS的全称是Compare And Swap即比较交换,其算法核心思想如下:执行函数:CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。
CPU指令对CAS的支持:
- CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断。
- Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。Java中CAS操作的执行依赖于Unsafe类的方法。
并发包中的原子操作类就是基于CAS原理。比如AtomicBoolean,AtomicInteger,AtomicLong。
CAS中的问题:
- ABA问题:CAS只能判断值最终是否有变化,无法判断中间是否变化。对于ABA问题,可以带上版本号,每次更新除了判断值是否有变化,还需版本号是否有变化。AtomicStampedReference类就是来解决此问题,AtomicStampedReference类是一个带有版本号的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的版本号。当对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时的ABA问题。
- 循环时间长开销大:长时间的自旋CAS,毫无疑问会消耗CPU。可以延迟一定时间再进行下一次循环,并且针对某些业务,CAS一定次数后,可以放弃执行。
- 只能保证一个共享变量的原子操作。多个共享变量可以考虑合并为一个对象。
5. Lock接口和synchronized
synchronized是java内置语言,Lock是接口。synchronized 代码简洁,Lock:获取锁可以被中断,超时获取锁,尝试获取锁,读多写少用。
ReentrantLock是典型的Lock实现,ReentrantLock和synchronized都是可重入锁,同时也是排它锁。什么是公平锁和非公平锁?如果在时间上,先对锁进行获取的请求,一定先被满足,这个锁就是公平的,不满足,就是非公平的。显然非公平锁效率更高。
6.LockSupport
park开头的方法负责阻塞线程,unpark(Thread thread)方法负责唤醒线程。这个类作用有点类似多线程工具类的Semaphore。通过许可证permit来联系使用它的线程。调用park方法消费这个许可证,不然会阻塞。调用unpark使这个许可证可用。和Semaphore不同的是,许可证最多只有一个。
和Object的notify和wait方法不同的是,因为有许可证的存在,park和unpark的先后顺序就不重要了。而wait和notify不同,先调用notify,再调用wait方法,线程依旧阻塞。