当一个程序运行时,对于操作系统而言它是以进程的形式存在的,如果是用的windows操作系统,打开任务管理器,能够看到进程标签页,在该标签页中能够看到当前正在运行的进程,而每个进程就是一个运行中的程序
当一个程序启动后进入内存运行时,它就成了一个操作系统的进程,对于操作系统而言进程是系统进行资源分配和调度的一个独立单位,而一个进程在运行时,内部可能包含多个顺序执行流,每个顺序执行流就是一个线程
大部分操作系统都支持多进程并发运行(虽然因为硬件和系统不同并发的策略很不一样),现代的操作系统几乎都支持同时运行多个任务,例如我们经常开着浏览器,开着编译器,同时还开这Word等,这些进程看上去像是在同时工作,但实际上对于一个CPU而言,它在某个时刻只能运行一个进程,CPU不断的在各个进程之间轮换执行,因为它太快了,人感知不到其中的切换,看上去像是在一起执行,假设我们开的程序足够多会发现每个程序都变慢了,也就感知到了它的切换变慢了
并发性concurrency和并行性parallel是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,表面上看似是同事执行的效果
多线程扩展了进程的概念,使得同一个进程可以同时并发处理多个任务,线程也被称为轻量级进程,实际上它是进程的执行单元,线程在程序中是独立的、并发的执行流,当进程初始化后,同时它的主线程也被创建了,通常情况下对于应用程序来说,仅要求有一个主线程,进程内可以有多个顺序执行流,这些顺序执行流也就是线程,每个线程是互相独立的。
Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例
// 通过继承Thread类来创建线程类
public class FirstThread extends Thread
{
private int i;
// 重写run方法,run方法的方法体就是线程执行体
public void run()
{
for ( ; i < 100; i++)
{
// 当线程类继承Thread类时,直接使用this即可获取当前线程
// Thread对象的getName()返回当前该线程的名字
// 因此可以直接调用getName()实例方法返回当前线程的名
System.out.println(getName() + " " + i);
}
}
public static void main(String[] args)
{
for (var i = 0; i < 100; i++)
{
// 调用Thread的currentThread类方法获取当前线程对象
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
// 创建、并启动第一条线程
new FirstThread().start();
// 创建、并启动第二条线程
new FirstThread().start();
}
}
}
}
执行程序的时候,会发现有3个线程,其中两个是代码里创建的,另一个是主线程,程序至少会创建一个主线程,主线程的线程执行体不是由run()方法确定的,而是由main()方法确定的,main()方法的方法体代表主线程的线程执行体
程序可以通过setName(String name)方法为线程设置名字,也可以通过getName()方法返回指定线程的名字,默认情况下主线程名字为main,用户启动的线程名为Thread-0、Thread-1…Thread-N
使用集成Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量
// 创建Runnable实现类的对象
SecondThread st = new SecondThread();
// 以Runnable实现类的对象作为Thread的target来创建Thread对象,即线程对象
new Thread(st);
//也可以在创建Thread对象时为该Thread对象指定一个名字
new Thread(st, "新线程OO");
// 通过实现Runnable接口来创建线程类
public class SecondThread implements Runnable
{
private int i;
// run方法同样是线程执行体
public void run()
{
for ( ; i < 100; i++)
{
// 当线程类实现Runnable接口时,
// 如果想获取当前线程,只能用Thread.currentThread()方法。
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public static void main(String[] args)
{
for (var i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " " + i);
if (i == 20)
{
var st = new SecondThread(); // ①
// 通过new Thread(target, name)方法创建新线程
new Thread(st, "新线程1").start();
new Thread(st, "新线程2").start();
}
}
}
}
Runnable接口中只包含一个抽象方法,Java8开始,Runnable接口使用了@FunctionalInterface修饰,也就是说它是个函数式接口,可以使用Lambda表达式创建Runnable对象
执行该代码示例,可以从结果中看出来,两个子线程的i变量是连续的,也就是采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量,这是因为在这种方式下,程序创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享同一个线程类的实例变量
Java5之后,提供了Callable接口,该接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大
Java5之后还提供了Future接口来代表Callable接口里的call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,并实现了Runnable接口,可以作为Thread类的target
Future接口里定义了如下几个公共方法来控制它关联的Callable任务:
boolean cancel(boolean mayInterruptIfRunning):试图取消该Future里关联的Callable任务
V get():返回Callable任务里call()方法的返回值,调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值
V get(long timeout, TimeUnit unit):返回Callable任务里call()方法的返回值,该方法让程序最多阻塞timeout和unit指定的时间,如果过了指定时间后Callable任务依然没有返回值,抛出TimeoutException异常
boolean isCancelled():如果Callable任务完成前被取消,则返回true
boolean isDone():如果Callable任务已经完成,则返回true
Callable接口有泛型限制,Callable接口里的泛型形参类型与call()方法返回值类型相同并且Callable接口是函数式接口,可以使用Lambda表达式创建Callable对象
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例,Java8开始可以直接使用lambda表达式创建Callable对象
使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
使用FutureTask对象作为Thread对象的target创建并启动新线程
调用FutureTask对象的get()方法来获取子线程执行结束后的返回值
import java.util.concurrent.*;
public class ThirdThread
{
public static void main(String[] args)
{
// 创建Callable对象
var rt = new ThirdThread();
// 先使用Lambda表达式创建Callable对象
// 使用FutureTask来包装Callable对象
FutureTask<Integer> task = new FutureTask<>((Callable<Integer>)() -> {
var i = 0;
for ( ; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
}
// call()方法可以有返回值
return i;
});
for (var i = 0; i < 100; i++)
{
System.out.println(Thread.currentThread().getName() + " 的循环变量i的值:" + i);
if (i == 20)
{
// 实质还是以Callable对象来创建、并启动线程
new Thread(task, "有返回值的线程").start();
}
}
try
{
// 获取线程返回值
System.out.println("子线程的返回值:" + task.get());
}
catch (Exception ex)
{
ex.printStackTrace();
}
}
}
程序使用了Lambda表达式直接创建了Callable对象,这样就无需先创建Callable实现类,再创建Callable对象了。
采用实现Runnable、Callable接口的方式创建多线程的优缺点:
采用继承Thread类的方式创建多线程的优缺点: