多线程(九):JUC组件

在来时juc组件前,我们先把上一章遗漏的部分给补上。

synchronized 实现策略:锁升级:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 

还有一个 :

锁消除

锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。

就是在编译阶段 做的优化手段 ~~ 检测到当前代码是否是在多线程状态下运行的 / 是否有必要去进行加锁操作!如果是不必要的,但是又把锁加上了,那么 在编译过程中就会自动把锁去掉。

锁粗化

我们之前了解了锁的粒度:描述被synchronized 修饰的代码块的长度;   

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

我们来画个图:

多线程(九):JUC组件_第1张图片

 在写代码时反复的加锁,编译器就对其进行优化,直接优化为从第一次加锁开始直到最后一次释放锁才释放。

Callable 接口

callable 是线程实现的方法之一:

它与之前三种的区别在于:

callable可以有返回值,也可以抛出异常的特性,而Runnable等没有。

它的用法类似于 Runnable ;

例如:

实现1 到 1000 的加法: 

多线程(九):JUC组件_第2张图片

这是我们不能像Runnable 方法一样直接放到 Thread 类中,我们还需要一个中转类:

FutureTask 类。

这里就好比我们点完餐后领取一个小票,没有这个小票我们就不能去领餐了。

多线程(九):JUC组件_第3张图片

这里的call 方法被 Thread 这个线程调用。

那么现在我们就可以总结一下我们可以实现线程的四个方法了:

1.  实现Thread 类

2.  继承Runnable 接口(本质都是重写run 方法)

3.  基于lambda 表达式(Lambda 表达式描述了一个代码块(或者叫匿名方法),可以将其作为参数传递给构造方法或者普通方法以便后续执行)

4.  实现Callable 接口

那么接下来正式开始本章的内容:

常见的 JUC 组件,这个组件就认识认识就好,都不需要背,需要的时候查找一下即可。

JUC 即 java.util.concurrent 的缩写。

ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

synchronized 是个关键字,进入被synchronized 修饰的代码块即被加锁,除了 代码块即解锁。

而 ReentrantLock 类提供了 lock 和 unlock 方法来进行加锁解锁。

我们大部分的情况下使用 synchronized 就够用了,ReentrantLock 是一个重要的补充。

ReentrantLock 和 synchronized 的区别:

  1. synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现)
  2. synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,但是也容易遗漏 unlock
  3. synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃
  4. synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式
  5. 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个:

  • AtomicBoolean
  • AtomicInteger
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

具体的案例这里就不实现了。

信号量 Semaphore

信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器

这里有个PV操作,PV是荷兰语申请资源和释放资源 单词的缩写。

P 操作申请资源 计数器 - 1

V 操作释放资源 计数器 +1

如果此时 计数器为0 ,那么继续申请资源就会阻塞等待。

而我们所谓的 锁 ;本质上就是个 计数器为1信号量。

而信号量是个广义的锁,不光能管理 0 和 1 的信号量还能管理多个资源。

具体的代码不过多演示,可以直接查。

CountDownLatch

同时等待 N 个任务执行结束。

这个就好像赛马:只有等每匹马都跑过终点了,才会公布成绩。

使用场景:

下载大文件:几十个 GB,我们单线程下载耗时非常长,那么就可以选择多线程下载;

我们把文件分成多份,每个线程只负责自己那部分文件的下载。

只有当最后一部分文件被下载完了才算下载完成。

线程安全的集合类(重点)

我们在数据结构中学过那么多集合类,其中大部分集合类都是线程不安全的;

只有 :Vector, Stack, HashTable, 是线程安全的(但不建议用), 其他的集合类不是线程安全的

如果需要在多线程下使用怎么办呢,直接加一个 synchronized 修饰(加锁)即可。

需要用到 ArrayList

简单介绍几个:

多线程(九):JUC组件_第4张图片

要用 ArrayList 时直接套壳即可。

 CopyOnWriteArrayList(写实拷贝集合类)

  1.  当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,
  2. 添加完元素之后,再将原容器的引用指向新的容器

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会
添加任何元素。

优点:

  1. 在读多写少的场景下, 性能很高, 不需要加锁竞争

缺点:

  1.  占用内存较多.
  2. 新写的数据不能被第一时间读取
     

多线程环境使用哈希表

在多线程环境下使用哈希表可以使用:

  1. Hashtable
  2. ConcurrentHashMap
     

Hashtable 安全的原因是:只是简单的把关键方法加上了 synchronized 关键字:

多线程(九):JUC组件_第5张图片

而我们还有一个类: ConcurrentHashMap

ConcurrentHashMap 和 Hashtable 的区别 (高频面试题)

对比而言,ConcurrentHashMap  相当于 Hashtable  的优化版本;

1. 加锁粒度不同(触发锁冲突的频率)

        Hashtable 是针对整个哈希表加锁的,任何一个增删改查的操作都会触发加锁,也就是会触发锁竞争。 如图:

多线程(九):JUC组件_第6张图片

ConcurrentHashMap  不是只有一把锁,每个链表(头结点)作为一把锁,每次进行操作,都是针对对应的锁进行加锁;
此时操作不同链表就是针对不同的锁加锁,不产生锁冲突
这样导致大部分加锁操作实际上没有锁冲突!

此时这里的加锁操作的开销就很低了 。

如图:

多线程(九):JUC组件_第7张图片

这是 Java8 提出来的,在之前Java1.7 即其以前采用 “分段锁”;目的和上述相似,但是是多个链表共用一把锁。

2. 充分利用 CAS 特性. 比如 获取元素个数,可以用size 属性通过 CAS 来更新. 避免出现重量级锁的情况.

3. 优化了扩容方式: 化整为零
发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
扩容期间, 新老数组同时存在.
后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小
部分元素.
搬完最后一个元素再把老数组删掉.
这个期间, 插入只往新数组加.
这个期间, 查找需要同时查新数组和老数组

好,聊到这里我们的多线程就可以告一段落了,后面还会经常用到多线程,多线程是结束更是开始,我们后面有时间再来常见的面试题。

你可能感兴趣的:(JavaEE(初阶),java,jvm,开发语言)