多线程JUC及扩展

JUC(java.util.concurrent)的常见类

ReentrantLock

先导

补充一种锁策略:独占锁/共享锁

独占锁概念

独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。JDK中的synchronized和 JUC中Lock的实现类就是互斥锁。

共享锁概念

共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。 独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。

java.util.concurrent.locks(顶级接口)

特性

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

ReentrantLock 也是可重入锁. "Reentrant" 这个单词的原意就是 "可重入"

可以通过传参实现公平锁

用法

  • lock():加锁,如果获取不到锁就死等
  • trylock(超时时间):加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁
  • unlock(): 解锁

ReentrantLock lock = new ReentrantLock(); ----------------------------------------- lock.lock(); try { // working } finally { //防止因异常无法释放锁 lock.unlock(); }

RenntrantLock VS synchronized

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

更适用于竞争锁激烈时

原子类

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

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

信号量Semaphore

信号量,用来表示“可用资源的个数”,其本质就是一个计数器

理解信号量

可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.

当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)

当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)

如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.

使用-构造函数

使用给定的数量作为资源数,来创建一个信号量

常用函数

一个线程申请一定数量的资源

acquire():默认== acquire(1)

信号量资源数 -= acquire资源数

一个线程释放一定数量的资源(不会有等待的情况)

release():默认== release(1)

信号量资源数 += release资源数

资源释放,最好写入finnaly块中,防止异常终止,无法释放资源!!!

使用场景

  1. 有限资源的并发执行,实现共享锁
  2. 多个线程的并发执行,要求其全部执行完后,某个线程继续执行

线程安全的集合类

多线程环境下的List

1.自己使用同步机制(synchronized 或 ReentrantLock)

参考synchronized 与 ReentrantLock 的使用

2. Collections.synchronizedList(List对象);

返回一个同步的List,内部都是Synchronized保证线程安全,但效率不高

3. CopyOnWriteArrayList;

CopyOnWrite容器即写时复制的容器。

CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

  • 写操作(添加,删除)时,先复制当前容器,在新容器中写元素
  • 添加完元素之后,再将原容器的引用指向新的容器
  • 读操作时,旧的容器可并发并行的读(获取元素)

对旧容器并发并行的读不需要加锁,因为当前容器不会添加任何元素。

优点:

1.读多写少效率高,无需加锁竞争

缺点:

1.空间占用大

2.新写入的数据无法第一时间被读取到

多线程环境使用队列

1. ArrayBlockingQueue

基于数组实现的阻塞队列

2. LinkedBlockingQueue(无边界)

基于链表实现的阻塞队列

3. PriorityBlockingQueue(实现定时器)

基于堆实现的带优先级的阻塞队列

4. TransferQueue

最多只包含一个元素的阻塞队列

多线程环境下的Map

HashMap本身不是线程安全的;

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

  • Hashtable(不推荐)
  • ConcurrentHashMap

Hashtable

底层数据结构:数组+链表

实现:

所有方法都是synchronized加锁保证线程安全(相当于锁Hashtable对象本身)

如果多线程访问同一个 Hashtable 就会直接造成锁冲突.

size 属性也是通过 synchronized 来控制同步, 也是比较慢的.

一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.

缺点:

效率不高

一个Hashtable 只有一个锁,两个线程访问任意数据就会出现锁竞争

ConcurrentHashMap:并发的HashMap

底层数据:数组+链表+红黑树

实现:

  • 读操作没有加锁,保证读读/读写并发安全(ConcurrentHashMap及Node中的属性通过volatile(保证可见性,禁止指令重排序)保证从内存读取结果);
  • 对写操作加锁,加锁方式:使用synchronized,不是锁整个对象,而是"锁桶”,(用每个链表的头结点作为锁对象)大大降低锁冲突的概率
  • 充分利用CAS特性

回顾put流程

1.使用key对象hashcode函数,继续hashcode得出在数组中的索引

2.该位置没有元素(不发生hash冲突),放入元素

此时:采用CAS+自旋的方式(此时数组位置无元素,线程冲突几率小)

3.若该位置存在元素(hash冲突)——采用synchronized(node)加锁

a.遍历链表,通过equals判断是否相等

相等:替换

不等:插入链表

4.插入后,可能扩容

  • 扩容优化:化整为零
    • 发现需要扩容线程,创建新的数组,同时只搬运几个元素过去
    • 扩容期间:新老数组同时存在
      • 插入只在新数组中
      • 查找同时查找新老数组
    • 后序操作ConcurrentHashMap的线程,都会参加搬运过程,每个操作负责搬运一小部分元素

ConcurrentHashMap每个哈希桶都有一把锁

只有两个线程访问同一个hash桶的数据才会出现锁冲突

死锁

什么是死锁

多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线 程被无限期地阻塞,因此程序不可能正常终止。

经典理解:哲学家进餐

如何避免死锁

产生死锁的四个必要条件

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就 形成了一个等待环路。

四个条件同时成立时,形成死锁,同理破环任意一个可以让死锁消失

容易破环的的时循环等待

解决死锁问题

破环任意一个条件

常用技术:锁排序(破环循环等待)

对锁排序,按照固定顺序获取锁

假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号

(1, 2, 3...M).

N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值