0X JavaSE-- 并发编程(并发容器、ThreadLocal、线程池)

线程池

什么是线程池

线程池其实是一种池化的技术实现,池化技术的核心思想就是实现资源的复用,避免资源的重复创建和销毁带来的性能开销。

线程池可以管理一堆线程,让线程执行完任务之后不进行销毁,而是继续去处理其它线程已经提交的任务。

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

构造线程池

Java 主要是通过构建 ThreadPoolExecutor 来创建线程池的。

因此,如果我们想要线程池操作线程,就先使用构造方法创建一个 ThreadPoolExecutor实例,再通过该实例创建线程。

通过 Executor 构造线程池,可以指定几种类型:

通过 ThreadPoolExecutor 构建线程池

Executor 是一个接口;ThreadPoolExecutor 是 Executor接口的一个实现类;Executors 是一个工具类。

ThreadPoolExecutor 可以更灵活地配置线程池的各种参数,如核心线程数、最大线程数、任务队列、线程空闲时间,提供了更精细的控制

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, // core pool size
    10, // maximum pool size
    60L, // keep-alive time
    TimeUnit.SECONDS, // time unit for keep-alive time
    new LinkedBlockingQueue<Runnable>() // task queue
);

executor.execute(() -> {
    // Your task code here
});

通过 Executors 构建线程池

Executor 是一个接口;ThreadPoolExecutor 是 Executor接口的一个实现类;Executors 是一个工具类。

		// 1. 创建一个固定大小的线程池,线程池大小为3
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);

		// 2. 创建一个单线程的线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

		 // 3. 创建一个可缓存的线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

		  // 4. 创建一个定时调度的线程池,核心线程数为3
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);

		// 5. 创建一个单线程的定时调度线程池
        ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();

通过 Executors 构造线程池,其内部仍然是调用 ThreadPoolExecutor 进行创建,也就是说 Executors 实质上只是把 ThreadPoolExecutor 封装起来了。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

这也是为什么后面我们说不推荐使用 ThreadPoolExecutor ,因为其灵活性太低了,可能出现一些问题。

线程池使用场景

在 Java 程序中,其实经常需要用到多线程来处理一些业务,但是不建议单纯继承 Thread 或者实现 Runnable 接口来创建线程,这样会导致频繁创建及销毁线程,同时创建过多的线程也可能引发资源耗尽的风险。

使用线程池是一种更合理的选择,方便管理任务,同时实现线程的重复利用。

Web服务器模拟

public class SimpleWebServer {
    private static final int NTHREADS = 100;
    private static final ExecutorService exec = Executors.newFixedThreadPool(NTHREADS);

    public static void main(String[] args) {
        while (true) {
            // 接收请求
            Runnable request = new Runnable() {
                public void run() {
                    // 处理请求
                    System.out.println("Request handled by " + Thread.currentThread().getName());
                }
            };

            // 使用 exec.execute(request)来提交 Runnable对象到线程池中执行。
            // 这意味着每当循环一次,就有一个新的任务提交给线程池,由线程池中的某个线程来执行。
            exec.execute(request);
        }
    }
}

并行计算

使用线程池进行并行的数值计算。

public class ParallelCalculation {

    private static final int NTHREADS = 4;
    private static final ExecutorService exec = Executors.newFixedThreadPool(NTHREADS);

    public static void main(String[] args) {
        Callable<Double> task = new Callable<Double>() {
            @Override
            public Double call() {
                // 这里模拟一些数值计算
                return Math.random() * 100;
            }
        };

        List<Future<Double>> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(exec.submit(task));
        }

        for (Future<Double> result : results) {
            try {
                System.out.println(result.get());
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }

        exec.shutdown();
    }
}

处理异步任务

在多线程的环境中中,我们当然不希望任意一个线程任务暂停后,主线程也随之暂停。
运行线程池,可以令空闲的 CPU 转去服务别的线程任务。

public class AsynchronousTaskProcessor {

