CP-17-锁优化机制

Java的线程是映射到操作系统的线程的。线程之间切换上下文是需要代价的。操作系统存在多个cpu时,多个线程可真正的实现并行的执行。

自旋锁

当一个线程获取到锁后,另一个线程则会获取失败,传统失败后的做法是将线程挂起,等待拿着锁的线程释放后通知信号,重新恢复现场执行。但是JVM团队注意到访问共享资源的加锁时间并不都是很长,有时同步块或加锁代码区中的代码执行远小于线程挂起切换的代价,此时,存在多个cpu时,多个线程能真正并行执行,一个线程在拿着锁访问共享资源,假定拿着锁的线程很快就会释放锁,可让另一个获取锁的线程不被挂起,而是让它执行一个忙循环,也就是自旋,继续占用cpu,实时检测锁是否可被获取,以此来避免线程被挂起调度切换所需的代价。

线程自旋并不能代替线程阻塞被挂起,因为线程自旋时,会浪费cpu时间做无用功,如果拿着锁的线程很快就释放,则效果很好,长时间不释放,则会白白浪费。所以自旋的时间要有一定限度。

自旋锁在JDK1.4.2引入,需手动-XX:+UseSpinning来开启,默认是限定次数10次,可以使用-XX:PreBlockSpin来更改。JDK6中已经变为默认开启了,并引入了自适应的自旋锁。自适应意味着自旋的时间不固定了,而是由上一次在锁上自旋的时间和线程当前的状态决定。

在同一锁对象上,如果一个线程通过自旋刚刚获得过锁,然后现在该锁被某个线程持有,下一次另一个线程通过自旋获取锁时,JVM会让自旋更长时间。如果对一个锁对象,自旋很少成功获得过,以后可能会省略掉自旋,直接挂起线程,避免浪费cpu时间。

自旋在轻量级锁中使用,重量级锁中线程不使用自旋

偏向锁 -> 轻量级锁 -> 重量级锁

偏向锁是JDK1.6引入的一种锁优化的机制,用来消除无竞争的锁的同步原语。

偏向锁会偏向于第一次获取该锁的线程,如果此后,没有其他线程来获取该锁,那它的锁同步会被消除。大多数情况下,锁住的代码通常被一个线程多次获取访问,为此引入了偏向锁。当有其他线程去尝试访问该锁时,偏向模式就宣告结束,通过自旋来获取锁,如果自旋成功则依然处于轻量锁,自旋失败则会膨胀为重量级锁。

悲观锁

假定每次获取锁都会发生冲突,屏蔽一切影响数据完整性的情况

乐观锁

假定每次获取锁都不会发生冲突,只是在提交时检测是否违反数据完整性。(使用版本号或时间戳配合实现)

共享锁

事务T对线程A加了共享锁后,线程B只能再加共享锁,而不能再加排他锁。

获取共享锁的事务只能读取数据,而不能修改。

排他锁

事务T对线程A加了排他锁后,其他线程既不能加共享锁,也不能添加排他锁。

获取排他锁的事务既可读数据,也可修改数据。

读写锁

如:ReentrantReadWriteLock。

互斥锁

一次最多只能有一个线程获取该锁。如:synchornzied和JUC的Lock。

非公平锁

多个线程在等待一个锁时,允许抢占的获取

会存在线程“饿死”,即很早就请求锁,但一直被其他线程抢占得不到锁而“饿死”。

效率相对公平锁会高,不需要额外的控制获取锁顺序。

可重入锁

如果在一个线程中,获取某个函数的锁后,在起内部调用其他需获取该锁的方法时,可再次获取到

ReentrantLock和synchornized都是可重入的。

可重入锁最大作用可避免死锁。

对象锁

同一个实例对象作为锁

类锁

使用类字节码作为锁

无锁

要保证现场安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。

  1. 无状态编程。无状态代码有一些共同的特征:不依赖于存储在对上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非无状态的方法等。可以参考Servlet。
  2. 线程本地存储。可以参考ThreadLocal
  3. volatile:保证其修饰变量的内存可见性
  4. CAS:利用cpu的cas指令,执行原子的修改变量
  5. 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。