进程和线程
进程是处于运行中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单元。
特征:
- 独立性:进程是系统中独立存在的实体,拥有自己的独立资源,没一个进程拥有自己私有的地址空间。没有经过允许的情况下,一个进程是不可以访问其他进程的地址空间。
- 动态性:进程与程序区别,一个是静态指令集合,一个是正在系统中活动的指令集合,加入了事件的概念。进程具有自己的生命周期和状态,程序没有。
- 并发性:多个进程可以在一个处理器上并发执行,不会互相影响。(注意并发与并行的区别)
进程调度策略:
- 共用式多任务操作策略(协作式)
- 抢占式多任务操作策略
线程也被称作轻量级进程,线程是进程的执行单元。各个线程之间共享进程的内存空间(代码段、数据段和堆空间)及一些进程级的资源(如打开的文件),但是各个线程都拥有自己的堆栈,程序计数器和局部变量。线程的执行是抢占式的。
多线程编程优点:
- 使用多线程可以减少程序的响应时间,如果某个操作很耗时,或者陷入长时间的等待,此时程序将不会响应鼠标和键盘等的操作,使用多线程后可以把这个耗时的线程分配到一个单独的线程去执行,从而使程序具备了更好的交互性。
- 与进程相比,线程创建和切换开销更小,同时多线程在数据共享方面效率非常高。
- 多CPU或者多核计算机本身就具备执行多线程的能力,如果使用单个进程,将无法重复利用计算机资源,造成资源的巨大浪费。在多CPU计算机使用多线程能提高CPU的利用率。
- 使用多线程能简化程序的结构,使程序便于理解和维护。
创建线程
主要有三种方式
继承Thread类
以下是主要步骤:
(1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
(2)创建Thread子类的实例,即创建了线程对象。
(3)调用线程对象的start()方法来启动该线程。
public class TestThread extends Thread {
int i;
public void run() {
for (; i < 100; i++)
System.out.println(getName() + " " + i);
}
}
public void main(String[] args) {
Thread mThread = new TestThread();
mThread.start();
}
使用继承Thread类的方式,多个线程之间无法共享线程类的实例变量。
实现Runnable接口
以下是主要步骤:
(1)自定义类并实现Runnable接口,实现run()方法。
(2)创建Thread子类的实例,用实现Runnable接口的对象作为target实例化该Thread对象。
(3)调用Thread的start()方法来启动该线程。
public class TestRunnable implements Runnable {
int i;
public void run() {
for (; i < 100; i++)
System.out.println(Thread.currentThread().getName() + " " + i);
}
}
public void main(String[] args) {
TestRunnable mTestRunnable = new TestRunnable();
Thread mThread = new Thread(mTestRunnable);
mThread.start();
}
使用实现Runnable接口的方式,多个线程之间可以共享线程类的实例变量。
使用Callable和Future
Callable接口实际是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更强大的功能,主要表现为:
- Callable中的call()方法提供一个返回值
- Callable中的call()方法可以抛出异常
两个问题:
- Callable不是Runnable的子接口,无法直接作为Thread的target?
- call()不是直接调用,而是作为线程执行体被调用,那么如何获取返回值呢?
Java5提供Future接口来代表Callable接口中call()方法返回值,并为Future接口提供了一个FutureTask实现类,该实现类实现了Future接口,且实现了Runnable接口,可以直接作为Thread的target。
Future接口提供了如下几个方法控制它关联的Callable任务:
boolean cancle(boolean mayInterruptIfRunning)
V get()//获取Callable任务返回值,该方法将导致线程阻塞,必须等到子线程结束才会得到返回值
V get(long timeout, TimeUnit unit)
boolean isCancelled()
boolean isDone()
有两种使用方式,以下是主要步骤:
方式一:
(1)创建Callable接口实现类,实现call()方法,再创建Callable实现类实例。
(2)用FutureTask类来包装Callable对象。
(3)使用FutureTask对象作为Thread的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行后的返回值。
public void main(String[] args) {
FutureTask<String> task = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return "Hello World";
}
});
new Thread(task).start();
try {
System.out.print(task.get()); //阻塞线程
} catch (Exception e) {
e.printStackTrace();
}
}
方式二:
(1)创建Callable接口实现类,实现call()方法,再创建Callable实现类实例。
(2)通过ExecutorService的submit方法执行Callable,并返回FutureTask 实例。
(3)调用FutureTask对象的get()方法来获得子线程执行后的返回值。
ExecutorService executor = Executors.newCachedThreadPool();
Future<String> future = executor.submit(() -> { //Lambda 是一个 callable, 提交后便立即执行,这里返回的是 FutureTask 实例
System.out.println("running task");
Thread.sleep(10000);
return "return task";
});
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("do something else"); //前面的的 Callable 在其他线程中运行着,可以做一些其他的事情
try {
System.out.println(future.get()); //等待 future 的执行结果,执行完毕之后打印出来
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
}
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/88e82d18f15d39807846389ce7eefd09.png)
三种方式对比
采用Runnable/Callable:
优点:
- 线程类只是实现接口,还可以继承其他类
- 多个线程可以共享同一个target对象,适合多个线程处理同一份资源情况
缺点:
- 编程稍复杂,需要用Thread.currentThread()访问当前线程
采用Thread
优点:
- 编程简单,使用this即可获取当前线程
缺点:
- 不能继承另外父类
线程生命周期
线程状态
(1). 新建状态(New):新创建了一个线程对象。
(2). 就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
(3). 运行状态(Running):就绪状态的线程获取了CPU,执行程序代码。
(4). 阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(5). 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
控制线程
join线程
当在某个程序执行流中调用其他线程的join方法时,调用线程将被阻塞,直到被join的线程执行完为止。
join()方法通常由使用线程的程序调用,以将大问题分解成许多小问题,每个小问题分配一个线程。当所有的小问题都处理后,再调用主线程来进一步操作。
join() //等待被join的线程执行完成
join(long millis) //如果在millis毫秒内还没有执行结束,则不再等待
join(long millis,int nanos) //millis毫秒加nanos微秒
public class JoinThread extends Thread {
public void run() {
for (int i = 0; i < 100; i++) {
System.out.print(getName() + " " + i);
}
}
}
public void main(String[] args) throws Exception{
for (int i = 0; i < 100; i++) {
if (i == 20) {
JoinThread jt = new JoinThread();
jt.start();
jt.join();
}
System.out.print(Thread.currentThread().getName() + " " + i);
}
}
后台线程
后台线程是为其他线程提供服务的,又称“守护线程”或“精灵线程”。JVM垃圾回收线程就是典型的后台线程。
特征:所有前台线程都死亡,后台线程自动死亡。
setDaemon(true); /**将线程转换为守护线程*/
isDaemon(); //判断是否后台线程
...
DeamonThread t = new DeamonThread();
t.setDaemon(true);
t.start();
注意setDaemon必须在start之前执行,否则引发IllegalThreadStateException。
线程睡眠sleep
调用Thread类静态方法实现当前运行线程睡眠。调用后当前线程处于阻塞状态,等待睡眠时间到。
static void sleep(long millis)
static void sleep(long millis, int nanos)
线程让步yield
调用Thread类静态方法实现当前运行线程让步。调用后当前线程处于就绪状态,等待被CPU调用。
sleep()方法与yield()方法区别:
- sleep()方法暂停当前线程后,会给其他线程执行机会,不会理会其他线程优先级。yield()方法只会给同优先级或更高优先级线程执行机会。
- sleep()方法将当前线程转入阻塞状态,而yield()强制将当前线程转入就绪状态,因此完全可能某个线程调用yield()后立即再次获得CPU资源。
- sleep()方法申明抛出InterruptException异常,要么捕捉要么显示抛出,而yield()没有申明抛出任何异常。
- sleep()比yield()有更好的移植性,不建议yield()控制并发线程执行。
线程优先级
Thread类提供了setPriority(int newPriority)、getPriority()方法设置和返回线程优先级。setPriority参数可以是一个整数,1-10之间,也可以使用Thread类三个静态常量:
- MIN_PRIORITY(0)
- NORM_PRIORITY(5)
- MAX_PRIORITY(10)
虽然Java提供10个优先级,但这些优先级仍然需要操作系统支持,不同操作系统上优先级并不相同,如Windows2000仅提供7个优先级,因此为保证程序可移植性,最好使用上面三个静态常量。
注:默认情况下,一个线程和它父线程有相同的优先级。
线程的底层实现
在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。
这里需要了解一个术语,内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器(Scheduler)进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。
由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。如下图
如图所示,每个线程最终都会映射到CPU中进行处理,如果CPU存在多核,那么就可以并行执行多个线程任务。