显式锁之自旋锁

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;

/**
 * 自旋锁。
 * 
  • 以原子性的设置当前线程作为临界条件,控制多个线程的访问,达到每次只有一个线程获取临界对象,其他线程等待
  • *
  • 无法获取锁时,不停的循环直到设置成功
  • * Created by 张三丰 on 2017-07-15. */ public class SpinLock { /** * 原子性的操作 */ private AtomicReference 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 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 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 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属性被所有线程访问的问题. *
  • 通过链表的形式保证顺序(公平性),同时每个节点只关注前一节点的isLocked属性
  • *
  • 临界条件:节点属性isLocked
  • */ class CLHLock { static class Node { /** * 节点的锁定状态 */ volatile boolean isLocked = true; Node next; } private volatile Node tailNode; private static final ThreadLocal NODE_HOLDER = new ThreadLocal<>(); private static final AtomicReferenceFieldUpdater 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 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 NODE_HOLDER = new ThreadLocal<>(); volatile MCSNode tail; private static final AtomicReferenceFieldUpdater 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节点的自旋锁
    * 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 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数量增加时,因为访问远地内存的延时远远超过本地内存,系统性能无法线性增加


    你可能感兴趣的:(精华,并发编程,Java多线程全解密,多线程)