本篇博客给大家带来的是集合类在多线程下的使用和死锁的知识点还包括常见的面试题.
🐎文章专栏: JavaEE初阶
🚀若有问题 评论区见
❤ 欢迎大家点赞 评论 收藏 分享
如果你不知道分享给谁,那就分享给薯条.
你们的支持是我不断创作的动力 .
王子,公主请阅🚀
要开心
要快乐
顺便进步
1. 线程安全的集合类
Vector, Stack, HashTable, 是线程安全的(不建议用), 其他的集合类不是线程安全的.
1.1 多线程环境使用ArrayList
① 使用同步机制 (synchronized 或者 ReentrantLock). ② 使用Collections.synchronizedList(new ArrayList); synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List. synchronizedList的关键操作上都带有synchronized③ 使用 CopyOnWriteArrayList
CopyOnWrite容器即写时复制的容器。当我们往⼀个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
优点:
在读多写少的场景下, 性能很高, 不需要加锁竞争.
缺点:
① 占用内存较多.
② 新写的数据不能被第一时间读取到
1.2 多线程环境使用队列
① ArrayBlockingQueue基于数组实现的阻塞队列② LinkedBlockingQueue基于链表实现的阻塞队列
③ PriorityBlockingQueue
基于堆实现的带优先级的阻塞队列
④ TransferQueue
最多只包含⼀个元素的阻塞队列
1.3 多线程环境使用哈希表
HashMap 本身不是线程安全的.
在多线程环境下可以使用:
① Hashtable
② ConcurrentHashMap
Hashtable:
Hashtable 只是简单的把关键方法加上了 synchronized 关键字
这相当于直接针对 Hashtable 对象本身加锁.
① 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.
② size 属性也是通过 synchronized 来控制同步, 也是比较慢的.
③ 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.
ConcurrentHashMap:
相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例
① 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然是用 synchronized, 但不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降低了锁冲突的概率.
② 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况
③ 优化了扩容方式: 化整为零
Ⅰ发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
Ⅱ 扩容期间, 新老数组同时存在.
Ⅲ 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小部分元素.
Ⅳ 搬完最后一个元素再把老数组删掉.
Ⅴ 这个期间, 插入只往新数组加.
Ⅵ 这个期间, 查找需要同时查新数组和老数组
ConcurrentHashMap 每个哈希桶都有一把锁.只有两个线程访问的恰好示同一个哈希桶上的数据才出现锁冲突.
1.4 相关面试题
1. ConcurrentHashMap的读操作是否要加锁,为什么?
读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了 volatile
关键字.
2. 介绍下 ConcurrentHashMap的锁分段技术?
这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个"段" (Segment), 针对每个段分别加锁.
目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.
3. ConcurrentHashMap在jdk1.8做了哪些优化?
取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对
象).
将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于 8 个元素)就转换成红黑树.
4. Hashtable和HashMap、ConcurrentHashMap 之间的区别?
① HashMap: 线程不安全. key 允许为 null
② Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 效率较低. key 不允许为 null.
③ ConcurrentHashMap: 线程安全. 使用synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用CAS 机制. 优化了扩容方式. key 不允许为 null
2. 死锁
什么是死锁?
死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止.
死锁是⼀种严重的 BUG!! 导致⼀个程序的线程 “卡死”, 无法正常工作!
关于死锁问题,很多资料上都谈论到"哲学家就餐问题".
一张圆桌前坐着五个哲学家,哲学家两边都有一根筷子,桌子中间放有一碗面,要想吃到面条得有两根筷子.
每个哲学家只做两件事: 思考人生或者吃面条. 思考人生时放下筷子,吃面条时拿起筷子.当筷子被占用时,就只能思考(阻塞等待)
假设同一时刻,五个哲学家同时拿起左手边得筷子,再去拿右手边的筷子,就会发现右手的筷子都被占用了. 哲学家之间互不相让,大家都吃不到面条, 这个时候就形成了死锁.
如何避免死锁?
死锁产生的四个必要条件:
① 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用.
② 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
③ 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
④ 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。其中最容易破坏的就是 “循环等待”
破坏循环等待.
最常用的一种死锁阻止技术就是锁排序. 假设有 N 个线程尝试获取 M 把锁, 就可以针对 M 把锁进行编号 (1, 2, 3…M).
N 个线程尝试获取锁的时候, 都按照固定的按编号由小到大顺序来获取锁. 这样就可以避免环路等待.
相关面试题
谈谈死锁是什么?如何避免死锁?实际解决过没有? <
上述内容都是答案./font>
3. 其他常见面试问题
1. 谈谈 volatile关键字的用法?
volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第⼀时间读取到最新的值.
2. Java多线程是如何实现数据共享的?
JVM 把内存分成了这几个区域:
方法区, 堆区, 栈区, 程序计数器.
其中堆区这个内存区域是多个线程之间共享的.
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到.
3. Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?
创建线程池主要有两种方式:
① 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
② 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.
4. Java线程共有几种状态?状态之间怎么切换的?
① NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
② RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在 CPU 上运行/在即将准备运行的状态.
③ BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.
④ WAITING: 调用 wait 方法会进入该状态.
⑤ TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
⑥ TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.
5. 在多线程下,如果对一个数进行叠加,该怎么做?
① 使用synchronized / ReentrantLock 加锁
② 使用 AtomInteger 原子操作.
6. Servlet是否是线程安全的?
① Servlet 本身是工作在多线程环境下.
② 如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行操作, 是可能出现线程不安全的情况的.
7. Thread和Runnable的区别和联系?
① Thread 类描述了一个线程.
② Runnable 描述了一个任务.
③ 在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用Runnable 来描述这个任务.
8. 多次start一个线程会怎么样?
第次调用 start 可以成功调用.后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常
9. 有synchronized两个方法,两个线程分别同时用这个方法,请问会发生什么?
synchronized 加在非静态方法上, 相当于针对当前对象加锁.
如果这两个方法属于同一个实例:
线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到锁之后才能执行方法内容.
如果这两个方法属于不同实例:
两者能并发执行, 互不干扰.
10. 进程和线程的区别?
① 进程是包含线程的. 每个进程至少有⼀个线程存在,即主线程。
② 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
③ 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
本篇博客到这里就结束啦, 感谢观看 ❤❤❤
🐎期待与你的下一次相遇😊😊😊