当前位置: 首页 > news >正文

JUC的常见类、多线程环境使用集合类

目录

JUC的常见类

 原子类

线程池

信号量

CountDownLatch

线程安全的集合类

多线程环境使用 ArrayList

多线程环境使用队列

多线程环境使用哈希表

总结:


JUC的常见类

Callable 接口、ReentrantLock:见上节

 原子类

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

  • AtomicBoolean
  • Atomiclnteger
  • AtomiclntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

其中 Atomiclnteger 的常见方法有:

addAndGet(int delta);  相当于    i += delta;
decrementAndGet();    相当于    --i;
getAndDecrement();    相当于    i--;
incrementAndGet();     相当于    ++i;
getAndIncrement();     相当于     i++;

Atomiclnteger / AtomicLong 应用场景:

1. 进行数据的统计

2. 统计服务器收到的请求数量

……

线程池

虽然创建销毁线程比创建销毁进程更轻量,但是频繁创建销毁线程的时候还是会比较低效。线程池就是为了解决这个问题

如果某个线程不再使用了,并不是真正把线程释放,而是放到一个 “池子” 中,下次如果需要用到线程就直接从池子中取,不必通过系统重新创建了

ExecutorService 和 Executors

代码示例:

ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}
});

ExecutorService 表示一个线程池实例

Executors 是一个工厂类,能够创建出几种不同风格的线程池

ExecutorService 的 submit 方法能够向线程池中提交若干个任务

Executors 创建线程池的几种方式:见前面几节

ThreadPoolExecutor

四种构造方法,7个参数:见前面几节

信号量

信号量(Semaphore[ˈseməfɔː(r)]),用来表示 “可用资源的个数",本质上就是一个计数器

申请一个资源,计数器就会 -1    ->  P操作(acquire)

释放一个资源,计数器就会 +1   ->  V操作(release)

计数器为 0 时,继续申请,就会阻塞等待,直到有其他线程释放资源