    private static final ExecutorService exec = Executors.newCachedThreadPool();

    public static void main(String[] args) {
        exec.execute(() -> {
            // 执行某些异步任务
            System.out.println("Async task started");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Async task completed");
        });

		  // 添加一个延迟,确保异步任务有机会先执行
            try {
                Thread.sleep(100); // 延迟 100 毫秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        System.out.println("Main thread continues to execute other operations.");
        exec.shutdown();
    }
}

在这里插入图片描述

使用 ThreadPoolExecutor 构建线程池的坏处

虽然 JDK 提供了 Executors 快速创建线程池的方法,但其实不推荐使用 Executors 来创建线程池。

因为从上面构造线程池的代码可以看出,newFixedThreadPool 线程池由于使用了 LinkedBlockingQueue,队列的容量默认无限大,实际使用中出现任务过多时会导致内存溢出;
newCachedThreadPool 线程池由于核心线程数无限大,当任务过多的时候会导致创建大量的线程,可能机器负载过高导致服务宕机。

ThreadLocal

线程安全问题的核心在于多个线程会对同一个临界区的共享资源进行访问,那如果每个线程都拥有自己的 “共享资源”,各用各的,互不影响,这样就不会出现线程安全的问题了。

通常,我们会使用 synchronzed 关键字 或者 lock 来控制线程对临界区资源的同步顺序,但这种加锁的方式会让未获取到锁的线程进行阻塞,很显然,这种方式的时间效率不会特别高。

ThreadLocal 是 java.lang包中的一个类,它提供了线程局部变量的功能。
简单来说,ThreadLocal 可以让每个使用它的线程拥有一个独立的变量副本,这样就可以确保每个线程操作的是自己的数据,不会影响到其他线程的数据。
从而实现线程隔离,用于解决多线程中共享对象的线程安全问题。

事实上,这就是一种“空间换时间”的思想。
每个线程拥有自己的“共享资源”,虽然内存占用变大了,但由于不需要同步,也就减少了线程可能存在的阻塞问题,从而提高时间上的效率。

  • ThreadLocal 实质就是一个泛型容器,这个容器里可以存进任意类型的数据(数据、基本变量),然后存储的数据就能屏蔽其它线程,只被当前线程读取。

0X JavaSE-- 并发编程(并发容器、ThreadLocal、线程池)_第1张图片

ThreadLocal 常见的使用场景:

ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:

  • 线程间数据隔离,各线程的 ThreadLocal 互不影响
  • 方便同一个线程使用 ThreadLocal 保存的数据 ,避免在多个对象实例中进行不必要的参数传递,例如用户登录信息、数据库连接、Session 对象、事务上下文、线程中的变量
  • 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
  • Spring 事务管理器采用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
  • 在处理 HTTP 请求时,通常需要将一些上下文信息(如用户 ID等)保存在线程局部变量中,以便在整个请求处理过程中使用。
  • 一个 APP 多个数据源,来回切换多个数据源进行查询数据。
  • 日期格式化实例多线程安全问题。

下面是一个使用 ThreadLocal 来保存用户登录信息的示例。这个示例适用于像 Web服务器这样的多线程环境,其中每个线程处理一个独立的用户请求。

这个示例定义了一个 UserAuthenticationService类,该类使用 ThreadLoca l来保存与当前线程关联的用户登录信息。
假设用户已经通过身份验证,将用户对象存储在 currentUser ThreadLocal变量中。
getCurrentUser方法用于检索与当前线程关联的用户信息。由于使用了ThreadLocal,因此不同的线程可以同时登录不同的用户,而不会相互干扰

public class UserAuthenticationService {

    // 创建一个 ThreadLocal实例,用于保存用户登录信息
    private static ThreadLocal<User> currentUser = ThreadLocal.withInitial(() -> null);

