一、Java线程的概念
Java 线程的本质:每个线程对应一个操作系统线程,由操作系统调度。JVM 通过调用操作系统 API(如 Linux 的 pthread
)创建线程。
关键点:
• 用户态与内核态:线程调度依赖操作系统(内核级线程),能直接利用多核 CPU。
• 线程生命周期:新建 → 就绪 → 运行 → 阻塞 → 终止。
• JVM 线程模型:每个 Java 线程对应一个 Thread
对象,通过 start()
触发操作系统线程的创建。
二、线程的使用方法
方式 1:继承 Thread
类(简单但不够灵活)
class MyThread extends Thread {@Overridepublic void run() {System.out.println("线程执行: " + Thread.currentThread().getName());}
}// 使用
MyThread t = new MyThread();
t.start(); // 注意:必须调用 start(),而不是直接 run()
方式 2:实现 Runnable
接口(推荐,避免单继承限制)
class MyTask implements Runnable {@Overridepublic void run() {System.out.println("任务执行: " + Thread.currentThread().getName());}
}// 使用
Thread t = new Thread(new MyTask());
t.start();
方式 3:带返回值的 Callable
(适合需要结果的任务)
• Callable
:带返回值的任务(像“下单”),比如 Callable<String>
表示这个任务最终会返回一个 String
。
• Future
:代表异步任务的“凭证”(像“订餐小票”),凭它未来可以取结果。
以订餐流程举例:
- 提交任务:你去餐厅点了一份炒饭,服务员给你一张小票(
Future
)。 - 后厨做菜:厨师(线程池中的线程)开始炒饭(执行
Callable
)。 - 等待结果:你可以干其他事情(不阻塞主线程),也可以随时拿小票问:“好了没?”(
future.isDone()
)。 - 取回结果:当炒饭做好后,凭小票取餐(
future.get()
拿到返回值)。
Future底层实现原理:
-
状态跟踪:
Future
内部维护任务状态:
• 未完成:任务还在执行。
• 已完成:任务正常结束,保存返回值。
• 已取消:任务被中断。
• 异常结束:保存抛出的异常。 -
阻塞获取:当调用
future.get()
时:
• 如果任务已完成 → 直接返回结果。
• 如果未完成 → 当前线程阻塞等待,直到任务完成(内部通过wait/notify
机制实现)。 -
结果存储:任务完成后,返回值(或异常)会被存入
Future
内部的成员变量,供后续读取。
代码示例:
import java.util.concurrent.*;public class FutureExample {public static void main(String[] args) throws Exception {// 1. 创建线程池(后厨)ExecutorService executor = Executors.newSingleThreadExecutor();// 2. 提交 Callable 任务(下单炒饭)Future<String> future = executor.submit(new Callable<String>() {@Overridepublic String call() throws Exception {Thread.sleep(2000); // 模拟炒饭需要2秒return "扬州炒饭做好了!";}});System.out.println("提交任务后,主线程继续做其他事情...");// 3. 检查是否完成(非阻塞)if (future.isDone()) {System.out.println("任务已经完成!");} else {System.out.println("任务还在进行中...");}// 4. 阻塞获取结果(类似等待取餐)String result = future.get(); // 这里会阻塞,直到任务完成System.out.println("取到结果:" + result);executor.shutdown(); // 关闭线程池(后厨下班)}
}
输出:
提交任务后,主线程继续做其他事情...
任务还在进行中...
(等待2秒后)
取到结果:扬州炒饭做好了!
- 异常处理:
如果任务中抛出异常,future.get()
会抛出ExecutionException
,可通过getCause()
获取原始异常:
try {future.get();
} catch (ExecutionException e) {System.out.println("任务出错:" + e.getCause());
}
- 超时控制:
避免无限等待,可以设置超时时间:
String result = future.get(3, TimeUnit.SECONDS); // 最多等3秒
- 取消任务:
如果不想等了,可以取消任务:
future.cancel(true); // true表示尝试中断正在执行的任务
三、Java 线程池机制详解
线程池的核心思想
• 复用线程:避免频繁创建/销毁线程的开销(类似餐厅固定几个服务员服务所有顾客,而不是每来一个顾客就雇佣新服务员)。
• 资源管控:通过队列缓冲任务,防止系统过载(类似餐厅的等候区,避免人太多挤爆店面)。
线程池的四大核心参数
ThreadPoolExecutor(int corePoolSize, // 核心线程数(常驻员工)int maximumPoolSize, // 最大线程数(临时工上限)long keepAliveTime, // 空闲线程存活时间(临时工多久没活就解雇)TimeUnit unit, // 时间单位BlockingQueue<Runnable> workQueue, // 任务队列(等候区座位数)RejectedExecutionHandler handler // 拒绝策略(人满时怎么处理新顾客)
)
参数详解:
- corePoolSize:核心线程即使空闲也不会被销毁(除非设置
allowCoreThreadTimeOut
)。 - maximumPoolSize:当队列满时,允许创建的最大线程数(核心线程 + 临时线程)。
- workQueue:常用队列类型:
• ArrayBlockingQueue:有界队列(固定容量)。
• LinkedBlockingQueue:无界队列(默认Integer.MAX_VALUE
,慎用易内存溢出)。
• SynchronousQueue:不存储任务,直接移交(适合瞬时高并发)。 - 拒绝策略(当队列和线程池全满时):
• AbortPolicy:抛异常(默认)。
• CallerRunsPolicy:让提交任务的线程自己执行。
• DiscardPolicy:默默丢弃新任务。
• DiscardOldestPolicy:丢弃队列最旧的任务,再尝试提交。
合理设置线程数:
• CPU密集型:线程数 ≈ CPU核数(避免过多上下文切换)。
• IO密集型:线程数 ≈ CPU核数 * 2(或更高,因线程常阻塞在IO)。
监控线程池状态:
// 查看活跃线程数
int activeCount = executor.getActiveCount();
// 查看任务队列大小
int queueSize = executor.getQueue().size();
线程池的工作流程
↗ 核心线程有空 → 立即执行
任务提交 → 检查核心线程↘ 核心线程忙 → 入队列 → 队列满? → 否 → 等待↘ 是 → 创建临时线程 → 超过最大数? → 是 → 拒绝
常用线程池(通过 Executors
工厂创建)
1. FixedThreadPool(固定大小团队)
ExecutorService fixedPool = Executors.newFixedThreadPool(4); // 4个核心线程
• 特点:核心线程=最大线程,无临时线程,使用无界队列(LinkedBlockingQueue
)。
• 适用场景:已知并发量且任务耗时较长(如后台计算)。
2. CachedThreadPool(弹性团队)
ExecutorService cachedPool = Executors.newCachedThreadPool();
• 特点:核心线程=0,最大线程=Integer.MAX_VALUE,空闲线程60秒回收,使用 SynchronousQueue
(直接移交任务)。
• 适用场景:短时高频小任务(如HTTP请求处理)。
3. SingleThreadExecutor(单人团队)
ExecutorService singlePool = Executors.newSingleThreadExecutor();
• 特点:核心线程=最大线程=1,无界队列,保证任务顺序执行。
• 适用场景:需要顺序执行的任务(如日志写入)。
4. ScheduledThreadPool(计划任务团队)
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// 延迟3秒执行
scheduledPool.schedule(() -> System.out.println("Run after 3s"), 3, TimeUnit.SECONDS);
// 固定频率执行(每隔1秒)
scheduledPool.scheduleAtFixedRate(() -> System.out.println("Run every 1s"), 0, 1, TimeUnit.SECONDS);
• 特点:支持定时、周期性任务。
• 适用场景:心跳检测、定时数据同步。
5. 自定义线程池
public class ThreadPoolDemo {public static void main(String[] args) {// 创建线程池:2核心线程,5最大线程,10容量队列,拒绝策略抛异常ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(10),new ThreadPoolExecutor.AbortPolicy());// 提交20个任务(测试队列满时的扩容)for (int i = 0; i < 20; i++) {final int taskId = i;try {executor.submit(() -> {System.out.println("任务 " + taskId + " 由线程 " + Thread.currentThread().getName() + " 执行");try { Thread.sleep(1000); } catch (InterruptedException e) {}});} catch (RejectedExecutionException e) {System.out.println("任务 " + taskId + " 被拒绝!");}}executor.shutdown(); // 平滑关闭(等待已有任务完成)}
}
输出分析:
• 前2个任务由核心线程立即执行。
• 接下来的10个任务进入队列。
• 当队列满后(10个),创建3个临时线程(总线程数=5)。
• 第18个任务提交时,线程数已达最大(5),队列满(10),触发拒绝策略抛异常。
场景 | 推荐线程池 | 参数要点 |
---|---|---|
长期稳定并发任务 | FixedThreadPool | 核心线程数=最大线程数,队列容量适中 |
短期突发小任务 | CachedThreadPool | 注意防止无限创建线程(适合可控的短任务) |
单线程顺序执行 | SingleThreadExecutor | 替代手动创建线程,保证顺序性 |
定时/周期性任务 | ScheduledThreadPool | 指定延迟和周期 |
高并发自定义需求 | ThreadPoolExecutor | 根据业务特点调整核心参数 |
线程池关闭方法
- shutdown():平滑关闭,不再接受新任务,等待已有任务完成。
- shutdownNow():立刻停止所有任务,返回未执行的任务列表。
List<Runnable> unfinishedTasks = executor.shutdownNow();
四、线程同步与安全,原理、用法及示例
在多线程环境下,当多个线程同时访问共享资源(如变量、文件、数据库)时,可能导致数据不一致或逻辑错误。线程同步的目的是协调线程间的执行顺序,确保线程安全。
详细参考:https://blog.csdn.net/gengzhikui1992/article/details/147230900?spm=1001.2014.3001.5501
常见同步方式及底层原理
1. synchronized
关键字:
• 原理:基于对象的内置锁(Monitor),每个对象关联一个Monitor。
执行synchronized
代码时,线程需获取对象的Monitor锁:
• 成功则持有锁,执行代码。
• 失败时线程进入锁的等待队列(EntrySet),阻塞等待唤醒。
• 用法:
// 同步方法
public synchronized void safeMethod() { /* ... */ }// 同步代码块
public void someMethod() {synchronized (this) { // 锁对象为当前实例// 临界区代码}
}
• 示例:
class Counter {private int count = 0;public synchronized void increment() {count++; // 原子操作}
}
解析:synchronized
确保同一时刻仅一个线程执行increment()
方法。
2. ReentrantLock
可重入锁:
• 原理:基于AQS(AbstractQueuedSynchronizer),维护一个CLH队列管理等待线程。
支持可重入性(同一线程可多次加锁)和公平性(可选)。
• 用法:
private final ReentrantLock lock = new ReentrantLock();public void safeMethod() {lock.lock(); // 手动加锁try {// 临界区代码} finally {lock.unlock(); // 必须手动释放}
}
• 示例(带超时):
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试1秒内获取锁try { /* ... */ } finally { lock.unlock(); }
} else { // 超时处理 }
优势:比synchronized
更灵活,支持尝试锁、公平锁等。
特性 | synchronized | ReentrantLock |
---|---|---|
锁的获取方式 | 自动获取和释放(JVM管理) | 手动 lock() 和 unlock() (需写finally) |
可中断性 | 不支持(阻塞时无法中断) | 支持 lockInterruptibly() |
超时机制 | 不支持(只能阻塞等待) | 支持 tryLock(timeout) |
公平锁 | 仅非公平锁 | 支持公平和非公平(构造参数控制) |
条件变量 | 只能绑定一个条件(wait() /notify() ) | 可创建多个条件(newCondition() ) |
性能 | JDK6后优化后性能接近 | 高并发竞争时性能更好 |
代码复杂度 | 简单(自动管理) | 复杂(需手动释放,易忘) |
锁的可见性 | 通过JVM内存模型保证 | 基于AQS的volatile变量保证 |
• synchronized
的优势:简单、安全、自动释放锁,适合快速开发。
• ReentrantLock
的优势:灵活、功能强大,适合需要超时、中断、公平锁等复杂场景。
最终建议:优先用 synchronized
,遇到它无法满足需求时再考虑 ReentrantLock
。
3. volatile
变量:
多线程环境下,变量操作可能引发两种问题:
- 可见性问题:A线程修改了变量,B线程看不到最新值。
- 原子性问题:看似一步的操作(如
i++
),实际分三步(读-改-写),中间可能被其他线程打断。
volatile 变量:解决了可见性问题,原理:通过内存屏障禁止指令重排序,确保变量的修改对所有线程立即可见(不保证原子性)。
• 强制读写主内存:volatile
变量修改后,其他线程立即可见。
• 禁止指令重排序:确保代码执行顺序符合预期。
使用场景
适合做 状态标志(如开关控制),不涉及复杂计算。
public class Server {private volatile boolean isRunning = true; // 状态标志public void stop() {isRunning = false; // 修改后,其他线程立即可见}public void run() {while (isRunning) { // 循环读取最新值// 处理请求...}}
}
局限性
• 不保证原子性:volatile
无法解决 i++
这种非原子操作的问题。
volatile int count = 0;
count++; // 实际分三步:读 -> 改 -> 写(线程不安全!)
4. 原子类(Atomic Classes):解决原子性问题
• 封装原子操作:通过 CPU 的 CAS(Compare-And-Swap) 指令,保证操作的原子性。
• 无需加锁:性能优于 synchronized
。
常见类
• AtomicInteger
、AtomicLong
:整型原子操作。
• AtomicReference
:对象引用原子操作。
• AtomicStampedReference
:解决 ABA 问题(版本号控制)。
使用场景
适合 计数器、累加器 等需要原子操作的场景。
public class Counter {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet(); // 原子自增}public int get() {return count.get();}
}
示例:AtomicReference 保证对象引用原子性
public class AtomicReferenceDemo {private AtomicReference<String> latestMessage = new AtomicReference<>("");public void updateMessage(String message) {latestMessage.set(message); // 原子更新引用}public String getMessage() {return latestMessage.get();}
}
示例:解决 ABA 问题(AtomicStampedReference)
public class ABADemo {private AtomicStampedReference<Integer> atomicValue = new AtomicStampedReference<>(100, 0); // 初始值100,版本号0public void update() {int[] stampHolder = new int[1];int currentValue = atomicValue.get(stampHolder); // 获取值和版本号int newValue = currentValue + 1;atomicValue.compareAndSet(currentValue, newValue, stampHolder[0], stampHolder[0] + 1);}
}
特性 | volatile | 原子类(如 AtomicInteger) |
---|---|---|
解决可见性问题 | ✔️(强制主内存读写) | ✔️(内部使用 volatile 变量) |
解决原子性问题 | ❌(如 i++ 仍不安全) | ✔️(封装原子操作) |
性能 | 高(无锁) | 高(CAS 无锁) |
适用场景 | 状态标志、开关控制 | 计数器、累加器、复杂原子操作 |
ABA 问题 | 无法解决 | 可通过 AtomicStampedReference 解决 |
• volatile
是轻量级的可见性解决方案,不保证原子性。
• 原子类通过 CAS 实现无锁原子操作,同时解决可见性和原子性。
• 两者性能均优于锁,但适用场景不同,不要混淆!
-
用
volatile
:
• 变量被多个线程共享,但只有一个线程修改它。
• 需要立即可见性,但不涉及复合操作(如i++
)。
• 典型场景:状态标志(boolean
开关)。 -
用原子类:
• 变量被多个线程频繁修改(如计数器)。
• 需要原子性操作(如addAndGet()
、compareAndSet()
)。
• 典型场景:并发计数器、无锁数据结构。 -
用
synchronized
或ReentrantLock
:
• 需要同步多步操作(如先读后写)。
• 原子类和volatile
无法满足复杂逻辑时。
6. 读写锁 ReentrantReadWriteLock
:
想象一个图书馆:
• 读锁:允许多个人同时读书(共享资源)。
• 写锁:当有人要修改书的内容时,必须清场(独占资源),其他人不能读也不能写。
核心规则:
- 读锁之间不互斥:多个线程可以同时持有读锁。
- 写锁与其他锁互斥:写锁生效时,其他线程不能获取读锁或写锁。
- 写锁优先:如果写锁在等待,新来的读锁会被阻塞(防止“写线程饥饿”)。
为什么用读写锁?
在 读多写少 的场景下(如缓存系统、配置管理),用读写锁比普通互斥锁(如 synchronized
)性能更高:
• 读操作:允许多线程并发读取。
• 写操作:保证独占修改,避免数据不一致。
场景:实现一个线程安全的缓存系统
public class Cache<K, V> {private final Map<K, V> cacheMap = new HashMap<>();private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private final Lock readLock = rwLock.readLock(); // 读锁private final Lock writeLock = rwLock.writeLock(); // 写锁// 读操作:允许多线程并发读public V get(K key) {readLock.lock();try {return cacheMap.get(key);} finally {readLock.unlock();}}// 写操作:独占访问public void put(K key, V value) {writeLock.lock();try {cacheMap.put(key, value);} finally {writeLock.unlock();}}// 复杂操作:先读后写(需要先释放读锁,再获取写锁)public void updateIfPresent(K key, V newValue) {readLock.lock();try {if (cacheMap.containsKey(key)) {// 释放读锁,获取写锁(注意:不能直接升级锁!)readLock.unlock();writeLock.lock();try {cacheMap.put(key, newValue);} finally {writeLock.unlock();}// 重新获取读锁(如果需要)readLock.lock();}} finally {readLock.unlock();}}
}
关键细节
- 避免锁升级:先释放读锁,再获取写锁。
- 写锁优先:如果写锁在等待,后续读锁会被阻塞(可通过公平锁缓解)。
- 锁的释放:必须用
try-finally
确保释放,否则会导致死锁。
在持有写锁时,可以获取读锁,然后释放写锁(保持数据可见性):
public void writeThenRead() {writeLock.lock();try {// 修改数据...readLock.lock(); // 锁降级(允许)} finally {writeLock.unlock();}try {// 读取数据...} finally {readLock.unlock();}
}
不能直接从读锁升级到写锁(会导致死锁):
public void readThenWrite() {readLock.lock();try {// 如果发现需要修改数据...writeLock.lock(); // 错误!会阻塞,因为读锁未释放,其他线程也无法释放写锁} finally {readLock.unlock();}
}
线程同步方式的对比与选择
方式 | 原理 | 适用场景 | 性能 |
---|---|---|---|
synchronized | 对象内置锁 | 简单同步,少量竞争 | 中等,自动释放锁 |
ReentrantLock | AQS队列锁 | 复杂控制(如超时、公平锁) | 高,需手动释放 |
volatile | 内存可见性 | 单变量状态标志 | 极高,无锁 |
原子类 | CAS指令 | 计数器,单变量原子操作 | 高,无锁竞争 |
ReentrantReadWriteLock | 读写分离锁 | 读多写少场景 | 读高,写中等 |
- 优先选择高级工具:如
java.util.concurrent
包下的并发集合(ConcurrentHashMap
,BlockingQueue
)。 - 避免锁粒度过大:尽量缩小同步范围,减少竞争。
- 资源释放:使用
Lock
时务必在finally
中释放锁。 - 分工协作:读写分离时使用
ReentrantReadWriteLock
提升性能。 - 监测工具:利用
jstack
和VisualVM
排查死锁和性能瓶颈。
五、Java线程安全集合详解
Java提供了多种线程安全的集合类,适用于高并发场景。它们通过内部优化(如分段锁、写时复制)实现高效并发,避免开发者手动加锁。以下是常见线程安全集合及其使用场景和示例:
1. ConcurrentHashMap(并发哈希表)
• 原理:将数据分为多个段(Segment,Java 8后改为Node数组+CAS),每个段独立加锁。允许多线程同时读写不同段的数据。
• 适用场景:高并发键值存储(如缓存、计数器)。
• 示例:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();// 线程安全的插入(若key不存在)
map.putIfAbsent("apple", 1);// 原子累加操作
map.compute("apple", (k, v) -> v == null ? 1 : v + 1);// 遍历(弱一致性迭代器,不抛ConcurrentModificationException)
map.forEach((k, v) -> System.out.println(k + ": " + v));
2. CopyOnWriteArrayList(写时复制列表)
• 原理:写操作时复制整个数组(加锁保证原子性),读操作无锁。适合读多写少的场景。
• 适用场景:监听器列表、配置信息列表。
• 示例:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("user1");// 读操作无需加锁
for (String user : list) {System.out.println(user);
}// 写操作会复制新数组
list.add("user2"); // 原数组:[user1],新数组:[user1, user2]
3. BlockingQueue(阻塞队列)
• 原理:当队列空时阻塞消费者,队列满时阻塞生产者。内部通过ReentrantLock
和Condition
实现。
• 常见实现:
• ArrayBlockingQueue:有界队列(数组实现)。
• LinkedBlockingQueue:可选有界或无界(链表实现)。
• PriorityBlockingQueue:优先级阻塞队列。
• 适用场景:生产者-消费者模型。
• 示例:
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);// 生产者
new Thread(() -> {try {queue.put("task1"); // 队列满时阻塞} catch (InterruptedException e) {e.printStackTrace();}
}).start();// 消费者
new Thread(() -> {try {String task = queue.take(); // 队列空时阻塞System.out.println("处理任务: " + task);} catch (InterruptedException e) {e.printStackTrace();}
}).start();
4. ConcurrentLinkedQueue(并发链表队列)
• 原理:基于无锁算法(CAS),实现非阻塞线程安全队列。
• 适用场景:高并发非阻塞队列(如任务分发)。
• 示例:
ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>();// 生产者
queue.offer(100); // 非阻塞添加// 消费者
Integer value = queue.poll(); // 非阻塞取出
5. ConcurrentSkipListMap(并发跳表映射)
• 原理:基于跳表(Skip List)数据结构,实现有序的并发Map。
• 适用场景:需要排序的高并发键值存储(如排行榜)。
• 示例:
ConcurrentSkipListMap<Integer, String> rank = new ConcurrentSkipListMap<>();
rank.put(90, "Alice");
rank.put(85, "Bob");// 按分数从高到低遍历
rank.descendingMap().forEach((score, name) -> {System.out.println(name + ": " + score);
});
6. CopyOnWriteArraySet(写时复制集合)
• 原理:基于CopyOnWriteArrayList
实现,用数组存储元素,写操作时复制。
• 适用场景:读多写少的集合(如IP白名单)。
• 示例:
CopyOnWriteArraySet<String> ips = new CopyOnWriteArraySet<>();
ips.add("192.168.1.1");// 检查IP是否存在(无需加锁)
if (ips.contains("192.168.1.1")) {System.out.println("IP允许访问");
}
7. Collections.synchronizedXXX(同步包装类)
• 原理:通过包装普通集合,对所有方法加synchronized
锁。
• 适用场景:低并发环境(性能低于专用并发集合)。
• 示例:
List<String> syncList = Collections.synchronizedList(new ArrayList<>());// 遍历时需手动加锁
synchronized (syncList) {for (String item : syncList) {System.out.println(item);}
}
线程安全集合对比表
集合名称 | 原理 | 适用场景 | 性能特点 |
---|---|---|---|
ConcurrentHashMap | 分段锁/CAS | 高并发键值存储 | 高吞吐量,低延迟 |
CopyOnWriteArrayList | 写时复制 | 读多写少的列表 | 写操作慢,读操作快 |
BlockingQueue | 锁+条件队列 | 生产者-消费者模型 | 阻塞操作,适用于任务调度 |
ConcurrentLinkedQueue | 无锁(CAS) | 高并发非阻塞队列 | 高并发,无锁 |
ConcurrentSkipListMap | 跳表 | 有序并发键值存储 | 查询和插入O(log n) |
CopyOnWriteArraySet | 基于CopyOnWriteArrayList | 读多写少的集合 | 同CopyOnWriteArrayList |
-
键值存储:
• 高并发写入:ConcurrentHashMap
• 需要排序:ConcurrentSkipListMap
-
列表/集合:
• 读多写少:CopyOnWriteArrayList
/CopyOnWriteArraySet
• 写操作频繁:使用ConcurrentHashMap
模拟(如Collections.newSetFromMap
) -
队列:
• 阻塞队列:ArrayBlockingQueue
/LinkedBlockingQueue
• 非阻塞队列:ConcurrentLinkedQueue