Java的各种锁

不管是工作还是跳槽面试,锁这个问题始终避不开,而且极易成为绊脚石,前几天看到一些比较好的文档,于是就搬过来做个笔记

  1. 锁的分类
    • 自旋锁:自旋,jvm默认是10次吧,有jvm自己控制。for去争取锁
    • 阻塞锁:被阻塞的线程,不会争夺锁。
    • 可重入锁:多次进入改锁的域
    • 读写锁:
    • 互斥锁:锁本身就是互斥的
    • 悲观锁:不相信,这里会是安全的,必须全部上锁
    • 乐观锁:相信,这里是安全的。
    • 公平锁:有优先级的锁
    • 非公平锁:无优先级的锁
    • 偏向锁:无竞争不锁,有竞争挂起,转为轻量锁
    • 对象锁:锁住对象
    • 线程锁:
    • 锁粗化:多锁变成一个,自己处理
    • 轻量级锁:CAS 实现
    • 锁消除:偏向锁就是锁消除的一种
    • 锁膨胀:jvm实现,锁粗化
    • 信号量:使用阻塞锁 实现的一种策略
    • 排它锁:X锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其他任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其他事务在T释放A上的锁之前不能再读取和修改A。
  1. 自旋锁
    • 自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时 才能进入临界区
      // 注:该例子为非公平锁,获得锁的先后顺序,不会按照进入lock的先后顺序进行。
      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,第二个线程才能进入临界区。由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
    • 自旋锁还有三种常见的锁形式:TicketLock ,CLHlock 和MCSlock
      • Ticket锁主要解决的是访问顺序的问题,主要的问题是在多核CPU上,代码如下,每次都要查询一个serviceNum 服务号,影响性能(必须要到主内存读取,并阻止其他CPU修改)。
        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);
            }
        }
        
      • CLHLock:Craig, Landin, and Hagersten Locks,是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性;CLH锁也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋;
        • 当一个线程需要获取锁时:
          1. 创建一个的CLHNode,将其中的locked设置为true表示需要获取锁;
          2. 线程对tail域调用getAndSet方法,使自己加入到队列的尾部,同时获取一个指向其前趋结点的引用preNode;
          3. 该线程就在前趋结点的locked字段上旋转,直到前趋结点释放锁;
          4. 当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前趋结点;
        • 示例代码如下:
        import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;        
        public class CLHLock {
            public static class CLHNode {
                private volatile boolean isLocked = true;
            }
            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;
            }
        }
        
      • MCSLock则是对本地变量的节点进行循环。MSC与CLH最大的不同并不是链表是显示还是隐式,而是线程自旋的规则不同:CLH是在前趋结点的locked域上自旋等待,而MCS是在自己的结点的locked域上自旋等待。正因为如此,它解决了CLH在NUMA系统架构中获取locked域状态内存过远的问题;不存在CLHlock 的问题。
        import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
        public class MCSLock {
            public static class MCSNode {
                volatile MCSNode next;
                volatile boolean locked = true;
            }
            private static final ThreadLocal<MCSNode> node = new ThreadLocal<MCSNode>();
            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.locked) {}
                }
            }
            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.locked = false;
                    currentNode.next = null;
                }
            }
        }
        
    • 自旋锁总结
      • 从代码上 看,CLH 要比 MCS 更简单;
      • CLH 的队列是隐式的队列,没有真实的后继结点属性;
      • MCS 的队列是显式的队列,有真实的后继结点属性;
      • JUC ReentrantLock 默认内部使用的锁 即是 CLH锁(有很多改进的地方,将自旋锁换成了阻塞锁等等);
  2. 阻塞锁
    • 与自旋锁不同,改变了线程的运行状态。在JAVA环境中,线程Thread有如下几个状态:1,新建状态;2,就绪状态;3,运行状态;4,阻塞状态;5,死亡状态
    • 阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,进入运行状态。
    • JAVA中,能够进入\退出、阻塞状态或包含阻塞锁的方法有 ,synchronized 关键字(其中的重量锁),ReentrantLock,Object.wait()\notify(),LockSupport.park()/unpart()(JUC经常使用)
    • 下面是一个JAVA 阻塞锁实例
      import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
      import java.util.concurrent.locks.LockSupport;
      public class CLHLock1 {
          public static class CLHNode {
              private volatile Thread locked;
          }
          private volatile CLHNode tail;
          private static final ThreadLocal<CLHNode> local   = new ThreadLocal<CLHNode>();
          private static final AtomicReferenceFieldUpdater<CLHLock1, CLHNode> updater = 
                    AtomicReferenceFieldUpdater.newUpdater(CLHLock1.class, CLHNode.class, "tail");
          public void lock() {
              CLHNode node = new CLHNode();
              local.set(node);
              CLHNode preNode = updater.getAndSet(this, node);
              if (preNode != null) {
                  preNode.locked = Thread.currentThread();
                  LockSupport.park(this);
                  preNode = null;
                  local.set(node);
              }
          }
          public void unlock() {
              CLHNode node = local.get();
              if (!updater.compareAndSet(this, node, null)) {
                  System.out.println("unlock\t" + node.locked.getName());
                  LockSupport.unpark(node.locked);
              }
              node = null;
          }
      }
      
      • 在这里我们使用了LockSupport.unpark()的阻塞锁。 该例子是将CLH锁修改而成。阻塞锁的优势在于,阻塞的线程不会占用cpu时间, 不会导致 CPu占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。在竞争激烈的情况下 阻塞锁的性能要明显高于 自旋锁。理想的情况则是; 在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。