    public static void main(String[] args) {
        // 模拟用户登录
        loginUser(new User("Alice", "password123"));
        System.out.println("User logged in: " + getCurrentUser().getUsername());

        // 模拟另一个线程处理另一个用户
        Runnable task = () -> {
            loginUser(new User("Bob", "password456"));
            System.out.println("User logged in: " + getCurrentUser().getUsername());
        };

        Thread thread = new Thread(task);
        thread.start();
    }

    // 模拟用户登录方法
    public static void loginUser(User user) {
        // 这里通常会有一些身份验证逻辑
        currentUser.set(user);
    }

    // 获取当前线程关联的用户信息
    public static User getCurrentUser() {
        return currentUser.get();
    }

    // 用户类
    public static class User {
        private final String username;
        private final String password;

        // 省略 Constructor、getter和setter...
    }
}

API

set

set 方法用于设置当前线程中 ThreadLocal 的变量值,该方法的源码如下:

public void set(T value) {
	//1. 获取当前线程实例对象
    Thread t = Thread.currentThread();

	//2. 通过当前线程实例获取到 ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);

    if (map != null)
	   //3. 如果 Map 不为 null,则以当前 ThreadLocal实例为 key,值为 value 进行存入
       map.set(this, value);
    else
	  //4.map 为 null, 则新建 ThreadLocalMap 并存入 value
      createMap(t, value);
}

每个线程都有自己的 ThreadLocalMap,这个映射表存储了线程的局部变量,其中键是 ThreadLocal 对象,值为特定于线程的对象。
如果 Map 不为 null,则以当前 ThreadLocal 实例为 key,值为 value 进行存入;如果 map 为 null,则新建 ThreadLocalMap 并存入 value。

get 方法

get 方法用于获取当前线程中 ThreadLocal 的变量值,同样的还是来看源码:

重点是第五步,可以看到,get方法实质上是不允许返回一个空值的。
这对 remove 方法会产生影响,看下去就知道。

public T get() {
  //1. 获取当前线程的实例对象
  Thread t = Thread.currentThread();

  //2. 获取当前线程的 ThreadLocalMap
  ThreadLocalMap map = getMap(t);
  if (map != null) {
	//3. 获取 map 中当前 ThreadLocal实例为 key 的值的 entry
    ThreadLocalMap.Entry e = map.getEntry(this);

    if (e != null) {
      @SuppressWarnings("unchecked")
	  //4. 当前 entitiy != null 的话,就返回相应的值 value
      T result = (T)e.value;
      return result;
    }
  }
  //5. 若 map == null 或者 entry == null ,通过该方法初始化,并返回该方法返回的 value
  return setInitialValue();
}

remove 方法

remove 方法的作用是从当前线程的 ThreadLocalMap 中删除与当前 ThreadLocal 实例关联的条目。
这个方法在释放线程局部变量的资源或重置线程局部变量的值时特别有用。

源代码如下:

public void remove() {
	//1. 获取当前线程的ThreadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
 	if (m != null)
		//2. 从map中删除以当前 ThreadLocal实例为 key 的键值对
		m.remove(this);
}

以下是使用 remove 方法的示例代码:

// 指定 ThreadLocal 的泛型为 String, 则该线程独享一个 String 类型的变量,
// 变量名为 threadLocal, 变量内容为 "Initial Value"  
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "Initial Value");

Thread thread = new Thread(() -> {
    System.out.println(threadLocal.get()); // 输出 "Initial Value"
    threadLocal.set("Updated Value");
    System.out.println(threadLocal.get()); // 输出 "Updated Value"
    threadLocal.remove();
    // 明明删除了值,为什么输出的还是第一次初始化的值?
    // get 不允许返回空值, 因此即使 remove了,我们下一次 get 的时候, 还是自动用最初的参数再初始化了一次.
    System.out.println(threadLocal.get()); // 输出 "Initial Value"
});
thread.start();

输出结果:
Initial Value
Updated Value
Initial Value

并发容器

  • 线程安全是并发容器的应有之义。即,并发容器是线程安全的子集,所有的并发容器都是线程安全的,但实现了线程安全的不一定是并发容器。
  • java.util 下提供了容器类(集合框架)。其中 Vector 和 Hashtable 是线程安全的,但实现方式比较粗暴,通过在方法上「sychronized」关键字实现。
  • java.util.concurrent 下提供了并发容器。并发容器都是线程安全的,比如 ConcurrentHashMap、阻塞队列和 CopyOnWrite容器等。这些并发容器通过内部的同步机制实现线程安全,性能更高。

Map 的并发版本 ConcurrentMap

ConcurrentMap 直接继承自 Map接口,如果只需要一个能并发的简单 Map,使用这个就对了

API

ConcurrentMap 接口继承了 Map 接口,在 Map 接口的基础上又定义了四个方法:

public interface ConcurrentMap<K, V> extends Map<K, V> {

    // 插入元素
    // 与原有 put 方法不同的是,putIfAbsent 如果插入的 key 相同,则不替换原有的 value 值;
    V putIfAbsent(K key, V value);

    // 移除元素
    // 与原有 remove 方法不同的是,新 remove 方法中增加了对 value 的判断,
    // 如果要删除的 key-value 不能与 Map 中原有的 key-value 对应上,则不会删除该元素;
    boolean remove(Object key, Object value);

    // 替换元素
    // 增加了对 value 值的判断,如果 key-oldValue 能与 Map 中原有的 key-value 对应上,才进行替换操作;
    boolean replace(K key, V oldValue, V newValue);

    // 替换元素
    // 与上面的 replace 不同的是,此 replace 不会对 Map 中原有的 key-value 进行比较,如果 key 存在则直接替换;
    V replace(K key, V value);

}

HashMap 的并发版本

  • HashMap 直接继承自 Map接口;ConcurrentHashMap 也直接继承自 Map接口。
    • 之前提到,util包下的集合容器中,只有 Vector 和 HashTable 是线程安全的。因此,如果想要使用线程安全的 Map/HashMap,必须要使用并发版本的实现.
  • HashMap 在 Map 的基础上以哈希表作为存储结构,提升了检索性能,是实际开发中使用更多的一种。
  • ConcurrentHashMap 提供了一种与 Hashtable 完全不同的加锁策略,提供了更高效的并发性和伸缩性。
  • 由于整张表只采用一把锁,ConcurrentHashMap 的大小计算可能不是精确的(否则就要暂停其它线程的操作去计算大小),但通常足够接近真实值。

手搓代码将 HashMap 改进为线程安全

通过 Collections 的Map synchronizedMap(Map m)将 HashMap 包装成一个线程安全的 map。

比如 SynchronzedMap 的 put 方法源码就是加锁过的:

public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}

synchronized 同步代码块的方式我们前面也讲过了,大家应该都还有印象。

J7 的 ConcurrentHashMap 使用分段锁

J7 的 ConcurrentHashMap 提供了一种细粒度的加锁机制,这种机制叫分段锁「Lock Striping」。
整个哈希表被分为多个段,每个段都独立锁定。读取操作不需要锁,写入操作仅锁定相关的段。这减小了锁冲突的几率,从而提高了并发性能。
分段锁,就是将数据分段,对每一段数据分配一把锁。当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

有些方法需要跨段,比如 size()、isEmpty()、containsValue(),它们可能需要锁定整个表而不仅仅是某个段,这需要按顺序锁定所有段,操作完后,再按顺序释放所有段的锁.

这种机制的优点:在并发环境下将实现更高的吞吐量,而在单线程环境下只损失非常小的性能。

分段锁的结构

0X JavaSE-- 并发编程(并发容器、ThreadLocal、线程池)_第2张图片
ConcurrentHashMap 是由 Segment数组结构和 HashEntry数组构成的。
Segment 是一种可重入的锁 ReentrantLock,HashEntry 则用于存储键值对数据。

一个 ConcurrentHashMap 里包含一个 Segment数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 里包含一个 HashEntry数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。

