显式锁之自旋锁

来源:互联网 发布:并行计算的编程模型 编辑:程序博客网 时间:2024/06/11 05:38

import com.google.common.collect.Lists;import java.util.List;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.atomic.AtomicReference;import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;/** * 自旋锁。 * <LI>以原子性的设置当前线程作为临界条件,控制多个线程的访问,达到每次只有一个线程获取临界对象,其他线程等待</LI> * <LI>无法获取锁时,不停的循环直到设置成功</LI> * Created by 张三丰 on 2017-07-15. */public class SpinLock {    /**     * 原子性的操作     */    private AtomicReference<Thread> lockProvider = new AtomicReference<>();    /**     * 设置当前线程,占用lockProvider,设置成功表示获取锁     */    public void lock() {        Thread thread = Thread.currentThread();        //进入自旋的临界条件:lockProvider已经被其他线程设置过        while (!lockProvider.compareAndSet(null, thread)) {            //不停自旋,直到compareAndSet成功            //当前线程一直在占用cpu,线程状态未改变        }    }    /**     * 不再占用lockProvider     */    public void unlock() {        Thread thread = Thread.currentThread();        //持有锁的线程调用才能成功        boolean compareAndSet = lockProvider.compareAndSet(thread, null);    }}class SpinTester {    public static void main(String[] args) throws InterruptedException {        //测试锁,循环1000次,要求多线程共享的值能按照顺序+1,最终得到正确的结果 1000        List<Integer> sharedValue = Lists.newArrayList(new Integer(0));        ExecutorService executorService = Executors.newFixedThreadPool(10);        SpinLock lock = new SpinLock();        for (int i = 0; i < 100; i++) {            executorService.execute(() -> {                lock.lock();                //把数据+1                sharedValue.add(0, sharedValue.get(0) + 1);                //输出的结构必然是按顺序的                System.out.println(sharedValue.get(0));                lock.unlock();            });        }//        executorService.awaitTermination(5, TimeUnit.SECONDS);//        //所有线程执行完毕后,查看最终结果//        System.out.println("===========" + sharedValue.get(0));        executorService.shutdown();        //优缺点:        //线程状态不改变,没有线程上下文的切换,响应速度快        //一直占用CPU,所以自旋线程比较多时(竞争比较激烈),性能下降明显        //适合竞争不太激烈,持有锁的时间很短的场景    }}/** * 解决SpinLock的公平性问题。TicketLock按照FIFO顺序处理线程 */class TicketLock {    //服务的号码,从0开始递增    private AtomicInteger serveNumber = new AtomicInteger();    //号牌号码,从0开始,每个线程递增获取    private AtomicInteger ticketNumber = new AtomicInteger();    //以上2个数字一致,则表示正在服务某个号牌持有者的线程    //每个线程获取ticket之后保存在这里    private static final ThreadLocal<Integer> ticketHolder = new ThreadLocal<>();    /**     * 根据号码自旋直到服务自己     */    public void lock() {        int ticket = ticketNumber.getAndIncrement();        ticketHolder.set(ticket);        //进入自旋的临界条件:当前线程拿到的号码与服务号码不匹配        while (serveNumber.get() != ticket) {            //自旋        }    }    /**     * 把服务号码递增,解除下一个自旋     */    public void unlock() {        if (serveNumber.get() == ticketHolder.get()) {            //正在服务的线程的调用才有效            //服务号码递增            serveNumber.incrementAndGet();        }    }}class TicketTester {    public static void main(String[] args) throws InterruptedException {        //测试锁,循环1000次,要求多线程共享的值能按照顺序+1,最终得到正确的结果 1000        List<Integer> sharedValue = Lists.newArrayList(new Integer(0));        ExecutorService executorService = Executors.newFixedThreadPool(10);        TicketLock lock = new TicketLock();        for (int i = 0; i < 100; i++) {            executorService.execute(() -> {                lock.lock();                //把数据+1                sharedValue.add(0, sharedValue.get(0) + 1);                //输出的结构必然是按顺序的                System.out.println(sharedValue.get(0));                lock.unlock();            });        }//        executorService.awaitTermination(5, TimeUnit.SECONDS);//        //所有线程执行完毕后,查看最终结果//        System.out.println("===========" + sharedValue.get(0));        executorService.shutdown();        //优缺点:        //线程状态不改变,没有线程上下文的切换,响应速度快        //一直占用CPU,所以自旋线程比较多时(竞争比较激烈),性能下降明显        //适合竞争不太激烈,持有锁的时间很短的场景        //解决了SpinLock的公平性问题,但是有属性被所有线程访问,对改属性的更改需要频繁刷新JMM中公共内存    }}/** * 解决TicketLock的serveNumber属性被所有线程访问的问题. * <LI>通过链表的形式保证顺序(公平性),同时每个节点只关注前一节点的isLocked属性</LI> * <LI>临界条件:节点属性isLocked</LI> */class CLHLock {    static class Node {        /**         * 节点的锁定状态         */        volatile boolean isLocked = true;        Node next;    }    private volatile Node tailNode;    private static final ThreadLocal<Node> NODE_HOLDER = new ThreadLocal<>();    private static final AtomicReferenceFieldUpdater<CLHLock, Node> FIELD_UPDATER =            AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, Node.class, "tailNode");    /**     * 建立当前节点与前一个节点的关联,并监听前一个节点的锁定状态     */    public void lock() {        Node node = new Node();        NODE_HOLDER.set(node);        Node preNode = FIELD_UPDATER.getAndSet(this, node);        if (preNode != null) {            //临界条件:前一个节点被锁定            while (preNode.isLocked) {                //自旋            }        }    }    /**     * 把当前节点的锁定状态置为false,解除自旋的监听当前节点锁定状态的线程     */    public void unlock() {        Node node = NODE_HOLDER.get();        node.isLocked = false;    }}class CLHTester {    public static void main(String[] args) throws InterruptedException {        //测试锁,循环1000次,要求多线程共享的值能按照顺序+1,最终得到正确的结果 1000        List<Integer> sharedValue = Lists.newArrayList(new Integer(0));        ExecutorService executorService = Executors.newFixedThreadPool(10);        CLHLock lock = new CLHLock();        for (int i = 0; i < 100; i++) {            executorService.execute(() -> {                lock.lock();                //把数据+1                sharedValue.add(0, sharedValue.get(0) + 1);                //输出的结构必然是按顺序的                System.out.println(sharedValue.get(0));                lock.unlock();            });        }//        executorService.awaitTermination(5, TimeUnit.SECONDS);//        //所有线程执行完毕后,查看最终结果//        System.out.println("===========" + sharedValue.get(0));        executorService.shutdown();        //优缺点:        //线程状态不改变,没有线程上下文的切换,响应速度快        //一直占用CPU,所以自旋线程比较多时(竞争比较激烈),性能下降明显        //适合竞争不太激烈,持有锁的时间很短的场景        //解决了Ticket锁的公共属性的问题,但是访问的是上一个线程持有的节点的属性,在NUMA架构下,可能造成频繁访问远程内存问题    }}/** * 同自旋自己节点的属性,解决CLH锁自旋pre节点,在NUMA架构下内存读取比较慢的问题 */class MCSLock {    static class MCSNode {        //是否被锁定,默认是true        //volatile        boolean isLocked = true;        MCSNode next;    }    private static final ThreadLocal<MCSNode> NODE_HOLDER = new ThreadLocal<>();    volatile MCSNode tail;    private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER =            AtomicReferenceFieldUpdater.newUpdater(MCSLock.class, MCSNode.class, "tail");    /**     * 自旋当前节点等待锁     */    public void lock() {        //创建当前节点        MCSNode node = new MCSNode();        NODE_HOLDER.set(node);        //获取之前的节点,即尾节点        MCSNode preTail = UPDATER.getAndSet(this, node);//第1步 设置当前节点并且获取之前的节点,当前节点变为尾节点        if (preTail != null) {  //说明之前已经有节点,线程已经被锁定了,那么就自旋等待            preTail.next = node;//第2步 关联节点            while (node.isLocked) {//第3步 自旋等待                //自旋            }        }    }    /**     * 解除next节点的自旋锁<br>     * lock在等待当前节点的状态,unlock需要检查next节点是否存在,如果存在置为false,以结束其他线程的自旋     */    public void unlock() {        MCSNode node = NODE_HOLDER.get();        //如果当前节点是尾节点,置为null。(当前线程已经结束,后面不能挂下一个节点了(挂了节点后无法取消其自旋))        if (!UPDATER.compareAndSet(this, node, null)) {            while (node.next != null) {//使用while循环,是因为lock里面第1步执行成功,可能第2步还没执行完成                node.next.isLocked = false;                node.next = null;// for GC            }        } else {            if (node.next != null) {                node.next.isLocked = false;                node.next = null;// for GC            }        }    }}class MCSTester {    public static void main(String[] args) throws InterruptedException {        //测试锁,循环1000次,要求多线程共享的值能按照顺序+1,最终得到正确的结果 1000        List<Integer> sharedValue = Lists.newArrayList(new Integer(0));        ExecutorService executorService = Executors.newFixedThreadPool(10);        MCSLock lock = new MCSLock();        for (int i = 0; i < 100; i++) {            executorService.execute(() -> {                lock.lock();                //把数据+1                sharedValue.add(0, sharedValue.get(0) + 1);                //输出的结构必然是按顺序的                System.out.println(sharedValue.get(0));                lock.unlock();            });        }//        executorService.awaitTermination(5, TimeUnit.SECONDS);//        //所有线程执行完毕后,查看最终结果//        System.out.println("===========" + sharedValue.get(0));        executorService.shutdown();        //优缺点:        //线程状态不改变,没有线程上下文的切换,响应速度快        //一直占用CPU,所以自旋线程比较多时(竞争比较激烈),性能下降明显        //适合竞争不太激烈,持有锁的时间很短的场景        //解决了CLH锁可能频繁访问远程内存的问题    }}

附:涉及到的处理器架构知识
1 SMP(Symmetric Multi-Processor)

对称多处理器结构,指服务器中多个CPU对称工作,每个CPU访问内存地址所需时间相同。其主要特征是共享,包含对CPU,内存,I/O等进行共享。

SMP能够保证内存一致性,但这些共享的资源很可能成为性能瓶颈,随着CPU数量的增加,每个CPU都要访问相同的内存资源,可能导致内存访问冲突,

可能会导致CPU资源的浪费。常用的PC机就属于这种。

2 NUMA(Non-Uniform Memory Access)

非一致存储访问,将CPU分为CPU模块,每个CPU模块由多个CPU组成,并且具有独立的本地内存、I/O槽口等,模块之间可以通过互联模块相互访问,

访问本地内存的速度将远远高于访问远地内存(系统内其它节点的内存)的速度,这也是非一致存储访问的由来。NUMA较好地解决SMP的扩展问题,

当CPU数量增加时,因为访问远地内存的延时远远超过本地内存,系统性能无法线性增加


原创粉丝点击