线程状态
我们首先介绍一下java中线程的几种状态,以及它们之间的转换关系。
- 新建状态(New) : 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
- 就绪状态(Runnable):也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
- 运行状态(Running) : 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
- 阻塞状态(Blocked) : 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
- 等待状态(Waiting) :通过调用Object.wait,Thread.join等,让线程等待某工作的完成。
- 超时等待(timed_waiting):通过 Thread.sleep,Object.wait(time),
Thread.join(time)等,让线程等待某工作完成一段时间。 - 结束状态(TERMINATED) :线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
线程同步和协作
接下来我们介绍java中常见的多线程之间进行同步和协作的关键字和方法,以及一些使用工具类。
线程同步-synchronized关键字
- 采用synchronized关键字实现的同步机制叫做互斥锁机制。
- 每个对象都自动含有单一的锁,也叫监视器(monitor),可以分为对象锁和类锁2种。
- 多个线程访问同一个锁上的代码块时,仅仅只有一个线程获得访问权限,其他线程将阻塞等待那个线程的同步代码段执行完成。
- 对象锁锁定的是某个对象,可以防止多个线程访问该对象中被synchronized保护的代码段。
- 类锁锁定的是整个类(其实是类的Class对象),该类所有对象被synchronized保护的代码段都会被保护。
- 被对象锁锁定的方法A可以被和类锁锁定的方法B同时被访问。
public class Test {
private Object mLock = new Object();
//对象锁,锁定testInstance
public synchronized void doTest1() {
}
//对象锁,锁定testInstance
public void doTest2() {
synchronized (this) {
}
}
//类锁,锁定Test.class
public synchronized static void doTest3() {
}
//类锁,锁定Test.class
public void doTest4() {
synchronized (Test.class) {
}
}
//对象锁,锁定mLock对象
public void doTest5(){
synchronized (mLock){
}
}
}
线程同步-Lock接口
- Lock接口在jdk1.5中,包含在java.util.concurrent被引入,作为一个类而不是语法特性对线程同步提供支持。
- 相比synchronized关键字,功能更强大和灵活,例如可以尝试获取锁,然后失败;或者尝试获取锁一段时间,然后放弃。并且可以自定义获取锁的策略等。
- 如果同步过程中发生了失败,可以在finally中做清理操作。
- 只有在解决特殊问题时,才需要考虑显示Lock对象。
Lock lock = new ReentrantLock();
//获取锁
lock.lock();
try {
// update object state
}
finally {
//这里释放锁
lock.unlock();
}
一段典型的使用显示Lock接口的代码如下,注意我们需要使用try,finally关键字包裹释放锁的代码lock.unlock。已保证在任何情况下都能正确的释放锁。
线程同步-原子类
Jdk1.5中引入了一些对基本数据类型int,long等进行原子操作的类,例如AtomicLong,AtomicInteger等,我们可以使用它来替代普通的int,long类型变量,它们是线程安全的。
线程副本-ThreadLocal类
- 该类为目标数据在每个线程中存储了一个副本,将实际数据保存在一个以当前Thread为key,值为目标数据的map中。
- 因为每个线程的数据其实存储的是一个单独的副本,因此不存在线程同步的问题。
- Android中的Looper就使用了该类,来为每个线程保存单独的一个Looper。
//创建ThreadLocal对象
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
//创建新的Looper并将它放入ThreadLocal,这样Looper就与当前线程相关联了
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
//获取与当前线程相关联的Looper
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
线程协作-wait,notify,notifyAll
- 这3个方法属于Object类而不是Thread类,因此任何java对象都可以使用。
- 调用这3个方法前必须拥有此对象的锁,也就是我们需要在synchronized代码块中调用。
- 调用wait方法后,当前线程释放锁并休眠,直到其他线程通过该对象的notify,notifyAll唤醒它或者超时为止。如果wait过程中当前线程被打断,则抛出异常。
- 调用notifyAll方法,将唤醒所有在此对象上wait的线程,但它不会释放锁。因此notify语句尽量放在同步语句块的最后。
- 一般情况下,应使用notifyAll,已避免某些线程得不到唤醒。
public class SingleObjTest{
private Object mObj;
//wait语句调用时,可能发生InterruptException
//wait语句需要在被synchronized包裹的代码块中使用,否则将抛出异常。
public synchronized void put(Object obj) throws InterruptedException {
//使用whille语句包裹判断条件,防止假唤醒,不能使用if语句,否则会出错。
//如果没有空间放入数据,我们就休眠等待
while(mObj!=null){
wait();
}
//现在有空间放入数据了,我们放入数据
mObj = obj;
//唤醒等待在该对象上的其他线程
notifyAll();
}
public synchronized Object get() throws InterruptedException {
//如果没有数据可供获取,我们就休眠等待
while(mObj==null){
wait();
}
//已经有新数据达到,我们获取数据
Object obj = mObj;
//将空间设置为空
mObj = null;
//唤醒等待在该对象上的其他线程
notifyAll();
return obj;
}
}
上面的代码我们借助wait和notify同步语句,实现了一个只有单个数据的生产者,消费者队列。当我们消费数据时,会一直休眠等待数据被其他线程生成出来为止;同理,我们要生成数据,必须一直休眠等待直到旧的数据被消费掉。
线程协作-Condition接口
- Jdk1.5中,引入了Condition接口来替代Object类的wait,notify,notifyAll方法。
- 使用await替代wait,signal代替notify, signalAll代替notifyAll
- Condition需要和之前线程同步时提到的Lock接口绑定。
- Condition对于同一个锁,可以设置不同条件
public class SingleObjTest {
private Object mObj;
//创建lock对象
private Lock mLock = new ReentrantLock();
//创建put条件,需要和lock对象关联
private Condition mPutCondition = mLock.newCondition();
//创建get条件,需要和lock对象关联
private Condition mGetCondition = mLock.newCondition();
public void put(Object obj) throws InterruptedException {
//为同步代码段加锁
mLock.lock();
try{
//我们在put条件上等待
while(mObj!=null){
mPutCondition.await();
}
mObj = obj;
//仅仅通知等待在get条件上的线程
mGetCondition.signalAll();
}
//在finally中释放锁,保证任何情况下都能释放
finally {
mLock.unlock();
}
}
public Object get() throws InterruptedException {
mLock.lock();
try {
while (mObj == null) {
mGetCondition.await();
}
Object obj = mObj;
mObj = null;
mPutCondition.signalAll();
return obj;
}
finally {
mLock.unlock();
}
}
}
这段代码的功能和之前使用wait,notify,notifyAll的代码相同,也是一个只有一个元素的生产者消费者队列,只不过我们采用了Condition接口来实现它。
我们用下面的图片来说明,为什么wait,notify语句使用时,wait条件判断一般使用while循环而不是if语句。
如图所示,我们现在产生了3个消费者线程,因为我们的mObj变量初始为空,因此这3个消费者线程,都将自己挂起。这时我们的生产者线程产生了一个数据,它会唤醒所有等待在该对象上的线程,也就是3个消费者线程,第一个获取到锁的消费者线程表现正常,然而第二个消费者获取到锁时,mObj已经被第一个消费者消费了,因此它会返回空,也就是发生了错误,这就是我们需要使用while循环判断条件的原因,使用while循环判断,第二个消费者发现mObj为空,则它会再次调用wait释放锁并陷入休眠中。
再说说我们为什么推荐使用notifyAll而不是notify呢,因为notify只是随机唤醒一个等待在该object上的线程,也就是有可能生产者唤醒的是生产者线程,而消费者线程没有被唤醒,这样会产生错误,因此我们推荐使用notifyAll唤醒所有等待线程。
而我们使用Condition接口,则可以为生产者和消费者建立起2个不同的条件,即mGetCondition和mPutCondiion,这样我们的生产者就只在mPutCondition上等待,也仅仅只唤醒消费者线程了。
线程协作-CountDownLatch类
- Jdk1.5中引入的一个同步辅助类,用来同步一个或多个任务,强制它们等待其他任务执行的一组操作完成。
- 使用构造方法为它设置一个初始计数值,任何在该对象上调用的await方法都将被阻塞直到计数值到达0。
- 其他任务结束工作时,可以在该对象上调用countDown方法减少计数值。
- 该类被设计为只能触发一次,计数值不能重置。
public class CountDownTest {
//初始化一个CountDownLatch,它的计数值为2
static CountDownLatch c = new CountDownLatch(2);
public static void main(String[] args) throws InterruptedException {
//开启一个子线程来执行第一个任务
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("do first thing");
Thread.sleep(1000);
//子线程任务执行完成,减少计数值
c.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//执行第二个任务的子线程
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("do second thing");
Thread.sleep(2000);
//子线程任务执行完成,减少计数值
c.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
System.out.println("wait countDown");
//主线程在此等待计数值降低到0,也就是2个子
//线程的任务都执行完成,在执行自己的操作
c.await();
System.out.println("count down finish");
}
}
上面就是一小段CountDownLatch类的使用示例,主线程创建了一个计数值为2的Latch对象,并开启2个子线程执行任务,然后就等待2个子线程的任务执行完成。最后再执行自己的任务。
线程协作- CyclicBarrier类
- 该类同样是jdk1.5引入,用法类似于CountDownLatch类
- CountDownLatch类强调的是一个或多个任务等待另一组任务完成。而CyclicBarrier类强调的是多个任务互相等待,直到所有任务都完成。
- CountDownLatch只能够使用一次,而CyclicBarrier类则可以使用reset方法重新初始化。
public class CyclicBarrierTest {
//初始化一个CyclicBarrier,它的计数值为2
static CyclicBarrier c = new CyclicBarrier(2);
public static void main(String[] args) throws InterruptedException {
//开启一个子线程来执行第一个任务
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("do first thing");
Thread.sleep(1000);
//等待所有任务执行完成
c.await();
System.out.println("first thing finish");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
//执行第二个任务的子线程
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("do second thing");
Thread.sleep(2000);
//等待所有任务执行完成
c.await();
System.out.println("second thing finish");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
System.out.println("do main");
}
}
上面的代码段展示了CyclicBarrier的基本用法,可以看到调用await方法的线程都会被阻塞住,直到所有线程的任务都完成为止。
生产者消费者模式和阻塞队列
下面我们来介绍多线程编程中经常使用的生产者消费者模式和阻塞队列,生产者消费者模式和阻塞队列在android的开发中都有着广泛的应用。
生产者消费者模式
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题,该模式通过平衡生产线程和消费线程的工作能力来提高程序整体处理数据的速度。
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这种生产消费能力不均衡的问题,便有了生产者和消费者模式。
参照上面生产者,消费者模式的示意图,我们来说说该模式的几个优点。
- 解耦,如果让生产者直接调用消费者的某个方法,那 么生产者对于消费者就会耦合,将来如果消费者的代码发生变化, 可能会影响到生产者。而采用该模式各个生产者和消费者仅仅依赖于阻塞队列,而它们彼此之间没有任何依赖。
- 支持并发,生产者生产出数据后只需要将数据放入阻塞队列,即可继续生产下一个数据,而不需等待消费者消费,同样,消费者也仅仅是从阻塞队列中获取数据并处理。至于中间的数据同步,队列满时生产者阻塞,队列空时消费者阻塞等过程,则都由阻塞队列实现。
- 平衡生产消费速度,比如生产数据的速度时快时慢,缓冲区的好处就体现出来 了,当生产数据快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。 等生产者的制造速度慢下来,消费者再慢慢处理掉。
public static class BlockingQueue<E>{
//存储数据的队列
private Queue<E> mQueue;
//队列的最大大小
private final int MAX_SIZE;
//同步锁
private Lock mLock;
//队列非空的Condition
private Condition mNotEmptyCondition;
//队列非满的Condition
private Condition mNotFullCondition;
public BlockingQueue(int maxSize){
MAX_SIZE = maxSize;
//初始化队列
mQueue = new ArrayDeque<>(MAX_SIZE);
//初始化锁和队列非空,非满2个条件
mLock = new ReentrantLock();
mNotEmptyCondition = mLock.newCondition();
mNotFullCondition = mLock.newCondition();
}
public void put(E e) throws InterruptedException {
//获取锁
mLock.lock();
try{
//循环检测队列已满,防止假唤醒
while(mQueue.size()>MAX_SIZE){
//在队列非满条件上等待(该条件由消费者触发)
mNotFullCondition.await();
}
//队列未满,则将当前数据放入队列
mQueue.add(e);
//唤醒等待在队列非空条件上的线程(消费者线程)
mNotEmptyCondition.signalAll();
}
finally {
//在finally中释放锁,保证锁一定会被释放
mLock.unlock();
}
}
public E take() throws InterruptedException {
//获取锁
mLock.lock();
try{
//循环检测队列为空,防止假唤醒
while(mQueue.isEmpty()){
//在队列非空条件上等待(该条件由生产者触发)
mNotEmptyCondition.await();
}
//队列非空,从队列中取出数据
E e = mQueue.poll();
//唤醒所有等待在队列非满条件上的线程(生产者线程)
mNotFullCondition.signalAll();
return e;
}finally {
//在finally中释放锁,保证锁在任何情况下得到释放
mLock.unlock();
}
}
}
上面的代码使用我们之前介绍的Lock和Condition接口实现了一个生产者,消费者模式的阻塞队列,可以看到核心逻辑并不复杂,只要我们熟悉了之前介绍的线程之间协作的机制,就可以自己利用java提供的wait,nofity或者Lock,Condition实现一个生产者消费者队列。
我们这里为什么采用Lock,Condition接口而不是wait,notify呢,这就是我们之前说的Lock,Condition比wait,notify更灵活的原因。使用Lock,Condition我们可以为生产者线程和消费者线程建立不同的条件,这样消费者线程执行完成只唤醒等待的生产者线程,同样,生产者线程执行完成只唤醒等待的消费者线程。而wait,notify方法实现的话,生产者线程将会唤醒所有生产,消费者线程,不够灵活。
阻塞队列-BlockingQueue
上面我们介绍了生产者消费者模式的应用,以及我们自己如何实现一个生产者消费者模式的阻塞队列的示例。其实jdk中早就已经存在这样的轮子了,那就是BlockingQueue接口。
BlockingQueue的父接口是Queue,它定义了一个先进先出(FIFO)队列的基本操作,所有的插入操作都在队列尾部,所有的删除操作都在队列头部,这样就可以保证队列的FIFO特性了。而BlockingQueue接口则进一步定义了2组阻塞的出入队操作。各个主要的操作见下表,标记为Block的红色列表示BlockingQueue增加的阻塞操作。
抛出异常 | 特殊值 | 阻塞(Block) | 超时(Block) | |
---|---|---|---|---|
插入 | add(E) | offer(E) | put(E) | offer(E,time,unit) |
移除 | remove | poll | take | poll(time,unit) |
检查 | element | peek | 不可用 | 不可用 |
BlockingQueue接口有4个常见的实现类,大家可以根据需求选择其中的一个实现,在线程池的实现中就会根据池子类型的不同选择不同的BlockingQueue。
- ArrayBlockingQueue:其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的。
- LinkedBlockingQueue:若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的。
- PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序。
- SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的,相当于阻塞队列的大小为1。
其他多线程中常用方法
- leep方法,使当前线程睡眠一段时间,让其他线程有机会执行,但是它不会释放同步锁,也就是说如果有synchronized同步块,则其他线程依然不能访问其中的数据。Sleep方法可以给低优先级的线程运行机会,并且执行过程中可能抛出InterruptedException异常。
- Yield方法,放弃线程自身的CPU控制权,给优先级相同或更高的线程运行机会。
- Join方法,t.join()方法阻塞调用此方法的线程(calling thread),直到线程t完成,此线程再继续执行。
线程复用
对于线程的创建和基本使用方法,想必大家都已经烂熟于心了吧,示例代码如下,创建一个Thread对象,传递一个Runnable接口,在其中书写我们需要的逻辑,最后调用start方法启动线程即可。
new Thread(new Runnable() {
@Override
public void run() {
//这这里做我们的事情
}
}).start();
线程的基本用法固然简单,不过我们也知道,创建和销毁线程是操作系统才有的权限,并且也会带来一定的系统消耗,因此频繁的创建和销毁线程显示是不经济的,那么我们的线程是否能够重用呢,机智的你做为一个老司机,表示毫无压力,这简单,直接用线程池嘛,线程管理我们都用它,方便又快捷。示例代码如下:
Executor exec = Executors.newFixedThreadPool(3);
exec.execute(new Runnable() {
@Override
public void run() {
//这里做我们的事情
}
});
线程池使用方便,大多数情况下我们使用它管理线程也已经足够,但是某些特殊的情况下,我们可能也会需要自己管理线程,因此我们还是需要知道一下如何能够达到复用线程的目的。
我们知道,一个线程被创建出来,是NEW状态,我们调用start方法后,处于Runnable(可执行)状态,等到某个时刻该线程被CPU选中,处于RUNNING状态时,会执行我们的run方法,run方法执行完成之后,线程也就会退出,进入最终的TERMINATED状态了。这样我们每执行一个任务就必须创建一个线程,重复以上过程。
那么我们有什么办法让线程不死亡呢,其实也很简单,那就是让run方法处于一个循环中,不直接结束不就好了,代码示例如下:
new Thread(new Runnable() {
@Override
public void run() {
while(未达到我们的退出条件){
//在这里做我们需要的事情
}
}
}).start();
这里解决了我们线程执行完成直接退出的问题,不过看起来好像并没有作用,毕竟它只能做一件事情,而我们想要复用的线程是想要它可以持续不断的做任何事情的,那怎么办呢,这个时候我们之前学习的生产者消费者模式和阻塞队列就派上用场了。
new Thread(new Runnable() {
@Override
public void run() {
while(未达到我们的退出条件){
//获取阻塞队列,这个阻塞队列连接生产者消费者线程
//这样其他线程包括ui线程就可以本线程下达不同的指令了
BlockingQueue<Object> queue = mQueue;
try {
//该线程作为阻塞队列的消费者,等待生产者产生事件
//如果没有事件则阻塞
Object obj = queue.take();
//我们在这里根据事件的不同类型,作为不同处理
//这样就达到了线程复用的目的
} catch (InterruptedException e) {
}
}
}
}).start();
这样该线程启动起来之后,可以根据外界给它的指令不断的执行操作,如果所有指令执行完成则陷入休眠等待。直到有新的指令到来或者需要退出为止。
这个看似简单的线程循环加上生产者消费者模式的示例,其实就是线程池的基本原理,线程池的源码我们以后分析。
另外该示例其实还展示了绝大多数的操作系统的运行原理,我们可以把它叫做信息机制,android,window等现代操作系统的运行都建立在信息机制的基础上。
比如android系统的主线程同样是一个信息循环加上信息队列,我们可以看一下android主线程所在的类ActivityThread的main方法中,利用android的handler,Looper,MessageQueue机制,同样建立了信息循环和信息队列。虽然不是使用java的阻塞队列,而是android自己的一套机制,但是原理上还是使用了一个循环的线程加上生产者消费者模式建立了整个android的运行环境。
public static void main(String[] args) {
//进入循环前的准备工作
//准备信息循环
Looper.prepareMainLooper();
//其他准备工作
//开始信息循环
Looper.loop();
//这里可以看出,主线程的循环实际上永远不会结束
throw new RuntimeException("Main thread loop unexpectedly exited");
}
这里的生产者是各个系统服务,例如ams要求启动停止活动,WMS要求调整和重绘窗口,IMS传回的各种输入事件,都通过信息队列交给android应用程序主线程处理,另外主线程自身也会向队列中插入一些事件,以便自己之后处理。而消费者就是主线程本身,它会循环从信息队列中接收信息,进行处理。这是整个android系统运行的源泉。
interrupt方法和线程退出
上面我们学习了线程复用的方法,现在我们在来学习一下如何安全的退出线程,停止线程的stop方法因为不安全所以被废弃了,java也强烈不推荐使用,因此我们不考虑它的使用。
那么我们应该怎样终止一个正在运行中的线程呢,一般我们会使用interrupt方法,让我们来看看interrupt方法的究竟在干什么,这个方法在这4种状态下,分别有不同的表现
- 线程没有阻塞,则线程线程的中断状态(interrupted status) 会被置位。我们可以通过Thread.currentThread().isInterrupted() 来检查这个布尔型的中断状态。
- 如果线程堵塞在object.wait,Thread.join和Thread.sleep方法,将会抛出InterruptedException,同时清除线程的中断状态。
- 如果线程堵塞在java.nio.channels.InterruptibleChannel的IO上,Channel将会被关闭,线程被置为中断状态,并抛出java.nio.channels.ClosedByInterruptException。
- 如果线程堵塞在java.nio.channels.Selector上,线程被置为中断状态,select方法会马上返回,类似调用wakeup的效果。
我们没有办法强制终止一个正在运行中的线程,也不推荐这么做。我们一般调用方法建议线程结束自身,至于线程是否结束,以及怎样结束,由线程自身处理。注意interrupt方法并不具备任何强制结束线程的能力,下面是一个退出线程的示例。主要用到了interrupt方法和isInterrupted方法。
我们使用Thread.currentThread.isInterrupted在while循环中判断线程是否被中断,如果被中断就直接退出,这对应我们上面说的interrupt方法调用时,线程不阻塞的情况。
如果线程在被外部调用interrupt方法时被阻塞在wait或者sleep等方法时,那么就会发生InterruptedException,这个时候我们捕获该异常,可以在这里退出线程循环。
注意,如果我们的线程正在执行一个耗时的计算或者一个从网络或者IO读写数据的费时操作,这时线程外部调用interrupt方法,并不能终止这个耗时操作,线程会继续运行直到该耗时操作完成才推出循环。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//如果线程被中断,则我们退出循环
while(Thread.currentThread().isInterrupted() && 其他条件){
try {
//实际处理逻辑,我们可以在必要的地方检测isInterrupted
} catch (InterruptedException e) {
//线程在sleep,wait等方法上阻塞时,
//被外部中断则会抛出该异常
}
}
}
}).start();
//其它线程中断该线程
thread.interrupt();