get方法执行的过程如下:

  • 为输入的 Key 做 Hash 运算,得到 hash 值。
  • 通过 hash 值,定位到对应的 Segment 对象
  • 再次通过 hash 值,定位到 Segment 当中数组的具体位置。

put 方法执行的过程如下:

  • 为输入的 Key 做 Hash 运算,得到 hash 值。
  • 通过 hash 值,定位到对应的 Segment 对象
  • 获取可重入锁
  • 再次通过 hash 值,定位到 Segment 当中数组的具体位置。
  • 插入或覆盖 HashEntry 对象。
  • 释放锁。

J8 的 ConcurrentHashMap 使用

  • 从 J8 开始,ConcurrentHashMap 有了较大的变化。
    • 舍弃了分段锁,并且使用了大量的 synchronized,以及 CAS 无锁操作以保证 ConcurrentHashMap 的线程安全性。
      • 分段锁(segment-based locking),即将整个哈希表分成多个段(Segment),每个段都有自己的锁,这样可以允许多个线程同时操作不同的段,从而提高并发性能。
      • 实际上,synchronzied 做了很多的优化:并发高时下会自动完成锁的升级,包括偏向锁、轻量级锁、重量级锁,因此,synchronized 相较于 ReentrantLock 的性能其实差不多,甚至在某些情况更优。
    • 同 HashMap 一样,链表也会在长度达到 8 的时候转化为红黑树,这样可以提升大量冲突时候的查询效率;

ConcurrentHashMap 的示例

在以下的例子中,我们模拟构建一个线程安全的高并发统计用户访问次数的功能。

首先要考虑的自然是如何存储访问次数这个变量。

由于用户线程当然是并发执行,为了不出现脏写,使用适合多线程环境的并发容器。

import java.util.concurrent.ConcurrentHashMap;

public class UserVisitCounter {

    private final ConcurrentHashMap<String, Integer> visitCountMap;

    public UserVisitCounter() {
        this.visitCountMap = new ConcurrentHashMap<>();
    }

    // 用户访问时调用的方法
    public void userVisited(String userId) {
        visitCountMap.compute(userId, (key, value) -> value == null ? 1 : value + 1);
    }

    // 获取用户的访问次数
    public int getVisitCount(String userId) {
        return visitCountMap.getOrDefault(userId, 0);
    }

    public static void main(String[] args) {
        UserVisitCounter counter = new UserVisitCounter();

        // 模拟用户访问
        counter.userVisited("user1");
        counter.userVisited("user1");
        counter.userVisited("user2");

        System.out.println("User1 visit count: " + counter.getVisitCount("user1")); // 输出: User1 visit count: 2
        System.out.println("User2 visit count: " + counter.getVisitCount("user2")); // 输出: User2 visit count: 1
    }
}

List 的版本

  • CopyOnWriteArrayList
    • 位于 java.util.concurrent 包中。
    • 适用于读操作远多于写操作的场景,因为每次写操作都会创建一个新的底层数组,代价较高。、
  • Synchronized List
    • 通过 Collections.synchronizedList 方法返回的线程安全的列表。
    • 适用于多线程环境,但需要手动同步迭代器操作。

CopyOnWrite 容器

在聊 CopyOnWrite 容器之前我们先来谈谈什么是 CopyOnWrite 机制,CopyOnWrite 是计算机设计领域的一种优化策略,也是一种在并发场景下常用的设计思想——写入时复制。

什么是写入时复制呢?

就是当有多个调用者同时去请求一个资源数据的时候,有一个调用者出于某些原因需要对当前的数据源进行修改,这个时候系统将会复制一个当前数据源的副本给调用者修改。

CopyOnWrite 容器即写时复制的容器,当我们往一个容器中添加元素的时候,不直接往容器中添加,而是将当前容器进行 copy,复制出来一个新的容器,然后向新容器中添加我们需要的元素,最后将原容器的引用指向新容器。

