
在前四篇中我们掌握了多线程的基础 API、锁机制以及常用的并发工具。但仅仅会用是不够的想要在面试中脱颖而出写出真正高性能的并发代码我们必须深入底层。本篇第五篇我们将进入多线程进阶从 JVM 的锁策略和 CAS 无锁机制讲起深入剖析synchronized的底层原理并全面解析 JUC 包下的核心类、线程安全集合。常见的锁策略1.1乐观锁vs悲观锁不是针对某一种具体的锁而是某个锁具有“悲观”或者“乐观”的特性。描述的是遇到的场景乐观锁加锁的时候预测接下来的锁竞争不激烈没有做额外的工作悲观锁加锁的时候预测接下来的锁竞争非常激烈针对这一预测做了额外的工作1.2重量级锁vs轻量级锁描述的是遇到场景之后的解决方案重量级锁面对悲观的场景下需要付出更多的代价 更低效轻量级锁面对乐观的场景付出的代价会更小 更高效1.3挂起等待锁vs自旋锁描述的是锁的实现挂起等待锁重量级锁的实现操作系统内核级别的。加锁的时候发现竞争就会进入阻塞状态后续需要内核唤醒。获取锁的周期更长很难做到及时获取到但另一角度说在这个过程中不会一直占用cpu资源。自旋锁轻量级锁的典型实现应用程序级别的。加锁的时候发现竞争一般也不会进入阻塞会忙等。获取锁的周期短能短时间内拿到锁但会一直消耗cpu。synchronized 中的轻量级锁策略大概率就是通过自旋锁的方式实现的1.4公平锁vs非公平锁公平锁顾名思义遵守“先来后到”。当A释放锁之后由于B比C先来的B就能先于C获取到锁非公平锁不遵守“先来后到”B和C都有可能获取到锁操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是非公平锁. 如果要想实现公平锁, 就需要依赖额外的数据结构, 来记录线程们的先后顺序。1.5可重入锁vs不可重入锁可重入锁字面意思即允许一个线程多次获取同一把锁。Java里只要以Reentrant开头命名的锁都是可重入锁而且JDK提供的所有现成的Lock实现类包括synchronized关键字锁都是可重入的1.6普通互斥锁vs读写锁多个线程读取同一个数据本身是线程安全的。但多个线程读取一个线程修改数据就涉及到线程安全问题了~~读写锁就是写的时候加锁然后解锁读的方式也加锁解锁。读写锁要求读者与读者之间不互斥。而写者要与任何人互斥读写锁最主要用在 频繁读, 不频繁写 的场景中CASCAS全称compare and swap字面意思比较和交换。是对于内存的读写和交换操作可以说CAS是在硬件的支持下实现了原子的特性在此就可以实现一个原子类不需要使用重量级锁就可以高效的多线程进行任务1.CAS的ABA问题1.1什么是ABA问题ABA问题引来的bug假设存在两个线程 t1 和 t2. 有⼀个共享变量 num, 初始值为 A.接下来, 线程 t1 想使用CAS 把 num 值改成 Z, 那么就需要先读取 num 的值, 记录到 oldNum 变量中.使用CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从B 改成了 A线程 t1 的 CAS 是期望 num 不变就修改. 但是 num 的值已经被 t2 给改了. 只不过又改成 A 了. 这个时候 t1 究竟是否要更新 num 的值为 Z 呢?到这一步, t1 线程无法区分当前这个变量始终是 A, 还是经历了一个变化过程.虽然有些时候对于反复修改最后的值影响不大但也避免不了些特殊情况比如说银行取钱问题。1.2解决方案给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.•CAS 操作在读取旧值的同时, 也要读取版本号.•真正修改的时候,如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 1.如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).JUCjava.util.concurrentCallable接口Callable 是⼀个 interface . 相当于把线程封装了⼀个 返回值. 方便程序员借助多线程的方式计算结果public static void main(String[] args) throws ExecutionException, InterruptedException { CallableInteger callablenew CallableInteger() { Override public Integer call() throws Exception { int result0; for (int i 0; i 100; i) { resulti; } return result; } }; FutureTaskInteger futureTasknew FutureTask(callable); Thread tnew Thread(futureTask); t.start(); Integer result futureTask.get(); // 这行代码会阻塞直到子线程算完 System.out.println(计算结果 result); // 输出 5050 }FutureTask是连接“有返回值的任务Callable”和“真正执行任务的线程Thread”之间的桥梁。ReentrantLockReentrantLock是可重入的互斥锁与synchronized是并列关系ReentrantLock有以下几种用法lock()加锁如果获取不到锁就等trylock(超时时间)加锁如果获取不到锁就等待设置的时间之后放弃unlock()解锁ReentrantLock 和 synchronized 的区别synchronized 是⼀个关键字, 是 JVM 内部实现的(⼤概率是基于 C 实现). ReentrantLock 是标准库的⼀个类, 在 JVM 外实现的(基于 Java 实现).•synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock•synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待⼀段时间就放弃.•synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.线程安全集合类1.为什么需要线程安全集合因为在多线程并发的情况下普通集合是不安全的。早期 Java 通过Collections.synchronizedList()、Collections.synchronizedMap()来包装集合原理是在每个方法上加synchronized锁这样的缺点是锁的力度太大而且只能适合并发量低的场景。2.Hashtable这只是简单的把方法加上了关键字synchronized。一个Hashtable只有一把锁两个线程访问Hashtable中的任意数据都会出现锁竞争3.ConcurrentHashMap相对于Hashtable做出了改进和优化。虽然加锁的方式仍然是用synchronized但加锁的不是整个对象而是“锁桶”头结点。因为头节点是这个桶的“入口”。不管你是要修改链表中间的某个节点还是往链表末尾加新节点或者是在红黑树里调整平衡都必须从入口头节点往下找。