容器(一文搞懂并发容器——ConcurrentHashMap、ThreadLocal和BlockingQueue)"/>
Java——常见并发容器(一文搞懂并发容器——ConcurrentHashMap、ThreadLocal和BlockingQueue)
1、常见的并发容器
ConcurrentHashMap
ThreadLocal
BlockingQueue
2、同步容器和并发容器?
- 同步容器: 可以简单地理解为通过
synchronized
来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如Vector
,Hashtable
,以及Collections.synchronizedSet
,synchronizedList
等方法返回的容器。可以通过查看Vector
,Hashtable
等这些同步容器的实现码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized
。 - 并发容器: 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在
ConcurrentHashMap
中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问map
,并且执行读操作的线程和写操作的线程也可以并发的访问map
,同时允许一定数量的写操作线程并发地修改map
,所以它可以在并发环境下实现更高的吞吐量。
3、Java 中的同步集合与并发集合有什么区别?
答: 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5
之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5
介绍了并发集合像ConcurrentHashMap
,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。
4、什么是ConcurrentHashMap
?
答: ConcurrentHashMap
是Java
中的一个线程安全且高效的 HashMap
实现。平时涉及高并发如果要用map
结构,那第一时间想到的就是它。相对于HashMap
来说,ConcurrentHashMap
就是线程安全的map
,其中利用了锁分段的思想提高了并发度。
JDK 1.6
版本关键要素,如何实现线程安全的?
segment
继承了ReentrantLock
充当锁的角色,为每一个segment
提供了线程安全的保障;segment
维护了哈希散列表的若干个桶,每个桶由HashEntry
构成的链表。
JDK1.8
后,ConcurrentHashMap
抛弃了原有的Segment
分段锁,而采用了CAS + synchronized
** **来保证并发安全性。
5、Java
中 ConcurrentHashMap
的并发度是什么?
答: ConcurrentHashMap
把实际 map
划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap
类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。
在
JDK8
后,它摒弃了Segment
(锁段)的概念,而是启用了一种全新的方式实现,利用CAS + synchronized
算法。同时加入了更多的辅助变量来提高并发度。
6、SynchronizedMap
和 ConcurrentHashMap
有什么区别?
SynchronizedMap
一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map
;ConcurrentHashMap
使用分段锁的思想来保证在多线程下的性能。ConcurrentHashMap
中则是一次锁住一个桶。ConcurrentHashMap
默认将hash
表分为 16 个桶,诸如get
,put
,remove
等常用操作只锁当前需要用到的桶。- 这样,原来只能一个线程进入,现在却能同时有 16 个写线程执行,并发性能的提升是显而易见的。
- 另外
ConcurrentHashMap
使用了一种不同的迭代方式。在这种迭代方式中,当iterator
被创建后集合再发生改变就不再是抛出ConcurrentModificationException
,取而代之的是在改变时new
新的数据从而不影响原有的数据,iterator
完成后再将头指针替换为新的数据,这样iterator
线程可以使用原来老的数据,而写线程也可以并发的完成改变。
7、ThreadLocal
是什么?有哪些使用场景?
答: ThreadLocal
是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap
对象,简单说 ThreadLocal
就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap
对象内的value
。通过这种方式,避免资源在多线程间共享。
7.1、原理
答: 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java
提供ThreadLocal
类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web
服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java
应用就存在内存泄露的风险。
经典的使用场景是为每个线程分配一个
JDBC
连接Connection
。这样就可以保证每个线程的都在各自的Connection
上进行数据库的操作,不会出现A
线程关了B
线程正在使用的Connection
; 还有Session
管理等问题。
7.2、使用例子
public class TestThreadLocal {//线程本地存储变量private static final ThreadLocal<Integer> THREAD_LOCAL_NUM= new ThreadLocal<Integer>() {@Overrideprotected Integer initialValue() {return 0;}};public static void main(String[] args) {for (int i = 0; i <3; i++) {//启动三个线程Thread t = new Thread() {@Overridepublic void run() {add10ByThreadLocal();}};t.start();}}/*** 线程本地存储变量加 5*/private static void add10ByThreadLocal() {for (int i = 0; i <5; i++) {Integer n = THREAD_LOCAL_NUM.get();n += 1;THREAD_LOCAL_NUM.set(n);System.out.println(Thread.currentThread().getName() + ":" +ThreadLocal num=" + n);j h}}}
打印结果:
Thread-0 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-0 : ThreadLocal num=2
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-0 : ThreadLocal num=4
Thread-2 : ThreadLocal num=2
Thread-1 : ThreadLocal num=3
Thread-1 : ThreadLocal num=4
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=5
Thread-2 : ThreadLocal num=4
Thread-2 : ThreadLocal num=5
Thread-1 : ThreadLocal num=5
123456789101112131415
7.3、为什么key
是弱应用?
答: 在以往我们使用完对象以后等着GC
清理,但是对于ThreadLocal
来说,即使我们使用结束,也会因为线程本身存在该对象的引用,处于对象可达状态,垃圾回收器无法回收。这个时候当 ThreadLocal
太多的时候就会出现内存泄漏的问题。
而我们将
ThreadLocal
对象的引用作为弱引用,那么就很好的解决了这个问题。当我们自己使用完ThreadLocal
以后,「当GC
的时候就会将我们创建的强引用直接干掉,而这个时候我们完全可以将线程Map
中的引用干掉,于是使用了弱引用,这个时候大家应该懂了为啥不使用软引用了吧」
8、什么是线程局部变量?
答: 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java
提供ThreadLocal
类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web
服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java
应用就存在内存泄露的风险。
9、ThreadLocal
造成内存泄漏的原因?
答: ThreadLocalMap
中使用的 key
为 ThreadLocal
的弱引用,而 value
是强引用。所以,如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key
会被清理掉,而 value
不会被清理掉。这样一来, ThreadLocalMap
中就会出现key
为null
的Entry
。假如我们不做任何措施的话,value
永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、 get()
、 remove()
方法的时候,会清理掉 key
为 null
的记录。使用完ThreadLocal
方法后最好手动调用 remove()
方法。
10、ThreadLocal
内存泄漏解决方案?
- 每次使用完
ThreadLocal
,都调用它的remove()
方法,清除数据。 - 在使用线程池的情况下,没有及时清理
ThreadLocal
,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal
就跟加锁完要解锁一样,用完就清理。
11、什么是阻塞队列?
答: 阻塞队列(BlockingQueue)
是一个支持两个附加操作的队列。
阻塞队列和普通的队列的区别是:
- 当阻塞队列是空的,从队列中获取元素的操作将会被阻塞;试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
- 当阻塞队列是满的,往队列里添加元素的操作将会被阻塞;同样,试图从满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他线程从列中移除一个或者多个元素或者完全清空队列使队列重新变得空闲起来并后续新增。
- 在多线程领域里:所谓阻塞,在某些情况下会挂起线程,一旦条件满足,被挂起的线程又会自动被唤醒。
12、阻塞队列的实现原理是什么?
答:BlockingQueue
接口是 Queue
的子接口,它的主要用途并不是作为容器,而是作为线程同步的的工具,因此他具有一个很明显的特性,当生产者线程试图向 BlockingQueue
放入元素时,如果队列已满,则线程被阻塞,当消费者线程试图从中取出一个元素时,如果队列为空,则该线程会被阻塞,正是因为它所具有这个特性,所以在程序中多个线程交替向 BlockingQueue
中放入元素,取出元素,它可以很好的控制线程之间的通信。
阻塞队列使用最经典的场景就是 socket
** 客户端数据的读取和解析**,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。
13、如何使用阻塞队列来实现生产者-消费者模型?
答: 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
Java 5
之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和线程同步可以实现生产者,消费者模式,主要的技术就是用好,wait,notify,notifyAll,sychronized
这些关键字。而在 Java 5
之后,可以使用阻塞队列来实现,此方式大大简少了代码量,使得多线程编程更加容易,安全方面也有保障。
14、常见的阻塞队列
ArrayBlockingQueue
: 一个由数组结构组成的有界阻塞队列。LinkedBlockingQueue
: 一个由链表结构(但大小默认值为Integer.MAX_VALUE
)组成的有界阻塞队列。PriorityBlockingQueue
: 一个支持优先级排序的无界阻塞队列。DelayQueue
: 一个使用优先级队列实现的无界阻塞队列。SynchronousQueue
: 不存储元素的阻塞队列,也即单个元素的队列,只存一个元素。LinkedTransferQueue
: 一个由链表结构组成的无界阻塞队列。LinkedBlockingDeque
: 一个由链表结构组成的双向阻塞队列。
更多推荐
Java——常见并发容器(一文搞懂并发容器——ConcurrentHashMap、ThreadLocal和BlockingQueue)
发布评论