一、死锁
1、死锁的定义
所谓的死锁是指两个或两个以上的线程在等待执行的过程中,因为竞争资源而造成的一种互相等待的现象。若不受外力作用,他们都将无法推进下去。此时,处于系统中所处的状态就是死锁。
2、发生死锁所必须具备的条件:
互斥条件:它是指进程对所分配的资源进行排他性的使用,在一定时间内,某资源只由一个进程在用,如果此时还有其他进程请求资源,请求者只能等待。直到占有资源的进程用完或释放之后才可以继续使用。
请求和保持条件:它是指进程已经保持了至少一个资源,又提出了新的资源请求,该资源已被其它进程占用。此时,请求进程阻塞,对自己或者其它资源保持不放;
不剥夺条件:指进程获取资源在用完之前不能被剥夺,只能在使用完后再由自己释放;
环路等待条件:发生死锁的时候,一定存在一个进程的资源是一个环形的链。
3、代码演示死锁:
/** * 一个简单的死锁类 * 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒 * 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒 * td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定; * td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定; * td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。 */ @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 { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o2) { log.info("1"); } } } if (flag == 0) { synchronized (o2) { try { 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(); } }
执行并打印结果:
21:13:12.979 [Thread-1] INFO com.mmall.concurrency.example.deadLock.DeadLock - flag:0 21:13:12.979 [Thread-0] INFO com.mmall.concurrency.example.deadLock.DeadLock - flag:1 Process finished with exit code 1
二、多线程并发最佳实践
1、使用本地变量:应该使用本地变量,而不是创建一个类或实例变量。通常情况下,开发人员使用对象实例作为一个类,可以节省内存,并可以重用。因为每次在方法中创建新的本地变量会消耗很多内存。
2、使用不可变类:如 String 、Integer等,一旦创建就不会改变了,不可变类可以降低代码中的同步数量
3、最小化锁的作用域范围: S=1/(1-a+a/n)
4、使用线程池的Executor,而不是直接new Thread执行:
创建一个线程的代价是昂贵的,如果你要得到一个可伸缩的java应用,那么你需要使用线程池,从线程池来管理线程,jdk中提供了各种方法实现
5、宁可使用同步也不要使用线程的wait和notify
从java1.5以后增加了许多的同步工具,要优先使用同步工具,而不是使用wait和notify方法
6、使用BlockingQueue实现生产-消费模式
7、使用并发集合而不是加了锁的同步集合
8、使用Semaphore创建有界的访问
9、宁可使用同步代码块,也不使用同步的方法
10、避免使用静态变量
三、Spring与线程安全
无状态对象:就是自身没有状态的对象,当然也就不会因为多个线程交替调度破坏自身的状态而导致安全问题。无状态对象包括经常使用的DTO、VO,只作为数据实体的模型对象。
四、HashMap与ConcurrentHashMap解析
1、HashMap 的数据结构:
在java编程语言中,最基本的结构有两种,一个是数组,另外一个就是指针,即引用。
HashMap的底层就是一个数组结构,数组的每一项又是一个链表,当我们新建HashMap的时候就会初始化一个数组出来。
HashMap有两个参数影响它的性能,分别是初始容量和加载因子。
由源码可知,HashMap的初始容量为16,
由上图中,可知HashMap的默认加载因子为0.75
当HashMap 的长度达到的容量长度满足初始值的0.75时,就会调用resize()方法进行扩容:
我们也可以根据需要指定HashMap的初始化容量和加载因子。
2、HashMap 的线程安全性:
HashMap是线程不安全的,主要体现在前面的resize()方法,它可能会导致死循环的发生,并且在使用迭代器的时候fasfree。当HashMap的长度超过了它的capacity * loadFactor时,就需要对它进行扩容,具体方法是:它要创建一个新的长度为原来容量的两倍的数组。它保证新的容量为2的N次方,从而保证寻址的方式依然适用。同时,它原来的数组会全部插入到新的数组中。这个过程我们称之为rehash。
这个方法并不保证线程安全,而且在多线程并发调用时可能陷入死循环。
3、HashMapd的ReHash 操作示意图:
单线程下的ReHash操作:
多线程下的ReHash操作:
4、ConcurrentHashMap的底层数据结构:
5、ConcurrentHashMap和HashMap的不同点:
- ConcurrentHashMap是线程安全的,HashMap是线程不安全的;
- HashMap允许key\value为空,而ConcurrentHashMap是不允许的
6、ConcurrentHashMap 改进后
Java7以后针对并发访问引入了Segment这个结构,实现了分段锁,提高并发度,与Segment的个数是相等的。Java8以后为了进一步提高并发性,它废弃了这里面的分段锁方案,并且直接使用一个大的数组,同时为了提高hash碰撞下的寻址做了性能优化。
Java8以后它的链表的长度超过一定的值(默认为8),这里的链表就会变成了红黑树。