在来时juc组件前,我们先把上一章遗漏的部分给补上。
synchronized 实现策略:锁升级:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
还有一个 :
锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。
就是在编译阶段 做的优化手段 ~~ 检测到当前代码是否是在多线程状态下运行的 / 是否有必要去进行加锁操作!如果是不必要的,但是又把锁加上了,那么 在编译过程中就会自动把锁去掉。
我们之前了解了锁的粒度:描述被synchronized 修饰的代码块的长度;
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
我们来画个图:
在写代码时反复的加锁,编译器就对其进行优化,直接优化为从第一次加锁开始直到最后一次释放锁才释放。
callable 是线程实现的方法之一:
它与之前三种的区别在于:
callable可以有返回值,也可以抛出异常的特性,而Runnable等没有。
它的用法类似于 Runnable ;
例如:
实现1 到 1000 的加法:
这是我们不能像Runnable 方法一样直接放到 Thread 类中,我们还需要一个中转类:
FutureTask 类。
这里就好比我们点完餐后领取一个小票,没有这个小票我们就不能去领餐了。
这里的call 方法被 Thread 这个线程调用。
那么现在我们就可以总结一下我们可以实现线程的四个方法了:
1. 实现Thread 类
2. 继承Runnable 接口(本质都是重写run 方法)
3. 基于lambda 表达式(Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行)
4. 实现Callable 接口
那么接下来正式开始本章的内容:
常见的 JUC 组件,这个组件就认识认识就好,都不需要背,需要的时候查找一下即可。
JUC 即 java.util.concurrent 的缩写。
可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.
synchronized 是个关键字,进入被synchronized 修饰的代码块即被加锁,除了 代码块即解锁。
而 ReentrantLock 类提供了 lock 和 unlock 方法来进行加锁解锁。
我们大部分的情况下使用 synchronized 就够用了,ReentrantLock 是一个重要的补充。
ReentrantLock 和 synchronized 的区别:
原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个:
具体的案例这里就不实现了。
信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器
这里有个PV操作,PV是荷兰语申请资源和释放资源 单词的缩写。
P 操作申请资源 计数器 - 1
V 操作释放资源 计数器 +1
如果此时 计数器为0 ,那么继续申请资源就会阻塞等待。
而我们所谓的 锁 ;本质上就是个 计数器为1信号量。
而信号量是个广义的锁,不光能管理 0 和 1 的信号量还能管理多个资源。
具体的代码不过多演示,可以直接查。
同时等待 N 个任务执行结束。
这个就好像赛马:只有等每匹马都跑过终点了,才会公布成绩。
使用场景:
下载大文件:几十个 GB,我们单线程下载耗时非常长,那么就可以选择多线程下载;
我们把文件分成多份,每个线程只负责自己那部分文件的下载。
只有当最后一部分文件被下载完了才算下载完成。
我们在数据结构中学过那么多集合类,其中大部分集合类都是线程不安全的;
只有 :Vector, Stack, HashTable, 是线程安全的(但不建议用), 其他的集合类不是线程安全的
如果需要在多线程下使用怎么办呢,直接加一个 synchronized 修饰(加锁)即可。
需要用到 ArrayList
简单介绍几个:
要用 ArrayList 时直接套壳即可。
CopyOnWriteArrayList(写实拷贝集合类)
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会
添加任何元素。
优点:
缺点:
在多线程环境下使用哈希表可以使用:
Hashtable 安全的原因是:只是简单的把关键方法加上了 synchronized 关键字:
而我们还有一个类: ConcurrentHashMap
ConcurrentHashMap 和 Hashtable 的区别 (高频面试题)
对比而言,ConcurrentHashMap 相当于 Hashtable 的优化版本;
1. 加锁粒度不同(触发锁冲突的频率)
Hashtable 是针对整个哈希表加锁的,任何一个增删改查的操作都会触发加锁,也就是会触发锁竞争。 如图:
ConcurrentHashMap 不是只有一把锁,每个链表(头结点)作为一把锁,每次进行操作,都是针对对应的锁进行加锁;
此时操作不同链表就是针对不同的锁加锁,不产生锁冲突
这样导致大部分加锁操作实际上没有锁冲突!此时这里的加锁操作的开销就很低了 。
如图:
这是 Java8 提出来的,在之前Java1.7 即其以前采用 “分段锁”;目的和上述相似,但是是多个链表共用一把锁。
2. 充分利用 CAS 特性. 比如 获取元素个数,可以用size 属性通过 CAS 来更新. 避免出现重量级锁的情况.
3. 优化了扩容方式: 化整为零
发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
扩容期间, 新老数组同时存在.
后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小
部分元素.
搬完最后一个元素再把老数组删掉.
这个期间, 插入只往新数组加.
这个期间, 查找需要同时查新数组和老数组
好,聊到这里我们的多线程就可以告一段落了,后面还会经常用到多线程,多线程是结束更是开始,我们后面有时间再来常见的面试题。