上篇文章讲到并发问题的由来,并发世界中的几个容易混淆的概念,并发级别以及衡量并发性能的两个定律。这篇博文将解决三个问题:
1. 线程和进程的区别?为什么调度线程而不调度进程?
2. 调度的线程在代码中是如何创建的?如何终止的?如何阻塞的?
3. 线程的类别和优先级?
我们经常说的“高并发”是我们要达到的目的,即系统的高可用、高响应、高吞吐量等。而实现“高并发”最底层的途径就是并发编程,并发编程简单理解就是通过编码使得多个线程正确运行。
用过电脑的人都打开过Windows的任务管理器,管理器界面上的Tab页会出现“应用程序”、“进程”、“服务”、“性能”等;“应用程序”是我们打开的一个程序,比如打开QQ,打开word文档,打开chrome等,每一个应用都是一个程序,这些都是我们看得见的程序,电脑运行还有很多我们开不见的程序,比如系统的运行、杀毒软件的运行、代理的运行等,这些都显示在“进程”中。
那么进程和线程之间有什么联系?
我们知道,“进程”是暴露给用户的,让用户知道这个程序的运行情况,比如进程的名称,状态,作用(描述),端口等,是指整个程序的运行状况;而程序(系统)是由线程来执行的,即程序的功能实现则由线程来操作;高并发也就是程序的各个功能能够快速且稳定的响应用户的操作,每个功能通过独立的线程来完成,进而造就高并发系统。举个例子理解进程和线程:一个公司的运作可以比作为一个进程的执行,公司内各个部门,如财务部门,人事部门,业务部门等组成进程,他们的运作才支撑起公司的运作,各个部门则可以理解为各个线程,每个线程用于完成不同的工作,进而使得进程能正确的运转;即线程组成进程,进程囊括线程,是一个组成和被组成的关系。
用术语来讲就是,进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。线程就是轻量级进程,是程序执行的最小单位。
使用多线程而不是用多进程去进行并发程序的设计,是因为线程间的切换和调度的成本远远小于进程。
进程的状态有两种:已停止和正在运行;由于线程的粒度较细,因此线程的状态有多种:创建,运行,阻塞,等待和结束。(生命周期)
线程的各种状态
2.1 创建并启动
java是面向对象的语言,所以它把任何操作的实体抽象成一个对象,所执行的动作抽象成一个方法;java语言中用Thread类来表示线程,创建线程就是new一个对象:
Thread thread = new Thread(); // 不带参数默认名字为Thread-n
Thread thread = new Thread(“t1”); // t1 表示线程的名字
java语言中通过.来调用对象及其方法,线程类创建完毕需要通过start()方法来启动线程;即
thread.start();
才能表示启动了一个线程;完整实例如下:
public static void main(String[] args) {
Thread thread = new Thread();
thread.start();
System.out.println(thread.getName());
Thread thread2 = new Thread("t2");
thread2.start();
System.out.println(thread2.getName()); // 打印线程名字
System.out.println(Thread.currentThread().getName()); // 主线程
}
结果:
Thread-0
t2
main
这里表示创建并启动了一个线程。
new Thread()过程,实际调用init()方法
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
init(g, target, name, stackSize, null, true);
}
通过上面的方法可知;
->init方法为私有方法,只能在Thread类内部调用,保证Thread类构造的安全性;
-> Thread类暴露给程序员的构造方法有很多种,以便满足不同场景下的需要;但实际调用的方法则是同一个方法;
->构造线程要给线程赋予名字name,组别threadGroup,运行内容target,栈堆大小stackSize,其中第五、六个参数分别表示保证线程安全的对象、是否为本地线程赋值来自构造函数的初始值(涉及到ThreadLocal,默认为true);
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// ①首先确定线程的名字,要么构造传进来,要么使用默认的名字
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
// ② 确认线程的组别 要么构造传进来 要么归属于安全框架设置的线程组 要么归属于当前线程的线程组;因为线程分级别,所以需要确定线程组;
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();
if (g == null) {
if (security != null) {
g = security.getThreadGroup();
}
if (g == null) {
g = parent.getThreadGroup();
}
}
// 确保正在运行的线程(主线程)是否具有修改该线程组的权限
g.checkAccess();
// ③ 如果存在安全对象,则还需要判断线程的相关运行时权限
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
// ④ 线程组中未启动线程数加1
g.addUnstarted();
// ⑤ 判断是否是守护线程,得到线程的优先级
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
// ⑥ 确定对象上下文加载器(来自运行线程的加载器)
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
// ⑦ 获取当前调用上下文的快照,并将其至于AccessControllerContext对象中
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
// ⑧ 运行内容和设置优先级
this.target = target;
setPriority(priority);
// ⑨ 设置当前线程的私有局部变量ThreadLocal
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// ⑩设置线程的堆栈大小,初始构造默认为0; 设置线程id
this.stackSize = stackSize;
tid = nextThreadID();
}
.start()启动线程如下:
public synchronized void start() {
/**
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
start方法实则调用本地方法start0():
// 实则为本地方法,用c或者c++实现;
private native void start0();
经过断点调试,start0()方法实则调用Thread类的run()方法,而run()方法调用target对象的run()方法
。若在初始化过程中没有指定target对象,则为空不执行run(),会立即执行exit()方法结束线程;
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
exist()方法意在清除掉该线程的所有变量或者引用。
到这里创建并启动了一个线程,但是线程的运行还没有开始。运行run()方法是调用Runnable类的run方法,Runnable类是一个接口,提供一个抽象run方法供实体类重写。其实Thread类本身也继承Runnable接口,Thread类本身也可以实现run方法,如下:
Thread thread = new Thread(){
@Override
public void run(){
System.out.println(" I am thread");
}
};
但是项目中都会定制线程,可以通过继承Thread类来自定义线程,重载run()方法执行想要执行的内容;但考虑到java语言是单继承的,继承特性比较宝贵,因此一般通过实现Runnable接口和Callable接口来定制化线程
;实例如下:
public class CreateThread {
public static void main(String[] args) {
ThreadCallable thread = new ThreadCallable();
try {
thread.call();
} catch (Exception e) {
e.printStackTrace();
}
Thread thread2 = new Thread(new ThreadRunnable(), "t2");
thread2.start();
System.out.println(Thread.currentThread().getName());
}
}
class ThreadRunnable implements Runnable{
@Override
public void run() {
System.out.println("I am ThreadRunnable");
}
}
class ThreadCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
System.out.println("I am ThreadCallable");
return null;
}
}
观察上面代码可能会有以下几个问题:
1. Callable接口的call()和Runnable接口的run()有什么区别?
2. 实现call()方法的类对象为何不需要调用start()就能直接运行线程?
首先Callable和Runnable接口都是实现线程功能的接口,唯一区别就是Callable接口的call()可以返回线程执行的结果且能抛出异常,而Runnable接口的run()则不返回任何内容;可以说两者在功能上互补。
然后,Callable类型的对象,直接调用call()就可运行线程;而Runnable类型的对象,则需要调用start()启动线程;通过call()的源码注释可知,call是放在Executor中执行,由执行器来调用,run()则依赖start()方法。(为什么要这样设计呢?)
2.2 停止线程
stop()是Thread类中定义的停止线程的方法,但stop方法太过暴力,在调用时直接停掉线程,可能会造成数据不一致性问题,因此已经被废弃掉。在实际开发中,不会单个的创建线程,而是由线程池来创建并管理线程;上面的例子中显示,线程执行完毕后会自定执行exist方法停止线程。
private void exit() {
if (group != null) {
group.threadTerminated(this);
group = null;
}
/* Aggressively null out all reference fields: see bug 4006245 */
target = null;
/* Speed the release of some of these resources */
threadLocals = null;
inheritableThreadLocals = null;
inheritedAccessControlContext = null;
blocker = null;
uncaughtExceptionHandler = null;
}
如果由线程池来管理线程,则是另外一种情况。
2.3 线程阻塞
线程阻塞是暂停线程的一种方法,关于线程中断有以下三个方法:
public void interrupt(); // 中断线程
public static boolean interrupted(); // 判断当前线程是否被中断,并清除中断状态
public boolean isInterrupted(); // 判断当前线程是否被中断
实例:
public class InterruptTest {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("my name is " + Thread.currentThread().getName());
if (Thread.currentThread().isInterrupted()) {
System.out.println("i have interrupted");
break;
}
}
}
});
thread.start();
thread.interrupt();
}
}
大意:启动一个线程,循环打印当前线程的名字;在主线程中启动thread线程然后调用interrupt()方法中断线程thread,当thread线程检测到自身被中断了则退出循环。
interrupt()
public void interrupt() {
// ① 调用该方法是否是线程本身,如果不是则进行安全检查;checkAccess()可能会抛出一个异常
if (this != Thread.currentThread())
checkAccess();
// ②blockerLock是一个object类型的对象,利用内置锁来保证中断的同步性
synchronized (blockerLock) {
Interruptible b = blocker; // IO操作过程的中断标志
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
// ③通过native方法实现线程中断
interrupt0();
}
isInterrupted()方法也是native方法;
interrupted()方法调用native方法;
在中断线程后,一般要在线程内部对中断后的线程进行响应。
2.4 线程等待
为了支持线程之间的协作,JDK在对象上设置了wait()和notify();在线程A中调用对象的wait()可使得该线程在该对象上等待,当线程B调用同一对象的notify(),则线程A等待结束,继续执行。
wait和notify为什么是对象的两个方法,而不是线程?这是因为对象有内置锁,通过内置锁使得线程等待。
public class WaitTest {
private static Object object = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ": t1 start!");
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("notify...");
System.out.println(System.currentTimeMillis() + ": t1 end!");
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println(System.currentTimeMillis() + ": t2 start!");
System.out.println("wait...");
object.notify();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis() + ": t2 end!");
}
}
});
t1.start();
t2.start();
}
}
结果:
1538915095285: t1 start!
1538915095285: t2 start!
wait...
1538915097285: t2 end!
notify...
1538915097285: t1 end!
从代码中可以看出,要想调用object对象的wait和notify方法,需要首先获得object对象的锁;因此线程在object对象上等待时,会释放object对象的内置锁。这点和Thread类的sleep方法不一样,sleep方法让线程睡眠一定时间,睡眠期间不能做任何事情。
wait和notify方法都属于native方法。
2.5 线程的加入和谦让
在线程执行过程中,有些线程依赖其他线程的结果,因此需要等待其他线程执行完成才执行本线程。JDK提供的join方法就可以实现这样的功能;
public class JoinTest {
private static volatile int i = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (i = 0; i < 1000000; i++) ;
}
});
t1.start();
t1.join();
System.out.println(i);
}
}
join方法有很多个:
join(); // 无限等待
join(long); // 等待一定时长
join(long, int); // 等待一定时长,int参数为纳秒
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
源码中的核心代码是wait(0),可知join方法本质是让调用线程wait在当前线程对象实例上;当被等待线程全部执行完毕,会notify等待线程继续执行。
yield()方法是一种谦让的方法,意思是尽量不会参与CPU的竞争,是native本地方法。
参考文献
《实战java高并发程序设计》