线程的基本概念
什么是线程
现代操作系统在运行一个程序的时候,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。线代操作系统调度的最小单位是线程。也叫做轻量级进程。在一个进程里可以创建多个线程,这些线程都拥有自己的程序计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉这些线程在同时执行。进程是资源分配的基本单位,线程时系统调用的基本单位。
实际上Java本身就是多线程程序,因为执行main方法的时候就是一个main线程,其实平时一个简单的main方法执行中,有负责分发处理给JVM的线程,也有清除对象引用的线程,还有调用对象的finalize方法的线程等。
线程的优先级
现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干个时间片,当线程时间片用完后,就会发生线程调度,并等待着下次分配。线程分配到的时间片多少也决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要多分配或者少分配处理器资源的线程属性。
来看一下Thread源码中关于线程优先级的定义。
private int priority;
//最低优先级是 1
public final static int MIN_PRIORITY = 1;
/**
* 默认的线程优先级是 5
*/
public final static int NORM_PRIORITY = 5;
/**
* 线程最高优先级是 10
*/
public final static int MAX_PRIORITY = 10;
在Java线程中,通过一个整形变量priority来控制优先级,优先级的范围从1~10,在下次构建的时候可以通过 setPriority方法来修改线程优先级,默认优先级是5,最高优先级是10,优先级高的线程分配的时间片的数量要多余优先级低的。
设置优先级时,如果是频繁阻塞的线程需要设置较高的优先级,而偏重于计算的线程需要设置较高的优先级,以确保处理器不会被抢占。
线程的状态
这里从Thread源码的角度说一下线程拥有哪些状态。
public enum State {
/**
* 初始状态,线程被构建 ,线程还没有开启start方法
*/
NEW,
/**
* 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统的称为“运行中”
*/
RUNNABLE,
/**
*阻塞状态,表示线程阻塞于锁
*/
BLOCKED,
/**
* 等待状态,表示线程进入等待状态进入该状态,表示当前线程需要等待其他线程做出通知或者中断
*以下方法会是一个线程进入等待状态
*<ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </u>
*/
WAITING,
/**
* 超时等待状态,它可以在指定时间内自行返回
* 以下是会造成 超时等待的方法
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,
/**
* 终止状态,表示当前线程已经执行完毕
*/
TERMINATED;
}
在给定的一个时刻,线程只能处于其中的一种状态。
从上图中可以看出,线程创建后,调用start()方法开始运行。当线程执行wait方法之后,线程进入等待状态。进入等待状态的线程需要依靠其他线程的通知才能返回到运行状态。而超时状态相当于在等待状态的基础上增加了超时限制,也就是超时时间到达时候会自动返回运行态。当线程调用同步方法是,在没有获取到锁的情况下,线程就会进入到阻塞状态。当线程执行完run方法后就代表这个线程执行完毕进入终止态。
守护线程
守护线程(Daemon线程)是一种支持型线程,它主要被用于程序中后台调度以及支持性工作。当虚拟机中不存在非Daemon线程时,Java虚拟机就会自动退出。
可以通过Thread.setDaemon(true)
将线程设置成守护线程。
举个例子
public class DaemonThread {
public static void main(String[] args) {
Thread daemonRunner = new Thread(new DaemonRunner(), "DaemonRunner");
daemonRunner.setDaemon(true);
daemonRunner.start();
}
static class DaemonRunner implements Runnable{
@Override
public void run() {
try{
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("DaemonThread finally run!");
}
}
}
}
运行main方法,发现控制台什么都没输出,当main线程启动了我们设置的守护线程,main方法执行完毕后终止,Java虚拟机中已经没有非守护线程,JVM退出。所以守护线程需要立即终止,因此守护线程中的finally代码块并没有执行。
使用守护进程的时候,尽量不要分配读写文件之类的任务给守护进程,因为你不知道在用户进程执行完程序之前,守护进程是否可以将数据读出或写入。
在构建Daemon线程时,不能依靠finally块中的内容来确保执行关闭或者清理资源的逻辑。
线程的初始化方式
构建线程
在运行线程之前需要先构建一个线程对象,线程对象在构造的时候需要提供线程所需要的属性,例如线程优先级,线程组,是否是守护线程等。
Thread源码中的init方法。
//g 代表线程组,target是我们要运行的对象,name是线程名字,默认是 "Thread-" + nextThreadNum(),nextThreadNum 返回自增数字,stackSize设置堆栈的大小
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) {
/* Determine if it's an applet or not */
/* If there is a security manager, ask the security manager
what to do. */
if (security != null) {
g = security.getThreadGroup();
}
/* If the security doesn't have a strong opinion of the matter
use the parent thread group. */
if (g == null) {
//如果没有线程组就使用父线程的线程组
g = parent.getThreadGroup();
}
}
/* checkAccess regardless of whether or not threadgroup is
explicitly passed in. */
g.checkAccess();
/*
* Do we have the required permissions?
*/
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}
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;
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
setPriority(priority);
//如果父线程的inheritThreadLocals不为空时,会把inheritThreadLocals属性的值全部传递给子线程。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* 最后设置线程ID */
tid = nextThreadID();
}
最终构建的线程对象放在堆中,等待被开启。
创建无返回值的线程的两种方式
- 继承Thread类,成为Thread的子类。
//继承Thread,实现run方法
class MyThread extends Thread{
@Override
public void run() {
log.info(Thread.currentThread().getName());
}
}
//直接start()开启线程就会执行run方法中的内容
public static void main(String[] args) {
new MyThread().start();
}
- 实现Runnable接口,作为Thread的入参
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.info("{} begin run",Thread.currentThread().getName());
}
});
thread.start()//开启线程去执行
thread.run()//不会开启线程,是在当前主线程上运行 run方法
start()方法源码分析
public synchronized void start() {
/**
*如果该线程没有进过初始化就抛出异常
*
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
//标识符,开始之前是false
boolean started = false;
try {
//start0是native方法,执行完成后创建一个新的线程并运行,target中的内容已经运行了
start0();
started = true;//这里执行的还是主线程
} finally {
try {
//如果started还是false代表执行失败
if (!started) {
//将线程从线程组中删除
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
线程之间的通信
每一个线程都拥有自己的栈空间,按照已经给定的代码一行一行的执行,直到执行完毕。但是如果各个线程之间只是孤立运行,那么产生的价值会很低,如果多个线程之间相互配合的话,将会产生更大的价值。
等待/通知机制有关的方法
等待/通知机制是指:例如线程A调用对象O的wait方法进而进入到了等待状态,而另一个线程B调用对象O的notify/notifyAll()方法,线程A收到通知后从对象O的wait方法中返回,继续执行后面的有关操作。A、B两个线程通过对象O来完成等待方和通信方之间的交互工作。这就是等待/通知机制。
- notify():通知一个对象上等待的线程,返回的前提是该线程获取到对象的锁
- notifyAll():唤醒所有处于该对象上等待状态的线程
- wait():调用该方法的线程进入WAITING状态,只有等待其他线程的通知或被中断才会返回,需要注意的是,wait方法会释放对象的锁。
- wait(long): 超时等待一段时间,这里的参数的单位是毫秒,也就是等待多少毫秒,如果没有超过指定的时间,没有收到其他线程的通知就会自动返回。
- wait(long,int):对于超时时间更细粒度的控制,可以精确到纳秒。
需要注意的是,如果有多个线程处于等待状态,notify方法从等待队列中唤醒一个线程(移动到同步队列中)并且是随机的,我们无法指定从中唤醒的是哪一个。notifyAll方法唤醒的是全部处于等待状态的线程,也就是将等待队列中的所有线程全部移到同步队列中,被移动的线程从WAITING–>BLOCKED。
Thread.join()
join的意思就是当前线程等待另一个线程执行完成之后们才能继续操作。线程除了提供join方法还提供了join(long mills)和join(long mills,int nanos)两个具备超时特性的方法。如果线程thread在给定时间内没有执行完,就会从该方法中返回。
举个例子
**
* 以下代码来自Java并发编程的艺术p103
*/
public class JoinDemo {
public static void main(String[] args) {
Thread previous = Thread.currentThread();
for (int i = 0; i < 10; i++) {
//每个线程都持有上一个线程的引用,需要等待上一个线程终止,才能从等待中返回
Thread thread = new Thread(new Domino(previous), String.valueOf(i));
thread.start();
previous = thread;
}
}
static class Domino implements Runnable {
private Thread thread;
public Domino(Thread thread) {
this.thread = thread;
}
@Override
public void run() {
try {
thread.join();
} catch (InterruptedException e) {
}
//打印当前线程的名字
System.out.println(Thread.currentThread().getName() + " terminate.");
}
}
}
//输出结果
0 terminate.
1 terminate.
2 terminate.
3 terminate.
4 terminate.
5 terminate.
6 terminate.
7 terminate.
8 terminate.
9 terminate.
sleep
public static native void sleep(long millis) throws InterruptedException;
sleep是native
方法,可以接受毫秒的一个入参,也可以接受毫秒和纳秒的两个入参。表示当前线程会沉睡n毫秒/纳秒,在下次沉睡期间,不会释放资源,所以沉睡时,其他线程无法得到锁。
yield
public static native void yield();
yield 是个native方法,表示当前线程做出让步,放弃cpu执行权,让CPU重新选择线程,避免线程过度使用CPU,需要注意的是:当前线程释放CPU后,重新竞争时,CPU可能还会选到当前线程。
在使用while循环时,可以使用yield方法使当前线程在规定时间内结束,防止CPU一直被死循环霸占。
interrupt
interrupt:线程的中断操作,意思是可以打断正在处于运行态或者等待状态的线程。
Object#wait ()、Thread#join ()、Thread#sleep (long) 这些方法运行后,线程的状态是 WAITING 或 TIMED_WAITING,这时候打断这些线程,就会抛出 InterruptedException 异常,使线程的状态直接到 TERMINATED;
在IO操作中,如果IO阻塞,主动打断线程,连接就会关闭,并抛出ClosedByInterrupException异常;
private Logger log = LoggerFactory.getLogger(InterruptTest.class);
@Test
public void testInterrupt() throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
log.info("{} begin run",Thread.currentThread().getName());
try {
log.info("子线程开始沉睡 30 s");
Thread.sleep(30000L);
} catch (InterruptedException e) {
log.info("子线程被打断");
e.printStackTrace();
}
log.info("{} end run",Thread.currentThread().getName());
}
});
// 开一个子线程去执行
thread.start();
Thread.sleep(1000L);
log.info("主线程等待 1s 后,发现子线程还没有运行成功,打断子线程");
thread.interrupt();
}
执行结果
参考:《Java并发编程的艺术》方腾飞