文章目录
- 一:线程安全集合类
- (1)多线程环境下使用ArrayList
- (2)多线程环境使用队列
- (3)多线程使用哈希表
- 二:死锁
- (1)概念
- (2)死锁产生的四个必要条件
- A:互斥条件
- B:不可剥夺条件
- C:持有并等待条件
- D:循环等待条件
- (3)如何处理预防死锁
一:线程安全集合类
在Java集合框架中,大部分都是线程不安全的,当然也存在如Vector这样的线程安全类,但不推荐使用,因为效率太低。所以这里我们对之前学过的集合框架进行说明,以展示他们如何在多线程环境下使用
(1)多线程环境下使用ArrayList
方法一:自己使用同步机制完成,如Synchronized
或ReentrantLock
方法二:使用collections.synchronizedList(new ArrayList)
,synchronizedList
是标准库提供的一个基于Synchronized
进行线程同步的List
- 此时
synchronizedList
所有关键操作都会带上synchronized
,所以有点粗暴,是一种选择,但不太推荐使用
方法三:使用CopyOnWriteArrayList
,它不涉及锁,适用于“一写多读”的场景(也即写的频率较低)
-
CopyOnWrite
容器是指“写时拷贝”容器- 当我们向一个容器添加元素的时候,不直接添加,而是先把当前容器进行拷贝,拷贝出一个新的容器后,然后向新的容器中添加元素
- 添加完元素之后,再让原容器的引用指向新的容器
-
优缺点
-
优点:可以对原容器进行并发的读,从而不需要加锁;因此在读多写少的情形下,性能很高
-
缺点
- 占用内存较多
- 新写的数据不能在第一时间读到
-
(2)多线程环境使用队列
主要有以下几种
- 基于数组实现的阻塞队列:
ArrayBlockingQueue
- 基于链表实现的阻塞队列:
LinkedBlockingQueue
- 基于堆实现的带优先级的阻塞队列:
PriorityBlockingQueue
- 最多只包含一个元素的阻塞队列:
TransferQueue
(3)多线程使用哈希表
-
前面说过,HashMap本身不是线程安全的,而在多线程环境下要想使用哈希表可以有以下两种选择
Hashtable
:不推荐使用ConcurrentHashMap
:下面介绍
ConcurrentHashMap
:相较于Hashtable
来说,做了很多的优化,更便于使用,主要优化有
①:加锁的粒度变细
-
Hashtable
直接对整个对象加锁,一个Hashtable
只有一把锁,因此锁竞争非常激烈,只要线程访问Hashtable
中的任意数据就会产生竞争
-
ConcurrentHashMap
只对写操作加锁,并且不是锁整个对象,而是对每个哈希桶分别加锁,这大大降低了锁竞争发生的概率,只有两个线程访问同一个哈希桶时才有锁冲突
②:充分利用到了CAS特性
③:对扩容方式进行了优化:扩容过程类似于写时拷贝
- 扩容过程中,会创建一个新的数组,它会和旧的数组同时存在一段时间
- 后面每一个来操作
ConcurrentHashMap
的线程,都会负责将一小部分元素搬运至型数组 - 在这个过程中,如果要查询元素,那么会在新数组和旧数组上同时进行
- 在这个过程中,如果要插入元素,那么只在新数组上插入
- 在搬运完最后一个元素时删除旧数组
二:死锁
(1)概念
死锁:所谓死锁,是指多个进程因竞争资源而造成的一种互相等待的局面,若无外力作用,这些进程将无法向前推进
- 举例:我拿了你房间的钥匙,而我在自己的房间;你拿了我的房间的钥匙,而你又在自己的房间。如果我要从自己的房间走出去,必须要拿到你手中的钥匙,但是你要走出来又必须要拿到我手中的钥匙,于是形成了死锁
如下是典型的死锁代码
thread1
首先尝试获取locker1
,获取到之后再尝试获取locker2
thread2
首先尝试获取locker2
,获取到之后再尝试获取locker1
thread1
和thread2
再尝试获取自己的第二把locker
时发生死锁,因为自己想要的locker
被对方持有
public class TestDemo6 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread thread1 = new Thread(){
@Override
public void run(){
System.out.println("线程1在尝试获取locker1");
synchronized (locker1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程1已获取locker1现在尝试获取locker2");
synchronized (locker2){
System.out.println("线程1获取locker1和locker2成功");
}
}
}
};
Thread thread2 = new Thread(){
@Override
public void run(){
System.out.println("线程2在尝试获取locker2");
synchronized (locker2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程2已获取locker2现在尝试获取locker1");
synchronized (locker1){
System.out.println("线程2获取locker1和locker2成功");
}
}
}
};
thread1.start();
thread2.start();
}
}
(2)死锁产生的四个必要条件
死锁产生的四个必要条件:死锁必须同时满足以下四个条件才会发生
- 互斥条件
- 持有并等待条件
- 不可剥夺条件
- 循环等待条件(注意发生死锁一定有循环等待,但是发生循环等待未必死锁)
A:互斥条件
互斥条件:是指只有对必须互斥使用的资源抢夺时才可能导致死锁。比如打印机设备就可能导致互斥,但是像内存、扬声器则不会
- 进程A已经获得资源,进程B只能等待
B:不可剥夺条件
不可剥夺条件:是指进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放
C:持有并等待条件
持有并等待条件:是指进程已经至少保持了一个资源,但又提出了新的资源请求,但是该资源又被其他进程占有,此时请求进程被阻塞,但是对自己持有的资源保持不放
D:循环等待条件
循环剥夺条件:是指存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求
(3)如何处理预防死锁
- 发生死锁有4个必要条件,只要破坏其中之一就可以预防死锁。在这个4个必要条件中互斥、持有并等待和不可剥夺破坏起来都是不太现实的,所以我们一般会在循环等待这个条件上做文章
破坏循环等待条件:可以采用顺序资源分配方法。首先给系统中的资源进行编号,规定每个进程必须按照编号递增的顺序请求资源,编号相同的资源(也就是同类资源)一次申请完
- 这是因为一个进程只有在已经占有小编号资源的同时,才有资格申请更大编号的资源。所以已经持有大编号资源的进程不可能逆向申请小编号的资源
例如对于上面的案例,我们可以让thread1
和thread2
加锁的顺序一致,即都按照先locker1
后locker2
的方式进行加锁,这样的话就不会发生死锁了
public class TestDemo7 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread thread1 = new Thread(){
@Override
public void run(){
System.out.println("线程1在尝试获取locker1");
synchronized (locker1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程1已获取locker1现在尝试获取locker2");
synchronized (locker2){
System.out.println("线程1获取locker1和locker2成功");
}
}
}
};
Thread thread2 = new Thread(){
@Override
public void run(){
System.out.println("线程2在尝试获取locker1");
synchronized (locker1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("线程2已获取locker1现在尝试获取locker2");
synchronized (locker2){
System.out.println("线程2获取locker1和locker2成功");
}
}
}
};
thread1.start();
thread2.start();
}
}