文章目录
前言
1 进程与线程
先介绍一些基本概念。
- 多线程:多就是字面意思,我们只要了解什么是线程就可以了
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
多线程可以简单理解为:应用软件中互相独立,可以同时运行的功能
其实,最简单的理解就是理解为,一个线程 = 一个任务 就好理解了。
有了多线程,我们就可以让程序同时做多件事情 - 进程(操作系统正在运行的一个软件就称为一个进程,例如QQ):进程是程序的基本执行实体(可以这么理解,每一个运行的软件就是一个进程(当然,也有软件使用多进程))
比较直观的进程理解就是,打开我们电脑上的任务管理器,
上图中每一个软件就是一个进程,一个进程下面有多个线程。
也举一个例子:360软件
这4个功能是可以同时运行的,这就是4个线程。所以多线程,可以简单理解:应用软件中互相独立,可以同时运行的功能 - 多线程的应用场景:只要你想让多个事情同时运行就需要用到多线程
比如: 软件中的耗时操作、所有的聊天软件、所有的服务器 - 线程和进程的关系:一个线程一定是属于某个进程,但一个进程可以拥有多个线程。
2 并发与并行
下面这个参考视频我感觉讲的很好,之前对这两个概念一种不是很了解,看了这个视频,感觉一下就通透了。
并发和并行的理解
(1)实际生活中的并发与并行
在讲编程中的并发和并行前,我们先来了解现实案例中的并发与并行。
有两个任务,劈柴和搬砖,一个人单独完成其中某项都是要10分钟完成。
- 并发:现在你只有你一个人去完成这两个任务,你采取了一种特殊的完成方法,频繁切换工作内容,不断的交替执行这两项任务。只要你切换的足够快,那么从宏观上来看的话,这两个任务是在同时被执行,但时间上任意一个时刻都只有其中一个任务在被推进。这就是我们现实生活中的并发。
- 并行:现在假设你的朋友过来帮你一起执行这两个任务,那么你去搬砖,你的朋友去劈柴。这种情况下,两个任务真正的同时被推进,这就是所谓的并行。
(2)编程中的并发与并行
回到编程中的场景:
-
cpu中的一个核心就相当于上面案例中的一个人,一个cpu核心 = 一个人
-
系统中的一个线程thread就相当于上面案例中的一个任务,一个线程 = 一个任务
线程就是cpu需要执行的任务。
a. 最早期的单核CPU情形下,我们如果有多个任务需要执行(多线程),很显然只能用并发的形式去执行这些线程(任务),不得不频繁切换工作任务(线程)。
假设现在系统中qq有两个任务(两个线程)要执行,但是由于我们是单核CPU,只有一个人能干活。那么这个单核cpu就不得不采用并发,频繁切换线程(工作任务)。单核cpu在这两个任务中切换的足够快到人感知不出来,所以从宏观上来看,qq中有两个程序在同时运行。
b. 现在都是多核CPU
不妨设我们是双核cpu
还是假设现在系统中qq有两个任务(两个线程)要执行,但是由于我们是双核CPU,有两个人能干活。那就简单了。一个核心去干这个线程,另一个核心去干另一个线程。此时两个线程(任务)同时推进,这就形成了并行。
更复杂一点就是并行和并发同时存在
以2核4线程计算机举例,2核指有两个干活的人,4线程就是该cpu最多能干的活的个数
现在如果还是qq中有两个任务,那简单,一个核心干一个,直接并行就解决了
但是如果是4个任务呢?很显然两个核心,人不够啊,那么这么干,核心1在任务1和任务2之间频繁切换干,核心2在任务3和任务4间频繁切换干。这种情况显然是并行和并发同时存在的。
也行你会问,如果有5个任务呢?很抱歉2核4线程计算机不能再宏观上看似在同时执行4个任务,其中一个任务会等前面有一个任务结束(而且现在还有一种新技术就是一个核心可以同时干两个线程,就相当于一个核心=2个人这种,这里我们不管这么多,我们暂时就将一个核心=1个人就可以了)
还有更加复杂的,上述的所有案列都是在一个假设情况下的,所以线程(任务)相互独立,没有依赖关系。如果有依赖关系呢?那就更复杂了
c. 任务(线程)间存在依赖关系
在编程中的处理方案就是,存在依赖关系的线程之间有一把锁,只有拿到锁的线程才可以运行。
存在依赖关系的线程是不能够并行的,依赖就意味着存在执行的先后关系。
即使是两个线程(任务)属于不同cpu核心,但锁的机制保证,就算你两是不同人来干,但是也只有拿到锁的那个任务才可以干。
- 最后回答一个问题:关于程序员在开发中是否要关心并发和并行者两个概念?
答:大部分情况下并不需要关心其细节,因为操作系统的存在,关于cpu核心和线程的调度问题系统会自动帮我们调度。
一、Java中的三种多线程实现方式
1 多线程的第一种实现方式:继承Thread类的方式进行实现
将一个类声明为 Thread 的子类。该子类应重写类 Thread 的 run 方法(run方法里面写我们具体要执行的代码,要干的任务是什么就可以了)。然后再调用start方法启动这个线程就可以了(特别注意是调用start方法,不是run方法)。
package cn.hjblogs.business;
public class Test {
public static void main(String[] args) {
Mythread t1 = new Mythread();
t1.start();
}
}
class Mythread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 重写run方法,run方法中的代码就是线程需要执行的代码,也就是这个线程要干的任务
System.out.println(this.getName() + ": Hello World!");
}
}
}
我们可以用线程来验证一下,多线程是交替切换运行的
public class Test {
public static void main(String[] args) {
Mythread t1 = new Mythread();
Mythread t2 = new Mythread();
// 设置线程名称,可取可不取,里面有默认的线程名称
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Mythread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
// 重写run方法,run方法中的代码就是线程需要执行的代码,也就是这个线程要干的任务
System.out.println(this.getName() + ": Hello World!");
}
}
}
2 多线程的第二种实现方式:实现Runnable接口(函数式接口)的方式进行实现
创建线程的另一种方法是声明一个实现 Runnable 接口的类。然后该类实现 run 方法。(run方法里面写我们具体要执行的代码,要干的任务是什么就可以了)。然后将这个实现类对象传给Thread就构建了一个线程,然后再调用start方法启动这个线程就可以了(特别注意是调用start方法,不是run方法)。
可以发现和继承那种写法基本一致,一个是继承重写run方法,这个是实现接口重新run方法;另外这种实现接口的方式多了一步,将实现类对象传给Thread对象。
public class Test {
public static void main(String[] args) {
Mythread mythread = new Mythread();
// 多了一步,将mythread对象传入Thread类中,然后调用start方法
Thread t1 = new Thread(mythread);
t1.start();
}
}
class Mythread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello world");
}
}
}
这么一看似乎这种调用方式没有第一种直接继承Thread类好用啊,但是接口接口,你如果想一想匿名内部类这种调用方式就有意义了
Runnable接口是一个函数式接口,那么使用匿名内部类,lambda表达式、方法引用就有意思了。
public class Test {
public static void main(String[] args) {
// 使用匿名内部类
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello");
}
}
});
t1.start();
// // 使用lambda表达式
// Thread t1 = new Thread(() -> {
// for (int i = 0; i < 10; i++) {
// System.out.println("hello");
// }
// });
// t1.start();
}
}
public class Test {
public static void main(String[] args) {
// 使用匿名内部类
Thread t1 = new Thread(Test::run);
t1.start();
}
public static void run() {
for (int i = 0; i < 10; i++) {
System.out.println("hello");
}
}
}
这才是这种方法的正确打开方式
3 多线程的第三种实现方式:实现Callable接口(优点:可以获得线程的返回值)
可以看到在前面两种方法中,run方法没有返回值,这意味着我们无法获得线程的返回值,如果我们想要获得返回值的话就需要采取这种方法。下面是步骤:
- step1: 创建一个类MyCallable实现callable接口
- step2: 重写call(是有返回值的,表示多线程运行的结果)
- step3: 创建MyCallable的对象(表示多线程要执行的任务)
- step4: 创建FutureTask的对象(作用管理多线程运行的结果)
- step5: 创建Thread类的对象,并启动(表示线程)
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建MyCallable对象(表示多线程要执行的任务)
MyCallable mc = new MyCallable();
// 创建FutureTask对象(作用管理多线程运行的结果),FutureTask是RunnableFuture接口的实现类
FutureTask<Integer> ft = new FutureTask<>(mc);
// 创建Thread对象,传入FutureTask对象
Thread t1 = new Thread(ft);
// 启动线程
t1.start();
// 获取线程执行结果
Integer res = ft.get();
System.out.println(res); // 5050
}
}
class MyCallable implements Callable<Integer> {
// 这里的泛型参数是返回值类型
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}
4 多线程的三种实现方式对比
优点 | 缺点 | |
---|---|---|
继承Thread类 | 编程比较简单,可以直接使用 ,编程比较简单,可以直接使用 | 可以扩展性较差,不能再继承其他的类 |
实现Runnable接口 | 扩展性强,实现该接口的同时还可以继承其他的类 | 编程相对复杂,不能直接使用 ,Thread类中的方法 |
实现Callable接口 | 扩展性强,实现该接口的同时还可以继承其他的类。最重要的是能返回多线程的结果 | 编程相对复杂,不能直接使用 ,Thread类中的方法 |
二、多线程的常用方法
方法名称 | 说明 |
---|---|
string getName() | 返回此线程的名称 |
void setName(string name) | 设置线程的名字(构造方法也可以设置名字) |
static Thread currentThread() | 获取当前线程的对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon( boolean on) | 设置为守护线程 |
public static void yield() | 出让线程/礼让线程 |
public static void join() | 插入线程/插队线程 |
1 线程的name
细节:如果我们没有给线程设置名字,线程也是有默认的名字的。格式: Thread-X(X序号,从0开始的)
(1)string getName() :返回此线程的名称
(2) void setName(string name):设置线程的名字(构造方法也可以设置名字)
2 两个静态方法 :获取当前线程对象和线程休眠
(1)static Thread currentThread():获取当前线程的对象
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 获取当前正在执行的线程对象
Thread cur_t = Thread.currentThread();
System.out.println(cur_t.getName()); // main
// 细节
// JVM虚拟机启动之后,会自动的启动多条线程其中有一条线程就叫做main线程
// 他的作用就是去调用main方法,并执行里面的代码
// 在以前,我们写的所有的代码,其实都是运行在main线程当中
}
}
(2)static void sleep(long time) :让线程休眠指定的时间,单位为毫秒
细节:
- 哪条线程执行到这个方法,那么哪条线程就公在这里停留对应的时间方法的参数:就表示唾I眠的时间,单位毫秒(1秒= 1800毫秒)
- 当时间到了之后,线程会自动的醒来,继续执行下面的其他代码
我们先来休眠我们的main线程试试
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println("main线程开始执行了");
Thread.sleep(5000); // 让main线程睡眠5秒
System.out.println("main休眠5秒后又开始执行了"); // 等5秒后才会执行这个打印代码
}
}
在来看我们自己的线程
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("当前线程名字:" + Thread.currentThread().getName() + " i=" + i);
try {
Thread.sleep(2000); // 线程休眠2秒在进行下一轮打印
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
3 线程的优先级(随机抢占cpu资源)
线程的优先级是什么,我们要知道在多线程的情况下,cpu的资源是有限的。所以Java采用的是枪占式调度(随机性),哪一个线程抢到了cpu资源这个时刻就归它用。既然是大家一起抢,那肯定可以设置有的人抢到的概率大,有的人抢到的概率小啊。
这里的线程优先级就是线程抢占cpu资源的级别,从底到高,一共 1-10级。
默认情况下我们不对线程的优先级进行设置,默认都是5(就算是main线程也是5)
(1)final int getPriority() :获取线程的优先级
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
int priority = Thread.currentThread().getPriority();
System.out.println("当前线程的优先级:" + priority); // 当前线程的优先级:5
}
}
(2)setPriority(int newPriority) :设置线程的优先级
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.setName("飞机");
t2.setName("火车");
t1.setPriority(10); // 飞机这个线程优先级我们设置为10,最高
t2.setPriority(1); // 火车这个线程优先级我们设置为1,最低
t1.start();
t2.start();
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + " i=" + i);
}
}
}
多运行几次,可以看到火车这个线程最后完成的概率比较大。但这不是绝对的,也是一直概率情况。毕竟火车这条线程抢cpu资源的能力按上面设置是抢不过飞机这条线程的。
4 设置为守护线程
(1)设置为守护线程:final void setDaemon( boolean on) – 设置为守护线程
- 守护线程:当其他的非守护线程执行完毕之后,守护线程会陆续结束。
public class Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
// 将备胎线程设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
}
}
class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i <= 10; i++) {
System.out.println(Thread.currentThread().getName() + " i=" + i);
}
}
}
class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
System.out.println(Thread.currentThread().getName() + " i=" + i);
}
}
}
从上面结果看出,非守护线程(女神)结束过后大概过了一小会守护线程(备胎)也结束了,并且守护线程才进行到25轮循环,都还没有运行完,这就是将一个线程设置成守护线程的效果。
(2)设置为守护线程的应用场景
这个有什么应用场景呢?
在qq聊天的场景中: