Java|锁--JAVA成长之路( 三 )


3. 只能保证一个共享变量的原子操作 。 对一个共享变量执行操作时 , CAS能够保证原子操作 , 但是对多个共享变量操作时 , CAS是无法保证操作的原子性的 。
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性 , 可以把多个变量放在一个对象里来进行CAS操作 。
2. 自旋锁 VS 适应性自旋锁
在介绍自旋锁前 , 我们需要介绍一些前提知识来帮助大家明白自旋锁的概念 。
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成 , 这种状态转换需要耗费处理器时间 。 如果同步代码块中的内容过于简单 , 状态转换消耗的时间有可能比用户代码执行的时间还要长 。
在许多场景中 , 同步资源的锁定时间很短 , 为了这一小段时间去切换线程 , 线程挂起和恢复现场的花费可能会让系统得不偿失 。 如果物理机器有多个处理器 , 能够让两个或以上的线程同时并行执行 , 我们就可以让后面那个请求锁的线程不放弃CPU的执行时间 , 看看持有锁的线程是否很快就会释放锁 。
而为了让当前线程“稍等一下” , 我们需让当前线程进行自旋 , 如果在自旋完成后前面锁定同步资源的线程已经释放了锁 , 那么当前线程就可以不必阻塞而是直接获取同步资源 , 从而避免切换线程的开销 。 这就是自旋锁 。
自旋锁本身是有缺点的 , 它不能代替阻塞 。 自旋等待虽然避免了线程切换的开销 , 但它要占用处理器时间 。 如果锁被占用的时间很短 , 自旋等待的效果就会非常好 。 反之 , 如果锁被占用的时间很长 , 那么自旋的线程只会白浪费处理器资源 。 所以 , 自旋等待的时间必须要有一定的限度 , 如果自旋超过了限定次数(默认是10次 , 可以使用-XX:PreBlockSpin来更改)没有成功获得锁 , 就应当挂起线程 。
自旋锁的实现原理同样也是CAS , AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作 , 如果修改数值失败则通过循环来执行自旋 , 直至修改成功 。
自旋锁在JDK1.4.2中引入 , 使用-XX:+UseSpinning来开启 。 JDK 6中变为默认开启 , 并且引入了自适应的自旋锁(适应性自旋锁) 。
自适应意味着自旋的时间(次数)不再固定 , 而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定 。 如果在同一个锁对象上 , 自旋等待刚刚成功获得过锁 , 并且持有锁的线程正在运行中 , 那么虚拟机就会认为这次自旋也是很有可能再次成功 , 进而它将允许自旋等待持续相对更长的时间 。 如果对于某个锁 , 自旋很少成功获得过 , 那在以后尝试获取这个锁时将可能省略掉自旋过程 , 直接阻塞线程 , 避免浪费处理器资源 。
在自旋锁中 另有三种常见的锁形式:TicketLock、CLHlock和MCSlock , 本文中仅做名词介绍 , 不做深入讲解 , 感兴趣的同学可以自行查阅相关资料 。
3. 无锁 VS 偏向锁 VS 轻量级锁 VS 重量级锁
这四种锁是指锁的状态 , 专门针对synchronized的 。 在介绍这四种锁状态之前还需要介绍一些额外的知识 。
首先为什么Synchronized能实现线程同步?
在回答这个问题之前我们需要了解两个重要的概念:“Java对象头”、“Monitor” 。
Java对象头
synchronized是悲观锁 , 在操作同步资源之前需要给同步资源先加锁 , 这把锁就是存在Java对象头里的 , 而Java对象头又是什么呢?
我们以Hotspot虚拟机为例 , Hotspot的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针) 。
Mark Word:默认存储对象的HashCode , 分代年龄和锁标志位信息 。 这些信息都是与对象自身定义无关的数据 , 所以Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据 。 它会根据对象的状态复用自己的存储空间 , 也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化 。


推荐阅读