欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 科技 > IT业 > JUC(java.util.concurrent)中的常见类

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

2024/10/25 3:22:40 来源:https://blog.csdn.net/2302_77179144/article/details/139989809  浏览:    关键词:JUC(java.util.concurrent)中的常见类

文章目录

  • Callable接口
  • ReentrantLock
    • ReentrantLock 和 synchronized 的区别:
    • 如何选择使用哪个锁?
  • 信号量Semaphore
  • CountDownLatch
  • 多线程环境使用ArrayList
  • 多线程使用 哈希表
  • 相关面试题

JUC放了和多线程有关的组件

Callable接口

和Runnable一样是描述一个任务,但是有返回值,表示这个线程执行结束要得到的结果是啥

Callable 通常需要搭配 FutureTask 来使用. FutureTask用来保存 Callable 的返回结果. 因为Callable 往往是在另⼀个线程中执行的, 啥时候执行完并不确定.FutureTask 就可以负责等待结果出来的工作

理解 FutureTask
想象去吃麻辣烫. 当餐点好后, 后厨就开始做了. 同时前台会给你⼀张 “小票” . 这个小票就是FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没

public class Test {private static int sum = 0;public static void main(String[] args) throws InterruptedException {//创建一个线程,让这个线程实现 1+2+3+...+1000Thread t = new Thread(new Runnable() {@Overridepublic void run() {int result = 0;for (int i = 1; i <= 1000; i++) {result += i;}//此处为了把result告知主线程,需要通过成员变量sum = result;}});t.start();t.join();System.out.println(sum);}
}

这个代码让主线程和t 线程耦合太大了
Callable就是为了降低耦合度的

• 创建⼀个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.
• 重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.
• 把 callable 实例使用 FutureTask 包装一下.
• 创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.
• 在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Test {public static void main(String[] args) throws InterruptedException, ExecutionException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result = 0;for(int i = 1; i <= 1000; i++) {result += i;}return result;}};//创建线程,把callable搭载到线程内部执行FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();System.out.println(futureTask.get());}
}

ReentrantLock

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

ReentrantLock 的用法:

  • lock(): 加锁, 如果获取不到锁就死等.
  • trylock(): 尝试加锁,如果锁已经被占用了,直接返回失败,而不会继续等待。还可以指定等待超时时间加锁, 如果获取不到锁, 等待⼀定的时间之后就放弃加锁.
  • unlock(): 解锁

ReentrantLock 和 synchronized 的区别:

  • synchronized 是⼀个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的⼀个类, 在 JVM 外实现的(基于 Java 实现).
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入⼀个 true 开启公平锁模式

在这里插入图片描述
ReentrantLock的参数是true就是公平锁,false或者不写就是非公平锁

  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
