本文中内容是从B站中看到一个视频的案例(https://www.bilibili.com/video/BV16J411h7Rd?p=200),
为了加深理解,特抄录如下:(详细说明都记录在comment里面了)
由于@Slf4j还没有调试通,故没办法只有sysout。。。
package java_proj_test;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义线程池
*
* @author tei
*
*/
@Slf4j(topic = "c.TestPool")
public class TestPool {
public static void main(String args[]) {
Logger log = LoggerFactory.getLogger(TestPool.class);
ThreadPool threadPool = new ThreadPool(2, 1000, TimeUnit.MILLISECONDS, 10);
System.out.println(Thread.currentThread().getName() + "线程池对象 :" + threadPool);
for (int i = 0; i < 5; i++) {
int tmp = i;
threadPool.excute(() -> {
System.out.println(Thread.currentThread().getName() + tmp);// log.debug({},tmp);
});
}
log.debug("执行完成");
}
}
// 2.线程池,就是一个线程的集合
@Slf4j(topic = "c.ThreadPool")
class ThreadPool {
// 任务队列
private BlokingQueue<Runnable> taskQueue;
// 线程集合
HashSet<Worker> workers = new HashSet<Worker>();
// 核心线程数,即常驻内存的线程线程数
private int coreSize;
// 获取任务的超时时间(当没有任务的时候,让线程一只运行也是一种资源浪费,所以当超过一定时间,应当回收线程)
private long timeOut;
// 时间单位/时间单元
private TimeUnit timeUnit;
// 构造方法
public ThreadPool(int coreSize, long timeOut, TimeUnit timeUnit, int queueCapcity) {
this.coreSize = coreSize;
this.timeOut = timeOut;
this.timeUnit = timeUnit;
this.taskQueue = new BlokingQueue<Runnable>(queueCapcity);
}
// 执行任务方法,要传递一个Runnable类型的任务对象,交给Worker对象来执行
public void excute(Runnable task) {
// 当当前的任务数没有超过核心线程数的时候呢,将该任务直接交给线程去执行;順便將该线程加入到线程集合
// 当超过当前核心线程数时,应该把该任务加入任务队列,暂存起来,等有核心线程空闲时在来执行该任务;
// 由于works这个集合是共享的,所以应当包证他的安全性
synchronized (workers) {
if (workers.size() < coreSize) {
Worker worker = new Worker(task);
// log.debug("新增worker {} {}", worker ,task)
System.out.println("Thread_" + Thread.currentThread().getId() + "新增worker " + worker + " " + task);
workers.add(worker);
worker.start();
} else {
// log.debug("加入任务队列 {}",task)
System.out.println("Thread_" + Thread.currentThread().getId() + "加入任务队列" + task);
taskQueue.putElement(task);
}
}
}
/**
* 包装线程类,应该是一个线程对象, 并将要执行的任务交给线程对象-构造方法来传递任务
*
* @author tei
*
*/
class Worker extends Thread {
private Runnable task;
public Worker(Runnable task) {
this.task = task;
}
@Override
public void run() {
// 执行任务,执行的时候有2种可能:
// [1] 马上执行该任务,( 因为构造的时候就已经将,任务传递过来了)
// [2] 当任务已经执行完了,也不能马上结束当前线程,应该看一下任务队列中还有没有未执行的任务,
// 如果有的话,则应该从队列中取出任务继续执行
while (task != null || (task = taskQueue.pollElement(timeOut, timeUnit)) != null) { // 当没有任务的话,就等待一定时间,超时后自动结束
// while (task != null || (task = taskQueue.takeElement()) != null) { // 当没有任务也一只等待
try {
// log.debug("正在执行。。。{}",task);
System.out.println("Thread_" + Thread.currentThread().getId() + "正在执行... " + task);
task.run(); // 执行任务
} catch (Exception e) {
e.printStackTrace();
} finally {
task = null;
}
}
// 一旦退出了上面的循环,则表示该线程结束,应当从线程集合中移除
synchronized (workers) {
// log.debug("worker 被移除 {}",this);
System.out.println("Thread_" + Thread.currentThread().getId() + "worker 被移除 " + this);
workers.remove(this);
}
}
}
}
// 1.阻塞队列
@Slf4j(topic = "c.BlokingQueue")
class BlokingQueue<T> {
// 1.任务队列 (<LinkedList和ArrayDeque都是双向队列,性能上ArrayDeque更优)
private Deque<T> queue = new ArrayDeque();
// private Deque<T> queue = new LinkedList();
// 2.锁
private ReentrantLock lock = new ReentrantLock();
// 3. 生产者/消费者的条件变量-> 等待
// [1] 一般来讲,这个队列是有容量限制的,同理生产者也是有一定容量的限制,否则内存溢出有可能
private Condition fullWaitSet = lock.newCondition();
// [2] 消费者的条件变量-> 等待
private Condition emptyWaitSet = lock.newCondition();
// 4.队列的容量上限
private int capcity;
// 构造方法
public BlokingQueue(int capcity) {
this.capcity = capcity;
}
// [1] 阻塞获取
public T takeElement() {
// [1]获取元素需要用锁来保护,最后将锁释放
lock.lock();
try {
// [2]如果队列中没有元素了,则应该将这个队列阻塞住,得等有元素了才可以执行
while (queue.isEmpty()) {
try {
// [3] 让消费者等待
emptyWaitSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// [4] 当队列不空了,就可以继续执行,获取队列的头部元素,返回
T t = queue.removeFirst();
// [5] 当取走元素后,说明队列又有空位置了,则应当将生产者唤醒
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
// [2]阻塞添加
public void putElement(T element) {
// [1]添加元素需要用锁来保护,避免多個生产者同時添加,最后将锁释放
lock.lock();
try {
// [2]添加的时候需要看队列是否已经满了?
// 如果是的话,則不能添加,应该将这个队列阻塞住,得等沒有元素了才可以添加
while (queue.size() == capcity) {
try {
// [3] 让生产者等待
fullWaitSet.await();// await() 是一只等待,永久阻塞
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// [4] 当队列不满了,就可以继续执行,向队列尾部添加元素
queue.add(element);
// [5] 当添加进去元素了,就应当将消费者唤醒
emptyWaitSet.signal();
} finally {
lock.unlock();
}
}
// 5.自定义2个方法:
// [1]从队列中获取元素的方法take [2]向队列中添加元素的方法put
// [3] 获取队列大小,希望指定当前队列中排队执行的任务还有几个
public int size() {
lock.lock();
try {
return queue.size();
} finally {
lock.unlock();
}
}
// [1] 阻塞获取改进为带超时的阻塞获取
// [1] 改进为超时等待而不是一直等待,因此需要添加1.最大等待时间,2.时间单位--TimeUnit(方便时间单位的转换)
public T pollElement(long timeOut, TimeUnit unit) {
lock.lock();
try {
// [1] 将TimeOut统一转换为纳秒
long nanos = unit.toNanos(timeOut);
while (queue.isEmpty()) {
try {
// [2] 改进为等待一定时间,而不必死等了,而此时唤醒后如果它还是没等够的话-->虚假唤醒?
// 再次循环就还得等那么长的时间,刚才的等待就等于白等了
// awaitNanos()这个方法好就好在他的返回值,是timeOut的时间减去刚才已经等待的时间,
// 因此我们把这个返回值再一次交给刚才的<nanos>这个变量,让其继续等待剩余的时间,
// 所以,返回值就等于剩余时间,但是如果返回值小于或者等于0,那说明已经超时了,应当返回null
if (nanos <= 0)
return null;
nanos = emptyWaitSet.awaitNanos(nanos);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
T t = queue.removeFirst();
fullWaitSet.signal();
return t;
} finally {
lock.unlock();
}
}
}
运行结果:
案例1:线程常驻内存,一只等待,
线程一只死死等待的原因: