当前位置: 代码迷 >> 综合 >> 并发编程——JUC中的重要组件(atomic、信号量、locks、ConcurrentHashMap、线程安全的集合类)这些你都知道吗?
  详细解决方案

并发编程——JUC中的重要组件(atomic、信号量、locks、ConcurrentHashMap、线程安全的集合类)这些你都知道吗?

热度:25   发布时间:2024-02-07 19:38:44.0

JUC中的重要组件

  • 原子类 java.util.concurrent.atomic
  • java.util.concurrent.locks
  • Callable/Future/FutureTask
  • Semaphore信号量
  • 线程安全的集合类
    • ConcurrentHashMap(重点)
      • 理解HashMap的put方法的执行过程.
      • 对比HashMap、HashTable、ConcurrentHashMap

原子类 java.util.concurrent.atomic

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

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

java.util.concurrent.atomic

java.util.concurrent.locks

在这里插入图片描述
ReentrantLock的加锁和解锁操作是分开的 使用更加灵活
常见方法
tryLock 付过获取锁失败是不会挂起等待的而是直接放弃等待
locklnterruptibly 设置等待锁的过程中是否会被异常中断

如果用户想自己实现锁, 就可以实现这个借口, 进一步开发 所以这个包的主要作用就是让用户实现一些自己定制的锁

java.util.concurrent.locks

Callable/Future/FutureTask

使用Callable和FutureTask也可以创建线程 (创建的是带返回值的线程 借助这个线程可以得到结果)
在这里插入图片描述

创建线程的几种方式

Executors/ExecutorService(ThreadPoolExcutor)
线程池 主要用途就是避免频繁的创建和销毁线程
在这里插入图片描述

为什么要引入线程池? 如何自己简单实现一个线程池?

Semaphore信号量

信号量本质就是一个计数器, 表示可用资源的个数如果要申请资源计数器就-1(P) 释放资源计数器就+1 (V) (P和V操作是原子的)

类似一个停车场一样 来一辆车显示停车数目加一 走一辆 显示停车数目就减一

信号量中有个特殊的存在:二元信号量
只有0和1俩个取值 这个信号量也可以当成是一个简单的锁

ReentrantLock可重入互斥锁
最大区别就是对于Synchronized来说,synchronize是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成
便利性:很明显Synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,而ReenTrantLock需要手工声明来加锁和释放锁,为了避免忘记手工释放锁造成死锁,所以最好在finally中声明释放锁。
锁的细粒度和灵活度:很明显ReenTrantLock优于Synchronized

线程安全的集合类

ConcurrentHashMap(重点)

  • 一般的集合大多是线程不安全的常见的线程安全的集合有Vector、Stak、HashTable(不建议使用,是直接用SychronizedList实现效率很低) 再就是ConcurrentHashMap

  • ArrayList / HashMap就是一个线程不安全的.
    使用线程安全的版本进行代替.
    对于ArrayList 来说:
    1.自己加锁.
    2.Collections.sychronizedList

  • 对于HashMap来说
    1.自己加锁
    2.HashTable
    3.ConcurrentHashMap

  • 并发编程实践中,ConcurrentHashMap是一个经常被使用的数据结构,它的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响
    读取数据是不需要加锁,但不保证一定读到最新的数据。
    写入时,不涉及扩容,则对链表的第一个结点进行加锁,即只有下标一样的线程才争抢同一把锁。如果发生了扩容,则情况就非常复杂了;为了效率,它支持多个线程同时进行扩容,而且没有加锁。

理解HashMap的put方法的执行过程.

在这里插入图片描述

对比HashMap、HashTable、ConcurrentHashMap

HashMap线程不安全.
HashTable和ConcurrentHashMap线程安全

  • HashTable使用是最简单粗暴的synchronize加锁来实现线程安全 这就是一把大锁控制所有,一个HashTable实力内部只有一把锁,这就意味着只要针对这个实例进行操作,都必需先申请锁冲突的概率比较大 所以效率比较低

  • ConcurrentHashMap的设计就很巧妙,使用多把锁降低冲突概率 他的内部每一个桶都有一把锁 也就是每次操作的时候只有是同一个哈希值的线程才会去竞争对应的锁 这就大大降低了冲突的概率 也就提高了效率

  • 还有在ConcurrentHashMap中 充分利用CAS操作 他做到了能尽量使用CAS操作修使用CAS操作完成(比如一些++操作)
    并且在ConcurrentHashMap中有更优化的扩容方案

HashTable的扩容就是一鼓作气某个操作插入元素后触发扩容,就得由该线程把整个hash表都完成扩容后才能算是插入完毕. (耗时非常久)

ConcurrentHashMap的思路"大事化小" ,当需要扩容的时候,不会由某一次操作彻底完成扩 容,而是由后续的一系列操作都要进行扩容. (每次扩容一点点)
进入扩容状态后,存在新旧两个数组.后续的操作会逐渐的把旧数组元素搬运到新数组上
每次插入/查找/删除搬运一部分.日积月累就搬运完毕,就可以释放旧的数组了

拿搬家举例子
在这里插入图片描述

  相关解决方案