赵云胡说--java并发编程
序
自上篇博客结束至今,学习并发编程已经大半月了,回头想想自己学的东西一团浆糊般,但是真的收益匪浅有种我之前学习java不久,去看动态代理(将动态生成的代理字节码反编译出来)的实现时的震撼,原来还可以这么玩啊!在这大半个月中认识到了java并发界的巨佬 Doug Lea.话说至此,却不知如何动笔.不如就按照学习的思路,复习一遍来写下该博客吧!
并发之线程物语
早在我还是个小白的时候,对于并发编程的理解仅仅是Thread和Sychonized,他们当然是及其重要的,面试时候,java线程的生命周期还是各种线程api的使用:notify,wait,join,interupt,yeild…,比较简单的线程的创建的问题…真的是折磨啊!!!我能说什么呢,只能怪自己菜呗,但是菜鸟也有一颗成为高手的心,就算往往会变成一只老菜鸟,也不能放弃我求知的心.在8月份后,公司的工作逐渐闲下来的我,恶补了计算机基础.在恶补到操作系统的时候,终于接触到了线程这个概念,抛弃了java的思想和api.见识到了操作系统里面的线程.
1.操作系统的线程
操作系统本来是没有线程的,本来有的应该只是进程而已.但是某年后,windows操作系统推出了线程这个概念,它在很大程度上优化了,相同的程序多次启动后需要多个进程的缺点.而是改用了多个线程去执行.(进程比线程耗费的资源多很多,进程需要有自己独立的取指位置,有自己的页表,有自己的缓存,有自己的内存去存储数据等等,但是线程因为是相同的代码,相比较而言,最起码页表,共享的数据,缓存什么的是可以一定程度上共享的,不能共享的例如取指令位置等可以变成栈去存储,没错!栈哦,是不是跟java的栈很相似呢?较小的占用资源不仅有内存消耗的优势,还有操作系统上下文切换的耗时减少),在线程这个思想诞生之后,各OS(操作系统)都实现了自己的线程,对于线程,各个操作系统的实现方法是不一样的,各种线程模型也挺多的.什么一对一,一对多,多对多,基本上笔者认为就是核心级线程和用户级线程的对应关系吧,那么什么是核心级线程,什么是用户级线程呢??(小朋友,你是不是有很多的问号,笔者水平有限,大致解释下)
用户级线程:其实就是呢,额,怎么说呢.只涉及到用户态的线程吧!
核心级线程:内核态里面的线程就是核心级线程吧!
系统调用:硬件为了电脑的安全把内存分为用户态和内核态,内核态可以进行系统调用去执行一些特殊操作(可以控制硬件的那种操作),实质 是对计算机的保护.因为这种保护措施吗,导致系统调用时进程或者线程需要切换上下文(保存寄存器信息和其他中间量信息,方便切换回来的时候使用)所以用户级线程想要调用系统调用的时候该怎么办呢,当然是让用户级线程去通知和他对应的核心级线程啦.这个对应就有了各个线程模型了啊
总结一句话:java并没有实现自己的线程,java的线程模型是跟所在操作系统上一一对应的,了解操作系统上线程的运行,我们就能够看到java线程的运行规则了
2.线程的并发执行
线程的并发执行是一种交替切换的执行,每个线程执行的时候,操作系统会给我给它分配一个时间片,当时间片耗尽,或者遇到中断啊,异常啊什么之类的,操作系统就会保存这个线程的上下文去执行另外的线程,执行前先恢复要执行线程的上下文,具体执行谁由操作系统内部的调度算法去控制.为什么要这么做是因为cpu速度太快,而很多系统调用需要耗时较长,在此期间cpu为了效率会去执行其他线程.而这个线程就会进入阻塞队列中,当阻塞结束后,会被唤醒到可运行的队列中,等待cpu的调度。而时间分片是为了防止一个线程无系统调用,但是一直霸占cpu.就在这种来回的切换中,线程执行完了.当然多核cpu是可以并行执行的.
3.多线程间的通信
操作系统层面的线程通信,有信号量,信号,共享内存等方式,这也变成了java中对线程通信操作的一种思路(后文来好好分析下)
4.java的线程方法
对于java线程而言,它是和操作系统的线程模型意义对应的,但是java本身也是提供了使用线程的api,常用的有yeild(), sleep(), join(), wait(), notify()等.遥想当年(一个月前)我是分不清这几个方法,但是今时不同往日,我要手撕了它们.不过在此之前,先补充点操作系统线程的知识吧.
在操作系统中的线程是有队列的,比如线程因为io操作会被发配到操作系统层面阻塞队列中,如果io操作结束会通过中断还是什么的方法(具体的笔者忘了),把阻塞队列的该线程放到操作系统层面就绪队列里面,线程调度会用一些算法去操作系统层面就绪队列里面拿出线程去执行.另外,时间片耗尽的线程是会直接到操作系统层面就绪队列中的.
yeild:线程让出cpu资源(可以理解为耗尽时间片了),线程回到操作系统层面就绪队列,
sleep:线程让出cpu资源,并且去睡眠一定时间(理解为线程进行IO操作),线程发配到了操作系统层面阻塞队列.,睡眠时间到了(IO操作结束),就会进入操作系统层面就绪队列
join: java层面的实现,可以让一个线程在另一个线程执行完之后再执行(使用了java层面的wait方法实现的).
wait: jvm层面的实现,必须在synchronized里面使用.因为synchronized关键字在jvm层面维护了两个线程队列.从操作系统层面来说,他们应该都是在操作系统层面的阻塞队列.因为在这两个队列的线程都是被阻塞了的,但是有所不同的是,竞争锁失败的进入了java层面的就绪队列,使用wait方法的线程进入了jvm层面的阻塞队列.当锁释放时,会唤醒jvm层面的就绪队列的第一个线程,让它进入操作系统层面的就绪队列,之后等待cpu调度从而获得cpu的资源,使用wait方法会释放锁
notify: jvm层面的实现,必须在synchronized里面使用.上述的jvm层面的阻塞队列的线程,使用该方法,将一个处于jvm层面的阻塞队列的线程连接到jvm层面的就绪队列.等待锁释放后,从jvm层面的就绪队列中进入操作系统层面的就绪队列,之后等待cpu调度从而获得cpu的资源.
(写完后看了看自己对这几个方法的总结,异常满意,不知道读者们懂了没,有没有被绕晕,实在有问题的话,可以先看下文中对synchronized的介绍,再回来看下)
因为yeild,sleep都不涉及jvm层面的队列,所以就跟synchronized无关,所以就不会有释放锁的概念,所以yeild,sleep都不会释放锁,因为他们根本就不知道synchronized,也获取不到synchronized,就不会释放synchronized的锁.
并发之AQS帝国
AQS全称 AbstractQueuedSynchronizer,大名鼎鼎的并发框架,Doug Lea开发.遥想lea哥当年,雄姿英发,羽扇纶巾,谈笑间,sychonized灰飞烟灭。jdk的1.6之前,sychonized关键字并没有像如今这样复杂,它仅仅是作为一个调用c++的pthread_mutex_lock这个方法,从而调用操作系统层面的独占锁去实现锁的。这种不分场景的使用独占锁的方法,无疑是很耗费性能的,因此Doug Lea在java的语言层面上写出了AQS框架,并基于此框架实现大名鼎鼎的显示锁ReetranLock.
1.AQS的源码分析
AQS全英文翻译过来,叫做抽象同步队列,没错它其实是一个队列,但是这个队列是那么的不简单,在分析AQS的原理之前,还是要补充点知识cas操作,cas操作,全程CompareAndSwap.翻译过来就是比较和交换操作,这是个原子性(本文对原子性不做解释)的操纵,也就是一条指令就可以执行出来的操作.这个操作是计算机通过硬件去实现的.在AQS中大量的使用了cas操作.另外,java提供了只要知道了线程对象,就能把它从操作系统层面阻塞队列唤醒到操作系统层面就绪队列中,LockSupport.unpark().其实AQS可以看作是java层面的就绪队列
那么开始分析cas吧,忽然笔者不知该如何说起了…拿着源码比照着去描述吗?好麻烦啊!不想这样-_-!在此思索了很久…思索完了,我回来了!首先,说到队列,是不是有数组和链表两种实现方法呢?没错AQS中的队列是通过链表去实现的,既然有链表,那么链表中的节点的结构是什么样子的呢?
贴图真香,很明显可以看出来,这个node里面存储的信息有prev上级节点,next下级节点,这是很标准的双向链表的结构,thread线程,waitStatus唤醒标志,以及一些常量吧.prev上级节点,next下级节点比较好理解,thread线程则是保存着阻塞的线程,提供唤醒的引用 ,waitStatus唤醒标志判断该节点的下个节点需不需要去唤醒.在shouldParkAfterFailedAcquire中操作
再来说下AQS的结构,不截图了!太长了,AQS中有很多方法,还有头和尾的Node,以及这个state,就是标志位,它就是标志着锁是否被占用,还有一个exclusiveOwnerThread它是父类的属性,表示当前持有锁的线程.
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
/**
* 这是父类的属性,表示当前持有锁的线程
*/
private transient Thread exclusiveOwnerThread;
你可以想象一下,当竞争锁失败后线程们把自己的信息放在了这个队列中,然后陷入沉睡的场景~~,然后,锁被释放以后,node按照顺序被唤醒的样子.当然其中还有很多细节是需要分析的.摘下几个比较重要的方法吧^ _ ^!(字都写注释上了)
acquire
//一般锁的lock方法其实调用的是AQS的acquire的方法
public final void acquire(int arg) {
//tryAcquire()是个抽象方法,交给具体的实现,比如公平锁,非公平锁,读写
//锁等都有自己的tryAcquire()方法,基本操作就是先尝试加锁,实现一下可重入
if (!tryAcquire(arg) &&
//如果尝试加锁失败,先执行addWaiter(Node.EXCLUSIVE)方法,添加好节点后,执行acquireQueued()方法
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
addWaiter
//该方法的操作是创建队列或者把新生节点加入尾部
private Node addWaiter(Node mode) {
//先初始化一个节点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//看看尾节点是否为空(队列有没有初始化)
if (pred != null) {
//已经初始化的话,直接cas操作去把加入尾节点
node.prev = pred;
//如果cas成功,则返回该节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
//如果cas失败,则说明尾节点发生变更(被其他线程修改了)
}
//队列未初始化或者上述cas失败会进入该方法,会死循环的去创建队列或者去把节点加入到队列尾部,再返回出来
enq(node);
return node;
}
acquireQueued
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//看过来这个!这个死循环
for (;;) {
final Node p = node.predecessor();
//当上一个节点为头节点并且加锁成功才能返回出来
if (p == head && tryAcquire(arg)) {
//把该节点设置为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//如果没加锁成功,不好意思,进下面的方法,shouldParkAfterFailedAcquire()是把前置节点的waitStatus改为-1,来告
//诉锁释放的时候,是否要唤醒下个节点,parkAndCheckInterrupt()方法中有一个LockSupport.park方法去让线程休眠
//只有线程被唤醒时会接着死循环,只有当满足当上一个节点为头节点并且加锁成功才不会进入该方法中沉睡
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
release
//同加锁方法会执行acquire方法,释放锁会执行release方法
public final boolean release(int arg) {
//tryRelease()是个抽象方法,大体是对重入进行一个接触,当重入为0时会返回true
if (tryRelease(arg)) {
Node h = head;
//当头节点不是空,且waitStatus不是0的时候才会去唤醒
if (h != null && h.waitStatus != 0)
//这里去唤醒队列的下一个线程
unparkSuccessor(h);
return true;
}
return false;
}
AQS的加锁解锁的代码分析完毕啦,当然,这里面有很多更复杂的操作.比如共享锁之类的方法啦.但是基本思路是一样的,就是骚操作更多哈哈
2.AQS的锁氏家族
基于AQS的框架,就可以实现锁啦,你也可以自己继承AQS去写一个自己的锁,只要把上述的抽象方法tryAcquire()和tryRelease(),实现了,那么就是一个锁了.首先吧,在这里把锁的概念梳理一下吧~~
自旋锁:就是被加锁后,自身不放弃cpu资源,进行无意义空转的锁.
可重入锁:什么是可重入锁呢,同一把锁,同一个线程能多次获取的锁.每次获取,状态加一,每次释放,锁状态减一,当锁状态为0,锁真正释放
公平锁:公平锁的意思就是,如果之前有很多线程尝试加锁失败了,他们组成了等待队列,这时候新的线程去尝试加锁的话,如果是公平锁,则不会去进行加锁操作,而是直接到队列尾部去等待.
非公平锁:非公平锁的意思就是,如果之前有很多线程尝试加锁失败了,他们组成了等待队列,这时候新的线程去尝试加锁的话,如果是非公平锁,会直接去进行加锁操作,如果成功就直接拿到了锁,失败了则到队列尾部去等待.(效率比公平锁的高)
读锁:和写锁一起用的,读读并发
写锁:和读锁一起用的,读写互斥
AQS实现的锁有ReentrantLock, ReentrantReadWriteLock,首先他们都是可重入的,即是重入锁 .
其中ReentrantLock内部分了公平锁和非公平锁.默认情况下是创建非公平锁.
public ReentrantLock() {
//Nonfair翻译过来就是非公平的意思
sync = new NonfairSync();
}
ReentrantLock中还有一个比较重要的运用.那就是条件锁ConditionObject,通过newCondition()方法去创建,之后提供了类似于线程wait的await方法和notify相似的signal方法.它广泛使用在消费者通知者的模式中,在并发编程中,这种等待通知机制是最基础,也是最核心的思想,AQS是java层面的就绪队列,那么ConditionObject就是java层面的阻塞队列(是用java实现的一个线程阻塞队列)
ReentrantReadWriteLock,顾名思义就是读写锁,读写锁的思想大致上总结就是:读读并发,读写互斥,写写互斥.读写锁的实现是比ReentrantLock复杂的 ,本博客大致描述下读写锁的设计吧.
ReentrantLock首先将状态存储位(其实就是一个int型的数,所以有32位),的前16位存储读的信息,后16位存储写的信息.
acquireShared
public final void acquireShared(int arg) {
//返回-1就会进入doAcquireShared()方法
if (tryAcquireShared(arg) < 0)
//这个方法是为共享锁涉及的入阻塞队的操作,大致跟上文中acquireQueued的思路一致,不做具体分析了
doAcquireShared(arg);
}
tryAcquireShared
//本代码是读写锁使用读锁的主要实现
protected final int tryAcquireShared(int unused) {
//这一段是先判断锁是否已经被写锁占用,如果被写锁占用的话,看下是否是自己这个线程占用了锁,有的话,就实现了
//重入,这就是读写锁的降级,同一个线程,先加写锁再加读锁是可以重入的,但是先加读锁,再加写锁是不可以的.
//!---
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//---!
int r = sharedCount(c);
//readerShouldBlock()是对公平锁和非公平锁的一个区别对待,当加读锁成功后,开始进行一系列操作,
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果是第一个读锁,就将线程信息存储,重入标志置为1
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//如果是持有第一个读锁的线程读读重入,重入标志加1
} else if (firstReader == current) {
firstReaderHoldCount++;
//如果不是第一个加读锁线程,就将重入的信息放入ThreadLocal中(这里做了封装)
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
//加锁失败的会进入到该方法中
return fullTryAcquireShared(current);
}
fullTryAcquireShared
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
//这里死循环,跳出的条件只有两种,一是加锁成功,而是加锁失败,失败的话公平锁失败是
//等待队列有线程等待,非公平锁则是等待队列的队头是写锁,则会加锁失败
for (;;) {
int c = getState();
//与上一段代码块:tryAcquireShared中一样的判断
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
//这里对公平锁和非公平锁不同加锁失败条件的判断
} else if (readerShouldBlock()) {
//依旧判断下是否可重入
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
//不可重入则进行一些操作,返回-1(加锁失败)
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
//读锁超过最多数量(基本不会出现)
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//尝试加锁,如加锁成功返回1,不成功的话,下次循环再次去尝试加锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
好累啊!!!不想分析了,读锁的较为简单,读者们自己看看吧,还有释放的话,也不想写了.有一点注意的:写锁释放时,会按照顺序唤醒队列中的线程,直到遇到下一个写锁阻塞,具体的跟下文中CountDownLatch的源码较为相似.
3.AQS的并发工具
基于AQS实现的并发工具类有CountDownLatch,Semaphore,CyclicBarrier等,本博客对这三种并发工具做一些简单的介绍
CountDownLatch:通过一个计数器来实现的,计数器的初始值为初始任务 的数量。每当完成了一个任务后,计数器的值就会减1 ,countDown()方法.当计数器值到达 0 时它表示所有的已经完成了任务然后在闭锁上等待 await()方法的线程就可以恢复执行任务。
Semaphore:是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源.
CyclicBarrier:与CountDownLatch相似,但是可以重复使用
简单分析下CountDownLatch的countDown代码:
doReleaseShared
//追踪下来CountDownLatch的countDown核心代码是在doReleaseShared中的
private void doReleaseShared() {
//死循环(自旋)
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//当是可以唤醒的条件时则执行
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//当头节点没有变化时则退出自旋,什么时候头节点发生变化呢?在唤醒的线程时,被唤醒的线程会修改头节点(详见下面的方法)
if (h == head) // loop if head changed
break;
}
}
doAcquireSharedInterruptibly这里是await方法的主要方法
//进入该方法一般是状态值为正数
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//增加等待的节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
//自旋
for (;;) {
//取该节点前的节点
final Node p = node.predecessor();
//如果节点前的节点是头节点,则会尝试拿锁
if (p == head) {
//如果状态值为0返回非负数
int r = tryAcquireShared(arg);
if (r >= 0) {
//设置本节点为头节点下面有该方法的详细分析
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//设置唤醒标志
if (shouldParkAfterFailedAcquire(p, node) &&
//休眠
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
//设置头节点
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
//满足条件会调用该方法,这是依次唤醒等待队列的关键!!!!!!!!!构成一个闭环了doReleaseShared
doReleaseShared();
}
}
不容易的原创图,细细品一下
并发之SYN新生
sychonized关键字在1.6之后,进行了一次重生,从语言层面进行了优化,其性能目前可能已经超越了AQS实现的锁们.那么现在的sychonized究竟变成了一个怎么样的存在呢?
sychonized变成了及其复杂的一个东西,特别的复杂.复杂到我不想写了,我想把这个大标题删了=_=!,可是自己挖的坑,哭着也要填上吧.死磕吧
sychonized现在有很多状态,分别为:偏向锁,轻量级锁,重量级锁,无锁.还有的就是sychonized是可重入的非公平锁
1.SYN之锁的结构
发动魔法卡,盗图(码字码到怀疑人生而精神恍惚的某人)
大家看我盗的这幅图哈,这是我上篇博客介绍的对象头里面的MarkWord的的结构哈.这里面详细的介绍了在不同状态下MarkWord的结构.我也打字再次介绍下:
无锁:时,并且没有计算hashCode的时候,应该前面的56位全零,然后分别是无用位,回收年龄(最大15),是否可偏向位,以及锁标志位,如果计算了hashCode那么,这个对象的前25位无用,用之后的32位去存hashCode值
偏向锁:在偏向锁状态的话,前54位存储的是偏向线程的信息,后面是epoch位,然后分别是无用位,回收年龄(最大15),是否可偏向位,以及锁标志位.
轻量级锁:存储轻量级锁记录的62位以及锁标志位.
重量级锁:存储重量级锁记录的62位以及锁标志位.
2.SYN之锁的膨胀
sychonized的锁膨胀可以分为以下几个过程:无锁到偏向锁,偏向锁到偏向锁,偏向锁到轻量级锁,轻量级锁到重量级锁
无锁到偏向锁:当一个对象,被一个线程第一次加锁的时候,他会变为偏向锁,并记录这个线程.下次这个线程再来的时候,会只比较下是否是之前的线程,如果是的话就不用任何的操作,释放锁也是不需要操作的,在无竞争的情况下.基本不会有性能上的损耗.
偏向锁到偏向锁:当有其他线程去尝试加锁的时候,类上的epoch和对象上的epoch对不上(发生过重偏向),此时会将上一个线程替换为该线程的偏向锁
偏向锁到轻量级锁:在偏向锁时,当有其他线程去尝试加锁的时候,没有发生重偏向,则会先撤销偏向锁,升级为轻量级锁,再去尝试加锁.
轻量级锁到重量级锁:当轻量级锁发生竞争的时候,此时就会升级为重量级锁
给大家准备一位大佬的博客,对syn分析的贼好
3.SYN之锁的分析
偏向锁和轻量级锁的话对于我们开发而言,是比较抽象,而且jvm为我们做了很好的控制,对于锁的分析,笔者对重量级锁进行一个叙述吧.看我刚盗的图~~.
从图中分析可知,在对象头中的maekWord里面的前62位指向了一个ObjectMonitor对象,这个对象可以分析出来有三个属性:线程,jvm层面的阻塞队列(waitSet),jvm层面的就绪队列(entryList),当在锁中调用wait方法时,线程休眠并且放入waitSet,竞争锁失败的话就放入entryList中.(是不是跟AQS的有点相似啊),如果锁释放了则会从entryList中取出来放入操作系统层面的就绪队列中.使用notify方法会将waitSet中的一个线程放入到entryList中.
并发之线程池
在实际的开发中,对于线程的管理不能是随意的new线程,而是需要做一定管理的,否则会出现很严重的问题,需要有一个能管理线程的容器,这个容器便是线程池
1.线程池的使用
上图中的最后一个方法.便是创建线程池的所有参数,首先的两个int分别是核心线程数和最大线程数,第三个long是非核心线程的超时时间,第四个参数是时间单位,第五个是线程池所使用的阻塞队列,第六个是产生线程的工厂方法,最后一个是当阻塞队列满了后定义的处理方法.
使用线程池,就是将new ThreadPoolExecutor(),之后获取一个线程池对象,然后将需要执行的任务提交给线程池就可以了.使用起来是很简单的.但是线程池使用一定要记得关闭,不然线程会一直存在操作系统中,浪费资源,累积到一定程度,服务器可能需要重启才行.
2.线程池的队列
针对线程池,对于其内部的解析是很有必要的!在分析线程池之前需要了解一下生产者-消费者模式,这个模式在池技术中被广泛的使用,线程池中也是不例外的.而实现了生产者-消费者模式的便是线程池的队列们了,所以先了解一下线程池的队列容器吧
首先线程池的队列都是要继承BlockingQueue
BlockingQueue有以上的方法针对这些方法,我又盗了一张图,阻塞队列中存着的是一个一个的线程需要执行的任务
其中最能体现消费者-生产者模式的就是put和take方法了,
当消费者线程使用take方法的时候,要是阻塞队列中是空的,那么消费者线程就会park(沉睡)在消费队列中
当生产者线程使用put方法的时候,要是阻塞队列满了,那么生产者线程会park(沉睡)在生产者队列中,
另外在每次消费者线程成功消费时,会去唤醒生产者队列中的线程,生产者线程成功生产会去唤醒消费者队列中的线程
其中的消费队列和生产者线程可以使用synochonized的waitSet去实现,也可以使用AQS的条件锁ConditionObject去实现,具体的实现看个人的选择.
jdk自身实现很多个阻塞队列:
ArrayBlockingQueue:基于数组实现的有界阻塞队列
LinkedBlockingDeque:基于链表实现的有界阻塞队列(双向的)
LinkedBlockingQueue:基于链表实现的有界阻塞队列
SynchronousQueue:是一个不存储元素的阻塞队列
LinkedTransferQueue:笔者不理解
PriorityBlockingQueue:是一个支持优先级的无界阻塞队列
DelayQueue:是一个支持延时获取元素的无界阻塞队列
上述的阻塞队列中,比较常用的是ArrayBlockingQueue和LinkedBlockingQueue.其中LinkedBlockingDeque是实现fork/join线程池中工作密取思想的一个阻塞队列,所谓工作密取,是在fork/join中,每个线程都有一个双向队列,当线程自己的队列中的任务被执行完了,会去其他线程的双向队列中取任务执行,所以队列是双向的,一向是持有线程执行任务,另一向是给别人偷任务的.
3.线程池的成员
回过头来看看,创建线程池的对象是有七个的,那么这些参数有各自又怎么样的意义的呢
核心线程数:它是线程池中不会被回收的线程数量corePoolSize
private volatile int corePoolSize;
最大线程数:它是线程池中允许存在的最多的线程数量,它是允许大于或者等于核心线程数线程数的,maximumPoolSize
private volatile int maximumPoolSize;
超时时间和时间单位:这是一对的东西,非核心线程最大的空闲时间,超过这个时间,非核心线程就会被回收.keepAliveTime
private volatile long keepAliveTime;
阻塞队列:这是线程池的核心之一,不同的阻塞队列有不同的作用.泛型是Runnable的
private final BlockingQueue<Runnable> workQueue;
线程工厂:初始话线程的工厂,一般的用于给线程起名字
private volatile ThreadFactory threadFactory;
workers:HashSet是存储线程的一个集合,所有的线程都存储在里面
这里介绍一下线程池的工作流程
当线程池初始化时,是没有线程的,当submit第一个任务时,会根据corePoolSize和maximumPoolSize去决定如何创建线程,当
workers的长度小于corePoolSize时,创建的是核心级线程.如果核心级线程达到上限,那么再次提交的任务会进入阻塞队列中
当阻塞队列满了后,workers的长度大于corePoolSize并且小于maximumPoolSize时,创建非核心级线程.当非核心级线程满了后,会触发拒绝策略
这里注意:只有阻塞队列满了才会创建非核心线程
当线程池中的线程去阻塞队列拿任务时,如果队列为空,则线程沉睡,如果拿到任务,会去唤醒添加任务的线程去添加任务.
当任务线程去加任务的时候,会判断阻塞队列满了没,满了就阻塞沉睡(具体应该是触发拒绝策略),如果任务成功入队,就去唤醒线程池中的线程
这两个过程就是生产者消费者模式,由阻塞队列实现
那么非核心级线程和核心级线程哪里不一样呢,其实就是在取阻塞队列准备沉睡时,核心级线程它是await,而非核心线程它是await(time),其实就是非核心线程采用了等待超时模式,这个模式和等待通知差不多,只是多了个超时时间,时间一到就会线程就会醒来,然后跳出了循环,最后被回收.
结
终于,写完了.并发编程,博大精深!还有许多没有写上去的内容,比如Future,比如ConcurrentHashMap.还有很多的疑惑在我心里没有解开,以后的学习依旧任重而道远.在此,为自己加个油!
如果各位大佬发现博客有说的不对的欢迎你们的指正,一定认真对待!!!