【多线程】JUC的常见类 | Callable接口 | ReentranLock | 线程安全的集合类

ops/2024/11/19 13:17:06/

文章目录

    • 一、JUC的常见类
      • 1.Callable接口
      • 2.ReentranrLock
        • 1.ReentranLock的优势
            • 1.两种加锁方法
            • 2.提供了公平锁的实现
            • 3.提供了更强大的等待通知机制。
    • 二、线程安全集合类
        • 1.多线程环境使用ArraList
          • 1.synchronizedList
          • 2.CopyOnWriteArrayList
            • 写时拷贝。
            • 局限性:
        • 2.多线程环境使用队列
        • 3.多线程环境使用哈希表
          • 1.ConcurrentHashMap


JUC_5">一、JUC的常见类

JUC :Java.util.concurrent

​ 并发(这个包里的内容,主要就是一些多线程相关的组件)

Callable_11">1.Callable接口

  • 也是一种创建线程的方式,配合FutureTask使用,FutureTask保存任务执行的结果
  • 适用于想让某个线程执行一个逻辑,并且返回结果的时候

相比之下,Runnable不关注结果。

java">    public static void main(String[] args) throws ExecutionException, InterruptedException {//定义任务Callable<Integer> callable = new Callable<Integer>() {//泛型的参数类型就是希望返回值的参数类型@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i < 1001; i++) {sum += i;}return sum;}};//把任务放进线程中去执行//callable不能直接传进线程当中,需要FutureTask类进行包装FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t1 = new Thread(futureTask);t1.start();//get方法来获取到callable的返回结果//由于线程是并发执行的,执行到get的时候,t线程可能还没执行完,get就会进行阻塞。System.out.println(futureTask.get());}
  • callable不能直接传进线程当中,需要FutureTask类进行包装
  • t线程还没执行完时,get就会进行阻塞

​ 就类似于食堂点餐,点餐完成之后会给你一个取餐牌,后厨就相当于一个线程,开始执行,这个过程需要进行等待。直到饭做好了,就可以凭餐牌取餐。futureTask就是小票,拿着小票来换取线程执行的结果。

创建线程的方式:

1.直接继承Thread,重写run(创建一个类/匿名内部类)

2.实现Runnable,重现run

3.实现Callable,重写call

4.Lambda表达式

5.ThreadFactory线程工厂

6.线程池


2.ReentranrLock

  • 可重入锁,效果和synchronized类似。
java">	ReentrantLock lock = new ReentrantLock(); 
-----------------------------------------
lock.lock();   
try {    // working    
} finally {    lock.unlock()    
}

要记得调用unlock(),进行解锁

ReentranLock_85">1.ReentranLock的优势
1.两种加锁方法

​ lock() :加锁后,如果遇到锁冲突,就会一直阻塞等待。

​ tryLock() : 尝试去加锁,如果没加上锁,就放弃了。

2.提供了公平锁的实现

默认情况下是非公平锁,可以通过构造方法传入一个 true 开启公平锁模式

公平锁(队列-实现加锁顺序)

3.提供了更强大的等待通知机制。

synchronized 是通过 Object 的 wait / notify 实现等待-唤醒,每次唤醒的线程是随机的

ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程


二、线程安全集合类

数据结构中的集合类大部分都是线程不安全的

Vector.Stack,HashTable的线程安全的。(都带有Synchronized)

Stack继承自Vector。Vector和HashTable都是历史遗留,是早期Java引入的集合类。在方法上都加上了synchronized.

针对其他线程不安全的集合类,如果在多线程环境中使用,就需要考虑线程安全问题。

可以自行进行加锁,同时Java标准库也提供了一些搭配的组件,来保证线程安全

1.多线程环境使用ArraList
1.synchronizedList
java">Collections.synchronizedList(new ArrayList)
  • 会返回一个新的对象,相当于给ArrayList套了层壳。在方法上直接使用synchronized。

就相当于ArrayList和加锁的外壳是可拆分的,吸取了Vector的教训,把本体和外壳分离开。在单线程环境下用本体,在多线程环境下用套外壳的本体。

2.CopyOnWriteArrayList
写时拷贝。

比如:多个线程同时使用一个ArrayList。可能会读,也可能会修改。

  • 如果两个线程都是读操作,不会涉及到线程安全问题。

  • 如果某个线程要进行修改,就把ArrayList复制出一份副本。修改线程就去修改这个副本,与此同时,另一个线程仍然可以读取数据(从原来的数据上进行读取),并不会相互干扰。

一旦修改完毕,就会使用修改好的数据,替换掉原来的数据(往往是引用赋值)

这样的修改过程,就不需要进行加锁。

局限性:

1.当前操作的ArrayList不能太大(拷贝成本不能太高)

2.更适合一个线程来修改,而不能是多个线程同时修改。(多个线程读,一个线程修改的场景)

​ 针对特定场景:适用于服务器的配置跟新。可以通过配置文件来描述配置的详细内容(文件本身不是很大),配置的内容就会被读到内存中,再由其他的线程,来读取这里的内容。而修改这个配置内容,往往只有一个线程来修改。使用某个命令让服务器重新加载配置,就可以使用写时拷贝的方式。

2.多线程环境使用队列

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

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

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

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

3.多线程环境使用哈希表

​ Hashtable保证线程安全的方式,主要就是给关键方法加上synchromized。只要两个线程,操作同一个Hashtable,就会出现锁冲突。

