美文网首页
Java的各种锁

Java的各种锁

作者: 帅可儿妞 | 来源:发表于2018-08-24 10:40 被阅读41次

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

    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占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。在竞争激烈的情况下 阻塞锁的性能要明显高于 自旋锁。理想的情况则是; 在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。

    相关文章

      网友评论

          本文标题:Java的各种锁

          本文链接:https://www.haomeiwen.com/subject/nxqbiftx.html