java 自旋锁

来源:互联网 发布:宿迁网络电视台 编辑:程序博客网 时间:2024/06/07 19:37

自旋锁是当前线程一直占用cup不停地执行循环体进行检查条件是否满足,不进行线程状态的改变,(java线程状态的改变都需要进行系统调用,上下文切换,代价相对较高),所以响应速度更快。但当线程数不停增加时,竞争激烈时,因为每个线程都需要执行,占用CPU时间,性能下降明显;

自旋锁适用场景
1.线程竞争小;
2.需要加锁同步的操作执行的非常快,能够迅速释放锁;

NUMA与SMP处理器架构

锁的种类有很多种,不同的锁适应不同的架构的处理器。先来了解一下两种处理器架构:

SMP(Symmetric Multi-Processor)对称多处理器结构

  服务器中多个CPU对称工作,每个CPU访问内存地址所需时间相同。其主要特征是共享,包含对CPU,内存,I/O等进行共享。SMP的优点是能够保证内存一致性,缺点是这些共享的资源很可能成为性能瓶颈,随着CPU数量的增加,每个CPU都要访问相同的内存资源,可能导致内存访问冲突,可能会导致CPU资源的浪费。常用的PC机就属于这种。

  服务器中多个 CPU 对称工作,无主次或从属关系。各 CPU 共享相同的物理内存,每个 CPU 访问内存中的任何地址所需时间是相同的,因此 SMP 也被称为一致存储器访问结构 (UMA : Uniform Memory Access) 。对 SMP 服务器进行扩展的方式包括增加内存、使用更快的 CPU 、增加 CPU 、扩充 I/O( 槽口数与总线数 ) 以及添加更多的外部设备 ( 通常是磁盘存储 ) 。

   SMP 服务器的主要特征是共享,系统中所有资源 (CPU 、内存、 I/O 等 ) 都是共享的。也正是由于这种特征,导致了SMP服务器的主要问题,那就是它的扩展能力非常有限。对于 SMP 服务器而言,每一个共享的环节都可能造成SMP 服务器扩展时的瓶颈,而最受限制的则是内存。由于每个 CPU 必须通过相同的内存总线访问相同的内存资源,因此随着CPU数量的增加,内存访问冲突将迅速增加,最终会造成CPU资源的浪费,使CPU性能的有效性大大降低。实验证明,SMP服务器CPU利用率最好的情况是2至4个CPU。

   由于 SMP 在扩展能力上的限制,人们开始探究如何进行有效地扩展从而构建大型系统的技术, NUMA 就是这种努力下的结果之一。利用 NUMA 技术,可以把几十个 CPU( 甚至上百个 CPU) 组合在一个服务器内。其 CPU 模块结构如图 2 所示:

NUMA(Non-Uniform Memory Access)非一致存储访问

   将CPU分为CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、I/O槽口等,模块之间可以通过互联模块( 如称为 Crossbar Switch)相互访问,因此每个 CPU 可以访问整个系统的内存 ( 这是 NUMA 系统与 MPP 系统的重要差别 ) ;
   但是cpu模块访问本地内存的速度将远远高于访问远地内存(系统内其它节点的内存)的速度,这也是非一致存储访问NUMA的由来。NUMA优点是可以较好地解决原来SMP系统的扩展问题,缺点是由于访问远地内存的延时远远超过本地内存,因此当CPU数量增加时,系统性能无法线性增加。
   由于此设计特点,为了更好地发挥系统性能,开发应用程序时需要尽量减少不同 CPU 模块之间的信息交互。

自旋锁种类

一 、最基本的自旋锁

public class SpinLock {  private AtomicReference<Thread> sign =new AtomicReference<>();  public void lock(){    Thread current = Thread.currentThread();    while(!sign .compareAndSet(null, current)){    }  }  public void unlock (){    Thread current = Thread.currentThread();    sign .compareAndSet(current, null);  }}

使用了CAS原子操作,lock函数将owner设置为当前线程,并且预测原来的值为空。unlock函数将owner设置为null,并且预测值为当前线程。

