进程、线程和协程
进程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建、运行到消亡的过程。
在Java中,当我们启动main函数其实就是启动了一个JVM进程,而main函数所在的线程其实就是这个进程中的一个线程,也称主线程。
在 Windows 中通过查看任务管理器的方式,我们就可以清楚看到 Windows 当前运行的进程(.exe 文件的运行)。
线程
线程与进程相似,但线程是一个比进程更小的执行单位,一个进程在其执行过程中可以产生多个线程。
各进程之间基本上是相互独立的,与进程不同的是同类线程共享进程的堆和方法区资源,但每个线程都有自己的程序计数器、Java虚拟机栈和本地方法栈。
系统产生一个线程或是在多个线程之间切换工作时,负担要比进程小得多,因此线程也被称作轻量级进程。
Java 程序天生就是多线程程序,一个 Java 程序的运行是 main 线程和多个其他线程同时运行。
进程和线程的区别
根本区别: 进程操作是系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
资源开销: 每个进程都有自己独立的代码和数据空间(程序上下文),进程之间的切换开销比较大;线程可以看作轻量级进程,同类的线程共享进程的堆和方法区(JDK1.7及之前实现为永久代,JDK1.8及之后实现为元空间)资源,但是每个线程都有自己的程序计数器、Java虚拟机栈和本地方法栈,线程之间的切换开销比较小。
包含关系: 如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配: 同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
影响关系: 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮。
执行过程: 每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
协程
协程是一种轻量级的线程,协程的实现方式是在一个线程中,通过切换执行上下文来实现多个任务的并发执行。协程的切换不需要操作系统的介入,因此协程的切换速度非常快,开销也非常小。协程的特点是可以在一个线程中开启多个协程,每个协程之间共享线程的内存空间,因此协程也可以完成复杂的任务,如异步编程、爬虫、游戏等。
线程与协程的区别
线程和协程都是多任务处理的技术,但是它们之间有很多区别,下面列举几点:
- 调度方式
线程是由操作系统负责调度的,它们是操作系统的一种资源。线程的调度和切换需要操作系统的介入,开销比较大,但是线程的并发性和并行性比较高。
**协程是由程序控制的,它们是用户空间的资源,不依赖于操作系统。**协程的切换和调度由程序控制,因此开销比较小,但是并发性和并行性比较低。 - 内存占用
每个线程都需要独立的堆栈和寄存器等资源,协程则共享线程的这些资源,内存占用比较小。 - 编程复杂度
线程需要考虑锁、同步、异步等问题,编程复杂度比较高。
协程可以通过 yield、await 等关键字来实现状态的保存和恢复,编程复杂度比较低。 - 执行效率
线程切换和调度需要操作系统的介入,开销比较大,执行效率比较低。
协程切换和调度由程序控制,开销比较小,执行效率比较高。
总的来说,线程适用于 CPU 密集型的任务,协程适用于 I/O 密集型的任务。
线程适用于 CPU 密集型的任务,协程适用于 I/O 密集型的任务
线程和协程的适用场景主要是由于它们在任务调度方面的特点决定的。
对于 CPU 密集型的任务,比如大量的计算、排序等,需要大量的 CPU 资源,线程可以让程序在多个 CPU 核心上并行执行,从而提高执行效率。但是线程的切换和调度需要操作系统的介入,开销比较大,因此线程数量过多时,会降低程序的效率。
对于 I/O 密集型的任务,比如网络请求、文件读写等,需要等待 I/O 操作完成后才能继续执行后续任务,这时线程的执行效率就会受到限制。而协程可以通过 yield、await 等关键字来实现状态的保存和恢复,从而避免了线程切换的开销,提高了程序的效率,因此协程适用于 I/O 密集型的任务。
总之,线程适用于需要大量的 CPU 资源的任务,而协程适用于需要大量的 I/O 操作的任务。在实际应用中,我们需要根据具体的场景选择合适的技术来实现任务的处理。
创建线程的三种方式
使用继承Thread类的方式创建多线程
Thread类是Java提供的线程顶级类,继承Thread类可快速定义线程。
- 使用多线程实现龟兔赛跑
public class TortoiseThread extends Thread{
@Override
public void run() {
while (true) {
System.out.println("乌龟领先了,加油.....," +
"当前线程的名称:" + this.getName() +
",当前线程的优先级别:" + this.getPriority());
}
}
public static void main(String[] args) {
TortoiseThread tortoiseThread = new TortoiseThread();
tortoiseThread.setName("乌龟线程");
tortoiseThread.start();
Thread.currentThread().setName("兔子线程");
while(true){
System.out.println("兔子领先了,add oil....,当前线程名称:"
+Thread.currentThread().getName()+",当前线程的优先级别:"
+Thread.currentThread().getPriority());
}
}
}
- 运行结果截取(这里只截取了部分结果,应该是无限循环的)
乌龟领先了,加油.....,当前线程的名称:乌龟线程,当前线程的优先级别:5
乌龟领先了,加油.....,当前线程的名称:乌龟线程,当前线程的优先级别:5
兔子领先了,add oil....,当前线程名称:兔子线程,当前线程的优先级别:5
兔子领先了,add oil....,当前线程名称:兔子线程,当前线程的优先级别:5
使用实现Runnable接口的方式创建多线程
- 使用多线程实现龟兔赛跑
public class TortoiseRunnable implements Runnable{
@Override
public void run() {
while (true) {
System.out.println("乌龟领先了,加油.....," +
"当前线程的名称:" + Thread.currentThread().getName() +
",当前线程的优先级别:" + Thread.currentThread().getPriority());
}
}
public static void main(String[] args) {
Thread thread = new Thread(new TortoiseRunnable());
thread.setName("乌龟线程");
thread.start();
Thread.currentThread().setName("兔子线程");
while(true){
System.out.println("兔子领先了,add oil....,当前线程名称:"
+Thread.currentThread().getName()+",当前线程的优先级别:"
+Thread.currentThread().getPriority());
}
}
}
- 运行结果截取(这里只截取了部分结果,应该是无限循环的)
乌龟领先了,加油.....,当前线程的名称:乌龟线程,当前线程的优先级别:5
乌龟领先了,加油.....,当前线程的名称:乌龟线程,当前线程的优先级别:5
兔子领先了,add oil....,当前线程名称:兔子线程,当前线程的优先级别:5
兔子领先了,add oil....,当前线程名称:兔子线程,当前线程的优先级别:5
使用实现Callable接口的方式创建多线程
- 使用多线程实现龟兔赛跑
public class TortoiseCallable implements Callable {
@Override
public Object call() throws Exception {
while (true) {
System.out.println("乌龟领先了,加油.....," +
"当前线程的名称:" + Thread.currentThread().getName() +
",当前线程的优先级别:" + Thread.currentThread().getPriority());
}
}
public static void main(String[] args) {
TortoiseCallable tortoiseCallable = new TortoiseCallable();
FutureTask futureTask = new FutureTask(tortoiseCallable);
Thread thread = new Thread(futureTask);
thread.setName("乌龟线程");
thread.start();
Thread.currentThread().setName("兔子线程");
while(true){
System.out.println("兔子领先了,add oil....,当前线程名称:"
+Thread.currentThread().getName()+",当前线程的优先级别:"
+Thread.currentThread().getPriority());
}
}
}
- 运行结果截取(这里只截取了部分结果,应该是无限循环的)
乌龟领先了,加油.....,当前线程的名称:乌龟线程,当前线程的优先级别:5
乌龟领先了,加油.....,当前线程的名称:乌龟线程,当前线程的优先级别:5
兔子领先了,add oil....,当前线程名称:兔子线程,当前线程的优先级别:5
兔子领先了,add oil....,当前线程名称:兔子线程,当前线程的优先级别:5
创建三种线程的方式对比
使用实现Runnable、Callable接口的方式创建多线程。
优势
Java的设计是单继承的设计,如果使用继承Thread的方式实现多线程,则不能继承其他的类,而如果使用实现Runnable接口或Callable接口的方式实现多线程,还可以继承其他类。
采用接口能够更好的实现数据共享。线程的启动需要Thread类的start方法,如果采用继承的方式每次新建一个线程时,每个新建线程的数据都会单独的存在线程内存中,这样每个线程会单独的操作自己线程的数据,不能更好的实现线程之间的数据共享。
劣势
- 代码复杂:相对于继承Thread类,实现接口的方式需要编写更多的代码,包括实现接口的方法和代码逻辑。
- 可维护性较差:使用接口创建多线程时,多个线程共享一个任务对象,需要维护共享数据的同步和线程之间的通信,容易出现线程安全问题。
- 没有返回值:实现Runnable接口的线程没有返回值,而实现Callable接口的线程可以返回一个计算结果。
- 使用callable接口需要使用 Executor 执行器来启动线程,然而使用 Thread 类则不需要。
- 由于多个线程共享同一个任务对象,无法直接传递参数给任务对象,需要通过构造方法或者其他方式传递参数。
总的来说,使用实现Runnable、Callable接口的方式创建多线程相对于继承Thread类的方式更加灵活和可扩展,但也增加了代码的复杂性和维护的难度。
使用继承Thread的方式实现多线程测试案例
public class ThreadTest extends Thread{
public int count = 10;
@Override
public void run() {
count--;
System.out.println(this.getName()+"的count:"+count);
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new ThreadTest();//thread1有自己的数据——count
Thread thread2 = new ThreadTest();//thread2有自己的数据——count
thread1.start();
Thread.sleep(100);
thread2.start();
}
}
运行结果
Thread-0的count:9
Thread-1的count:9
使用实现Runnable、Callable接口的方式创建多线程测试案例
public class RunnableTest implements Runnable{
public int count = 10;
@Override
public void run() {
count--;
System.out.println(Thread.currentThread().getName()+"的count:"+count);
}
public static void main(String[] args) throws InterruptedException {
RunnableTest runnableTest = new RunnableTest();
//threa1和threa2共享同一个数据——count
Thread thread1 = new Thread(runnableTest);
Thread thread2 = new Thread(runnableTest);
thread1.start();
Thread.sleep(100);
thread2.start();
}
}
```
**运行结果**
```java
Thread-0的count:9
Thread-1的count:8
使用继承Thread类的方式创建多线程
优势
- 方便且简单:继承Thread类后,只需要重写run方法,将要执行的任务放在其中即可。这种方式更加直观和容易理解,使多线程的编写变得简单和易于实现。
- 线程资源独立:每个继承了Thread类的对象都是一个独立的线程,它们拥有独立的线程栈和资源,彼此之间不会互相影响。这样可以更好地实现并发执行,提高程序的性能和效率。
- 代码结构清晰:使用继承Thread类的方式创建多线程,可以将任务与线程分离,使代码结构更加清晰和易于维护。通过定义一个继承自Thread类的类,并将任务封装在该类的run方法中,可以实现逻辑的封装和复用。
- 更好的面向对象编程:采用继承Thread类的方式创建多线程符合面向对象的设计原则,使得代码更加模块化、可扩展和可维护。同时,可以通过继承Thread类,添加成员变量和方法来满足不同线程的特定需求。
- 充分发挥多核处理器的优势:继承Thread类的方式创建多线程可以更好地利用多核处理器的优势。每个继承了Thread类的对象可以被分配到不同的CPU核心上并行执行,提高程序的并发性和整体性能。
劣势
- 缺乏灵活性:通过继承Thread类创建多线程,线程与任务之间的关系是静态的,无法动态地切换和调整,限制了线程的灵活性。如果需要在多个任务之间共享数据或资源,或者需要动态地调整线程的运行状态,可能需要额外的同步和通信机制。
- 无法被其他类继承:如果程序需要继承其他类来获得更多的功能,例如继承自其他框架或库,那么由于Java不支持多继承,使用继承Thread类的方式就不可行了。
- 高耦合度:通过继承Thread类创建多线程会导致线程与任务之间的代码高度耦合,增加了代码的复杂性和难度。当需要对任务进行更加灵活的管理、组合和协调时,可能需要额外的复杂性处理。
- 可扩展性差:当需要创建大量的线程时,使用继承Thread类的方式会受到操作系统或硬件的限制。每个继承自Thread类的对象都会占用较多的系统资源,可能会导致系统性能下降或无法创建更多的线程。
- 难以复用:继承Thread类创建的线程对象通常与特定的任务紧密绑定,难以复用于其他不同的任务。如果需要在不同的场景中重复使用线程对象,可能需要重新编写代码或添加大量的额外逻辑。
Runnable和Callable的区别
与Runnable相比,Callable功能更强大些
- Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
- call方法可以抛出异常,run方法不可以。
- Callable是支持泛型的
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
Runnable接口源码
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Callable接口源码
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result //可以有返回值
* @throws Exception if unable to compute a result //可以抛出异常
*/
V call() throws Exception;
}
Callable接口使用案例
/*
创建线程的方式三:实现Callable接口。-----JDK1.5新增
*/
//1.创建一个实现Callable的实现类
class NumThread implements Callable{
//2.实现call方法,将此线程需要执行的操作声明在call()中
@Override
public Object call() throws Exception{
int sum=0;
for (int i=1;i<=100;i++){
sum+=i;
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//3.创建Callable接口实现类的对象
NumThread numThread=new NumThread();
//4.将此Callable接口实现类的对象作为传递到FutureTask构造中,创建FutureTask的对象
FutureTask futureTask=new FutureTask(numThread);
//5.将FutureTask的对象组排位哦参数传递到thread类的构造器中,创建Thread对象,并调用start()
new Thread(futureTask).start();
//6.获取Callable中call方法的返回值
//get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
Object sum = futureTask.get();
System.out.println("总和为:"+sum);
}
}
Threa类基本属性和方法(最基本的)
基本属性
/**
* 线程的优先级
* 线程分为10个优先级,分别用整数1——10表示,其中1为最低优先级,10为最高优先级,5为默认优先级,
* 在操作系统中,线程可以划分优先级,线程优先级越高,获得 CPU 时间片的概率就越大,但线程优先级的高低与线程的执行顺序并没有必然联系,优先 * 级低的线程也有可能比优先级高的线程先执行。
*/
// 线程可以具有的最高优先级
static int MAX_PRIORITY
// 线程可以具有的最低优先级
static int MIN_PRIORITY
// 分配给线程的默认优先级(默认为5)
static int NORM_PRIORITY
基本方法
//返回对当前正在执行的线程对象的引用
static Thread currentThread()
//返回该线程的名称
String getName()
//返回线程的优先级
int getPriority()
//如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
void run()
//设置线程名称
void setName(String name)
//设置线程的优先级。
void setPriority(int newPriority)
//使该线程开始执行,Java 虚拟机调用该线程的 run 方法。
void start()