• JUC中的锁、信号量、并发集合


    一、JUC中锁的机制

    (1)AQS:AQS全名AbstractQueuedSynchronizer,是并发容器JUC(java.util.concurrent)下locks包内的一个类。它实现了一个FIFO(FirstIn、FisrtOut先进先出)的队列。底层实现的数据结构是一个双向链表。

    工作原理:

    AQS使用一个int state成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。

    AQS使用CAS对该同步状态进行原子操作实现对其值的修改,当state大于0的时候表示锁被占用,如果state等于0时表示没有占用锁。

    CAS:https://blog.csdn.net/weixin_53455615/article/details/126535949?spm=1001.2014.3001.5501

    理解:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用, 获取不到锁的线程加入到队列中。然后排队获取资源。

    二、JUC中的锁

    JUC中锁的底层使用的就是AQS

    1. ReentrantLock: Lock接口的实现类, 可重入锁。相当于synchronized同步锁

    2. ReentrantReadWriteLock:ReadWriteLock接口的实现类。类中包含两个静态内部类,ReadLock读锁、WriteLock写锁。

    3. Condition:是一个接口,都是通过lock.newCondition()实例化。属于wait和notify的替代品。提供了await()、signal()、singnalAll()与之对应

    4. LockSupport:和Thread中suspend()和resume()相似

     三、ReentrantLock重入锁

    可重入锁:https://blog.csdn.net/weixin_53455615/article/details/126483112?spm=1001.2014.3001.5501

    提供了2种类型的构造方法。

    1. ReentrantLock(): 创建非公平锁的重入锁。

    2. ReentrantLock(boolean): 创建创建锁。取值为true表示公平锁,取值为false表示非公平锁。

    公平锁:多线程操作共一个资源时, 严格按照顺序执行。

    非公平锁:新添加的线程先尝试获取获取资源,若获取到直接执行此线程,若没有获取到就添加到队列中去排队。

    注意:

    1. ReentrantLock出现异常时, 不会自动解锁

    2. 多线程的情况下, 一个线程出现异常, 并没有释放锁, 其他线程也获取不到锁, 容易出现死锁

    3. 建议把解锁方法finally{}代码块中

    4. synchronized加锁与释放锁不需要手动的设置, 遇到异常时, 会自动的解锁

    5.避免死锁,需要将解锁放到finally{}中

    1. package com.java.test;
    2. import java.util.concurrent.locks.ReentrantLock;
    3. public class Test02 {
    4. static int a = 0;
    5. public static void main(String[] args) throws InterruptedException {
    6. ReentrantLock rt = new ReentrantLock();
    7. for (int i = 0; i < 5; i++) {
    8. new Thread(new Runnable() {
    9. @Override
    10. public void run() {
    11. for (int i1 = 0; i1 < 1000; i1++) {
    12. rt.lock();//加锁
    13. a++;
    14. rt.unlock();//解锁
    15. }
    16. }
    17. }).start();
    18. }
    19. Thread.sleep(3000);
    20. System.out.println(a);//5000
    21. }
    22. }

    四、Condition等待 | 唤醒(线程通信)

    condition.await(); //线程等待

    condition.signal(); //唤醒一个线程
    condition.signalAll(); //唤醒所有线程

    注意:等待 | 唤醒,会释放锁

    1. package com.java.test;
    2. import java.util.concurrent.locks.Condition;
    3. import java.util.concurrent.locks.ReentrantLock;
    4. /*
    5. * 两个线程配合输出《静夜思》
    6. * 两个线程一个线程一句话
    7. * */
    8. public class Test03 {
    9. public static void main(String[] args) {
    10. //创建Condition实现类
    11. ReentrantLock rt = new ReentrantLock();
    12. Condition condition = rt.newCondition();
    13. new Thread(new Runnable() {
    14. @Override
    15. public void run() {
    16. try {
    17. //加锁
    18. rt.lock();
    19. System.out.println("床前明月光");
    20. //线程等待
    21. condition.await();
    22. System.out.println("举头望明月");
    23. //唤醒线程
    24. condition.signal();
    25. } catch (InterruptedException e) {
    26. e.printStackTrace();
    27. } finally {
    28. //解锁
    29. rt.unlock();
    30. }
    31. }
    32. }).start();
    33. new Thread(new Runnable() {
    34. @Override
    35. public void run() {
    36. try {
    37. rt.lock();
    38. System.out.println("疑是地上霜");
    39. condition.signal();
    40. condition.await();
    41. System.out.println("低头思故乡");
    42. } catch (InterruptedException e) {
    43. e.printStackTrace();
    44. } finally {
    45. rt.unlock();
    46. }
    47. }
    48. }).start();
    49. }
    50. }

    五、ReentrantReadWriteLock读写锁

    ReadLock 读锁,又称为共享锁。允许多个线程同时获取该读锁

    WriteLock 写锁,又称为独占锁。只有一个线程能获取,其他写的线程等待, 避免死锁

    1. package com.java.test;
    2. import java.util.concurrent.locks.ReentrantReadWriteLock;
    3. public class Test04 {
    4. public static void main(String[] args) {
    5. //创建读写锁
    6. ReentrantReadWriteLock rrw = new ReentrantReadWriteLock();
    7. //获取读锁 多个线程可以同时持有 , 共享锁
    8. ReentrantReadWriteLock.ReadLock readLock = rrw.readLock();
    9. //获取写锁 只能一个线程持有 , 独占性
    10. ReentrantReadWriteLock.WriteLock writeLock = rrw.writeLock();
    11. new Thread(new Runnable() {
    12. @Override
    13. public void run() {
    14. //readLock.lock();//加读锁
    15. writeLock.lock();//加写锁
    16. System.out.println(Thread.currentThread().getName());
    17. }
    18. }).start();
    19. }
    20. }

    六、LockSupport 暂停 | 恢复

    LockSupport是Lock中实现线程暂停和线程恢复。suspend()和resume()是synchronized中的暂停和恢复。

    注意: 暂停恢复不会释放锁, 避免死锁问题。

    1. package com.java.test;
    2. import java.util.concurrent.locks.LockSupport;
    3. public class Test05 {
    4. public static void main(String[] args) throws InterruptedException {
    5. Thread thread = new Thread(new Runnable() {
    6. @Override
    7. public void run() {
    8. System.out.println("5秒后继续执行");
    9. //暂停线程
    10. LockSupport.park();
    11. System.out.println("执行结束");
    12. }
    13. });
    14. thread.start();
    15. Thread.sleep(5000);
    16. LockSupport.unpark(thread);
    17. }
    18. }

    七、CountDownLatch计数器

    在开发中经常遇到在主线程中开启多个线程去并行执行任务,并且主线程需要等待所有子线程执行完毕后再进行汇总的场景。之前是使用join() | 主线程休眠实现的,但是不够灵活,某些场合和还无法实现,所以开发了CountDownLatch这个类。底层基于AQS。

    CountDown是计数递减的意思,Latch是门闩的意思。内部维持一个递减的计数器。可以理解为初始有n个Latch,等Latch数量递减到0的时候,结束阻塞, 执行后续操作。

    1. package com.java.test;
    2. import java.util.concurrent.CountDownLatch;
    3. public class Test06 {
    4. static int a = 0;
    5. public static void main(String[] args) throws InterruptedException {
    6. //创建计数器
    7. CountDownLatch count = new CountDownLatch(5);
    8. for (int i = 0; i < 5; i++) {
    9. new Thread(new Runnable() {
    10. @Override
    11. public void run() {
    12. test();
    13. count.countDown();//计数器 -1
    14. }
    15. }).start();
    16. }
    17. count.await();//当计数器为0时继续执行
    18. System.out.println(a);
    19. }
    20. private synchronized static void test() {
    21. for (int i = 0; i < 1000; i++) {
    22. a++;
    23. }
    24. }
    25. }

    八、CyclicBarrier回环屏障

    CountDownLatch优化了join()在解决多个线程同步时的能力,但CountDownLatch的计数器是一次性的。计数递减为0之后,再调用countDown()、await()将不起作用。为了满足计数器可以重置的目的,JDK推出了CyclicBarrier类。

    使用原理:await()方法表示当前线程执行时计数器值不为0则等待。如果计数器为0则继续执行。每次await()之后计算器会减少一次。当减少到0下次await从初始值重新递减。九、

    九、Semaphore 信号量

    CountDownLatch和CyclicBarrier的计数器递减的,而Semaphore的计数器是可加可减的,并可指定计数器的初始值,并且不需要事先确定同步线程的个数,等到需要同步的地方指定个数即可。且Semaphore也具有回环重置的功能,这一点和CyclicBarrier很像。底层也是基于AQS。

    1. package com.java.test;
    2. import java.util.concurrent.Semaphore;
    3. public class Test07 {
    4. public static void main(String[] args) throws InterruptedException {
    5. //创建信号量
    6. Semaphore semaphore = new Semaphore(3);
    7. //信号量+1
    8. semaphore.release();
    9. //信号量+n
    10. semaphore.release(5);
    11. //信号量-1
    12. semaphore.acquire();
    13. //信号量-n 信号量的值小于0 , 线程阻塞执行
    14. semaphore.acquire(10);
    15. //获取信号量中的值
    16. int i = semaphore.availablePermits();
    17. System.out.println(i);
    18. }
    19. }

    十、并发集合

    并发集合类:主要是提供线程安全的集合

    比如:

    1. ArrayList对应的并发类是CopyOnWriteArrayList

    2. HashSet对应的并发类是 CopyOnWriteArraySet

    3. HashMap对应的并发类是ConcurrentHashMap

    十一、CopyOnWriteArrayList

    使用方式和ArrayList相同, 当然CopyOnWriteArrayList线程为安全的。

    Vector:也是ArrayList的子类,它也是线程安全的。

    原理:写时复制

    通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素。

    添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

    十二、CopyOnWriteArraySet

    CopyOnWriteArraySet在CopyOnWriteArrayList 的基础上使用了Java的装饰模式,所以底层是相同的。而CopyOnWriteArrayList本质是个动态数组队列,所以CopyOnWriteArraySet相当于通过动态数组实现的Set, CopyOnWriteArrayList中允许有重复的元素;但CopyOnWriteArraySet是一个Set集合,所以它不能有重复数据。因此, CopyOnWriteArrayList额外提供了addIfAbsent()和addAllAbsent()这两个添加元素的API,通过这些API来添加元素时,只有当元素不存在时才执行添加操作!

    十三、ConcurrentHashMap

    (1)Segment段锁:Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,使用多个锁来控制对hash表的不同部分(段segment)进行的修改,如果多个修改操作发生在不同的段上,他们就可以并发进行,从而提高了效率。

    (2)ConcurrentHashMap在JDK8中进行了巨大改动。它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现, 利用synchronized + CAS, 如果没有出现hash冲突, 使用CAS直接添加数据, 只有出现hash冲突的时候才会使用同步锁添加数据, 又提升了效率, 它底层由"数组"+链表+红黑树的方式思想(JDK8中HashMap的实现), 为了做到并发,又增加了很多辅助的类,例如TreeBin,Traverser等对象内部类。

    (3)辅助类

    Node节点: 默认数组上的结点就是Node结点。Node只有一个next指针,是一个单链表,提供find方法实现链表查询, 当出现hash冲突时,Node结点会首先以链表的形式链接到table上,当结点数量大于等于8并且数组长度大于64,链表会转化为红黑树。

    TreeNode节点: TreeNode就是红黑树的结点,TreeNode不会直接链接到table[i]——桶上面,而是由TreeBin链接,TreeBin会指向红黑树的根结点。

    TreeBin节点: TreeBin会直接链接到table[i]——桶上面,该结点提供了一系列红黑树相关的操作,以及加锁、解锁操作。

    ForwardingNode: ForwardingNode 在table扩容时使用,内部记录了扩容后的table,即nexttable。

    ReservationNode: 在并发场景下、在从 Key不存在 到 插入 的 时间间隔内,为了防止哈希槽被其他线程抢占,当前线程会使用一个reservationNode节点放到槽中并加锁,从而保证线程安全。

  • 相关阅读:
    Redis 常见问题
    【深度学习】之 卷积神经网络(CNN)概念 简析:名词介绍 || 为何要用卷积? || 卷积 || 激活函数 || 池化层 || 全连接层 || CNN的优点
    SEO搜索引擎优化-SEO搜索引擎优化软件
    自动化测试工程师--pyhon基础知识体系
    铭文是什么?有什么价值?
    物联网?快来看 Arduino 上云啦
    第六篇:集合常见面试题
    MATLAB算法实战应用案例精讲-【图像处理】机器视觉(基础篇)(十)
    Linux C/C++ 学习笔记(七):DNS协议与请求
    舒服,给Spring贡献一波源码。
  • 原文地址:https://blog.csdn.net/weixin_53455615/article/details/126542155