本节书摘来异步社区《Java线程与并发编程实践》一书中的第1章,第1.1节,作者: 【美】Jeff Friesen,更多章节内容可以访问云栖社区“异步社区”公众号查看。

第1章 Thread和Runnable

Java线程与并发编程实践

Java程序是通过线程执行的,线程在程序中具有独立的执行路径。当多条线程执行时,它们彼此之间的路径可以不同。举个例子,一条线程可能在执行switch语句的某个case分支,另一条线程很可能在执行其他case分支。

每个Java应用程序都有一个执行main()函数的默认主线程。应用程序也可以创建线程在后台操作时间密集型任务,以确保对用户的响应。这些封装了代码执行序列的线程对象就被称为runnable。

Java虚拟机给每条线程分配独立的JVM栈空间以免彼此干扰。独立的栈使得线程可以追踪它们自己下一条要执行的指令,这些指令会依线程不同而有所区别。栈空间也为每条线程单独准备了一份方法参数、局部变量以及返回值的拷贝。

Java主要是通过java.lang.Thread类以及ava.lang.Runnable接口实现线程机制的。本章将介绍这些类型。

1.1 Thread和Runnable简介

Thread类为底层操作系统的线程体系架构提供一套统一接口(操作系统通常负责创建和管理线程)。单个的操作系统线程和一个Thread对象关联。

Runnable接口为关联Thread对象的线程提供执行代码。这些代码放在Runnable的void run()方法中,这个方法虽然不接收任何参数且没有返回值,但是有可能抛出异常,这点我们会在第4章讨论。

1.1.1 创建Thread和Runnable对象

除了默认主线程,线程都是通过创建合适的Thread和Runnable对象进入应用程序的。Thread类声明了几个构造方法来初始化Thread对象。其中有几个构造方法需要接收Runnable对象作为参数。

我们有两种方式创建Runnable对象。第一种方式是创建一个实现了Runnable接口的匿名类,如下:

Runnable r = new Runnable()

{

@Override

public void run()

{

// perform some work

System.out.println("Hello from thread");

}

};

在Java 8之前,这是唯一一种创建runnable的方式。不过,Java 8引入lambda表达式更为快捷地创建runnable:

Runnable r = () -> System.out.println("Hello from thread");

lambda确实比匿名类更简洁。我会在本章及随后的章节继续使用这些语言特性。

注意:

一个lambda表达式是一个被传递到构造函数或者普通方法以供后续执行的匿名函数。和Runnable类似,lambda表达式以functional interfaces(声明单个抽象方法的接口)的形式工作。

创建Runnable对象之后,你可以把它传递到Thread类接收Runnable作为参数的构造函数中。举个例子,Thread(Runnable runnable)方法用指定的runnable初始化了一个新的Thread对象。下面的代码片段示范了这一做法:

Thread t = new Thread(r);

少数构造函数不会接收Runnable作为参数。例如,Thread()构造方法就不会接收它来初始化线程。你必须继承Thread类继而重写它的run()方法(Thread类实现了Runnable接口)并提供运行代码。如下面的代码片段所示:

class MyThread extends Thread

{

@Override

public void run()

{

// perform some work

System.out.println("Hello from thread");

}

}

// ...

MyThread mt = new MyThread();

1.1.2 获取和设置线程状态

一个Thread对象关联着一条线程的状态。这个状态由线程名称、线程存活的标识、线程的执行状态(是否正在执行?)、线程的优先级以及线程是否为守护线程等标识构成。

1.获取和设置线程的名称

每个Thread对象都会被赋予一个名称,这样有利于调试。这个名称如果不是显式指定的,那么默认会以一个Thread-作为前缀。你可以通过调用Thread的String getName()方法来获取这个名称。若要设置名称,则得把名称传递给一个合适的构造函数,如Thread(Runnable r, String name),或者调用Thread的void setName(String name)方法。如下面的代码片段所示:

Thread t1 = new Thread(r, "thread t1");

System.out.println(t1.getName()); // Output: thread t1

Thread t2 = new Thread(r);

t2.setName("thread t2");

System.out.println(t2.getName()); // Output: thread t2