这样做的好处在于,我们可以在并发的场景下对容器进行"读操作"而不需要"加锁",从而达到读写分离的目的。从 JDK 1.5 开始 Java 并发包里提供了两个使用 CopyOnWrite 机制实现的并发容器,分别是 CopyOnWriteArrayList(后面会细讲,戳链接直达) 和 CopyOnWriteArraySet(不常用)。

Set 的并发版本

ConcurrentSkipListSet 是线程安全的有序集合。底层是使用 ConcurrentSkipListMap 来实现。

谷歌的 Guava 实现了一个线程安全的 ConcurrentHashSet:

Set<String> s = Sets.newConcurrentHashSet();

Set 日常开发中用的并不多。

Queue 的并发版本

BlockingQueue接口 阻塞队列

我们假设一种场景,生产者一直生产资源,消费者一直消费资源;资源存储在一个缓冲池中,生产者将生产的资源存进缓冲池中,消费者从缓冲池中拿到资源进行消费,这就是大名鼎鼎的生产者-消费者模式。

实现这个模式需要考虑以下几点:

  • 可能有多个线程同时操作共享变量(即资源),所以很容易引发线程安全问题,造成重复消费和死锁。
  • 当缓冲池空了,我们需要阻塞消费者,唤醒生产者;当缓冲池满了,我们需要阻塞生产者,唤醒消费者。

  • JDK 提供了阻塞队列(BlockingQueue)接口,解决了以上两个问题。
    • 也就是说,如果我们想要在数据上实现生产者-消费者模式,那么将数据存入实现了 BlockingQueue接口的类对象中,自动就实现了生产者-消费者模式。
    • BlockingQueue 是 Java.util.concurrent包下重要的数据结构,区别于普通的队列,BlockingQueue 提供了线程安全的队列访问方式,并发包下很多高级同步类的实现都是基于 BlockingQueue 实现的。
    • BlockinQueue 是一个接口,这个接口只定义了存取数据的规则(即阻塞版的生产者-消费者模型),底层可以采用数组(ArrayBlockingQueue)、链表(LinkedBlockingQueue)不同实现。
API

阻塞队列提供了四组不同的方法用于插入、移除、检查元素,针对修改时出错可能的措施,可以选择不同的方法。
0X JavaSE-- 并发编程(并发容器、ThreadLocal、线程池)_第3张图片

  • 抛出异常:如果操作无法立即执行,会抛异常。当阻塞队列满时候,再往队列里插入元素,会抛出 IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出 NoSuchElementException异常 。
  • 返回特殊值:如果操作无法立即执行,会返回一个特殊值,通常是 true / false。
  • 一直阻塞:如果操作无法立即执行,则一直阻塞或者响应中断。
  • 超时退出:如果操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回一个特定值以告知该操作是否成功,通常是 true / false。
BlockingQueue 的实现类 ArrayBlockingQueue

由数组结构组成的有界阻塞队列。内部结构是数组,具有数组的特性。

可以初始化队列大小,一旦初始化将不能改变。

LinkedBlockingQueue

由链表结构组成的有界阻塞队列。

DelayQueue

该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
注入其中的元素必须实现 java.util.concurrent.Delayed 接口。

DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

PriorityBlockingQueue

基于优先级的无界阻塞队列(优先级的判断通过构造函数传入的 Compator 对象来决定),内部控制线程同步的锁采用的是非公平锁。

SynchronousQueue

这个队列比较特殊,没有任何内部容量,甚至连一个队列的容量都没有。并且每个 put 必须等待一个 take,反之亦然。

ConcurrentLinkedQueue

  • ConcurrentLinkedQueue 和 LinkedBlockingQueue 有啥不同?
    • LinkedBlockingQueue 是 BlockingQueue 接口的一个实现,它基于链表结构。
    • ConcurrentLinkedQueue 是一个基于链表结构的无界并发队列。

你可能感兴趣的:(JavaSE,java)