最详细的图文解析Java各种锁(终极篇)

前言

线程并发系列文章:

Java 线程基础
Java 线程状态
Java “优雅”地中断线程-实践篇
Java “优雅”地中断线程-原理篇
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
线程池必懂系列

前面的十几篇文章都是从源码的角度分析线程并发涉及到的知识点,本篇将重点总结、归纳、提炼知识点,尽量少贴代码。遇到有疑惑的点,请查看对应文章的分析。
通过本篇文章,你将了解到:

1、锁的全家福
2、如何验证公平/非公平锁
3、底层如何获取锁/释放锁
4、自旋锁与自适应自旋
5、为什么需要等待/通知机制

1、锁的全家福

image.png

2、如何验证公平/非公平锁

公平与非公平区别之处在于获取锁时的策略。


image.png

如上图:

1、线程1持有锁。
2、线程2、线程3、线程4 在同步队列里排队等候锁。

这时线程5也想要获取锁,根据公平与否分为两种不同策略。

公平锁

线程5先判断同步队列是是否有线程在等待,明显地此时同步队列里有线程在等待,于是线程5加入到同步队列的尾部等待。

非公平锁

1、线程5不管同步队列是否有线程等待,管他三七二十一先去抢锁再说。若是运气好就能直接捡到便宜获取了锁,若是失败再去排队。
2、线程5还是有机会捡便宜的,若是此时线程1刚好释放了锁,并唤醒线程2,线程2醒过来后去获取锁。若在线程2获取锁之前线程5就去抢锁了,那么它会成功。它的成功对于线程2、线程3、线程4来说是不公平的。

我们知道ReentrantLock 可实现公平/非公平锁,来验证一下。

先来验证公平锁:

public class TestThread {
    private ReentrantLock reentrantLock = new ReentrantLock(true);
    private void testLock() {
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(runnable);
            thread.setName("线程" + (i + 1));
            thread.start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 启动了,准备获取锁");
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + " 获取了锁");
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    };

    public static void main(String args[]) {
        TestThread testThread = new TestThread();
        testThread.testLock();
    }
}

打印如下:


image.png

可以看出,线程2、3、4、5 按顺序获取锁,实际上拿到锁也是按照这顺序的。
因此,符合先到先得,是公平的。

再来验证非公平锁

public class TestThread {
    private ReentrantLock reentrantLock = new ReentrantLock(false);
    private void testLock() {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(runnable);
            thread.setName("线程" + (i + 1));
            thread.start();
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void testUnfair() {
        try {
            Thread.sleep(500);
            while (true) {
                System.out.println("+++++++我抢...+++++++");
                boolean isLock = reentrantLock.tryLock();
                if (isLock) {
                    System.out.println("========我抢到锁了!!!===========");
                    reentrantLock.unlock();
                    return;
                }
                Thread.sleep(10);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName() + " 启动了,准备获取锁");
                reentrantLock.lock();
                System.out.println(Thread.currentThread().getName() + " 获取了锁");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();
            }
        }
    };

    public static void main(String args[]) {
        TestThread testThread = new TestThread();
        testThread.testLock();
        testThread.testUnfair();
    }
}

打印如下:


image.png
image.png

这俩张图结合来看:

1、第一张图:线程1~线程10 依次调用lock抢锁,然后主线程开始抢锁。
2、只要有一次能够证明主线成比线程1~线程10之间的某个线程先获得锁,那么就证明该锁为非公平锁。
3、第二张图:主线程比线程4~线程10 先获得了锁,说明过程是非公平的。

值得注意的是:

此处使用tryLock()抢占锁,tryLock()和lock(非公平模式)核心逻辑是一样的。

3、底层如何获取锁/释放锁

一直在提线程获取了锁,线程释放了锁,到底这个逻辑如何实现的呢?
从第一张全家福的图,可以看出锁的基本数据结构包含:

共享锁变量、volatile、CAS、同步队列。

假设设定共享变量为:volatile int threadId。

threadId == 0表示当前没有线程获取锁,thread !=0 表示有线程占有了锁。

获取锁

1、线程调用 CAS(threadId, 0, 1),预期threadId == 0, 若是符合预期,则将threadId设置为1,CAS成功说明成功获取了锁。
2、若是CAS失败,说明threadId != 0,进而说明有已经有别的线程修改了threadId,因此线程获取锁失败,然后加入到同步队列。

释放锁