​ 实际上,如果不考虑触发扩容的前提下。对于链地址法的哈希表来说,操作不同链表上的内容,此时是线性安全的。只有在操作同一个链表上的内容时,才会发生线程安全问题。但是整个Hashtable只有一把锁。

1.ConcurrentHashMap
  • 1.调整了锁的粒度

​ ConcurrentHashMap最核心的改进,就是把一个全局的大锁,改进成每个链表独立的小锁。从而大幅度的降低锁冲突的概率。

​ 把每个链表的头结点,作为锁对象

​ 分段锁:Java8之前,concurrentHashMap就是基于分段锁的方式实现的(多个链表共用一把锁)。从Java8之后,就成了直接在链表头结点加锁。

  • 2.充分利用到了CAS的特性,把一些不必要加锁的环节省略了。

​ 比如使用一个变量来记录哈希表的中的元素个数,此时就没必要对变量来进行加锁,直接通过原子操作。用CAS来维护元素个数。

  • 3.针对读操作没有加锁。(激进)

读和读之间,读和写之间,都没有锁竞争。写和写还需要加锁。

底层修改的时候,避免了使用++这种非原子操作。而是使用 赋值“=”进行修改,保证写操作本身就是原子的。读的时候要么是写之前的旧值,要么是写之后的新值,不会出现读一半的情况。

  • 4.针对扩容操作,做了单独的优化。

​ 本身Hashtable或者HashMap在扩容的时候,都需要把所有元素都拷贝一遍。如果元素很多,拷贝就比较耗时。在极端情况下,用户访问了1000次,999次很流畅。而第1000次触发扩容,就会造成严重的卡顿问题。

  • 用化整为零的方法来解决:一旦需要进行扩容,确实需要搬运。但是不是一次搬运完,而是分为了多次进行搬运。每次只搬运一部分数据来避免单次操作过于卡顿的问题。

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


点击移步博客主页,欢迎光临~

偷cyk的图


http://www.ppmy.cn/ops/17291.html

相关文章

SQLite FTS5 扩展(三十)

返回&#xff1a;SQLite—系列文章目录 上一篇:SQLite的知名用户(二十九) 下一篇:SQLite 的命令行 Shell(三十一&#xff09; 1. FTS5概述 FTS5 是一个 SQLite 虚拟表模块&#xff0c;它为数据库应用程序提供全文搜索功能。在最基本的形式中&#xff0c; 全文搜索引擎允许…

力扣1146 快照数组

思路&#xff1a;初始时&#xff0c;使用的思路是对于每个快照的数组都进行一次副本保存&#xff0c;但是提交后是时间超出。因此基于 灵神. - 力扣&#xff08;LeetCode&#xff09; 的思路不构建数组&#xff0c;而是保存每个数组位置set的记录&#xff0c;记录采用的是键值对…

Mysql个人复习总结

最近想把mysql的知识点再过一遍,带着自己的理解使用简短的话把一些问题总结一下,尤其是开发中和面试中的高频问题,基础知识点可以参考之前写的如下几篇博客,这篇不再赘述,阅读顺序由浅入深依次递进。 一、MySQL 概述 数据库&表操作 数据增删改; 二、MySQL 单表查询 …

JS实现对用户名、密码进行正则表达式判断,按钮绑定多个事件,网页跳转

目标&#xff1a;使用JS实现对用户名和密码进行正则表达式判断&#xff0c;用户名和密码正确时&#xff0c;进行网页跳转。 用户名、密码的正则表达式检验 HTML代码&#xff1a; <button type"submit" id"login-btn" /*onclick"login();alidate…

C++ vs Rust vs Go性能

比较 C、Rust 和 Go 的性能涉及许多因素&#xff0c;包括编程语言本身的特性、编译器优化、代码实现方式等。我将提供一个简单的代码示例&#xff0c;演示如何使用这三种语言编写一个简单的计算斐波那契数列的程序&#xff0c;并在每种语言下进行性能比较。 C 代码示例&#x…

8点法估计基础矩阵

估计基础矩阵 文章目录 估计基础矩阵8点法归一化 8点法 8点法 根据两幅图像中8个对应点对之间的关系&#xff0c;采用SVD求 解最小二乘方 约束&#xff1a;det(F) 0 假设已知N对点的对应关系&#xff1a; { x i , x i ′ } i 1 N \{x_i,x^{\prime}_i\}_{i1}^N {xi​,xi′​…

transformer 最简单学习3, 训练文本数据输入的形式

1、输入数据中&#xff0c;源数据和目标数据的定义 def get_batch(source,i):用于获取每个批数据合理大小的源数据和目标数据参数source 是通过batchfy 得到的划分batch个 ,的所有数据&#xff0c;并且转置列表示i第几个batchbptt 15 #超参数&#xff0c;一次输入多少个ba…

AI论文速读 |2024[TPAMI]【综述】自监督学习在时间序列分析的分类、进展与展望

题目&#xff1a; Self-Supervised Learning for Time Series Analysis: Taxonomy, Progress, and Prospects 作者&#xff1a;Kexin Zhang, Qingsong Wen(文青松), Chaoli Zhang, Rongyao Cai, Ming Jin(金明), Yong Liu(刘勇), James Zhang, Yuxuan Liang(梁宇轩), Guansong…