多线程
线程的7种状态
- 新建(new):线程被创建
- 就绪(runnable或ready):线程正在参与竞争cpu的使用权
- 运行(running):线程获取到了cpu的使用权,正在执行
- 阻塞(blocked):线程为等待某个对象的“锁”而暂时放弃cpu的使用权,且不再参与cpu使用权竞争。直到条件满足时,重新回到就绪状态,重新参与竞争cpu。
- 等待(waiting):线程无限等待某个对象的“锁”,或等待另一个线程结束的状态到来。
- 计时等待(time_waiting):在一段时间内等待某个对象的“锁”,或者主动休眠,抑或等待另外一个线程结束(join)。除非被中断,否则时间一到,(超时)线程将自动回到runnable状态,被中断的方法通常会抛出中断异常(InterruptedException)。超时方法会抛出超时异常(TimeoutException)。
- 终止(terminated或dead):线程所运行的代码被执行完毕;执行过程中出现异常;或受到外界干预而中断执行。
这些线程的状态可在java.lang.Thread.State
中找到。 同样,线程模型在JVM中也有定义,可通过jstack 命令获取到运行瞬时的线程状态。
进程与线程
在操作系统中,进程是资源分配的最小单位。每个进程都有独立的代码和数据空间,称为进程上下文。 进程之间的切换会有较大开销,因此引入了线程。线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC)。是cpu调度的最小单位。 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。但线程中的状态只是虚拟机状态,它不反映任何操作系统的线程状态。
waiting和block的区别
block是等待获取monitor对象,waiting是等待另一个线程完成某个操作。因此wating可以被interrupt()中断或者notify()唤醒,获取到monitor时直接进入runnable状态,而同类的其它线程则进入block状态。
Monitor 监视器
monitor定义于jvm中,每个对象都有一个monitor。 monitor中有一个owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。 每个线程都有一个可用monitor record列表,和一个全局的可用列表。 synchronized关键字便是用monitorenter与monitorexit指令实现的。
Thread常用方法
以下t为Thread的实例
• t.start();启动线程t,t将从new状态转化为runnable状态,开始参与cpu竞争。
• t.checkAccess();检查当前线程是否有权限访问线程t
• t.interrupt(); 尝试通知线程t中断,需要在线程的任务代码中使用。
• t.isInterruped(); 检测线程是否被要求中断。当此方法返回true时,当前线程应判断是否要中断执行。如果此时不中断执行,再次调用此方法将返回false
• t.setPriority(8); 设置线程t的优先级1~10,值越大,得到执行的机会越高
• t.isDaemno(); 判断线程t是否为守护线程,当进程中仅剩守护线程时jvm将退出
• t.setDaemno(true); 仅用于在start()前设置线程t是否为守护线程
• t.isAlive(); 判断线程t是否存活
• t.join(100L); 当前线程等待线程t终止。参数为超时时间
• Thread.yield(); 当前线程让出cpu,并转为runnable状态,重新参与cpu的竞争。只有优先级大于或等于当前线程的线程才可能获得cpu使用权。
• Thread.sleep(100L); 当前线程让出cpu,睡眠100ms后回到runnable状态,重新参与cpu竞争
• Thread.currentThread(); 得到当前线程对象的引用。
其中的大多数方法都是native的,都可在JVM的c语言中找到对应的实现。
wait与sleep的区别
wait()方法是所有Object类的方法,是线程同步的重要手段之一。 两者都可以让程序阻塞指定毫秒数,都可以通过interrupt()方法打断。 但两者有很大的不同之处:
• wait()方法必须在synchronized同步代码块或方法中使用。
• wait()方法会释放持有的monitor锁,而sleep不会释放资源。
• wait()方法形成的阻塞,可以通过同一对象的notify()来唤醒;sleep()无法被唤醒,只能等待时间到来或被interrupt()中断。
sleep与yield的区别
• sleep()方法先转入block状态,后回到runnable状态。yield()直接进入runnable状态。
• sleep()后,其它线程无论优先级高低,都有机会得以执行。yield()后只有比它优先级高或同等的线程才有机会执行。
• sleep()方法需要声明抛出InterruptedException,而yield()没有声明任何异常。
• sleep()比yield()具有更好的可移植性。
线程池
线程的创建和销毁都会消耗资源。在高并发的情况下,频繁的创建和销毁线程会严重降低系统的性能。这也是《阿里巴巴java开发规范》六-3中所强制要求的。
最小线程数 | 最大线程数 | 线程最大空闲时长 | 描述 | |
SingleThreadExecutor | 1 | 1 | 0 | 只有一个线程在工作,串行执行所有任务,按照提交的先后顺序。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。 |
FixedThreadPool | n | n | 0 | 线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。 |
CachedThreadPool | 0 | int_max | 60s | 如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小 |
ScheduledThreadPool | n | int_max | DEFAULT_KEEPALIVE_MILLIS | 定时以及周期性执行任务 |
在线程池中使用BlockingQueue作为任务队列。
BlockingQueue
当获取队列元素但是队列为空时,会阻塞等待队列中有元素再返回;也支持添加元素时,如果队列已满,那么等到队列可以放入新元素时再放入。BlockingQueue有如下4个常用实现.
ArrayBlockingQueue
该类主要是通过ReentrantLock
来实现等待和通知的。 ReentrantLock有公平锁(FairSync
)和非公平锁(NonfairSync
,默认实现)两种实现,他们都是继承自AbstractQueuedSynchronizer
(AQS)的代码实现。 在AQS
中,首先试着抢锁,抢到了则执行下去;没抢到则CAS的方式放入阻塞队列,挂起当前线程(LockSupport.park(this);
),等待被唤醒,或者中断。 阻塞队列
的结构是一个双向链表。头节点和当前正在运行的节点不在其中。取消排队的节点不会第一时间主动移除。当前节点的waitStatus代表的是后一节点需要做的操作: CANCELLED(1)取消了争抢这个锁。 SIGNAL(-1)需要被唤醒。 ArrayBlockingQueue 通过它内部的ReentrantLock创建了两个Condition
(条件队列),notEmpty和notFull.在进入到条件队列时,都得获取到ReentrantLock的锁,才能继续操作。 在take()时,如果任务数=0,当前调度线程将放入notEmpty条件队列,完全释放锁,并进入阻塞状态,等待(put())唤醒或中断。 在put()时,如果任务数=max_size,任务线程户籍放入notFull条件队列,完全释放锁,并进入阻塞状态,等待(take())唤醒或中断。 唤醒条件队列中的线程后,会被放入阻塞队列
重新参与锁的竞争。
线程中断
中断代表着一个线程的状态、标识,是一个 true 或 false 的 boolean 值。与上文所说的线程七态
并不是同一概念。与操作系统中的线程中断也不是一个概念,只是修改了中断状态,而非切换了上下文。中断之后的操作(抛出异常、忽略或者别的什么),需自己制定。 常用的三种中断方法:
-
public boolean isInterrupted()
检测当前线程是否中断。 -
public static boolean interrupted()
返回中断状态的同时,会将标识重置为 false。 -
public void interrupt()
设置一个线程的中断状态为true. 中断需要我们自己编码去监控,如下代码所示:
while (!Thread.interrupted()) {
doWork();
System.out.println("我做完一件事了,准备做下一件,如果没有其他线程中断我的话");
}
复制代码
除了代码轮询监控外,处于如下4种情况的线程,能自动感知到被中断了:
- Object的wait()方法、Thread的join(),sleep()方法;中断后会抛出中断异常(InterruptedException)
- 实现了 InterruptibleChannel 接口的类中的一些 I/O 阻塞操作。中断后会抛出异常(ClosedByInterruptException)并重置中断状态。
- Selector 中的 select 方法。一旦中断,方法立即返回。
- 线程阻塞在 LockSupport.park(Object obj) 方法。唤醒后不会重置中断状态。
中断异常(InterruptedException)
通常的,带有 throws InterruptedException
的方法称为阻塞方法。如果希望它能早点返回的话,往往可以通过中断来实现。
处理中断
一般会有显示声明中断异常和没有中断异常两种方法。 没有声明中断异常的,在中断发生后不做任何处理,仅记录中断状态。 而声明中断异常的方法中,往往第一行代码便是检测是否中断。
if (Thread.interrupted())
throw new InterruptedException();
复制代码
LockSupport.park 线程挂起
park的实现是调用unsafe方法中的park(),unsafe中的方法大都是native的c++代码实现。 在虚拟机HotSpot中,每个java线程都有一个Parker的实例。用condition
和mutex
维护了一个_counter变量。park时,变量_counter置为0,unpark时,变量_counter置为1。
LinkedBlockingQueue
内部维护了一个链表队列,分别持有它的头、尾指针,使用AtomicInteger
计数。 读操作时,需要获取读锁ReentrantLock takeLock
,如果读取队列为空,此时需要等待Condition notEmpty
条件。如果读取之后的count>1,则通知挂起的读线程notEmpty.signal();
。最后,如果count == capacity(初始容量),而此时又消耗掉了一个元素,说明队列不满了,则唤醒写线程进行写入notFull.signal();
。写操作同理。 它与BlockingQueue
不同之处有三点:
-
LinkedBlockingQueue
内部维护了一个链表,而BlockingQueue
使用的是ReentrantLock
自带的数组结构。 -
LinkedBlockingQueue
中的读写队列分别持有不同的ReentrantLock
,BlockingQueue
则使用一个ReentrantLock
的两个条件队列。 -
BlockingQueue
中的take()和pull(),是两个条件队列相互唤醒。而LinkedBlockingQueue
中除了最后的相互唤醒之外,还可以‘自我’唤醒。
SynchronousQueue
SynchronousQueue
的最大特征是同步,当一个写线程向里面写入元素的时候,它不会立即返回,而是等待一个读线程来把元素直接取走。一个读线程匹配一个写线程。 它不提供任何空间(一个都没有)来存储元素。数据必须从某个写线程交给某个读线程,而不是写到某个队列中等待被消费。 其中最重要的交换代码是TransferQueue
的transfer()来实现的。
PriorityBlockingQueue
PriorityBlockingQueue
的两大特点是无界和优先级。 PriorityBlockingQueue
使用了基于数组的二叉堆来存放元素,所有的 public 方法采用同一个 lock 进行并发控制。 无界表现在堆可以用数组存储,因此可以很方便的动态扩容。 优先表现在堆的特性:从根节点到任意结点路径上所有的结点都是有序的。
堆
理解堆的三个关键点:
- 完全二叉树
- 任一结点的值是其左右子树的最大值
- 用数组实现
堆定义
class MaxHeap<T extends Comparable<T>> {
private List<T> list;
public MaxHeap(){
list = new ArrayList<>();
}
}
复制代码
堆的元素插入实现
public void put(T node){
list.add(node);
int nowIndex = list.size() -1;
int pIndex = nowIndex / 2;
while(list.get(pIndex).compareTo(node) <= 0 && nowIndex >= 1){
list.set(nowIndex,list.get(pIndex));
nowIndex = pIndex;
pIndex = nowIndex / 2;
}
list.set(nowIndex, node);
printList();
}
复制代码
插入的三大要点:
- 新插入的结点添加到数组最后
- 和其父结点比较大小,如果大于父结点,就用父结点替换当前位置,同时自己的位置上移。
- 直到父结点不再大于自己或者是位置已近到了数组第一个位置,就找到属于自己的位置了。
堆的删除实现
public void take(T node){
if(list.size() <= 0){
return;
}
int nIndex = list.indexOf(node);
list.set(nIndex, list.get(list.size()-1));
T nNode = list.get(nIndex);
int childIndex = nIndex *2;
while(childIndex <= list.size() -1){
// 先左右子节点比较,找到最大的,再和node比较。
if(childIndex != list.size() -1 && list.get(childIndex).compareTo(list.get(childIndex+1)) <0){
childIndex++;
}
// 当前节点比左右子节点都大,符合最大堆的定义,位置找到,退出循环。
if(nNode.compareTo(list.get(childIndex)) >=0){
break;
}else{
// 子节点替换父节点位置
list.set(nIndex, list.get(childIndex));
nIndex = childIndex;
childIndex = childIndex *2;
}
}
// 插入位置
list.set(nIndex, nNode);
// 移除最后的元素,因为最后的元素被移动到删除位置,然后经过比较,移动到了nIndex。
list.remove(list.size()-1);
printList();
}
复制代码
删除的四大要点:
- 找到要删除的结点在数组中的位置
- 用数组中最后一个元素替代这个位置的元素
- 当前位置和其左右子树比较,保证符合最大堆的结点间规则
- 删除最后一个元素