1、持有锁的线程不需要锁后要释放锁,假设是独占锁(互斥),因为同时只有一个线程能获取锁,因此释放锁时修改threadId不需要CAS,直接threadId == 0,说明释放锁成功。
2、成功后,唤醒在同步队列里等待的线程。

synchronized 和 AQS 获取/释放锁核心思想就是上面几步,只是控制得更复杂,精细,考虑得更全面。

注:CAS(threadId, xx, xx)是伪代码

4、自旋锁与自适应自旋

很多文章说CAS是自旋锁,这说法是有问题的,本质上没有完全理解CAS功能和锁。

1、CAS 全称是比较与交换,若是内存值与期望值一致,说明没有其它线程更改目标变量,因此可以放心地将目标变量修改为新值。
2、CAS 是原子操作,底层是CPU指令。
3、CAS 只是一次尝试修改目标变量的操作,结果要么成功,要么失败,最后调用都会返回。

通过上个小结的分析,我们知道synchronized、AQS底层获取/释放锁都是依赖CAS的,难道说synchronized、AQS 也是自旋锁,显然不是。

自旋锁是不会阻塞的,而CAS也不会阻塞,因此可以利用CAS实现自旋锁:

    class MyLock {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        private void lock() {
            boolean suc = false;
            do {
                //底层是CAS
                suc = atomicInteger.compareAndSet(0, 1);
            } while (!suc);
        }   
    }

如上所示,自定义锁MyLock,线程1,线程2分别调用lock()上锁。

1、线程1调用lock(),因为atomicInteger== 0,所以suc == true,线程1成功获取锁。
2、此时线程2也调用lock(),因为atomicInteger==1,说明锁被占用了,所以suc==false,然而线程2并不阻塞,一直循环去修改。只要线程1不释放锁,那么线程2永远获取不了锁。

以上就是自旋锁的实现,可以看出:

1、自旋锁最大限度避免了线程挂起/与唤醒,避免上下文切换,但是无限制的自旋也会徒劳占用CPU资源。
2、因此自选锁适用于线程执行临界区比较快的场景,也就是获得锁后,快速释放了锁。

既想要自旋,又要避免无限制自旋,因此引入了自适应自旋:

    class MyLock {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        //最大自旋次数
        final int MAX_COUNT = 10;
        int count = 0;
        private void lock() {
            boolean suc = false;
            while (!suc && count <= MAX_COUNT) {
                //底层是CAS
                suc = atomicInteger.compareAndSet(0, 1);
                if (!suc)
                    Thread.yield();
                count++;
            }
        }
    }

可以看出,给自旋设置了最大自旋次数,若还是没能获取到锁,则退出死循环。

实际上synchronized、ReentrantReadWriteLock 等的实现里,同样为了尽量避免线程挂起/唤醒,在抢占锁的过程中也是采用了自旋(自适应自旋)的思想,但这只是它们锁实现的以小部分,它们并不是自旋锁。

5、为什么需要等待/通知机制

先看独占锁的伪代码:

    //Thread1
    myLock.lock();
    {
        //临界区代码
    }
    myLock.unLock();

    //Thread2
    myLock.lock();
    {
        //临界区代码
    }
    myLock.unLock();

Thread1、Thread2 互斥拿到锁后各干各的,互不干涉,相安无事。
若是现在Thread1、Thread2 需要配合做事,如:

    //Thread1
    myLock.lock();
    {
        //临界区代码
        while (flag == false)
            wait();
        //继续做事
    }
    myLock.unLock();

    //Thread2
    myLock.lock();
    {
        //临界区代码
        flag = true;
        notify();
        //继续做事
    }
    myLock.unLock();

如上代码,Thread1需要判断flag == true才会往下运行,而这个值需要Thread2来修改,Thread1、Thread2 两者间有协作关系。于是Thread1需要调用wait 释放锁,并阻塞等待。Thread2在Thread1释放锁后拿到锁,修改flag,然后notify 唤醒Thread1(唤醒时机在Thread2执行完临界区代码并释放锁后)。Thread1 被唤醒后继续抢锁,然后判断flag==true,继续做事。
于是,Thread1、Thread2愉快配合完成工作。
为啥wait/notify 需要先获取锁呢?flag 是线程间共享变量,需要在并发条件下正确访问,因此需要锁。

至此,线程并发系列文章暂时告一段落了。大家对这系列文章有疑惑,请评论留言。

本文基于jdk 1.8。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容