并发编程—并发容器&工具篇🔥
Juc 有哪些并发容器?实际业务开发经常使用哪些?
Java 的并发包 java.util.concurrent
(JUC)提供了一些线程安全的并发容器,主要包括以下几种:
- ConcurrentHashMap:这是一个线程安全的HashMap,它通过分段锁技术来实现高效的并发操作。
- CopyOnWriteArrayList 和 CopyOnWriteArraySet:这两个类是线程安全的List和Set实现。它们在每次修改操作(如add或remove)时,都会复制一份新的数据结构,然后在新的数据结构上进行修改,最后再将引用指向新的数据结构。这样可以在避免锁的同时,提供较好的并发性能。但是由于每次修改都需要复制整个数据结构,所以它们更适合读多写少的场景。
- ConcurrentLinkedQueue 和 ConcurrentLinkedDeque: 这两个类是线程安全的Queue和Deque实现。它们使用了一种无锁的链表结构(基于CAS操作),可以提供较好的并发性能。
- BlockingQueue接口和它的实现类,如ArrayBlockingQueue, LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue等。这些是线程安全的队列,提供了阻塞操作,可以用于生产者-消费者模式。
- BlockingDeque接口和它的实现类LinkedBlockingDeque。这是一个双端的阻塞队列。
- ConcurrentSkipListMap 和 ConcurrentSkipListSet: 这两个类是线程安全的SortedMap和SortedSet实现。它们使用了跳表(Skip List)数据结构,可以提供较好的并发性能。
结合具体业务场景讲讲每种容器的使用?
在实际业务开发中,我们经常会使用到以下几种并发容器:
- ConcurrentHashMap:这是一个线程安全的HashMap,常用于缓存数据,比如在社交应用中,我们可能需要缓存用户的信息,以便快速获取。当有多个线程同时读写用户信息时,ConcurrentHashMap可以确保数据的一致性。
- CopyOnWriteArrayList:这是一个线程安全的List,适用于读多写少的场景。比如在直播应用中,我们可能需要维护一个在线用户列表,当有新用户加入或者用户离开时,需要更新这个列表。由于用户的加入和离开相对于读取操作来说较少,所以可以使用CopyOnWriteArrayList。
- BlockingQueue:这是一个线程安全的队列,提供了阻塞操作,常用于生产者-消费者模式。比如在游戏应用中,我们可能需要处理玩家的操作请求,可以使用BlockingQueue来存储这些请求,然后由后台线程处理。
- ConcurrentLinkedQueue:这是一个线程安全的Queue,适用于高并发场景。比如在购物应用中,我们可能需要处理大量的订单请求,可以使用ConcurrentLinkedQueue来存储这些请求,然后由后台线程处理。
- ConcurrentSkipListMap:这是一个线程安全的SortedMap,适用于需要排序的场景。比如在社交应用中,我们可能需要按照时间顺序显示用户的动态,可以使用ConcurrentSkipListMap来存储这些动态。
以上就是在实际业务开发中常用的几种并发容器,它们可以有效地处理多线程并发问题,提高应用的性能和稳定性
ArrayList 和 Vector有什么不同之处?
ArrayList和Vector都是Java中的线性表数据结构,它们都是基于动态数组实现的,但是在并发处理和性能上有一些不同:
- 线程安全性:Vector是线程安全的,它的大部分方法都是同步的,可以在多线程环境下使用。而ArrayList是非线程安全的,如果在多线程环境下使用,可能会出现数据不一致的问题。
- 性能:由于Vector的方法是同步的,所以在单线程环境下,它的性能会比ArrayList差一些。如果不需要处理多线程并发问题,通常推荐使用ArrayList。
- 扩容:当数组满时,ArrayList和Vector都会进行扩容。ArrayList默认的扩容策略是增长到原来的1.5倍,而Vector默认的扩容策略是增长到原来的2倍。这意味着Vector的扩容操作可能会更消耗内存。
- 枚举方法:Vector类有一个elements()方法,返回一个Enumeration对象。这是一个历史遗留的方法,现在已经不推荐使用。ArrayList没有这个方法,它提供了一个iterator()方法,返回一个Iterator对象。
总的来说,ArrayList和Vector的主要区别在于线程安全性和性能。在不需要处理多线程并发问题的情况下,通常推荐使用ArrayList。如果需要处理多线程并发问题,可以考虑使用Vector,或者使用并发包中的线程安全容器,如CopyOnWriteArrayList或ConcurrentHashMap。
为什么HashTable是线程安全的?
HashTable是线程安全的,主要是因为它的关键方法(如get、put、remove等)都被synchronized关键字修饰。这意味着在多线程环境下,HashTable的操作是串行化的,同一时刻只有一个线程能够访问。
优点:
- 线程安全:在多线程环境下,HashTable可以保证数据的一致性,不会出现并发问题。
- 键值对:HashTable存储的是键值对,这使得数据的检索非常快速。
- 不允许空键和空值:HashTable不允许空键和空值,这在某些情况下可以避免空指针异常。
缺点:
- 性能:由于HashTable的方法是同步的,所以在高并发环境下,性能可能会受到影响。
- 过时:HashTable类在Java 1.2版本后被HashMap取代,HashMap提供了更好的性能,并且允许空键和空值。
- 扩容耗时:当HashTable需要扩容时,会新建一个大约是原来两倍大小的数组,并将原数组的所有元素复制到新数组中,这个过程可能会很耗时。
- 内存占用:由于HashTable的扩容策略,如果数据量不大但是刚好超过了当前容量,那么HashTable可能会占用比实际需要更多的内存。
总的来说,HashTable的线程安全性是以牺牲性能为代价的。在现代的Java应用中,我们通常会使用更高效的并发容器,如ConcurrentHashMap,它通过分段锁技术实现了更高的并发性能。
多线程场景我们选用什么List?
当然可以,以下是在多线程环境下,我们可以选择的List:
- Vector:这是一个传统的线程安全的List,所有的公共方法都是同步的,可以在多线程环境下安全使用。但是,由于同一时间只能有一个线程访问,所以在高并发环境下性能较低。
- CopyOnWriteArrayList:这是一个线程安全的List,适用于读多写少的场景。它的原理是,每次写操作都会复制一份新的List,读操作则直接访问原List。由于读操作不需要加锁,所以在读多写少的场景下性能较高。
- Collections.synchronizedList:这是一个将普通List转换为线程安全List的方法。它返回的List的所有公共方法都是同步的,可以在多线程环境下安全使用。但是,由于同一时间只能有一个线程访问,所以在高并发环境下性能较低。
- ConcurrentLinkedQueue:虽然这不是一个List,但是它是一个线程安全的队列,可以在多线程环境下安全使用。它的性能通常比Vector和synchronizedList要高。
以上就是在多线程环境下,我们可以选择的List。需要注意的是,选择哪种List取决于你的具体需求,包括读写比例、是否需要随机访问等因素。
用过ConcurrentHashMap,讲一下他和HashTable的不同之处?
ConcurrentHashMap和HashTable都是线程安全的哈希表,但它们在实现方式和性能上有一些不同。
- 锁的粒度:
- HashTable:使用一个全局锁,无论是读操作还是写操作,都需要获取这个全局锁,这意味着在同一时刻只有一个线程能访问HashTable,所以并发性能较低。
- ConcurrentHashMap:使用分段锁,整个哈希表被分为多个段(Segment),每个段都有自己的锁,这样就允许多个线程同时访问哈希表,只要它们访问的是不同的段。因此,ConcurrentHashMap的并发性能较高。
- 性能:
- HashTable:由于使用全局锁,所以在高并发环境下性能较低。
- ConcurrentHashMap:由于使用分段锁,所以在高并发环境下性能较高。
- Null键和Null值:
- HashTable:允许存储null键和null值。
- ConcurrentHashMap:不允许存储null键和null值,如果尝试存储null键或null值,ConcurrentHashMap会抛出NullPointerException。
- 遍历操作:
- HashTable:遍历操作不是线程安全的,需要手动同步。
- ConcurrentHashMap:提供了线程安全的弱一致性遍历操作。
总的来说,虽然HashTable和ConcurrentHashMap都是线程安全的哈希表,但是ConcurrentHashMap在实现方式和性能上都优于HashTable。如果你的应用需要大量的并发读写操作,那么应该优先考虑使用ConcurrentHashMap。
如果你要去对一个map去维护线程安全,除了用synchronized,有其他更好的方式吗?
Java提供了多种线程安全的Map,除了使用synchronized关键字外,还有以下几种方式:
- ConcurrentHashMap:如前面所述,ConcurrentHashMap使用分段锁技术,允许多个线程并发读/写,提供了较高的并发性能。
- Collections.synchronizedMap:这是一种简单的线程安全Map实现,它会对所有的公共方法添加synchronized关键字,确保每次只有一个线程可以访问Map。但是它的并发性能不如ConcurrentHashMap。
- Hashtable:这是一个古老的线程安全Map实现,和Collections.synchronizedMap类似,也是使用synchronized关键字来实现线程安全。但是它不支持null键和null值。
- 使用读写锁(ReentrantReadWriteLock):读写锁允许多个读线程并发访问,但是在写线程访问时,所有的读线程和其他写线程都会被阻塞。读写锁适用于读多写少的场景。
ConcurrentHashMap如何保证线程安全?底层实现原理?
SynchronizedMap 和 ConcurrentHashMap 有什么区别?
SynchronizedMap和ConcurrentHashMap都是用于多线程环境下的Map实现,它们都能保证线程安全,但是在实现方式和性能上有一些区别。
- 实现方式:
- SynchronizedMap:是通过Collections.synchronizedMap()方法返回的包装类,它将传入的Map包装成一个线程安全的Map。所有的方法都通过synchronized关键字来保证线程安全,这意味着在同一时刻只有一个线程能访问Map。
- ConcurrentHashMap:是Java并发包中的一个类,它使用了一种叫做分段锁的技术。在ConcurrentHashMap中,整个Map被分为一定数量的段(Segment),每个段都有自己的锁,这样就允许多个线程同时访问Map,只要它们访问的是不同的段。
- 性能:
- SynchronizedMap:由于在同一时刻只有一个线程能访问,所以在高并发环境下性能较低。
- ConcurrentHashMap:由于使用了分段锁,允许多个线程同时访问,因此在高并发环境下性能较高。
- 功能:
- SynchronizedMap:基本功能和普通的Map没有区别。
- ConcurrentHashMap:除了基本的Map功能外,还提供了一些并发编程中常用的方法,如putIfAbsent()、remove()等。
总的来说,SynchronizedMap和ConcurrentHashMap都可以用于多线程环境,但是在高并发环境下,ConcurrentHashMap的性能更优。如果你的应用需要大量的并发读写操作,那么应该优先考虑使用ConcurrentHashMap。
CopyOnWriteArrayList 是什么?底层实现原理?结合实际业务场景讲讲?
CopyOnWriteArrayList是Java并发包java.util.concurrent中的一个类,它是ArrayList的线程安全版本。其基本思想就是当我们进行修改操作(add、set等)时,不直接在当前容器上进行修改,而是先将当前容器进行一份拷贝,然后在新的容器上进行修改,修改完成之后再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWriteArrayList进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。
底层实现原理:
- 数据存储:CopyOnWriteArrayList内部以一个volatile修饰的数组保存所有数据,保证了数据的内存可见性。
- 写操作:每次写操作都会先将当前数组进行复制,然后在新的数组上进行修改,修改完成后再将原数组的引用指向新的数组。这个过程通过ReentrantLock来保证线程安全。
- 读操作:读操作完全不需要加锁,因为写操作不会影响原数组,所以可以直接读取。
结合实际业务场景:
- 适用场景:CopyOnWriteArrayList适用于读多写少的并发场景。例如,一个在线聊天室的用户列表,用户列表的读取操作(显示在线用户)明显多于写入操作(用户登录、退出)。
- 不适用场景:如果业务场景是写操作非常多,那么CopyOnWriteArrayList的性能就会大打折扣,因为每次写操作都需要复制数组。例如,一个高并发的股票交易系统,用户的买卖操作明显多于读取操作,这时使用CopyOnWriteArrayList就不合适。
总的来说,CopyOnWriteArrayList是一种读写分离思想的体现,适用于读多写少的并发场景。但是由于写操作的开销较大,所以不适用于写操作较多的场景。