import java.util.concurrent.locks.ReentrantLock;public class Test {public static void main(String[] args) {ReentrantLock locker = new ReentrantLock(true);try {//加锁locker.lock();} finally {//解锁locker.unlock();}}
}
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待⼀段时间就放弃.

  • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

如何选择使用哪个锁?

• 锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便.
• 锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等.
• 如果需要使用公平锁, 使用 ReentrantLock

信号量Semaphore

信号量就是一个计数器,描述了可用资源的个数

围绕信号量有两个基本操作

  1. P操作:计数器-1,申请资源
  2. V操作:计数器+1,释放资源

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

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

代码示例
• 创建 Semaphore 实例, 初始化为 4, 表示有 4 个可用资源.
• acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)

import java.util.concurrent.Semaphore;public class Test {public static void main(String[] args) throws InterruptedException {//4个可用资源Semaphore semaphore = new Semaphore(4);semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.release();}
}

在这里插入图片描述
总共四个可用资源,进行第五次P操作会阻塞直到其他线程执行V 操作

import java.util.concurrent.Semaphore;public class Test {public static void main(String[] args) throws InterruptedException {//4个可用资源Semaphore semaphore = new Semaphore(4);semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.release();}
}

在这里插入图片描述
锁其实是特殊的信号量
如果信号量只有0 , 1两个取值,此时就称为"二元信号量",本质上就是一把锁

import java.util.concurrent.Semaphore;public class Test {private static int count = 0;public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(1);Thread t1 = new Thread(()-> {try {for (int i = 0; i < 50000; i++) {semaphore.acquire();count++;semaphore.release();}} catch (InterruptedException e) {throw new RuntimeException(e);}});Thread t2 = new Thread(()-> {try {for (int i = 0; i < 50000; i++) {semaphore.acquire();count++;semaphore.release();}} catch (InterruptedException e) {throw new RuntimeException(e);}});t1.start();t2.start();t1.join();t2.join();System.out.println("count = " + count);}
}

在这里插入图片描述

CountDownLatch

当我们把一个任务拆分成多个的时候,可以通过这个工具类识别任务是否整体执行完毕了

IDM这种比较专业的下载工具就是多线程下载,把一个大的文件拆分成多个部分,每个线程都独立和人家服务器建立连接,分多个连接进行下载,等所有线程下载完毕之后,再对结果进行合并。
这时候就需要识别出所有线程是否都执行完毕了,此处就可以使用CountDownLatch

代码示例

  • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
  • 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
  • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了
import java.util.concurrent.CountDownLatch;public class Test {public static void main(String[] args) throws InterruptedException {CountDownLatch latch = new CountDownLatch(10);//有10个线程for (int i = 0; i < 10; i++) {int id = i;Thread t = new Thread(()-> {try {//假设这里进行一些"下载"这样的耗时操作Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException();}System.out.println("线程结束" + id);latch.countDown();});t.start();}//通过 await 等待所有的线程调用countDown//await 会阻塞等待到countDown调用的次数和构造方法指定的次数一致的时候,await才会返回latch.await();System.out.println("所有线程结束");}
}

在这里插入图片描述
await 不仅仅能替代 join,还可以判断任务是否全部完成

多线程环境使用ArrayList

Vector每个方法都有synchronized加锁
如果ArrayList这样没加锁的集合类想达到类似于Vector的效果就可以用Collections.synchronizedList(new ArrayList);

synchronizedList 是标准库提供的⼀个基于 synchronized 进行线程同步的 List.

CopyOnWriteArrayList(写时拷贝)也是一种解决线程安全问题的做法
假设有个数组有1,2,3,4这四个数据
多个线程读取,一个线程将2改为200,这样就有可能读取不到2,这是bug

我们就可以用原来的数组去读,新建一个数组去修改,写完之后用新的数组的引用代替旧的数组的引用(引用赋值的操作是原子的)

这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。CopyOnWrite容器也是⼀种读写分离的思想,读和写不同的容器。

  • 优点:

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

  • 缺点:
  1. 占用内存较多.
  2. 新写的数据不能被第⼀时间读取到.

上述过程,没有任何加锁和阻塞等待,也能确保读线程不会读出"错误的数据"

有些服务器程序,需要更新配置文件/数据文件,就可以采取上述策略

显卡渲染画面到显示器就是按照写时拷贝的方式,在显示上一个画面的时候,在背后用额外的空间生成下一个画面,生成完毕就用下一个画面代替上一个画面

多线程使用 哈希表

HashMap 是不带锁的
Hashtable 虽然带锁,但线程不一定更安全,只是简单的把关键方法加上了 synchronized 关键字.

  • 一个Hashtable就只有一把锁,如果多线程访问同⼀个 Hashtable 就会直接造成锁冲突.
  • size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
  • ⼀旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低

标准库提供了更好的代替: ConcurrentHashMap

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是是用 synchronized, 但是不是"一把全局锁", 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率。不同线程针对不同的链表进行操作是不涉及锁冲突的(不涉及修改"公共变量",也就不涉及到线程安全问题),这样大部分的操作没有锁冲突,就只是偏向锁
  • 像size方法,即使插入/删除的元素是不同链表上的元素,也会涉及到多线程修改同一个变量。引入CAS的方式来修改size,提高了效率也避免了加锁的操作
  • 优化了扩容方式: 化整为零

HashMap要在一次put的过程中完成整个扩容的过程,就会使put操作效率变得很低。 ConcurrentHashMap在扩容的时候就会搞两份空间,一份是扩容之前的空间,一份是扩容之后的空间。后续每个来操作 ConcurrentHashMap 的线程,都会把一部分数据从旧空间搬运到新空间,分多次搬运。

搬的过程中:

  • 插入操作就插入到新的空间里面
  • 删除操作就是新的旧的空间里面的都要删除掉
  • 查找就是新的旧的空间都要查找

相关面试题

  1. 线程同步的方式有哪些?

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.

  1. 为什么有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例,

  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待⼀段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入⼀个 true 开启公平锁模式.
  • synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是⼀个随机等待的线程.ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
  1. 信号量听说过么?之前都用在过哪些场景下?

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

使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作.

  1. 谈谈 volatile关键字的用法?

volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第⼀时间读取到最新的值.

  1. Java多线程是如何实现数据共享的?

JVM 把内存分成了这几个区域:方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.

  1. 在多线程下,如果对⼀个数进行叠加,该怎么做?
  • 使用 synchronized / ReentrantLock 加锁
  • 使用 AtomInteger 原子操作.

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com