public class Demo43 {public static void main(String[] args) throws InterruptedException {// 指定可用资源的个数是 "3"Semaphore semaphore = new Semaphore(3);semaphore.acquire();System.out.println("进行一次 P 操作");semaphore.acquire();System.out.println("进行一次 P 操作");semaphore.acquire();System.out.println("进行一次 P 操作");semaphore.acquire(); // 代码走到这里会阻塞System.out.println("进行一次 P 操作");}
}

信号量的一个特殊情况:初始值为1的信号量

取值要么是1要么是0(二元信号量)等价于 “锁”

普通的信号量,就相当于锁的更广泛的推广,普通的初始值为 N 的信号量,可以限制同时有多少个线程来执行某个逻辑

使用二元信号量的方式对 count++ 加锁:

public class Demo44 {private static int count = 0;public static void main(String[] args) {Semaphore semaphore = new Semaphore(1);Thread t1 = new Thread(new Runnable() {public void run() {for (int i = 0; i < 50000; i++) {try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}}});Thread t2 = new Thread(new Runnable() {public void run() {for (int i = 0; i < 50000; i++) {try {semaphore.acquire();count++;semaphore.release();} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t1.start();t2.start();try {t1.join();t2.join();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Final count: " + count);}
}

CountDownLatch

经常把一个大的任务拆分成多个子任务,使用多线程执行这些子任务,从而提高程序的效率

如何衡量,这多个子任务都完成了呢?(整个任务都完成了呢?)

可以使用 CountDownLatch 同时等待 N 个任务执行结束

使用过程:

1)构造 CountDownLatch 实例,假设构造方法中指定的参数是10,就表示有10个任务

2)每个任务执行完毕之后,都调用一次 latch.countDown 方法,在 CountDownLatch 内部的计数器会同时自减,当一共调用了10次 countDown(CountDownLatch 计数器自减到0),说明任务全都完成了

3)主线程中调用 latch.await 方法,等待所有任务执行完毕,当调用 countDown 的次数达到设置的任务数时,await 就会返回,否则会阻塞等待

public class Demo45 {public static void main(String[] args) throws InterruptedException {// 现在把整个任务拆成 10 个部分. 每个部分视为是一个 "子任务".// 可以把这 10 个子任务丢到线程池中, 让线程池执行.// 当然也可以安排 10 个独立的线程执行.// 构造方法中传入的 10 表示任务的个数.CountDownLatch latch = new CountDownLatch(10);ExecutorService executor = Executors.newFixedThreadPool(4);for (int i = 0; i < 10; i++) {int id = i;executor.submit(() -> {System.out.println("子任务开始执行: " + id);try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("子任务结束执行: " + id);latch.countDown();});}// 这个方法阻塞等待所有的任务结束// await 的 a 表示 alllatch.await();System.out.println("所有任务执行完毕");executor.shutdown();}
}

线程安全的集合类

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

多线程环境使用 ArrayList

1. 自己使用同步机制(synchronized 或 ReentrantLock),分析清楚,要把哪些代码打包到一起,成为一个“原子“操作  [推荐]

2. Collections.synchronizedList(new ArrayList):使用 synchronizedList 方法套壳,返回的新的 List 的各种关键方法都是带有 synchronized 的,类似于 Vector / Hashtable / StringBuffer  [不推荐]

3. 使用 CopyOnWriteArrayList

CopyOnWrite:编程中的一种常见思想方法(读写分离的思想),即写时拷贝

  • 当在数组中添加元素的时候,不直接在当前数组添加,而是先对当前数组进行 Copy,复制出一个新的数组,然后在新的数组里添加元素
  • 添加完元素之后,再将原数组的引用指向新的数组

详细说明:

1. 复制过程中,如果其他线程在读,就直接读取旧版本的数据,虽然复制过程不是原子的(会消耗一定的时间),但由于提供了旧版本的数据,所以不影响其他线程读取

2. 新数组复制完毕之后,直接进行引用的修改(引用的赋值是 “原子” 的),后续的读操作就都是针对新数组了

3. 这样就可以确保在读取过程中,要么读到的是旧版数据,要么读到的是新版数据,不会读到 “修改一半” 的数据(读到新版和旧版数据都是正确的操作)

4. 这样做的好处是可以对数组进行并发的读,而不需要加锁,不会产生阻塞

优点:

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

缺点:

  • 数组非常大时,拷贝一份非常大的数据,非常低效
  • 占用的内存较多
  • 多个线程同时修改,可能出现数据丢失的问题(原数组的引用只指向最新修改的数组)
  • 新写的数据不能被第一时间读取到

所以上述过程只适合特定场景,比如服务器进行重新加载配置的时候

1. 服务器正在运行,但是需要修改配置,正常来说,修改配置文件之后,是不会立即生效的,需要重启服务器后生效,但是重启的过程中服务器不能工作,这样是不可以的

2. 所以很多服务器会提供配置重加载(reload),这个功能可以在服务器运行过程中让修改后的配置立即生效

重加载:

配置会被读取到服务器的内存中,以数组/哈希存储,服务器代码中的其他逻辑会读取这些数组/哈希中的值。当程序员手动修改配置文件之后,手动触发 reload 功能,服务器就会创建新的数组/哈希,加载新的配置,加载完毕之后,使用新配置,代替旧配置。这就是写时拷贝的做法

多线程环境使用队列

1. ArrayBlockingQueue:基于数组实现的阻塞队列

2. LinkedBlockingQueue:基于链表实现的阻塞队列

3. PriorityBlockingQueue:基于堆实现的带优先级的阻塞队列

4. TransferQueue:最多只包含一个元素的阻塞队列

多线程环境使用哈希表

1. HashMap:线程不安全

2. Hashtable:线程安全

只是简单的把关键方法加上了 synchronized 关键字,是直接针对 Hashtable 对象本身(this)加锁的

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

  • 如果多线程访问同一个Hashtable 就会直接造成锁冲突
  • size 属性也是通过 synchronized 来控制同步的,效率很低
  • 一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,需要进行长时间的加锁,效率非常低

3. ConcurrentHashMap:效率更高。是按照桶级别进行加锁,而不是给整个哈希加一个全局锁,可以有效降低锁冲突的概率

是对 Hashtable 做出的一系列改进和优化

  • 读操作没有加锁(但是使用了volatile保证从内存读取结果),只对写操作进行加锁。加锁的方式仍然是是用 synchronized,但是不是锁整个对象,而是 "锁桶"(用每个链表的头结点作为锁对象),大大降低了锁冲突的概率
  • size 属性通过原子类(CAS)更新的,避免出现重量级锁的情况
  • 优化了扩容方式:化整为零。
    • 发现需要扩容的线程,会创建一个新的数组,同时只搬一小部分元素
    • 扩容期间,新老数组同时存在
    • 后续每个操作 ConcurrentHashMap 的线程,都会参与搬运的过程,每个操作都负责搬运一小部分元素
    • 这个期间,插入只往新数组插,查找需要同时查新数组和老数组
    • 搬完最后一个元素再把老数组删掉

一口气进行所有的搬运比较耗时,ConcurrentHashMap 的扩容会把整个的搬运拆成多次来完成

总结:

1.ConcurrentHashMap 的读是否要加锁,为什么?

答:读操作没有加锁,目的是为了进一步降低锁冲突的概率。为了保证读到最新修改的数据,搭配了 volatile 关键字

2. 介绍下 ConcurrentHashMap 的锁分段技术?

答:这个是Java1.7中采取的技术,Java1.8中已经不再使用了,简单的说就是把若干个哈希桶分成若干 "段" (Segment),针对每个段分别加锁。目的也是为了降低锁竞争的概率,当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争

3. ConcurrentHashMap 在 jdk1.8 做了哪些优化?

答:取消了分段锁,直接给每个哈希桶(每个链表)都分配一个锁(以每个链表的头结点对象作为锁对象)。将原来数组+链表的实现方式改进成数组+链表/红黑树的方式,当链表较长的时候(大于等于8个元素)就转换成红黑树

4. Hashtable 和 HashMap、ConcurrentHashMap 之间的区别?

HashMap: 线程不安全;key 允许为 null

Hashtable:线程安全。使用 synchronized 锁 Hashtable 对象,效率较低;key不允许为null

ConcurrentHashMap:线程安全。使用 synchronized 锁每个链表头结点,锁冲突概率低;充分利用 CAS 机制来更新 size 属性;优化了扩容方式;key不允许为null


文章转载自:

http://M7JWa1ul.hhxwr.cn
http://sOYcX7Ty.hhxwr.cn
http://XdIBxLfH.hhxwr.cn
http://B6CAvHpP.hhxwr.cn
http://XHkfDHbh.hhxwr.cn
http://I3q6GexQ.hhxwr.cn
http://WoS4rzd3.hhxwr.cn
http://h7ziWv13.hhxwr.cn
http://kx6rV2VT.hhxwr.cn
http://9X5NsH6l.hhxwr.cn
http://IG4OZMop.hhxwr.cn
http://GPLXDJcd.hhxwr.cn
http://o3cFY9Cw.hhxwr.cn
http://sjR2Hvd8.hhxwr.cn
http://VB4axkmH.hhxwr.cn
http://EYMtrT83.hhxwr.cn
http://3806c0sI.hhxwr.cn
http://6594fn6C.hhxwr.cn
http://5p3jahBO.hhxwr.cn
http://DEBp8idQ.hhxwr.cn
http://nZwaw16Y.hhxwr.cn
http://OQQDn0QX.hhxwr.cn
http://Ee2PdW7L.hhxwr.cn
http://aGLjH5ox.hhxwr.cn
http://W7WVgIU1.hhxwr.cn
http://N3LhYyJI.hhxwr.cn
http://2b1WpEGS.hhxwr.cn
http://WE5aJM4h.hhxwr.cn
http://OIInLreJ.hhxwr.cn
http://XAqD2anp.hhxwr.cn
http://www.dtcms.com/a/377506.html

相关文章:

  • 《C++ 108好库》之1 chrono时间库和ctime库
  • C++篇(7)string类的模拟实现
  • 弱加密危害与修复方案详解
  • 【Linux】Linux常用指令合集
  • Android- Surface, SurfaceView, TextureView, SurfaceTexture 原理图解
  • 如何设计Agent 架构
  • MySQL主从不一致?DBA急救手册:14种高频坑点+3分钟定位+无损修复!
  • 拍我AI:PixVerse国内版,爱诗科技推出的AI视频生成平台
  • 3D柱状图--自定义柱子颜色与legend一致(Vue3)
  • LeetCode热题100--199. 二叉树的右视图--中等
  • Next系统学习(三)
  • Python深度学习:NumPy数组库
  • Django时区感知
  • PostgreSQL15——Java访问PostgreSQL
  • Shell 函数详解
  • 【系统分析师】第21章-论文:系统分析师论文写作要点(核心总结)
  • Linux 命令(top/ps/netstat/vmstat/grep/sed/awk)及服务管理(systemd)
  • 【图像生成】提示词技巧
  • 揭秘Linux:开源多任务操作系统的强大基因
  • (ICLR-2025)深度压缩自动编码器用于高效高分辨率扩散模型
  • 《Why Language Models Hallucinate》论文解读
  • 【机器学习】通过tensorflow实现猫狗识别的深度学习进阶之路
  • AD5362BSTZ电子元器件 ADI 高精度数字模拟转换器DAC 集成电路IC
  • DMA-M2M存储器与存储器之间读写
  • Mistral Document AI已正式登陆Azure AI Foundry(国际版)
  • 机器学习实战(二):Pandas 特征工程与模型协同进阶
  • Flutter 朦胧效果布局大全:5种方法实现优雅视觉层次
  • 【CVPR2023】奔跑而非行走:追求更高FLOPS以实现更快神经网络
  • PHP学习(第三天)
  • 数仓简要笔记-1