一、线程概述
1.线程与进程
几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
进程: 当一个程序进入内存运行时,即变成一个进程。进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。一般而言,进程包含如下三个特征:独立性,动态性,并发性。
线程: 多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,一个线程必须有一个父进程。
线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量,但不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便:但必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
2.多线程的优势
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性—多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段、进程的公有数据等。利用这些共享的数据,线程很容易
实现相互之间的通信。
优点:
(1)进程之间不能共享内存,但线程之间共享内存非常容易。
(2)系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高。
(3)Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多程编程。
并发性( concurrency )和并行性( paralle )是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
二、多线程的三种创建方式以及启动
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流(一段顺序执行的代码), Java使用线程执行体来代表这段程序流。
1.继承Thread类创建线程类
步骤:
(1)定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务。因此把run)方法称为线程执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
示例如下:
public class FirstThread extends Thread{
@Override
public void run() {
int i = 0;
for ( ; i < 100; i ++) {
//当线程类继承Thread类时,直接使用this即可获取当前线程
//Thread对象的getName() 返回当前线程的名字
//因此可以直接调用getName() 方法返回当前线程的名字
System.out.println(getName() + " " + i );
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
//Thread.currentThread().getName() 总是获得当前线程名字
System.out.println(Thread.currentThread().getName() + " " + i );
if (i == 20) {
//创建并启动第一个线程
new FirstThread().start();
//创建并启动第二个线程
new FirstThread().start();
}
}
}
}
运行后发现,有三个线程:man,Thread-0,Thread-1。
man即主线程,主线程的线程执行体不是由run()方法确定的,而是由main()方法确定的—main()方法的方法体代表主线程的线程执行体。
示例中的方法:
(1)Thread.currentThread().getName():Thread类的静态方法,该方法总是返回当前正在执行的线程对象。
(2)getName():Thread类的静态方法,该方法返回调用该方法的线程名字。
从上图可以看出, Thread-0和Thread-1两个线程输出的i变量不连续—注意:i变量是FirstThread的实例变量,而不是局部变量,但因为程序每次创建线程对象时都需要创建一个
FirstThread对象,所以Thread-0和Thread-1不能共享该实例变量。
2.实现Runnable接口创建线程类
步骤:
(1)定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start)方法来启动该线程。
public class SecondThread implements Runnable{
@Override
public void run() {
int i = 0;
for ( ; i < 100; i++) {
//当线程类实现Runnable接口时
//如果想获取当前线程,只能用Thread.currentThread()方法
System.out.println( Thread.currentThread().getName() + " " + i );
}
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " " + i );
if(i == 20){
SecondThread secondThread = new SecondThread();
//通过new Thread (target, name)方法创建新线程
new Thread(secondThread,"线程一").start();
new Thread(secondThread,"线程二").start();
}
}
}
}
从上图可以看出,两个子线程的i变量是连续的,也就是采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量。这是因为在这种方式下,程序所创建的Runnable对象只是线程的target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类(实际上应该是线程的target类)的实例变量。
3.使用Callable接口和Future接口创建线程
通过实现Runnable接口创建多线程时, Thread类的作用就是把run()方法包装成线程执行体。那么是否可以直接把任意方法都包装成线程执行体呢? Java目前不行!但Java的模仿者C#可以(C#可以把任意方法包装成线程执行体,包括有返回值的方法)。
也许受此启发,从Java 5开始, Java提供了Callable接口,该接口怎么看都像是Runnable接口的增强版, Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大。
注意:
(1)call()方法可以有返回值。
(2)call()方法可以声明抛出异常。
因此完全可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。问题是: Callable接口是Java 5新增的接口,而且它不是Runnable接口的子接口,所以Callable对象不能直接作为Thread的target。而且call()方法还有一个返回值—call()方法并不是直接调用,它是作为线程执行体被调用的。那么如何获取call()方法的返回值呢?
Java 5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口—可以作为Thread类的target。
Future接口中定义的方法:
V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值。该方法让程序最多阻塞timeout和unit指定的时间,如果经过指定时间后Callable任务依然没有返回值,将会抛出TimeoutException异常。
Callable接口有泛型限制, Callable接口里的泛型形参类型与call()方法返回值类型相同。而且Callable接口是函数式接口,因此可使用Lambda表达式创建Callable对象。
步骤:
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。从Java 8开始,可以直接使用Lambda表达式创建Callable对象。
(2)使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
//创建线程的方法3,使用Callable接口(Runnable接口增强版,不是Runnable的子接口),Future接口及其实现类FutureTask类(也实现了Runnable接口)
public class ThirdThread {
public static void main(String[] args) {
//ThirdThread thirdThread = new ThirdThread();
FutureTask<Integer> futureTask = new FutureTask<Integer>( (Callable<Integer>)() ->{//创建FutureTask类对象
int i = 0;
for ( ; i < 100;i++) {
System.out.println(Thread.currentThread().getName() + i );
}
return i;
});
for (int i = 0; i < 100;i ++) {
System.out.println(Thread.currentThread().getName() + i);
if( i == 20){
new Thread( futureTask,"有返回值的线程-").start();//FutureTask对象作为Thread对象的target
}
}
try {
System.out.println( "子线程的返回值:" + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
上面程序中使用Lambda表达式直接创建了Callable对象,这样就无须先创建Callable实现类,再创建Callable对象了。实现Callable接口与实现Runnable接口并没有太大的差别,只是Callable的call()方法允许声明抛出异常,而且允许带返回值。
上面程序中是以Callable对象来启动线程的关键代码。程序先使用Lambda表达式创建一个Callable对象,然后将该实例包装成一个FutureTask对象。主线程中当循环变量i等于20时,程序启动以FutureTask对象为target的线程。程序最后调用FutureTask对象的get()方法来返回call()方法的返回值—该方法将导致主线程被阻塞,直到call()方法结束并返回为止。
运行上面程序,将看到主线程和call()方法所代表的线程交替执行的情形,程序最后还会输出call()方法的返回值。
4.三种方式的对比
采用实现Runnable,Callable接口的方式创建多线程的优缺点:
(1)线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
(2)在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(3)劣势是,编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread)方法。
采用继承Thread类的方式创建多线程的优缺点:
(1)劣势是,因为线程类已经继承了Thread类,所以不能再继承其他父类。
(2)优势是,编写简单,如果需要访问当前线程,则无须使用Thread.currentThread)方法,直接使用this即可获得当前线程。
鉴于上面分析,因此一般推荐采用实现Runnable接口、Callable接口的方式来创建多线程。