注意:

Thread的long getId()方法会返回一个长整型的唯一名称。这个数字在线程的生命周期内不会改变。

2.获取一条线程的存活状态

你可通过调用Thread的boolean isAlive()方法来判断一条线程的存活状态。当线程是活的,该方法返回true,反之,返回false。一条线程的寿命仅仅起始于它真正在start()方法(后面会讨论)中被启动起来,而结束于它刚刚离开run()方法,此时线程死亡。下面的代码片段打印了一条新创建的线程的存活状态:

Thread t = new Thread(r);

System.out.println(t.isAlive()); // Output: false

3.获取一条线程的执行状态

线程的执行状态由Thread.State枚举常量标识:

NEW:该状态下线程还没有开始执行。

RUNNABLE:该状态下线程正在JVM中执行。

BLOCKED:该状态下线程被阻塞并等待一个监听锁(我会在第2章中介绍监听锁)。

WAITING:该状态下线程无限期地等待另外一条线程执行特定的操作。

TIMED_WAITING:该状态下线程在特定的时间内等待另外一条线程执行某种操作。

TERMINATED:该状态下线程已经退出。

Thread通过提供Thread.State getState()方法来让应用程序判断线程的当前状态。示例如下:

Thread t = new Thread(r);

System.out.println(t.getState()); // Output: NEW

4.获取和设置线程的优先级

当计算机有足够的处理器或处理内核,操作系统就会为每个处理器或核心分配单独的线程,这些线程可以同时执行。一旦计算机没有足够的处理器或核心的时候,多条线程只能轮转着使用共享的处理器和核心了。

注意:

可以调用java.lang.Runtime类的int availableProcessors()方法,以确定JVM上可用的处理器或处理器核心的数量。方法的返回值可能会在JVM执行时发生变化,但是不会小于1。

操作系统使用调度器来决定什么时候一个等待线程得以执行。下面的列表展示了3种不同的调度器:

