锁专题(1)java 常见锁介绍,高级程序员必知必会( 二 )


在许多场景中 , 同步资源的锁定时间很短 , 为了这一小段时间去切换线程 , 线程挂起和恢复现场的花费可能会让系统得不偿失 。 如果物理机器有多个处理器 , 能够让两个或以上的线程同时并行执行 , 我们就可以让后面那个请求锁的线程不放弃CPU的执行时间 , 看看持有锁的线程是否很快就会释放锁 。
而为了让当前线程“稍等一下” , 我们需让当前线程进行自旋 , 如果在自旋完成后前面锁定同步资源的线程已经释放了锁 , 那么当前线程就可以不必阻塞而是直接获取同步资源 , 从而避免切换线程的开销 。 这就是自旋锁 。
锁专题(1)java 常见锁介绍,高级程序员必知必会文章插图
输入图片说明
自旋锁本身是有缺点的 , 它不能代替阻塞 。
自旋等待虽然避免了线程切换的开销 , 但它要占用处理器时间 。 如果锁被占用的时间很短 , 自旋等待的效果就会非常好 。 反之 , 如果锁被占用的时间很长 , 那么自旋的线程只会白浪费处理器资源 。
所以 , 自旋等待的时间必须要有一定的限度 , 如果自旋超过了限定次数(默认是10次 , 可以使用-XX:PreBlockSpin来更改)没有成功获得锁 , 就应当挂起线程 。
自旋锁的实现原理同样也是CAS , AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作 , 如果修改数值失败则通过循环来执行自旋 , 直至修改成功 。
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;}自旋锁在JDK1.4.2中引入 , 使用-XX:+UseSpinning来开启 。
JDK 6中变为默认开启 , 并且引入了自适应的自旋锁(适应性自旋锁) 。
自适应意味着自旋的时间(次数)不再固定 , 而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定 。 如果在同一个锁对象上 , 自旋等待刚刚成功获得过锁 , 并且持有锁的线程正在运行中 , 那么虚拟机就会认为这次自旋也是很有可能再次成功 , 进而它将允许自旋等待持续相对更长的时间 。 如果对于某个锁 , 自旋很少成功获得过 , 那在以后尝试获取这个锁时将可能省略掉自旋过程 , 直接阻塞线程 , 避免浪费处理器资源 。
在自旋锁中另有三种常见的锁形式:TicketLock、CLHlock和MCSlock , 本文中仅做名词介绍 , 不做深入讲解 , 感兴趣的同学可以自行查阅相关资料 。
无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁这四种锁是指锁的状态 , 专门针对 synchronized 的 。
详情见 synchronized
公平锁、非公平锁公平锁(Fair)加锁前检查是否有排队等待的线程 , 优先排队等待的线程 , 先来先得 。
公平锁是指多个线程按照申请锁的顺序来获取锁 , 线程直接进入队列中排队 , 队列中的第一个线程才能获得锁 。 公平锁的优点是等待锁的线程不会饿死 。 缺点是整体吞吐效率相对非公平锁要低 , 等待队列中除第一个线程以外的所有线程都会阻塞 , CPU唤醒阻塞线程的开销比非公平锁大 。
非公平锁(Nonfair)加锁时不考虑排队等待问题 , 直接尝试获取锁 , 获取不到自动到队尾等待 。
非公平锁是多个线程加锁时直接尝试获取锁 , 获取不到才会到等待队列的队尾等待 。 但如果此时锁刚好可用 , 那么这个线程可以无需阻塞直接获取到锁 , 所以非公平锁有可能出现后申请锁的线程先获取锁的场景 。
非公平锁的优点是可以减少唤起线程的开销 , 整体的吞吐效率高 , 因为线程有几率不阻塞直接获得锁 , CPU不必唤醒所有线程 。 缺点是处于等待队列中的线程可能会饿死 , 或者等很久才会获得锁 。
ReentrantLock 锁内部提供了公平锁与分公平锁内部类之分 , 默认是非公平锁 , 如:


推荐阅读