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占用率过高,但进入时间以及恢复时间都要比自旋锁略慢。在竞争激烈的情况下 阻塞锁的性能要明显高于 自旋锁。理想的情况则是; 在线程竞争不激烈的情况下,使用自旋锁,竞争激烈的情况下使用,阻塞锁。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269