Linux 2.6到2.6.23使用O(1)调度器(http://en.wikipedia.org/ wiki/O(1)_scheduler)。

Linux 2.6.23也使用了Completely Fair调度器(http://en.wikipedia. org/wiki/Completely_Fair_Scheduler)``,它也是默认的调度器。

Windows基于NT的操作系统(如NT、XP、Vista,以及7)使用了多级反馈队列调度器(http://en.wikipedia.org/wiki/Multilevel_ feedback_queue),这一调度器已经被Windows Vista和Windows 7调整优化了性能。

多级反馈队列调度器和很多其他线程调度器都会考虑优先级(线程的相对重要性),通常会结合抢占式调度(高优先级线程抢占、中断并取代低优先级线程运行)和轮转时间片调度(同等优先级的线程享有同等的时间片段,也称为时间片,然后依次执行)。

注意:

当探究线程究竟是并行还是并发的时候,通常会遇到两个概念。根据Oracle的《Multithreading Guide》,并行是“一种发生在至少有两个线程同时执行的场景”,相反地,并发是“一种存在于至少有两个线程前进的场景,它是一种更泛化的并行,包括基于时间片的虚拟并行模式”。

Thread通过返回当前优先级的intgetPriority()方法以及设置优先级的void setPriority(int priority)方法来实现优先级操作。传递给优先级的值介于Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间,而Thread.NORMAL_PRIORITY则确定了默认的优先级。请看下面的代码片段:

Thread t = new Thread(r);

System.out.println(t.getPriority());

t.setPriority(Thread.MIN_PRIORITY);

警告:

使用setPriority()会影响应用程序跨操作系统的可移植性,因为不同的调度器会采取不同的方式处理优先级。举个例子,一个操作系统的调度器可能会推迟低优先级线程执行,直到高优先级线程完成执行,这会导致无限延迟。而当无限期地等待执行时,低优先级线程会“饿死”,这会严重损害应用程序的性能。而其他操作系统的调度器可能不会无限期地延迟低优先级的线程,从而改善应用程序的性能。

5.获取和设置线程的守护线程状态

Java将线程分为守护和非守护线程。一条守护线程扮演非守护线程辅助者的角色,并且会在应用程序最后一条非守护线程消失之后自动死亡,因此应用程序才能终止。

你可以通过调用Thread的boolean isDaemon()方法来判断线程是守护线程还是非守护线程,这个方法会针对一个守护线程返回true。

Thread t = new Thread(r);

System.out.println(t.isDaemon()); // Output: false

默认情况下,和Thread对象关联的线程都是非守护线程。想要创建一个守护线程,你必须调用Thread的void setDaemon(boolean isDaemon)方法并传入true作为参数。示例如下:

Thread t = new Thread(r);

t.setDaemon(true);

注意:

当非守护默认主线程终止后,应用程序还会等到所有后台的非守护线程终止之后才会终止。如果后台的线程本来就是守护线程,那么当默认的主线程终止时,应用程序会立刻终止。

1.1.3 启动线程

在创建一个Thread对象或者其子类的对象之后,你可以通过调用Thread的void start()方法启动与该对象关联的线程。如果线程之前已经启动且处于运行状态,又或者线程已经死亡,这个方法就会抛出

java.lang. IllegalThreadStateException:

Thread t = new Thread(r);

t.start();

调用start()方法会在运行时创建底层线程,同时调度run()方法中的指令(start()方法并不会等到所有这些任务都完成才返回)。当run()方法执行完毕,线程就会被销毁,调用start()方法的Thread对象不再可用,这也是start()方法会导致IllegalThreadStateException的原因。

我已经创建了一个应用,它包含了从线程创建到启动的多种基本示例。请查看清单1-1。

清单1-1 线程基础示例

public class ThreadDemo

{

public static void main(String[] args)

{

boolean isDaemon = args.length != 0;

Runnable r = new Runnable()

{

@Override

public void run()

{

Thread thd = Thread.currentThread();

while (true)

System.out.printf("%s is %salive and in %s " +

"state%n",

thd.getName(),

thd.isAlive() ? "" : "not ",

thd.getState());

}

};

Thread t1 = new Thread(r, "thd1");

if (isDaemon)

t1.setDaemon(true);

System.out.printf("%s is %salive and in %s state%n",

t1.getName(),

t1.isAlive() ? "" : "not ",

t1.getState());

Thread t2 = new Thread(r);

t2.setName("thd2");

if (isDaemon)

t2.setDaemon(true);

System.out.printf("%s is %salive and in %s state%n",

t2.getName(),

t2.isAlive() ? "" : "not ",

t2.getState());

t1.start();

t2.start();

}

}

首先,默认的主线程基于命令行参数是否存在来初始化isDaemon变量。只要有一个参数传递过来,isDaemon就会设置成true。否则,设置为false。

其次,一个runnable对象被创建了。这个对象首先调用Thread的静态方法Thread currentThread()获得当前执行线程关联的Thread对象的引用。该引用继而会用于获取该线程的相关信息,这些信息最终会被打印出来。

到这里,初始化为上面的runnable且名为thd1的Thread对象就被创建好了。如果isDaemon是true,那么这个线程对象就会标记成守护线程,它的名字、存活状态以及执行状态随后会被打印。

第二个初始化为runnable且名为thd2的线程对象也被创建好了。与之前相同,如果isDaemon是true,该线程对象会标记成守护线程,它的名字、存活状态以及执行状态也会被打印出来。

最后,两条线程都启动起来。下面来编译清单1-1:

javac ThreadDemo.java

运行程序:

java ThreadDemo

以下是我在64位的Windows 7操作系统上观测到的不断打印的结果中最开始的部分:

thd1 is not alive and in NEW state

thd2 is not alive and in NEW state

thd1 is alive and in RUNNABLE state

thd2 is alive and in RUNNABLE state

你很可能在你的操作系统看到不同的输出结果。

提示:

为了终止无限运行的应用程序,可以在Windows或者非Windows系统上同时按住Ctrl和C键。

现在,运行程序:

java ThreadDemo x

和前面所有非守护线程执行的结果不同,命令行参数的存在导致所有的线程都会作为守护线程执行。因此,这些线程执行到默认主线程终止。你应该能观测到更简略的打印结果。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