进程是程序的运行实例。进程是程序想操作系统申请资源(如内存空间和文件句柄)的基本单位。线程是进程中可独立执行的最小单位。一个进程可以包含多个线程。同一个线程中的所有线程共享该进程中的资源,如内存空间、文件句柄等。
线程所要完成的计算就被称为任务,特定的线程总是在执行着特定的任务。任务代表线程所要完成的工作,是一个相对的概念。
函数式编程中的函数是基本抽象单位,面向对象编程中的类是基本抽象单位。类似地,多线程编程就是以线程为基本抽象单位的一种编程范式。但是多线程编程又不仅仅是使用多个线程进行编程那么简单,其自身又有其需要解决的问题。
提高效率。
Java标准库类java.lang.Thread就是java平台对线程的实现。Thread类或其子类的一个实例就是一个线程。
每个线程都有其要执行的任务。线程的任务处理逻辑可以在Thread类的run实例方法中直接实现或者通过该方法进行调用。因此run方法相当于线程的任务处理逻辑的入口方法,它由java虚拟机在运行相应线程时直接调用,而不是由应用代码进行调用。
运行一个线程实际上就是让java虚拟机执行该线程的run方法,从而使相应线程的任务处理逻辑代码得以执行。首先要启动线程。Thread类的start方法的作用是启动相应的线程。启动一个线程的实质是请求Java虚拟机运行相应的线程,而这个线程具体何时能够运行是由线程调度器决定的。因此start方法调用结束并不意味着相应线程已经开始运行。
Thread类的两个常用构造器是:Thread()和Thread(Runnable target)。相应地,Java语言中创建线程有两种方式,一种是定义Thread类的子类,在该子类中覆盖run方法并在该方法中实现线程处理任务逻辑;另一种方法是创建一个java.lang.Runnable接口的实例,并在该实例的run方法中实现任务处理逻辑,然后以该Runnable接口实例作为构造器的参数直接创建一个Thread类的实例。
代码1:以定义Thread类子类的方式创建线程
package com.company;
class ThreadDemo extends Thread{
@Override
public void run() {
System.out.printf("2.Thread Method! Thread Info: %s .%n",Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread threadDemo = new ThreadDemo();
threadDemo.start();
System.out.printf("1.Thread Method! Thread Info: %s .%n",Thread.currentThread().getName());
}
}
代码2:以创建Runnable接口实例的方式创建线程
package com.company;
class RunnableTask implements Runnable{
@Override
public void run() {
System.out.printf("2.Runnable Thread, Info: %s.%n",Thread.currentThread().getName());
}
}
public class RunnableDemo {
public static void main(String[] args){
Thread runnableThread = new Thread(new RunnableTask());
runnableThread.start();
System.out.printf("1.Runnable Thread, Info: %s.%n",Thread.currentThread().getName());
}
}
不管是采用哪种方式创建线程,一旦线程的run方法执行(由Java虚拟机调用)结束,相应的线程的运行也就结束了。当然,run方法执行结束包括正常结束(run方法返回)以及代码中抛出异常而导致的中止。运行结束的线程所占用的资源会如同其他Java对象一样被Java虚拟机垃圾回收。
线程属于“一次性用品”,不能通过重新调用一个已经运行结束的线程的start方法来使其重新运行。事实上,start方法也只能够被调用一次,多次调用同一个Thread实例的start方法会导致其抛出IllegalThreadStateException异常。
在Java平台中,一个线程就是一个对象。对象的创建离不开内存空间的分配。创建一个线程与创建其他类型的Java对象所不同的是,Java虚拟机会为每个线程分配调用栈所需的内存空间。调用栈用于跟踪Java代码(方法)间的调用关系以及Java代码对本地代码(Native Code,通常是C代码)的调用。另外,Java平台中的每个线程可能还有一个内核线程与之对应。因此相对来说,创建线程对象比创建其他类型的对象的成本要高一些。
Java平台中的任意一端代码总是由确定的线程负责执行的,这个线程就相应地被成为这段代码的执行线程。同一段代码可以被多个线程执行。如果我们没有启动线程而是在应用代码中直接调用线程的run方法的话,那么这个线程的run方法其实运行在当前线程(即run方法的调用方代码的执行线程)之中而不是运行在其自身线程之中,从而违背了创建线程的初衷。如下所示(避免这样做):
package com.company;
public class ThreadDemo3 {
public static void main(String[] args){
//创建线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.printf("2.Thread, Info: %s.%n",Thread.currentThread().getName());
}
});
//启动线程
thread.start();
//调用线程的run方法,用于演示
thread.run();
System.out.printf("1.Thread, Info: %s.%n",Thread.currentThread().getName());
}
}
Runnable接口只定义了一个方法,该方法的声明如下:public void run()
Runnable接口可以被看作对任务进行抽象,任务的处理逻辑就体现在run方法中。Thread类实际上是Runnable接口的一个实现类。Thread类所实现的任务处理逻辑是要么什么也不做(target为null),要么直接执行target所引用的Runnable实例所实现的任务处理逻辑。Thread类的run方法的这种处理逻辑决定了创建线程的两种方法:一种是在Thread子类的run方法中直接实现任务处理逻辑;另一种方法是在一个Runnable实例中实现任务处理逻辑,该逻辑由Thread类的run方法负责调用。
从面向对象编程的角度看:第一种创建方式(创建Thread类的子类)是一种基于继承的技术,第2种创建方式(以Runnable接口实例为构造器参数直接通过new创建Thread实例)是一种基于组合的技术。
从对象共享的角度来看:第2种创建方式意味着多个线程实例可以共享一个Runable实例。在某些情况下这可能导致程序的运行结果出乎我们的意料。
从对象创建成本的角度看:Java中的线程实例是一个特殊的Runnable实例,因为在创建它的时候Java虚拟机会为其分配调用栈空间、内核线程等资源。因此,创建一个线程实例比起创建一个普通的Runnable实例来说,其成本要相对昂贵一点。如果创建Runnable实例再将其作为方法参数传递给其他对象使用而不必利用它来创建相应的线程即可满足我们的计算需要,那么就不要创建线程实例。
线程的两种创建方式区别:
package com.company;
import java.util.Random;
public class TwoMethods {
public static void main(String[] args) {
Thread t;
CountingTask ct = new CountingTask();
//获取处理器的个数
final int numberOfCpu = Runtime.getRuntime().availableProcessors();
for (int i = 0; i < 2 * numberOfCpu; i++) {
//直接创建线程
t = new Thread(ct);
t.start();
}
for (int i = 0; i < 2 * numberOfCpu; i++) {
//以子类的方式创建线程
t = new CountingThread();
t.start();
}
}
private static class CountingTask implements Runnable {
private Counter counter = new Counter();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
doSomething();
counter.increment();
}
System.out.println("CountingTask:" + counter.value());
}
private void doSomething() {
//使当前线程休眠随机时间
Tools.randomPause(80);
}
}
private static class Counter {
private int count = 0;
public void increment() {
count++;
}
public int value() {
return count;
}
}
private static class CountingThread extends Thread {
private Counter counter = new Counter();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
doSomething();
counter.increment();
}
System.out.println("CountingThread:" + counter.value());
}
private void doSomething() {
Tools.randomPause(80);
}
}
private static class Tools {
private static void randomPause(int x) {
Random random = new Random();
int y = random.nextInt(x);
try {
Thread.sleep(y);
} catch (InterruptedException e) {
System.out.println("Thread interruptd!");
}
}
}
}
线程的属性包括线程的编号(ID)、名称(Name)、线程类别(Daemon)和优先级(Priority)。
按照线程是否会阻止Java虚拟机正常停止,将Java中的线程分为守护线程和用户线程。用户线程会阻止Java虚拟机的正常停止,而守护线程则不会影响Java虚拟机的正常停止。如果Java虚拟机是被强制停止的,那么即使是用户线程也无法阻止Java虚拟机的停止。
方法 | 功能 | 备注 |
---|---|---|
static Thread currentThread() |
返回当前线程,即当前代码的执行线程(对象) | 同一段代码对Thread.currentThread()的调用,其返回值可能对应着不同的线程(对象) |
void run() | 用户实现线程的任务处理逻辑 | 该方法是由Java虚拟机直接调用的,一般情况下应用程序不应该调用该方法 |
void start() | 启动相应线程 | 该方法的返回并不代表相应的线程已经被启动。一个Thread实例的start方法只能够被调用一次,多次调用会导致异常的抛出 |
void join() | 等待相应线程运行结束 | 若线程A调用线程B的join方法,那么线程A的运行会被暂停,直到线程B运行结束 |
static void yield() | 使当前线程主动放弃其对处理器的占用,这可能导致当前线程被暂停 | 这个方法是不可靠的。该方法被调用时当前线程可能仍然继续运行 |
static void sleep(long millis) | 使当前线程休眠(暂停运行)指定的时间 |
Java中的任何一段代码总是执行在某个线程之中。执行当前代码的线程就被称为当前线程。Thread.currentThread()可以返回当前线程。
简易的倒计时器:
package com.company;
public class SimpleTimer {
private static int count;
public static void main(String[] args) {
count = args.length >= 1 ? Integer.valueOf(args[0]) : 60;
int remaining;
while (true) {
remaining = countDown();
if (0 == remaining) {
break;
} else {
System.out.println("Remaining:" + count + " second(s)");
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
System.out.println("Done.");
}
private static int countDown() {
return count--;
}
}
方法 | 功能 |
---|---|
stop | 停止线程的运行 |
suspend | 暂停线程的运行 |
resume | 使被暂停的线程继续运行 |
Java平台本身就是一个多线程的平台。Java虚拟机启动的时候会创建一个main线程,该线程负责执行Java程序的入口方法。Java代码的执行线程示例:
package com.company;
public class RunThread {
public static void main(String[] args) {
//获取当前线程
Thread current = Thread.currentThread();
//读取当前线程的线程名
String currThreadName = current.getName();
System.out.printf("The main method was executed by thread: %s \n", currThreadName);
Helper helper = new Helper("Java Thread Anywhere");
helper.run();
}
private static class Helper implements Runnable {
private final String message;
public Helper(String message) {
this.message = message;
}
private void doSomething(String message) {
//获取当前线程
Thread currThread = Thread.currentThread();
//当前线程名称
String currThreadName = currThread.getName();
System.out.printf("The doSomething method was executed by thread: %s \n", currThreadName);
System.out.println("Do somethind with " + message);
}
@Override
public void run() {
doSomething(message);
}
}
}
Java虚拟机垃圾回收器负责对Java程序中不再使用的内存空间进行回收,而这个回收的动作实际上也是通过专门的线程(垃圾回收线程)实现的,这些线程由Java虚拟机自行创建。从垃圾回收的角度看,Java平台中的线程可以分为垃圾回收线程和应用线程。应用线程由Java程序开发者创建。
为了提高Java代码的执行效率,Java虚拟机中的JIT(Just In Time)编译器会动态地将Java字节码编译为Java虚拟机宿主机处理器可直接执行的机器码。这个动态编译的过程实际上是由Java虚拟机创建的专门的线程负责执行的。
Java平台中的线程不是孤立的,线程与线程之间总是存在一些联系。在Java平台中,一个线程是否是一个守护线程默认取决于其父线程:默认情况下父线程是守护线程,则子线程也是守护线程;父线程是用户线程,则子线程也是用户线程。另外,父线程在创建子线程后启动子线程之前可以调用该线程的setDaemon方法,将相应的线程设置为守护线程(或者用户线程)。
一个线程的优先级默认值为该线程的父线程的优先级。
Java平台中并没有API用于获取一个线程的父线程,或者获取一个线程的所有子线程,并且父线程和子线程之间的生命周期也没有必然的联系。
习惯上,称某些子线程为工作者线程(Worker Thread)或者后台线程(Background Thread)。工作者线程通常是其父线程创建来用于专门负责某项特定任务的执行的。Java虚拟机中对内存进行回收的线程通常称为GC(Garbage Collection)工作者线程。
Java线程的状态可以使用监控工具查看,也可以通过Thread.getState()调用来获取。Thread.getState()的返回值类型Thread.State是一个枚举类型(Enum)。Thread.State所定义的线程状态包括以下几种。
NEW:一个已创建而未启动的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程只可能有一次处于该状态。
RUNNABLE:包括两个子状态:READY和RUNNING。前者表示处于该状态的线程可以被线程调度器进行调度而使之处于RUNNING状态。后者表示处于该状态的线程正在运行,即相应线程对象的方法所对应的指令正在由处理器执行。执行Thread.yield()的线程,其状态可能会由RUNNING转换为READY。处于READY子状态的线程也被称为活跃状态。
BLOCKED:一个线程发起一个阻塞式I/O操作后,或者申请一个由其他线程持有的独占资源时,相应的线程会处于该状态。处于BLOCKED状态的线程并不会占用处理器资源。当阻塞式I/O操作完成后,或者线程获得了其申请的资源,该线程的状态又可以转换为RUNNABLE。
WAITING:一个线程执行了某些特定方法之后就会处于这种等待其他线程执行另外一些特定操作的状态。能够使其执行线程变更为WAITING状态的方法包括:Object.wait()、Thread.join()和LockSupport.park(Object)。能够使相应线程从WAITING变更为RUNNABLE的相应方法包括:Object.notify()/notifyAll()和LockSupport.unpark(Object)。
TIMED_WAITING:该状态和WAITING类似,区别在于处于该状态的线程并非无限制地等待其他线程执行特定操作,而是处于带有时间限制的等待状态。当其他线程没有在指定时间内执行该线程所期望的特定操作时,该线程的状态自动转换为RUNNABLE。
TERMINATED:已经执行结束的线程处于该状态。由于一个线程实例只能够被启动一次,因此一个线程也只可能有一次处于该线程。Thread.run()正常返回或者由于抛出异常而提前终止都会导致相应线程处于该状态。
对线程进行监视的主要途径使获取并查看程序的线程转储(Thread Dump)。一个程序的线程转储包含了获取这个线程转储的那一刻该程序的线程信息。这些信息包括程序中有哪些线程以及这些线程的具体信息。
JDK自带的工具jvisualvm适合于在开发和测试环境中监视Java系统中线程情况。不仅可以用来获取线程转储,还可以直接选中一个线程来查看该线程的调用栈。
实现的功能:根据执行的URL下载一批文件。
package com.company;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.logging.Logger;
public class DownSomething {
static Logger logger = Logger.getLogger("DownSomething");
public static void main(String[] args) {
Thread downloadThread = null;
for (String url : args
) {
//创建文件下载器线程
downloadThread = new Thread(new FileDownloader(url));
//启动文件下载器线程
downloadThread.start();
}
}
//文件下载器
private static class FileDownloader implements Runnable {
private final String fileURL;
public FileDownloader(String fileURL) {
this.fileURL = fileURL;
}
@Override
public void run() {
logger.info("Downloading from " + fileURL);
String fileBaseName = fileURL.substring(fileURL.lastIndexOf('/') + 1);
try {
URL url = new URL(fileURL);
String localFileName = System.getProperty("java.io.tmpdir") + "/viscent-" + fileBaseName;
logger.info("Saving to: " + localFileName);
downloadFile(url, new FileOutputStream(localFileName), 1024);
} catch (Exception e) {
e.printStackTrace();
}
logger.info("Done downloading from " + fileURL);
}
private void downloadFile(URL url, OutputStream outputStream, int bufSize) throws IOException {
//建立HTTP链接
final HttpURLConnection httpConn = (HttpURLConnection) url.openConnection();
httpConn.setRequestMethod("GET");
ReadableByteChannel inChannel = null;
WritableByteChannel outChannel = null;
try {
//获取HTTP状态码
int responseCode = httpConn.getResponseCode();
//HTTP响应非正常:响应码不以2开头
if (2 != responseCode / 100) {
throw new IOException("Error: HTTP " + responseCode);
}
if (0 == httpConn.getContentLength()) {
logger.info("Nothing to be downloaded " + fileURL);
return;
}
inChannel = Channels.newChannel(new BufferedInputStream(httpConn.getInputStream()));
outChannel = Channels.newChannel(new BufferedOutputStream(outputStream));
ByteBuffer buf = ByteBuffer.allocate(bufSize);
while (-1 != inChannel.read(buf)) {
buf.flip();
outChannel.write(buf);
buf.clear();
}
} finally {
//关闭指定的channel以及HTTPURLConnection
silentClose(inChannel, outChannel);
httpConn.disconnect();
}
}
}
public static void silentClose(Closeable... closeable) {
if (null == closeable) {
return;
}
for (Closeable c : closeable) {
if (null == c) {
continue;
}
try {
c.close();
} catch (Exception ignored) {
}
}
}
}
优势:提高系统的吞吐率,提高响应性,充分利用多核处理器资源、最小化对系统资源的使用、简化程序的结构。
风险:线程安全问题、线程火星问题、上下文切换、可靠性等。
参考资料:《Java多线程编程实战指南》