自旋锁是指一个线程尝试获取某个锁时,如果该锁已经被其他线程占用了,就一直循环检测锁释放被释放,而不是像互斥锁一样让线程进入挂起或者睡眠状态。
自旋锁的的缺点就是会一直死循环一直到获取锁为止,这样会一直消耗cpu内存,但是与互斥锁把线程阻塞,然后再次被唤醒相比在性能方面还是有优势的,因为频繁的从用户态切到内核态,需要消耗系统资源,性能也更惨,但是目前的jvm对synchronized实现做了修改采用自旋的到一定次数或者时间就进入阻塞状态,结合了自旋和阻塞,使得目前的synchronized性能大大提高了。
非公平的重入自旋锁的实现过程:
-
加锁:判断cas中线程是否是当前线程,如果是则计数器+1,如果不是则死循环一直到修改成功(即获取锁成功),才结束循环
-
解锁:判断当前获取到锁的线程是否是cas中的线程,如果是则看计数器重入了多少次,如果重入了直接计数器-1,否则直接通过cas修改释放锁。
-
/** * @Author:苏牧夕 * @Date:2020/3/19 23:46 * @Version 1.0 */ public class AtoLock { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); System.out.println(atomicInteger.get()); ThreadPoolExecutor poolExecutor = ThreadPool.InstancePool.poolExecutor; SpilLock spilLock = new SpilLock(); // FairSpilLock spilLock = new FairSpilLock(); for (int i = 0; i < 5; i++) { int x = i; poolExecutor.execute( () -> { spilLock.lock(); spilLock.lock(); try { System.out.println(Thread.currentThread().getName() + x + ",各种骚操作"); TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } spilLock.unlock(); spilLock.unlock(); } ); } // System.out.println("操作了几次:" + atomicInteger.get()); poolExecutor.shutdown(); } static class SpilLock implements Lock { private static AtomicReference<Thread> cas = new AtomicReference<>(); private static AtomicInteger count = new AtomicInteger(0); @Override public void lock() { Thread thread = Thread.currentThread(); if (thread==cas.get()){ count.incrementAndGet(); System.out.println(Thread.currentThread().getName()+"上锁成功"); return; } while (!cas.compareAndSet(null, thread)) { } System.out.println(Thread.currentThread().getName()+"上锁成功"); } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { Thread thread = Thread.currentThread(); Long timeout= unit.toNanos(time); if (cas.get()==thread){ count.incrementAndGet(); return true; } Long start = System.nanoTime(); while (!cas.compareAndSet(null,thread)){ if (thread.isInterrupted()){ thread.interrupt(); throw new InterruptedException(); } long tt=System.nanoTime()-start; if (tt>=timeout){ return false; } } return true; } public void unlock() { Thread thread = Thread.currentThread(); if (thread==cas.get()){ if (count.get()>0){ System.out.println(Thread.currentThread().getName()+"解锁成功"); count.decrementAndGet(); }else{ if (cas.compareAndSet(thread, null)) { System.out.println(Thread.currentThread().getName()+"解锁成功"); } } } } @Override public Condition newCondition() { return null; } } }
-
公平自旋锁
逻辑:
- 加锁:一个标记记录当前获得锁的编号currNo,然后给每条线程匹配一个编号,如果当前锁编号和当前线程的编号匹配则解锁自旋
- 解锁:通过cas修改 锁的编号currNo,改为当前获得锁的线程的编号+1
static class FairSpilLock implements Lock{ AtomicInteger currNo = new AtomicInteger(0); AtomicInteger threadNo = new AtomicInteger(0); ThreadLocal<Integer> local = new ThreadLocal<>(); AtomicInteger count = new AtomicInteger(0); @Override public void lock() { //生成一个线程编号 int thNo = threadNo.getAndIncrement(); //把编号和线程联系起来 local.set(thNo); //自旋当前锁的编号curr释放和当前线程编号一样,如果一样则结束自旋 while (thNo!=currNo.get()){ Thread.yield();//如果不是则当前线程礼让,可以不加,加了减少循环次数 } System.out.println(Thread.currentThread().getName()+"加锁成功"); } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public void unlock() { //获取当前线程的编号 Integer thNo = local.get(); //cas修改当前锁编号 if (currNo.compareAndSet(thNo,thNo+1)){ System.out.println(Thread.currentThread().getName()+"解锁成功!"); } } @Override public Condition newCondition() { return null; } }
优点:实现了公平性,使得每条线程都有机会执行
缺点:每条线/进程都占用处理器在读写标记变量,导致系统性能降低
-
CLH公平自旋锁
来解决频繁读写标记,就出现了CLH自旋锁算法
首先简单说一下ThreadLocal这个类的作用,该类负责提供线程都有的本地变量(局部变量),在该类存储的变量,在整个线程存活的过程中可以使用,但是其他线程是无法取的其他线程的局部变量的,类似于一每条线程都有一个私有的Map存储参数,通常使用该类来存储线程的上下文或者id,又因为变量都是线程内部的所以不存在并发问题,所以是线程安全的。
CLH算法则是利用了ThreadLocal来存储每条线程的节点和它前一条线程的节点作为前继节点,每条线程只自旋判断前继节点的状态,如果前继节点释放锁状态改变,则本线程获取到锁,整个过程在模拟队列,但是该队列是非真实存在的,只是逻辑上是队列,也正是因为通过模拟队列来每条线程有序执行,从而达到公平。
那么多个线程直接是如何链接起来的呢,首先每条线程都有一个节点用于保存自身的状态是否加锁或者解锁状态,并且存储在ThreahLocal,同时也存储来前一条线程的节点作为,该线程前继节点,那么每条线程是如何获取前一个线程的节点作为前继节点的呢,这里我们需要使用原子引用类来存储尾节点,每条线程创建的时候通过cas操作,把自身作为尾节点,把前一个尾节点作为其前继节点,也就是做,每次有线程申请锁就获取保存了前一条线程节点的尾节点作为其前继节点,这样就把多条线程链接起来,而且还是有序的。
此外,ThreadLocal容易出现内存泄漏,为什么会出现内存泄漏呢?因为该类内部使用Map存储,而且key继承软引用类,然后value映射起来的,只有内存不足,gc进行垃圾回收就会把软引用回收,也就是key回收了,但是value还在,而且是无法获取的,因为key已经被回收了。
CLH锁逻辑上是链表形式,所以同样具有很好的扩展性,性能也比较高。
public class AtoLock { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); System.out.println(atomicInteger.get()); ThreadPoolExecutor poolExecutor = ThreadPool.InstancePool.poolExecutor; // SpilLock spilLock = new SpilLock(); // FairSpilLock spilLock = new FairSpilLock(); CLHSpilLock spilLock = new CLHSpilLock(); for (int i = 0; i < 5; i++) { int x = i; poolExecutor.execute( () -> { spilLock.lock(); // spilLock.lock(); try { System.out.println(Thread.currentThread().getName() + x + ",各种骚操作"); TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } // spilLock.unlock(); spilLock.unlock(); } ); } // System.out.println("操作了几次:" + atomicInteger.get()); poolExecutor.shutdown(); } static class CLHSpilLock implements Lock{ CLHSpilLock() { tail = new AtomicReference<Node>(new Node()); //init方法有延迟加载的作用 prev = new ThreadLocal<Node>(){ @Override protected Node initialValue() { return null; } }; currNode = new ThreadLocal<Node>(){ @Override protected Node initialValue() { return new Node(); } }; } static class Node{ //用于判断前继节点释放释放锁。 private volatile boolean locked; } // private final ThreadLocal<Node>prev; //线程的本地变量(局部变量),存储自身节点的状态 private final ThreadLocal<Node>currNode; //存储尾节点(也是前一条线程的节点,后一条线程通过这个tail获取前一条线程变量的内存地址并且设置尾前继节点) private final AtomicReference<Node> tail ; @Override public void lock() { //ThreahLocal存储的本地变量(局部变量),只有线程自己才可以访问到,其他的线程获取不到 //相当于是线程的私有内存 //获取当前线程的节点标记。 final Node node = currNode.get(); //设置为当等锁的自旋状态 node.locked=true; //通过cas为节点设置为当前变量,并且获取之前的尾节点作为前继节点 //这一步是线程之间执行顺序连接起来,preNode是前一条线程的节点, Node predNode = tail.getAndSet(node); prev.set(predNode); //注意CLH,通过把前继节点和自身的节点存存储在ThreadLocal中, // 每条线程只根据自己的前继节点释放锁来判断自己释放获取到锁。 //自旋当前线程节点的前继节点释放已经释放锁,如果释放锁则停止自旋。 while (predNode.locked){ } System.out.println(Thread.currentThread().getName()+"加锁成功"); } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public void unlock() { //获取线程的私有节点,然后修改状态为false表示释放锁,因为locked使用了volavte修饰, //当前节点的状态是下一条线程自旋判断前继节点的状态,因此修改为false时它里面可以发现状态改变, // volatilt保证变量在线程之间的可进行 final Node node = currNode.get(); node.locked=false; //然后把当前节点从本地变量(局部变量)(线程本地内存,反正就是只有自己可以看到的变量)中移除防止内存溢出 currNode.remove(); System.out.println(Thread.currentThread().getName()+"释放锁成功"); } @Override public Condition newCondition() { return null; } } }
总结:CLH是逻辑上的队列(链表),并不是真正的队列,而且自旋是自旋前继节点,根据前继节点来判断是否获得锁。
-
MCS公平自旋锁
MCS算法和CLH算法思路非常相似,但是而且最大的区别是,MCS自旋的是自身节点,而不是前继节点,同时MCS是真正的链表,
算法思想:每条线程都有一个自身的节点Node(Node存储着线程当前锁的状态和后继节点),存储在线程的局部变量ThreadLocal中,通过一个原子类存储上一次最新申请获取锁的线程的节点称为尾节点,并且后续线程通过cas,获取上一次最后的节点,并且把当前线程节点设置为尾节点,然后把上一次尾节点的后继节点设置为当前节点,然后当前节点进行自旋,那么自旋是如何解锁的呢,这通过解锁过程中,把当前线程节点的后继节点的状态修改为获得锁状态(每次修改后继节点的状态和链表保证了线程执行的有序性)。
/** * @Author:苏牧夕 * @Date:2020/3/19 23:46 * @Version 1.0 */ public class AtoLock { public static void main(String[] args) { AtomicInteger atomicInteger = new AtomicInteger(); System.out.println(atomicInteger.get()); ThreadPoolExecutor poolExecutor = ThreadPool.InstancePool.poolExecutor; // SpilLock spilLock = new SpilLock(); // FairSpilLock spilLock = new FairSpilLock(); // CLHSpilLock spilLock = new CLHSpilLock(); MCSSpilLock spilLock = new MCSSpilLock(); for (int i = 0; i < 5; i++) { int x = i; poolExecutor.execute( () -> { spilLock.lock(); // spilLock.lock(); try { System.out.println(Thread.currentThread().getName() + x + ",各种骚操作"); TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } // spilLock.unlock(); spilLock.unlock(); } ); } // System.out.println("操作了几次:" + atomicInteger.get()); poolExecutor.shutdown(); } static class MCSSpilLock implements Lock { private final ThreadLocal<Node> curnode; private AtomicReference<Node> tail; public MCSSpilLock() { //第一条线程直接初始化当前节点和尾节点 this.curnode = new ThreadLocal<Node>() { @Override protected Node initialValue() { return new Node(); } }; tail = new AtomicReference<Node>(); } @Override public void lock() { //获取当前线程局部变量是否已经存在node节点 Node currNode = curnode.get(); //线程的节点为null,则创建一个节点 if (currNode == null) { currNode = new Node(); } //然后通过尾插法,先把尾节点取处理,并把当前节点设为尾节点。 Node predNode = tail.getAndSet(currNode); if (predNode != null) { //如果不是第一个节点则设置为true,处于等待锁状态 currNode.locked = true; //把前一条线程的节点后继节点设置尾当前节点,保持公平 predNode.next = currNode; } //轮询当前节点 while (currNode.locked) {} System.out.println(Thread.currentThread().getName()+"上锁成功"); } @Override public void lockInterruptibly() throws InterruptedException { } @Override public boolean tryLock() { return false; } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return false; } @Override public void unlock() { //获取当前线程的节点 Node node = curnode.get(); //如果是最后一个节点则把tail也进行回收。 if (node.next == null) { tail.compareAndSet(node, null); } //如果有后继线程,则修改状态为获取锁状态。并且把当前节点的引用设置为null if (node.next != null) { node.next.locked = false; node.next = null; //回收当前线程的节点。防止内存泄漏 curnode.remove(); } System.out.println(Thread.currentThread().getName()+"解锁成功"); } @Override public Condition newCondition() { return null; } static class Node { //false表示获得锁。 private volatile boolean locked = false; private Node next; } }