目录
1.多线程环境使用集合类
2.多线程环境使用队列
3.多线程环境使用哈希表
3.1HashTable
3.2ConcurrentHashMap
4.死锁
4.1死锁是什么
4.2死锁的代码示例
4.3产生死锁的原因
4.4如何避免死锁
这里有一个代码示例:
定义一个普通的集合类,通过多线程同时对这个集合类进行add操作,并打印集合。
public static void Demo01() throws InterruptedException {
List<Integer> list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
int finalI=i;
Thread thread=new Thread(()->{
list.add(finalI);
System.out.println(list);
});
thread.start();
}
TimeUnit.SECONDS.sleep(1);
System.out.println("=================");
System.out.println(list);
}
却抛出了异常,这是一个并发修改异常,也就是说在多线程环境下使用了线程不安全的集合类。
那么在多线程环境下如何使用线程安全的集合类?
1.多线程环境使用集合类
在多线程环境下如何使用线程安全的集合类?
1.使用Vector,HashTable等JDK提供的线程安全的类(不建议用)
2.自己使用同步机制(synchronized或者ReentrantLock)(同上,不建议用)
3.使用工具类转换Collections.synchronizedList(new ArrayList)
//通过工具类来创建一个线程安全的集合
List<Object>list= Collections.synchronizedList(new ArrayList<>());
实现方式是在普通集合对象外层又包裹了一层synchronized完成的线程安全。(不建议用)
4.CopyOnWriteArrayList
他时JUC包下的一个类,使用的是一种叫写时复制技术来实现的。
//使用CopyOnWriteArrayList
CopyOnWriteArrayList<Integer>list=new CopyOnWriteArrayList<>();
写时复制技术:
1.当要修改一个集合时,先复制这个集合的复本
2.修改复本的数据,修改完成后,用复本覆盖原始集合
优点:
在读多写少的场景下,性能很高,不需要加锁竞争
缺点:
1.占用内存较多,因为复制了一份新的数据需要修改
2.新写的数据不能被第一时间读取到
在多线程环境中如果需要使用集合类那么优先考虑CopyOnWriteArrayList
2.多线程环境使用队列
多线程环境下使用队列都是基于底层的数据结果,并具备其特性。
1.ArrayBlockingQueue
基于数组实现的阻塞队列
2.LinkedBlockingQueue
基于链表实现的阻塞队列
3.PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
4.TransferQueue
最多只包含一个元素的阻塞队列
3.多线程环境使用哈希表
HashMap本身不是线程安全的。在多线程环境下使用哈希表可以使用:
- HashTable
- ConcurrentHashMap
3.1HashTable
只是简单的把关键方法加上了synchronized关键字。
这相当于直接针对HashTable对象本身加锁。读写的时候都加锁这样效率比较低,不推荐使用。
- 如果多线程访问同一个HashTable就会直接造成锁冲突
- size属性也是通过synchronized来控制同步,也是比较慢的
- 一旦触发扩容,就由该线程完成整个扩容过程,这个过程会涉及到大量的元素拷贝,效率会降低
一个HashTable只有一把锁,两个线程访问HashTable中的任意数据都会出现锁竞争。
3.2ConcurrentHashMap
相比于HashTable做出了一系列的改进和优化。
多线程环境下强烈推荐使用这种方式保证线程安全,它与HashTable,Collections不同,并不是使用synchronized关键字实现加锁的,而是通过JUC包下的ReentrantLock实现加锁。(ReentrantLock使用的是CAS,用户态来实现加锁)
优化:
1.更小的锁粒度
HashTable加锁的方式,对所有的操作全部加锁,必然对性能有影响
ConcurrentHashMap对每个Hash桶进行加锁,提高并发能力
2.只给写加锁,不给读加锁
加锁的方式是ReentrantLock,大量运用CAS操作,而且共享变量使用volatile修饰
3. 充分利用CAS特性。比如size属性通过CAS来更新,避免出现重量级锁的情况
4.对扩容进行了特殊优化
- 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
- 扩容期间, 新老数组同时存在.
- 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.
- 搬完最后一个元素再把老数组删掉.
- 这个期间, 插入只往新数组加.
- 这个期间, 查找需要同时查新数组和老数组
是一个典型的以空间换时间的用例。
4.死锁
4.1死锁是什么
死锁就是一个线程加上锁之后不运行也不释放僵持住了。死锁会导致程序无法运行,是一个最严重的bug之一。
举个栗子理解死锁滑稽老哥和女神一起去饺子馆吃饺子 . 吃饺子需要酱油和醋 .滑稽老哥抄起了酱油瓶 , 女神抄起了醋瓶 .滑稽 : 你先把醋瓶给我 , 我用完了就把酱油瓶给你 .女神 : 你先把酱油瓶给我 , 我用完了就把醋瓶给你 .如果这俩人彼此之间互不相让 , 就构成了死锁 .酱油和醋相当于是两把锁 , 这两个人就是两个线程
4.2死锁的代码示例
定义两个锁对象
//定义两个锁对象
Object locker1=new Object();
Object locker2=new Object();
线程1,先获取locker1,在获取locker2
//线程1,先获取locker1,再获取locker2
Thread t1=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"t1申请locker1");
synchronized (locker1){
System.out.println(Thread.currentThread().getName()+"t1申请到了locker1");
//模拟业务处理过程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取locker2
System.out.println(Thread.currentThread().getName()+"t1申请locker2");
synchronized (locker2) {
System.out.println(Thread.currentThread().getName() + "t1申请到了两把锁");
}
}
});
//启动t1
t1.start();
线程2,先获取locker2,在获取locker1
//线程2,先获取locker2,再获取locker1
Thread t2=new Thread(()->{
System.out.println(Thread.currentThread().getName()+"t2申请locker2");
synchronized (locker2){
System.out.println(Thread.currentThread().getName()+"t2申请到了locker2");
//模拟业务处理过程
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取locker2
System.out.println(Thread.currentThread().getName()+"t2申请locker2");
synchronized (locker1) {
System.out.println(Thread.currentThread().getName() + "t2申请到了两把锁");
}
}
});
//启动t2
t2.start();
运行结果
这样就造成了死锁,程序无法退出。两个线程对于加锁的顺序没有约定,就容易产生环路等待。
4.3产生死锁的原因
1.互斥使用:A被线程1占用了,线程2就不能用了
2.不可抢占:A被线程1占用了,线程2不能主动把锁A抢过来,除非线程1主动释放
3.请求保持:有多把锁,线程1拿到了锁A之后,不释放还要继续再拿锁B
4.循环等待:线程1等待线程2释放锁,线程2要释放锁得等待线程3先释放锁...形成了循环关系
4.4如何避免死锁
以上四条是形成死锁的必要条件,只要打破其中任何一条就可以避免死锁。
1.互斥使用和不可抢占是锁的基本特性,无法打破。
2.请求保持是有可能打破的,取决于代码怎么写
3.循环等待,约定好加锁顺序就可以打破循环等待。在4.2的代码示例中t1.locker1->locker2,t2.locker2->locker1这个顺序造成了循环等待,如果调整加锁顺序,就可以避免循环等待。
4.2示例代码改正:
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
synchronized (lock1) {
synchronized (lock2) {
// do something...
}
}
}
};
t2.start();