Java——常见并发容器(一文搞懂并发容器——ConcurrentHashMap、ThreadLocal和BlockingQueue)

1、常见的并发容器

  • ConcurrentHashMap
  • ThreadLocal
  • BlockingQueue

2、同步容器和并发容器?

  • 同步容器: 可以简单地理解为通过 synchronized 来实现同步的容器,如果有多个线程调用同步容器的方法,它们将会串行执行。比如 VectorHashtable,以及 Collections.synchronizedSetsynchronizedList 等方法返回的容器。可以通过查看 VectorHashtable 等这些同步容器的实现码,可以看到这些容器实现线程安全的方式就是将它们的状态封装起来,并在需要同步的方法上加上关键字synchronized
  • 并发容器: 并发容器使用了与同步容器完全不同的加锁策略来提供更高的并发性和伸缩性,例如在ConcurrentHashMap 中采用了一种粒度更细的加锁机制,可以称为分段锁,在这种锁机制下,允许任意数量的读线程并发地访问 map,并且执行读操作的线程和写操作的线程也可以并发的访问 map,同时允许一定数量的写操作线程并发地修改 map,所以它可以在并发环境下实现更高的吞吐量。

3、Java 中的同步集合与并发集合有什么区别?

答: 同步集合与并发集合都为多线程和并发提供了合适的线程安全的集合,不过并发集合的可扩展性更高。在 Java1.5 之前程序员们只有同步集合来用且在多线程并发的时候会导致争用,阻碍了系统的扩展性。Java5 介绍了并发集合像ConcurrentHashMap,不仅提供线程安全还用锁分离和内部分区等现代技术提高了可扩展性。

4、什么是ConcurrentHashMap

答: ConcurrentHashMapJava中的一个线程安全且高效的 HashMap实现。平时涉及高并发如果要用map结构,那第一时间想到的就是它。相对于HashMap来说,ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度。

JDK 1.6版本关键要素,如何实现线程安全的?

  • segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;
  • segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。

JDK1.8后,ConcurrentHashMap抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized** **来保证并发安全性。

5、JavaConcurrentHashMap 的并发度是什么?

答: ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的,它是 ConcurrentHashMap 类构造函数的一个可选参数,默认值为 16,这样在多线程情况下就能避免争用。

JDK8 后,它摒弃了 Segment(锁段)的概念,而是启用了一种全新的方式实现,利用CAS + synchronized 算法。同时加入了更多的辅助变量来提高并发度。

6、SynchronizedMapConcurrentHashMap 有什么区别?

  • SynchronizedMap 一次锁住整张表来保证线程安全,所以每次只能有一个线程来访为map
  • ConcurrentHashMap 使用分段锁的思想来保证在多线程下的性能。
  • ConcurrentHashMap 中则是一次锁住一个桶。ConcurrentHashMap 默认将hash 表分为 16 个桶,诸如 getputremove 等常用操作只锁当前需要用到的桶。
  • 这样,原来只能一个线程进入,现在却能同时有 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>() {
      @Override
        protected Integer initialValue() {
          return 0;
        }
   };
    public static void main(String[] args) {
        for (int i = 0; i <3; i++) {//启动三个线程
            Thread t = new Thread() {
              @Override
              public 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 中使用的 keyThreadLocal 的弱引用,而 value 是强引用。所以,如果ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来, ThreadLocalMap 中就会出现keynullEntry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 keynull的记录。使用完ThreadLocal 方法后最好手动调用 remove() 方法。

10、ThreadLocal内存泄漏解决方案?

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
  • 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

11、什么是阻塞队列?

Java——常见并发容器(一文搞懂并发容器——ConcurrentHashMap、ThreadLocal和BlockingQueue)_第1张图片

答: 阻塞队列(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,java,开发语言)