当有第二个线程调用lock操作时由于owner值不为空,导致循环一直被执行,直至第一个线程调用unlock函数将owner设置为null,第二个线程才能进入临界区。

注:该例子为非公平锁,获得锁的先后顺序,不会按照进入lock的先后顺序进行。此例子共享了sign变量,每个cup都将serviceNum从主内存读取到自己的工作缓存,一旦一个cup做了更改,就要同步每个cup的工作缓存;

二、Ticket锁,解决顺序访问

package com.alipay.titan.dcc.dal.entity;import java.util.concurrent.atomic.AtomicInteger;public class TicketLock {    private AtomicInteger serviceNum = new AtomicInteger();    private AtomicInteger ticketNum  = new AtomicInteger();    private static final ThreadLocal<Integer> LOCAL = new ThreadLocal<Integer>();    public void lock() {        int myticket = ticketNum.getAndIncrement();        LOCAL.set(myticket);        while (myticket != serviceNum.get()) {        }    }    public void unlock() {        int myticket = LOCAL.get();        serviceNum.compareAndSet(myticket, myticket + 1);    }}

Ticket锁主要解决的是访问顺序的问题,主要的问题是在多核cpu上,每次都要查询一个serviceNum 服务号,影响性能(每个cup都将serviceNum 从主内存读取到自己的工作缓存,一旦一个cup做了更改,就要同步每个cup的工作缓存)。

三、CLHLock 公平锁,采用链表的形式进行排序

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;public class CLHLock {    public static class CLHNode {        private volatile boolean isLocked = true;    }    @SuppressWarnings("unused")    private volatile CLHNode tail;    private static final ThreadLocal<CLHNode> LOCAL   = new ThreadLocal<CLHNode>();    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,                                                                                   CLHNode.class,"tail");    public void lock() {        CLHNode node = new CLHNode();        LOCAL.set(node);        CLHNode preNode = UPDATER.getAndSet(this, node);        if (preNode != null) {            while (preNode.isLocked) {            }            preNode = null;            LOCAL.set(node);        }    }    public void unlock() {        CLHNode node = LOCAL.get();        if (!UPDATER.compareAndSet(this, node, null)) {            node.isLocked = false;        }        node = null;    }}

CLHlock是不停的查询前驱变量, 导致不适合在NUMA 架构下使用(在这种结构下,每个线程分布在不同的物理内存区域)

四、MCSLock公平锁,采用链表的形式进行排序

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;public class MCSLock {    public static class MCSNode {        volatile MCSNode next;        volatile boolean isLocked = true;    }    private static final ThreadLocal<MCSNode> NODE = new ThreadLocal<MCSNode>();    @SuppressWarnings("unused")    private volatile MCSNode                                           queue;    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater.newUpdater(MCSLock.class,MCSNode.class, "queue");    public void lock() {        MCSNode currentNode = new MCSNode();        NODE.set(currentNode);        MCSNode preNode = UPDATER.getAndSet(this, currentNode);        if (preNode != null) {            preNode.next = currentNode;            while (currentNode.isLocked) {            }        }    }    public void unlock() {        MCSNode currentNode = NODE.get();        if (currentNode.next == null) {            if (UPDATER.compareAndSet(this, currentNode, null)) {            } else {                while (currentNode.next == null) {                }            }        } else {            currentNode.next.isLocked = false;            currentNode.next = null;        }    }}

MCSLock则是对本地变量的节点进行循环。不存在CLHlock 的问题。

从代码上 看,CLH 要比 MCS 更简单,

CLH 的队列是隐式的队列,没有真实的后继结点属性。

MCS 的队列是显式的队列,有真实的后继结点属性。

JUC ReentrantLock 默认内部使用的锁 即是 CLH锁(有很多改进的地方,将自旋锁换成了阻塞锁等等)。

参考

http://ifeve.com/java_lock_see2/

http://blueyan.iteye.com/blog/2284883

0 0
原创粉丝点击