多线程(基础3)
Posted ohana!
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程(基础3)相关的知识,希望对你有一定的参考价值。
目录
2.List(在多线程中不能使用ArrayList,LinkedList)
1.HashMap—HashTable—ConcurrentHashMap区别
一,callable接口
1.理解
- Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, Runnable描述的是不带返回值的任务.
- Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
- FutureTask 就可以负责这个等待结果出来的工作
2.与Runnable的区别与联系
- Callable 是一个 interface . 相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算结果
- Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, Runnable描述的是不带返回值的任务.
- Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
- FutureTask 就可以负责这个等待结果出来的工作
3.Callable创建线程
具体步骤:
1)定义一个Callable对象,重写带返回值的call方法
2)创建一个FutureTask未来任务对象
3)将任务交给线程执行
4)接收返回值,当前线程阻塞等待FutureTask任务执行完成,并获取结果
package day20220325;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableTest
public static void main(String[] args) throws ExecutionException, InterruptedException
Callable<Integer> callable = new Callable<Integer>()
@Override
public Integer call() throws Exception
int i = 1;
i = i + 1;
return i;
;
FutureTask<Integer> task = new FutureTask<>(callable);
Thread thread = new Thread(task);
thread.start();
System.out.println(task.get());
二,CountDownLatch
1.理解
- 同时等待 N 个任务执行结束.
- 好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩
2.构造
- 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
- 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
- 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
3.使用场景
等待多个线程全部执行完,在执行某个任务(只能减少,不能增加)
4.实现
package day20220325;
import java.util.concurrent.CountDownLatch;
public class MyCountDownLatch
public static void main(String[] args) throws InterruptedException
CountDownLatch count = new CountDownLatch(10);
for(int i = 0;i < 10;i++)
int j = i;
new Thread(new Runnable()
@Override
public void run()
System.out.println(j);
count.countDown();
).start();
count.await();
System.out.println("main");
三,Semaphore
1.理解
- 信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.
- Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用
理解信号量
可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.
当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)
当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)
如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.
2.构造
3.实现
package day20220325;
import java.util.concurrent.Semaphore;
public class MySemaphore
public static void main(String[] args)
Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable()
@Override
public void run()
try
System.out.println("申请资源");
semaphore.acquire();
System.out.println("我获取到资源了");
Thread.sleep(1000);
System.out.println("我释放资源了");
semaphore.release();
catch (InterruptedException e)
e.printStackTrace();
;
for (int i = 0; i < 20; i++)
Thread t = new Thread(runnable);
t.start();
四,线程安全的集合类
1.线程安全的集合类
Vector,HashTable,Stack,ConcurrentHashMap
2.List(在多线程中不能使用ArrayList,LinkedList)
- 同步的List
五,ConcurrentHashMap
1.HashMap—HashTable—ConcurrentHashMap区别
HashMap 是线程不安全的,效率高;HashTable 是线程安全的,效率低。
ConcurrentHashMap 可以做到既是线程安全的,同时也可以有很高的效率,得益于使用了分段锁
2.ConcurrentHashMap的底层原理
- 是基于哈希表实现的
- 底层数据结构是Node节点(本质上是一个单向链表),存放的是Node<K,V>数组,对Node键值对进行volatile修饰,并且采用了cas+synchronized来保证线程安全
- 采用数组+链表/红黑树的结构才保存数据
- 当达到存放数据的阈值之后,会进行扩容
3.加锁细节
1)对Node进行volatile修饰
对于读读操作来说本身就是具有原子性,使用volatile修饰,可以保证可见性,顺序性,因此,就保证了线程安全
2)put元素的细节(cas)
如果在进行数据插入时,并且当前桶的元素为空时,就进行cas+自旋的方式进行插入元素,确保不会因为加锁释放锁消耗太多的时间(对于桶空的情况来说,证明线程冲突比较小,符合cas)
3)put元素的细节(加锁)
对于真正的桶不空的情况来说,就要考虑加锁了,但加锁只对Node(头节点)位置进行加锁,插入的方式就是头插,因此只用对头节点进行加锁
4.扩容
如果数据已经超过了(插入数据的阈值,负载因子*容量),就需要进行扩容
六,死锁
1.什么是死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止所产生的bug
2.产生死锁的必要条件
- 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
- 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
- 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路
简单点说
锁的层面:锁本身必须互斥,并且不能被抢占
线程方面:线程不执行完不释放已经得到的锁,并且,还不会主动的放弃,只会死等
3.如何避免死锁
- 破坏产生死锁的任意一个条件都行
- 一般是从循环等待解决,多个线程约定好,都以某一个顺序进行执行就可以了
4.如何检测死锁
在jconsole中提供了检测死锁的方式
以上是关于多线程(基础3)的主要内容,如果未能解决你的问题,请参考以下文章