一、死锁
1.1、死锁是什么
两个或两个以上的进程在执行过程中因争抢对方的资源而导致的互相等待现象,如果没有外力作用,他们都将无法继续下去,这就称系统处于死锁状态或者系统参数了死锁,这种互相等待的进程,就称为死锁进程。
1.2、死锁发生的必要条件
1.2.1、互斥条件排他性的使用
互斥条件排他性的使用,即一段时间内某资源仅有一个进程进行占用,如果此时还有其它进程请求资源,那么请求者只能等待,直到占有资源的进程用完,释放出来才可以进程对所分配的资源进行。
1.2.2、请求和保持条件
进程至少已经保持了一个资源,但又提出了心的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
1.2.3、不剥夺条件
指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时自己释放。
1.2.4、环路等待条件
发生死锁的时候,死锁的进程之间的资源一定是一个环形的链。
1.2.5、并发编码原则
要经常在并发或者锁的地方记录日志
1.3、实现死锁
1.3.1、代码
@Slf4j
public class DeadLock implements Runnable {
public int flag = 1;
//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
log.info("flag:{}", flag);
if (flag == 1) {
synchronized (o1) {
try {
//等待500ms防止该进程运行太快,另一个进程还没进入
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o2) {
log.info("1");
}
}
}
if (flag == 0) {
synchronized (o2) {
try {
//等待500ms防止该进程运行太快,另一个进程还没进入
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
synchronized (o1) {
log.info("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
//td2的run()可能在td1的run()之前运行
new Thread(td1).start();
new Thread(td2).start();
}
}
1.3.3、出现死锁原因
- 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
- 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
- td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
- td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
- td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
1.4、检测是否出现死锁
1.4.1、程序无法执行,并且CPU占用为0
在真实的环境中,我们发现,这样就有理由怀疑产生了死锁,但是光怀疑是不行的,我们需要一个实际的验证方法。
1.4.2、使用jdk提供的工具来检测是否真正发生了死锁。
(1)控制台输入jps
(2)找到怀疑发生死锁的进程,我的进程id是11860,输入jstack 11860
1.5、避免死锁
- 注意加锁顺序(这个很好理解,就像上边的例子)
- 加锁时限(超过时限放弃加锁)
- 实现方式–使用重入锁。
- 死锁检测(较难,就像分析上边的线程情况)
二、并发最佳实践
2.1、使用本地变量
应该增加使用本地变量,而不是创建一个类或者是实例变量,通常情况下开发人员使用对象实例作为变量,可以节省内存并可以重用,因为他们认为每次在方法中创建本地变量会消耗本地内存
2.2、使用不可变类
降低代码中的同步数量
2.3、最小化锁的作用域范围
公式:S=1/(1-a+a/n)
a是进程计算部分所占的比例,n是并行处理的节点个数,s代表的是(加速比)
当1-a=0的时候,这时候没有串行只有并行,最大的(加速比)s是等于n的。当a=0的时候只有串行,没有并行,这时候最小的(加速比)s=1.当n趋向于无穷大时,极限的(加速比)趋向于1:1-a,这就是加速比的上线:阿姆达尔定律
2.4、使用线程池的Executor而不是直接new Thread
创建一个线程的代价时昂贵的,如果想要一个可伸缩的Java应用,那么就需要使用线程池,使用线程池来管理线程,jdk中提供了各种线程池。
2.5、宁可使用同步也不要使用线程的wait和notify方法
从Java1.5之后增加了许多同步工具,也可以使用countdownlash来实现线程的等待
2.6、使用BlockingQueue实现生产-消费模式
大部分并发模型都可以使用生产消费设计实现,而BlockingQueue是最好的实现方式,阻塞队列不仅能实现单个生产单个消费,也可以处理多个生产和消费
2.7、使用并发集合而不是加了锁的同步集合
有5大并发集合copy…,不要使用connections.sync的同步锁集合
2.8、使用Semaphore创建有界的访问
为了建立可靠的稳定的系统,对于数据库文件系统以及socked等资源必须要做有序的访问。Semaphore是一个限制这些资源开销的选择,如果某个资源不可以使用Semaphore,可以以最低的代价阻塞线程等待,我们可以通过Semaphore来控制同时访问指定资源的线程数
2.9、宁可使用同步代码块,也不使用同步的方法
针对synchronized关键字,使用这个关键字的同步代码块只会锁定一个对象,而不是锁定整个方法,如果更改共同变量或类的字段首先选择的
是原子性变量,然后使用XXXX,如果需要互斥锁,也可以使用上面讲的那个锁。
2.10、避免使用静态变量
静态变量在并发执行的情况下会出现很多问题,如果必须使用静态变量,那么优先让它称为final变量,如果是用来保存集合connection的话,那就可以考虑只读集合,否则的话一定要做特别多的同步处理以及并发处理。
三、Spring与线程安全
Spring bean :singleton、prototype
无状态对象(Service、Controller、Dao无状态,是用来执行某些操作的)
四、HashMap
4.1、HashMap的结构
- 过数组和指针引用实现的。
- 初始化的时候可以传两个值,一个是初始容量和加载因子
4.2、hashmap的扩容
4.2.1、hashmap的扩容存在的线程安全问题
hashmap的扩容存在线程安全问题rehash
(1)单线程情况下扩容操作无问题
(2)多线程情况下扩容操作会出现指针死循环
4.2.2、ConcurrentHashMap解决上诉问题
4.2.3、ConcurrentHashMap与HashMap的区别
- ConcurrentHashMap是线程安全的,hashmap是非线程安全的。
- HashMap允许key和value为空,ConcurrentHashMap不允许
- Java7实现分段锁,Java8取消分段锁,使用红黑树
- 红黑树看下图