synchronized同步方法最主要的问题是线程阻塞和唤醒带来的性能消耗,阻塞同步是悲观的并发策略,只要有可能出现竞争,都认为一定要先加锁;然而还有一种乐观的并发策略,直接操作数据,如果没有发现其他线程同时操作数据则认为这个操作是成功的,如果其他线程也操作了数据,那么操作是失败的,一般采用不断重试的手段(自旋),直到成功为止。乐观策略适用于并发程度不高且临界区较小的场景,优点是不需要阻塞线程,属于非阻塞同步手段,性能更高。
乐观锁并发策略主要有两个重点阶段,一个是对数据进行操作,另一个是检测是否发生冲突(即是否存在同时操作数据的其他线程),这里操作数据和冲突检测需要具备原子性,即操作数据和冲突检查必须同时成功或者同时失败,这个原子性通过CAS指令来实现,目前绝大多数的CPU都支持CAS指令。
CAS指令需要三个参数:分别是内存地址,期望的旧值,新值。
自旋锁是乐观锁的一种实现,当线程曲获取一个锁时,如果发现该锁被其他线程占用,那么进入一个无意义的循环不断尝试加锁,直到成功获取锁。自旋锁适用于临界区比较小的场景,如果锁被持有的时间过长,那么自旋本身会长时间的白白浪费CPU资源。
public class SpinLock {
private final AtomicReference<Thread> owner = new AtomicReference<Thread>();
public void lock(){
while (!owner.compareAndSet(null, Thread.currentThread())){
System.out.println(Thread.currentThread().getName() + " loop");
}
System.out.println(Thread.currentThread().getName() + " lock");
}
public void unlock(){
owner.compareAndSet(Thread.currentThread(), null);
System.out.println(Thread.currentThread().getName() + " unlcok");
}
}
上述代码中owner变量保存了持锁线程,这里有两个缺点,第一个是没有保证公平性,另一个是由于多个线程同时操作同一个共享变量owner,而每个CPU都会缓存该变量,任意一个线程加锁和解锁后,其他所有CPU中的owner缓存都立刻失效,而线程自旋过程中又都会使用(读)该变量,所以各个CPU都需要重新读内存(CPU的缓存一致性原理),因此自旋锁会频繁的进行缓存一致性同步操作,每次加锁和解锁都会带来一次,这导致繁重的系统总线流量和内存操作,会降低性能。
为了解决公平性问题,可以让锁维护一个编号来表示下次该获取锁的线程,每个线程在申请锁时首先被分配一个编号,然后始终自旋直到轮到自己然后尝试加锁。虽然解决了公平性问题,但是依然存在缓存同步导致性能下降的问题。
public class FairSpinLock {
private final AtomicInteger nextNo = new AtomicInteger();
private final AtomicInteger threadNo = new AtomicInteger();
public int lock(){
int myNo = threadNo.getAndIncrement();
while (nextNo.get() != myNo){
}
System.out.println(Thread.currentThread().getName() + " lock");
return myNo;
}
public void unlock(int threadNo){
int next = threadNo + 1;
nextNo.compareAndSet(threadNo, next);
System.out.println(Thread.currentThread().getName() + " unlcok");
}
}
自旋锁之所以频繁的发生缓存失效的问题,是因为所有线程加锁和解锁都会操作同一个变量,因此如果是不同的变量,或者说多个线程操作不同变量,那么可避免高频率的缓存同步操作,这就是MCS的实现思路。
MCS基于链表实现,每个申请锁的线程都对应链表上的一个节点,这些线程一直轮询自身节点来确定自己是否获得了锁。获得锁的线程在释放锁的时候,负责通知后继节点已获取锁,即更新后继节点的运行状态,这会导致其他CPU中该变量的缓存失效,但并不是所有线程都会使用这个后继节点,所以不会发生所有CPU同时进行缓存一致性同步操作,而且仅在线程通知后继线程时发生一次缓存失效,这样缓存同步操作就减少很多,降低了系统总线和内存的开销。
不支持重入,可加一个ThreadLocal变量记录重入次数来实现。
public class McsLock {
/**
* 队尾
*/
private volatile Node tail;
/**
* 队尾原子操作
*/
private static final AtomicReferenceFieldUpdater<McsLock, Node> TAIL_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(McsLock.class, Node.class, "tail");
/**
* 线程到节点的映射
*/
private ThreadLocal<Node> currentThreadNode = new ThreadLocal<>();
/**
* 加锁
*/
public void lock(){
Node cNode = currentThreadNode.get();
if (cNode == null){
cNode = new Node();
currentThreadNode.set(cNode);
}
//原子地入队并返回原队尾
Node predecessor = TAIL_UPDATER.getAndSet(this, cNode); //step 1
if (predecessor != null){
//需要排队
predecessor.setNext(cNode); //step 2
while (cNode.isWaiting){
//自旋等待前置线程更新自己的状态 //step
}
}else{
//无需排队,自己更新自己状态
cNode.setWaiting(false);
}
System.out.println(Thread.currentThread().getName() + " lock");
return;
}
/**
* 解锁
*/
public void unlock(){
// 获取当前线程对应的节点
Node cNode = currentThreadNode.get();
if (cNode == null || cNode.isWaiting){
throw new RuntimeException(cNode + " not lock");
}
if (cNode.getNext() == null && !TAIL_UPDATER.compareAndSet(this, cNode, null)){
// 没有后继节点的情况,将tail置空
// 如果CAS操作失败了表示突然有节点排在自己后面了,可能还不知道是谁,下面是等待后继节点入队
// 这里之所以要忙等是因为上述的lock操作中step 1执行完后,step 2可能还没执行完
while (cNode.getNext() == null){
//step 5
}
}
if (cNode.getNext() != null){
// 通知后继节点获取锁
cNode.getNext().setWaiting(false);
//help GC
cNode.setNext(null);
}
System.out.println(Thread.currentThread().getName() + " unlock");
}
/**
* 队列节点类
*/
private static class Node {
//默认是等待状态
private volatile boolean isWaiting = true;
//后继节点
private volatile Node next;
public boolean isWaiting() {
return isWaiting;
}
public void setWaiting(boolean waiting) {
isWaiting = waiting;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
CLH锁和MCS锁的原理大致相同,都是各个线程各自关注各自的变量,来避免多线程同时操作同一个变量,从而减少缓存同步;不同点在于MCS自旋轮询的是当前节点的属性,而CLH轮询的是前驱节点的属性,来判断前一个线程是否释放了锁。
public class ClhLock {
public static class Node {
//是否结束
private volatile boolean isOver = false;
}
private volatile Node tail;
private static AtomicReferenceFieldUpdater<ClhLock, Node> TAIL_UPDATER =
AtomicReferenceFieldUpdater.newUpdater(ClhLock.class, Node.class, "tail");
public void lock(Node cThread){
Node predesser = TAIL_UPDATER.getAndSet(this, cThread);
if (null != predesser){
while (predesser.isOver){}
}
System.out.println(Thread.currentThread().getName() + " lock");
}
public void unlock(Node cThread){
if (!TAIL_UPDATER.compareAndSet(this, cThread, null)){
//存在等待线程
cThread.isOver = true;
}
System.out.println(Thread.currentThread().getName() + " unlock");
}
从代码可知,CLH比MCS要简洁很多;CLH是在前驱节点的属性上自旋,其等待队列是隐式的,节点并不实际持有前驱或者后继,通过每个节点都轮询前驱形成逻辑链表,而MCS的队列是物理存在的