一、线程创建的基本知识
1.创建线程
创建线程有两种方式:继承Thread或实现Runnable。
Thread实现了Runnable接口,提供了一个空的run()方法,所以不论是继承Thread还是实现Runnable,都要有自己的run()方法。
一个线程创建后就存在,调用start()方法就开始运行,调用wait进入等待或调用sleep进入休眠期,顺利运行完毕或休眠被中断或运行过程中出现异常而退出销毁。
2.线程的生命周期
当线程被创建之后,不是一启动就进入执行状态,也不是一直处于执行状态,
在其生命周期中,要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)五种状态。
线程在创建之后,不可能一直霸占着CPU独立运行,需要在多个线程之间切换,所以大部分时间处于运行、阻塞之间切换。
3.线程间的状态转换
新建状态(New) 新创建了一个线程对象。
就绪状态(Runnable) 线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
运行状态(Running) 就绪状态的线程获取了CPU,执行程序代码。
阻塞状态(Blocked) 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
4.Jstack中常见的线程状态
Jstack的输出中,Java线程状态主要是以下几种:
- RUNNABLE 线程运行中或I/O等待
- BLOCKED 线程在等待monitor锁(synchronized关键字)
- TIMED_WAITING 线程在等待唤醒,但设置了时限
- WAITING 线程在无限等待唤醒
这里Jstack使用的关键字描述的线程状态上面的线程状态不太一样,所以可能理解上的可能会出现混淆。实际上虽然Java中的线程有5种状态,但在实际情况下线程新建状态和死亡状态持续很短,我们也并不太关心。大多时候我们关注的是运行状态/阻塞状态,这里弄清楚Jstack的输出含义即可。
下面用简单的代码产生出以上4种状态。
public static void main(String[] args) { System.out.println(Utils.pid()); runnable(); // 1 // blocked(); // 2 // waiting(); // 3 // timedWaiting(); // 4 } public static String pid() { String name = ManagementFactory.getRuntimeMXBean().getName(); return name.split("@")[0]; }
这里为了方便得到java进程id,直接使用pid()函数输出。为了方便,我们把观察线程固定为”main”,因为JVM还有其他线程都会存在输出中。
我们可以通过关键字”main”找到我们要观察的线程,命令jstack -l [pid]。
1) 让线程一直处于RUNNABLE
public static void runnable() { long i = 0; while (true) { i++; } }
没什么好解释的,死循环即可。
2) 让线程一直处于BLOCKED
public static void blocked() { final Object lock = new Object(); new Thread() { public void run() { synchronized (lock) { System.out.println("i got lock, but don't release"); try { Thread.sleep(1000L * 1000); } catch (InterruptedException e) { } } } }.start(); try { Thread.sleep(100); } catch (InterruptedException e) {} synchronized (lock) { try { Thread.sleep(30 * 1000); } catch (InterruptedException e) { } } }
主线程sleep,先让另外一个线程拿到lock,并长期持有lock(sleep会持有锁,wait不会)。此时主线程会BLOCK住等待lock被释放,此时jstack的输出可以看到main线程状态是BLOCKED。这里要注意的是只有synchronized这种方式的锁(monitor锁)才会让线程出现BLOCKED状态,等待ReentrantLock则不会。
3) 让线程处于TIMED_WAITING状态
public static void timedWaiting() { final Object lock = new Object(); synchronized (lock) { try { lock.wait(30 * 1000); } catch (InterruptedException e) { } } }
用Lock.tryLock(timeout, timeUnit),这种方式也会看到TIMED_WAITING状态,这个状态说明线程当前的等待一定是可超时的。
4) 让线程处于WAITING状态
public static void waiting() { final Object lock = new Object(); synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { } } }
无超时的等待,必须等待lock.notify()或lock.notifyAll()或接收到interrupt信号才能退出等待状态。同理,ReentrantLock.lock()的无参方法调用,也会使线程状态变成WAITING。
二、使用线程池的意义
1.线程复用
使用线程池技术可以实现工作线程的复用,即一个工作线程创建和销毁的生命周期期间内可以执行处理多个任务,从而总体上降低线程创建和销毁的频率和时间,提升了系统性能。
2.并发控制,平滑高峰请求
线程池中的任务队列起到了一个缓冲的作用,服务器资源有限,超过服务器性能的过高并发设置会成为系统的负担,造成CPU大量耗费于上下文切换,可能会产生内存溢出等后果。
通过线程池技术可以控制系统最大并发数和最大处理任务量,从而很好的实现流量控制,保证系统不至于崩溃。
三、线程池的应用和设计
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
1.启动线程完成任务所需的时间
服务器启动一个线程完成一项任务所需时间可以分为三个部分:
- 创建线程时间T1
- 在线程中执行任务的时间T2
- 销毁线程时间为T3
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。
它把T1,T3分别安排在服务
器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有T1.T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且还可以显著减少创建线程的数目。
2.线程池组成部分
一个线程池包括以下四个基本组成部分:
- 线程池管理器(ThreadPool):
用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
- 工作线程(PoolWorker):
我们把用来执行用户任务的线程称为工作线程,工作线程就是不断从队列中获取任务对象并执行对象上的业务方法。
线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
- 任务接口(Task):
每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
- 任务队列(taskQueue):
用于存放没有处理的任务。提供一种缓冲机制。