文章目录
- 线程池
- 使用线程池有什么好处
- 线程池的7个参数
- 线程池怎么排队?
- 比较常见的线程池类型
- 阻塞队列
- ArrayBlockingQueue
- LinkedBlockingQueue
- SynchronousQueue
- CountDownLatch
- CyclicBarrier
- ThreadLocal
- Atomic
- 杂七杂八多线程知识点
线程池
即存放线程的池子。
Client调用ThreadPoolExecutor.submit(Runnable task)提交任务,然后线程池用里面的线程来处理这个任务。
线程池内部维护的工作者线程的数量就是该线程池的线程池大小,有3种形态:
- 当前线程池大小:表示线程池中实际工作者线程的数量
- 最大线程池大小(maxinumPoolSize):表示线程池中允许存在的工作者线程的数量上限
- 核心线程大小(corePoolSize ):表示一个不大于最大线程池大小的工作者线程数量上限
注意:当前线程池大小等于核心线程大小在核心线程未达最大值时,核心线程达到最大值但未新建线程时,他们两个也相等,但是核心线程达到最大值且还增加了线程,那么当前线程大小大于核心线程大小
使用线程池有什么好处
- 线程池可以重复利用已创建的线程,一次创建可以执行多次任务,有效降低线程创建和销毁所造成的资源消耗;
- 线程池技术使得请求可以快速得到响应,节约了创建线程的时间;
- 线程的创建需要占用系统内存,消耗系统资源,使用线程池可以更好的管理线程,做到统一分配、调优和监控线程,提高系统的稳定性。
线程池的7个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime :线程空闲但是保持不被回收的时间
- unit:时间单位
- workQueue:存储线程的队列
- threadFactory:创建线程的工厂
- handler:拒绝策略
线程池怎么排队?
- 如果运行的线程少于corePoolSize,则Executor始终首选添加新的线程,而不进行排队
- 如果运行的线程等于或者多于corePoolSize,则Executor始终首选将请求加入队列,而不是添加新线程
- 如果无法将请求加入队列,即队列已经满了,则创建新的线程,除非创建此线程超出maxinumPoolSize,在这种情况下,任务默认将被拒绝。
比较常见的线程池类型
注意:创建线程池最好还是用new ThreadPoolExecutor()自定义来创建,而不是直接使用下面的这些类型。使用下面的这些类型可能会导致内存溢出,因为队列可以存放很多任务。此外线程池提供了一些监控API,可以很方便的监控当前以及塞进队列的任务数以及当前线程池已经完成的任务数等。
newCachedThreadPool( )
- 核心线程池大小为0,最大线程池大小不受限,来一个创建一个线程
- 适合用来执行大量耗时较短且提交频率较高的任务
newFixedThreadPool( )
- 固定大小的线程池
- 当线程池大小达到核心线程池大小,就不会增加也不会减小工作者线程的固定大小的线程池
newSingleThreadExecutor( )
- 便于实现单(多)生产者-消费者模式
阻塞队列
线程池的任务可能需要在队列中进行排队等候,而排队就是在阻塞队列中,阻塞队列又有几种
ArrayBlockingQueue
- 内部使用一个数组作为其存储空间,数组的存储空间是预先分配的
- 优点是 put 和 take操作不会增加GC的负担(因为空间是预先分配的)
- 缺点是 put 和 take操作使用同一个锁,可能导致锁争用,导致较多的上下文切换。
- ArrayBlockingQueue适合在生产者线程和消费者线程之间的并发程序较低的情况下使用。
LinkedBlockingQueue
- 是一个无界队列(其实队列长度是Integer.MAX_VALUE)
- 内部存储空间是一个链表,并且链表节点所需的存储空间是动态分配的
- 优点是 put 和 take 操作使用两个显式锁(putLock和takeLock)
- 缺点是增加了GC的负担,因为空间是动态分配的。
- LinkedBlockingQueue适合在生产者线程和消费者线程之间的并发程序较高的情况下使用。
SynchronousQueue
SynchronousQueue可以被看做一种特殊的有界队列。生产者线程生产一个产品之后,会等待消费者线程来取走这个产品,才会接着生产下一个产品,适合在生产者线程和消费者线程之间的处理能力相差不大的情况下使用。
newCachedThreadPool这种线程池来一个任务,线程就创建一个,这是因为其内部队列使用了SynchronousQueue,所以不存在排队。
CountDownLatch
CountDownLatch是一个倒计时协调器,它可以实现一个或者多个线程等待其余线程完成一组特定的操作之后,继续运行。
内部结构:
- CountDownLatch内部维护一个计数器,CountDownLatch.countDown()每被执行一次都会使计数器值减少1。
- 当计数器不为0时,CountDownLatch.await()方法的调用将会导致执行线程被暂停,这些线程就叫做该CountDownLatch上的等待线程。
- CountDownLatch.countDown()相当于一个通知方法,当计数器值达到0时,唤醒所有等待线程。当然对应还有指定等待时间长度的CountDownLatch.await( long , TimeUnit)方法。
CyclicBarrier
CyclicBarrier是一个栅栏,可以实现多个线程相互等待执行到指定的地点,这时候这些线程会再接着执行,在实际工作中可以用来模拟高并发请求测试。
例子:我们爬山的时候,到了一个平坦处,前面队伍会稍作休息,等待后边队伍跟上来,当最后一个爬山伙伴也达到该休息地点时,所有人同时开始从该地点出发,继续爬山。
内部结构
- 使用CyclicBarrier实现等待的线程被称为参与方(Party),参与方只需要执行CyclicBarrier.await()就可以实现等待,该栅栏维护了一个显示锁,可以识别出最后一个参与方,当最后一个参与方调用await()方法时,前面等待的参与方都会被唤醒,并且该最后一个参与方也不会被暂停。
- CyclicBarrier内部维护了一个计数器变量count = 参与方的个数,调用await方法可以使得count -1。当判断到是最后一个参与方时,调用singalAll唤醒所有线程。
ThreadLocal
ThreadLocal维护变量时,其为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立的改变自己的副本,而不会影响其他线程对应的副本。
内部结构
- 每个线程内部都会维护一个类似HashMap的对象,称为ThreadLocalMap,里边会包含若干了Entry(K-V键值对),相应的线程被称为这些Entry的属主线程
- Entry的Key是一个ThreadLocal实例,Value是一个线程特有对象。Entry的作用是为其属主线程建立起一个ThreadLocal实例与一个线程特有对象之间的对应关系
- Entry对Key的引用是弱引用;Entry对Value的引用是强引用。
Atomic
经典问题:i++是线程安全的吗?
i++操作并不是线程安全的,它是一个复合操作,包含三个步骤:
- 拷贝i的值到临时变量
- 临时变量i++操作
- 拷贝回原始变量i
案例
class ThreadTest implements Runnable {
static int i = 0;
public void run() {
for (int m = 0; m < 1000000; m++) {
i++;
}
}
};
public class Test {
public static void main(String[] args) throws InterruptedException {
ThreadTest mt = new ThreadTest();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
// 休眠一下,让线程执行完毕。
Thread.sleep(500);
System.out.println(ThreadTest.i);
}
}
输出:1023873(不一定哈)。显然不对结果
这是一个复合操作,不能保证原子性,所以这不是线程安全的操作。那么如何实现原子自增等操作呢?
这里就用到了JDK在java.util.concurrent.atomic包下的AtomicInteger等原子类了。AtomicInteger类提供了getAndIncrement和incrementAndGet等原子性的自增自减等操作。Atomic等原子类内部使用了CAS来保证原子性。
案例
import java.util.concurrent.atomic.AtomicInteger;
class ThreadTest implements Runnable {
static AtomicInteger i = new AtomicInteger(0);
public void run() {
for (int m = 0; m < 1000000; m++) {
i.getAndIncrement();
}
}
};
public class Test {
public static void main(String[] args) throws InterruptedException {
ThreadTest mt = new ThreadTest();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
t1.start();
t2.start();
// 休眠一下,让线程执行完毕。
Thread.sleep(500);
System.out.println(ThreadTest.i.get());
}
}
输出始终是2000000
杂七杂八多线程知识点
- 什么是happened-before原则?
- JVM虚拟机对内部锁有哪些优化?
- 如何进行无锁化编程?
- CAS以及如何解决ABA问题?
- AQS(AbstractQueuedSynchronizer)的原理与实现。