目录
一.前言
1.关于进程调度
(1)为什么要调度?
(2)调度的真正对象
(3)调度的资源
2.线程
(1).线程的写法
(2)线程创建的方法
1.继承Thread
(1)使用继承Thread,重写run的方式来创建线程
(2)继承Thread,使用匿名内部类
2.实现Runnable
(1)使用实现Runnable,重写run
(2)实现Runnable,使用匿名内部类
3.基于lambda表达式
4.实现callable
三.优缺点总结
继承Thread类
实现Runnable接口
基于Lambda表达式
实现Callable接口
四.总结体会
一.前言
1.关于进程调度
(1)为什么要调度?
通俗来说,就是狼多肉少.
计算机中的CPU,内存等资源都是很有限的.
(2)调度的真正对象
CPU是按照并发的方式来执行进程的
引入进程,就是为了能够实现多个任务并发执行这样的效果
进程有个重大的问题就是比较重量,如果频繁的创建/销毁进程,成本会比较高
进程里面包括线程,一个进程里可以有一个线程,或者多个线程
每个线程都是一个独立的执行流.多个线程之间,也是并发执行的
多个线程可能是在多个 CPU 核心上, 同时运行
也可能是在一个 CPU 核心上, 通过快速调度,进行运行
操作系统,真正调度的,是在调度线程,而不是进程
线程是 提作系统 调度运行 的基本单位
进程是 操作系统 资源分配 的基本单位
前面所说的进程调度,指的是这些进程里面只有一个线程
(3)调度的资源
当我们创建了一个进程之后,操作系统会创建一个PCB,把这个PCB加入到链表上
PCB中提供了一些属性,进程的优先级,进程的状态,进程的上下文,进程的记账信息...
一个进程中的多个线程之间,共用同一份系统资源
1)内存空间
2) 文件描述符表
只有在进程启动,创建第一个线程的时候,需要花成本去申请系统资源一旦进程(第一个线程)创建完毕,此时,后续再创建的线程,就不必再申请资源了,创建/销毁 的效率就提高了不少了.
既然线程的效率这么高,那是不是线程越多越好呢?
当然不是
CPU的核心数是固定的,此时创建出大量线程,没法立即被处理的线程就只能阻塞等待,就算此时强行进行调度,调度上了一个线程,那也势必会挤掉其它线程,总并发程度仍然是固定的.
真正有效果的是,再搞几个CPU,也就是再搞一个主机,这也就是我们所说的分布式系统
关于分布式系统,详情可见我的另一篇文章http://t.csdn.cn/DdQHj
由于线程就是进程的一部分,因此,如果一个线程出现异常,那么很有可能其它线程也会不能运行.
因此,我们要能够明确区分进程和线程之间的区别:
- 进程包含线程
- 进程有自己独立的内存空间和文件描述符表.同一个进程中的多个线程之间,共享同一份地址空间和文件描述符表
- 进程是操作系统资源分配的基本单位,线程是操作系统调度执行的基本单位.
- 进程之间具有独立性,一个进程挂了,不会影响到别的进程:同一个进程里的多个线程之间,一个线程挂了,可能会把整个进程带走,影响到其他线程的
那么Java怎么进行多线程编程呢?
首先,大家可能会有一个疑问,为什么Java不学习多进程编程呢?
虽然Java里提供了一组多进程编程的API,但是JDK里面没有封装这些多进程的API,因此Java里不提倡多进程编程.
2.线程
接下来,我们就来具体学习多线程编程
(1).线程的写法
我们先来了解一下线程.
Java标准库里提供了一个类Thread能够表示一个线程.
package thread;
class MyThread extends Thread{
@Override
public void run(){
System.out.println("Hello Thread");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t=new MyThread();
t.start();
}
}
我们来分析这段代码
上述代码涉及到两个线程
1.main方法所对应的线程(一个进程里面至少有一个线程),也可以称为主线程
2.通过t.start创建新的线程
我们现在对代码进行调整,具体体会一下,"每一个线程是一个独立的执行流"
代码如下:
package thread;
class MyThread extends Thread{
@Override
public void run(){
while(true){
System.out.println("Hello Thread");
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t=new MyThread();
t.start();
while(true){
System.out.println("hello main");
}
}
}
运行结果如下
我们可以看到,hello Thread 和hello main都能打印出来
run()方法
run叫做入口方法,不是构造方法
run方法不是我们随便写的一个方法,是重写了父类的方法
这种重写一般是功能的扩展,一般这样的重写方法不需要我们自己手动调用,已经有其它代码来调用
run方法可以成为是一个特殊的方法,也就是线程的入口方法
而start方法,是调用操作系统中的api,创建新线程,新的线程里面调用run方法
(2)线程创建的方法
线程创建主要有以下几种方法:
1.继承Thread
2.实现Runnable
3.基于lambda
4.实现callable
接下来,我们来详细介绍这几类方法
1.继承Thread
(1)使用继承Thread,重写run的方式来创建线程
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("Hello t");
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
Thread t=new MyThread();
//start会创建新的线程
t.start();
//run不会创建新的线程,run是在main的线程中执行的
while(true){
System.out.println("Hello main");
}
}
}
运行结果如下:
同时打印的原理是由于两个线程在同时执行,并且每个线程都有自己的输出流。在这段代码中,主线程和MyThread线程都在执行无限循环,分别打印"Hello main"和"Hello t"。
当两个线程同时执行时,它们会竞争CPU的资源,操作系统会根据调度算法来决定哪个线程获得CPU的执行权。由于线程的执行速度非常快,所以看起来就像是同时执行。
那这里的hello main的打印和hello t的打印有什么规律吗?
实际上是没有的,这是由于调度的随机性
当两个线程同时执行时,它们会竞争CPU的资源,操作系统会根据调度算法来决定哪个线程获得CPU的执行权。由于线程的执行速度非常快,所以看起来就像是同时执行。
每个线程都有自己的输出流,所以它们可以独立地打印输出。
当主线程执行System.out.println("Hello main")时,它会将输出发送到主线程的输出流中。而MyThread线程执行System.out.println("Hello t")时,它会将输出发送到MyThread线程的输出流中。
由于输出流是独立的,所以两个线程的输出可以同时显示在控制台上。但是由于输出的速度和顺序是不确定的,所以可能会出现交错的情况,即"Hello t"和"Hello main"的输出顺序可能会不一致。
(2)继承Thread,使用匿名内部类
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t=new Thread(){
@Override
public void run() {
while(true){
System.out.println("Hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//sleep睡眠过程中就将其打断
// throw new RuntimeException(e);
e.printStackTrace();
}
}
}
};
t.start();
while(true){
System.out.println("Hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//sleep睡眠过程中就将其打断
// throw new RuntimeException(e);
e.printStackTrace();
}
}
}
}
运行结果如下:
2.实现Runnable
(1)使用实现Runnable,重写run
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("Hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//sleep睡眠过程中就将其打断
// throw new RuntimeException(e);
e.printStackTrace();
}
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
MyRunnable runnable=new MyRunnable();
Thread t=new Thread(runnable);
t.start();
while(true){
System.out.println("Hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//sleep睡眠过程中就将其打断
// throw new RuntimeException(e);
e.printStackTrace();
}
}
}
}
我们可以看到打印的结果如下:
Runnable的字面意思是可运行的,使用Runnable 来描述一个具体的任务
第一种写法是使用Thread的run来描述线程入口
这一种是使用Runnable interface 描述线程入口
这两种方法之间并没有本质区别,只是使用方法的不同
(2)实现Runnable,使用匿名内部类
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
while(true){
System.out.println("Hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//sleep睡眠过程中就将其打断
// throw new RuntimeException(e);
e.printStackTrace();
}
}
}
});
t.start();
while(true) {
System.out.println("Hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//sleep睡眠过程中就将其打断
// throw new RuntimeException(e);
e.printStackTrace();
}
}
}
}
运行结果如下:
3.基于lambda表达式
这个方法是创建线程最推荐的写法,使用lambda表达式,这也是最简单直观的写法.
在Java里面,函数(方法)是无法脱离类的,但是lambda就相当于一个例外,所以这样的函数一般都是一次性的,用完就会被销毁.
lambda表达式的基本写法
()->{
}
()里面放参数,如果只有一个参数,可以省略()
{}里面存放函数体,如果这里面只有一行代码,也可以省略{}
举一个代码例子:
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t=new Thread(() -> {
while(true){
System.out.println("Hello t");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//sleep睡眠过程中就将其打断
// throw new RuntimeException(e);
e.printStackTrace();
}
}
});
t.start();
while(true) {
System.out.println("Hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//sleep睡眠过程中就将其打断
// throw new RuntimeException(e);
e.printStackTrace();
}
}
}
}
运行结果如下:
4.实现callable
Callable的用法非常类似于Run
使用Runnable写出的代码描述了一个任务,也就是一个线程要做什么.
然而,Runnable通过run方法描述,返回类型是void.但是很多时候,我们是希望任务有返回值的.二+而callable的call方法就是由返回值的.
比如说,我们写个代码,创建一个线程,用这个来计算1+2+...+1000.
我们来看具体的代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class ThreadDemo27 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
int sum = 0;
@Override
public Integer call() throws Exception {
for (int i = 1; i <= 1000; i++) {
sum += i;
}
return sum;
}
};
//找一个线程完成这个任务
//Thread不能直接传入callable,需要再包装一层
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
System.out.println(futureTask.get());
}
}
运行结果如下:
我们对代码进行分析:
三.优缺点总结
-
继承Thread类
- 优点:继承Thread类可以直接重写run()方法,非常简单直观。
- 缺点:由于Java不支持多继承,所以如果使用继承Thread类创建线程,就无法再继承其他类。
-
实现Runnable接口
- 优点:实现Runnable接口可以避免单继承的限制,可以继续继承其他类。
- 缺点:需要额外定义一个类来实现Runnable接口,并重写run()方法。
-
基于Lambda表达式
- 优点:使用Lambda表达式可以更简洁地创建线程,不需要显式地创建一个新的类或实现接口。
- 缺点:Lambda表达式可能会降低代码的可读性,特别是对于复杂的线程逻辑。
-
实现Callable接口
- 优点:Callable接口可以返回线程执行的结果,可以通过Future对象获取线程的返回值。
- 缺点:使用Callable接口创建线程相对复杂,需要使用ExecutorService来执行Callable任务,并获取返回值。
四.总结体会
继承Thread类和实现Runnable接口是最常见的线程创建方法,它们都可以实现多线程的功能。
使用Lambda表达式可以简化线程的创建过程,特别适合简单的线程逻辑。
实现Callable接口可以获取线程的返回值,适用于需要线程执行结果的场景。
在选择线程创建方法时,我们需要根据具体的需求和代码结构来选择合适的方法。