文章目录
多线程
1.1简单了解多线程
是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提高性能。
举个例子,如上图,你正在开开心心的打游戏,旁边有可乐,好烟伺候着,是不是很爽啊~~此时你的右手一会点鼠标,一会又拿烟,一会又在取可乐,忙的不亦说乎,假如你的右手足够快,上面三种动作相当于同时进行,你的右手就相当于电脑的 CPU,鼠标,好烟,可乐相当于在电脑中实际运行的三个程序,也就是相当于三个进程,电脑上CPU执行进程的操作非常快,以至于我们会误以为多个进程在同时执行,实际上,在某个确定时刻,CPU只会执行一个进程。
1.2并发和并行
-
并行:在同一时刻,有多个指令在多个CPU上同时执行。
举个例子,饭店厨房,有三个厨师分别在做西红柿炒番茄、青椒肉丝不放青椒不放肉丝(ps:有的朋友就想说了,那你丫的还炒什么饭),海参炒饭,此时三个厨师同时在做三种饭,这就是并行关系。 -
并发:在同一时刻,有多个指令在单个CPU上交替执行。
接着上面的例子继续讲,突然老板把其他两个厨师叫走, 只剩下这一个厨师,继续做饭,此时,他需要一会做西红柿炒番茄,一会去做青椒肉丝,一会又去做海参炒饭,忙的满头大汗,嚷着让老板加钱(ps:当然老板是不会加的),这一个厨师同时做三种饭,就相当于并发关系。
1.3进程和线程
-
进程:是正在运行的程序
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
- 动态性:进程的实质是程序的一次执行过程,进程是动态性产生,动态消亡的。
- 并发性:任何进程都可以同其他进程一起并发执行。
-
线程:是进程中的单个顺序控制流,是一条执行路径。
- 单线程:一个进程如果只有一条执行路径,则称为单线程程序。
- 多线程:一个进程如果有多条执行路径,则称为多线程程序。
举个例子,360安全卫士就相当于一个多线程程序,里边的木马查杀、电脑清理、系统修复、优化加速等等功能就相当于各个线程。
1.4实现多线程方式一:继承 Thread类
- 方法介绍
方法名 | 说明 |
---|---|
void run() | 在线程开启后,此方法将被调用执行 |
void start() | 使此线程开始执行,Java虚拟机会调用 run 方法() |
- 实现步骤
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写 run()方法
- 创建MyThread类的对象
- 启动线程
- 代码演示
public class MyThread extends Thread {
@Override
public void run() {
//代码就是线程在开启之后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("线程开启了"+ i);
}
}
}
public class Demo {
public static void main(String[] args) {
//创建一个线程对象
MyThread t1 = new MyThread();
//创建一个线程对象
MyThread t2 = new MyThread();
// 开启一条线程
t1.start();
//t1.run(); 表示的仅仅是创建对象,用对象去调用方法,并没有开启多线程。
//开启第二条线程
t2.start();
}
}
- 运行结果
注意:
1.多线程程序涉及到CPU的切换,cpu在多个线程之间切换是随机的。
2.如果我们不去调用start()方法开启线程,直接去调用MyThread类中重写的run方法例如这样:
public class Demo1 {
public static void main(String[] args) {
//创建一个线程对象
MyThread t1 = new MyThread();
//创建一个线程对象
MyThread t2 = new MyThread();
t1.run();
t2.run();
}
}
这样表示的仅仅只是创建对象,用对象去调用方法,并没有开启多线程。
运行结果:
- 两个小问题
- 为什么要重写 run() 方法?
- 因为 run() 是用来封装被线程执行的代码
- run() 方法和 start() 方法的区别?
- run() :封装线程执行的代码,直接调用,相当于普通方法的调用。
- start() :启动线程;然后由 JVM 调用此线程的 run() 方法
- 为什么要重写 run() 方法?
1.5实现多线程的方式二:实现 Runnable接口
- Thread构造方法
方法名 | 说明 |
---|---|
Thread(Runnable target) | 分配一个新的Thread对象 |
Thread(Runnable target,String name) | 分配一个新的Thread对象 |
- 实现步骤
- 定义一个类 MyRunnable 实现 Runnable 接口
- 在 MyRunnable 类中重写 run() 方法
- 创建 MyRunnable 类的对象
- 创建 Thread 类的对象,把 MyRunnable对象作为构造方法的参数
- 启动线程
- 代码演示
public class MyRunnable implements Runnable {
@Override
public void run() {
//表示线程启动后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"第二种方式实现多线程"+ i);
}
}
}
public class Demo {
public static void main(String[] args) {
//创建了一个参数对象
MyRunnable mr = new MyRunnable();
//创建了一个线程对象,并把参数传递给这个线程
//在线程启动之后,执行的就是参数里面的 run 方法
Thread t1 = new Thread(mr);
//开启线程
t1.start();
MyRunnable mr2 = new MyRunnable();
Thread t2 = new Thread(mr2);
t2.start();
}
}
- 运行结果
注意:
Thread.currentThread().getName()是获取线程的名称,此时我们没有设置线程的名称,通过查看源码,默认情况下线程名称为 Thread-后边的数字(默认从零开始)
我们也可以通过第二个构造方法,给线程设置名称,代码如下:
public class Demo1 {
public static void main(String[] args) {
//创建了一个参数对象
MyRunnable mr = new MyRunnable();
//创建了一个线程对象,并把参数传递给这个线程
//在线程启动之后,执行的就是参数里面的 run 方法
Thread t1 = new Thread(mr,"线程一");
//开启线程
t1.start();
MyRunnable mr2 = new MyRunnable();
Thread t2 = new Thread(mr2,"线程二");
t2.start();
}
}
运行结果:
1.6实现多线程方式三:实现Callable接口
- 方法介绍
方法名 | 说明 |
---|---|
V call() | 计算结果,如果无法计算结果,抛出一个异常 |
FutureTask(Callable callable) | 创建一个 FutureTask,一旦运行就执行给定的 Callable |
V get() | 如有必要,等待计算完成,然后获取其结果 |
- 实现步骤
- 定义一个类 MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类 FutureTask对象,把MyCallable对象作为构造方法的参数
- 启动线程
- 再调用 get 方法,就可以获取线程结束之后的结果。
- 代码演示
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白"+i);
}
//返回值就表示线程运行完毕之后的结果
return "答应";
}
}
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启之后需要执行里边的call方法
MyCallable mc = new MyCallable();
//可以获取线程执行完毕之后的结果,也可以作为参数传递给 Thread对象
FutureTask<String> ft = new FutureTask<>(mc);
//创建线程对象
Thread t1 = new Thread(ft);
//开启线程
t1.start();
String s = ft.get();
System.out.println(s);
}
}
- 运行结果
注意:
为什么创建的FutureTask对象可以传给 Thread类的对象作为参数呢?
在这里我们去查询API
通过查询可以发现,类FutureTask< V>不光实现了 Future接口,也实现了Runnable接口,这也是为什么创建的FutureTask对象可以传给 Thread类的对象作为参数了。
1.7三种实现方式的对比
- 实现 Runnable、Callable接口
- 好处:扩展性强,实现该接口的同时还可以继承其他的类 。
- 缺点:编程相对复杂,不能直接使用 Thread 类中的方法。
- 继承 Thread 类
- 好处:编程比较简单,可以直接使用 Thread类中的方法。
- 缺点:可以扩展性较差,不能再继承其他的类
1.8设置和获取线程名称
- 方法介绍
方法名 | 说明 |
---|---|
void setName(String name) | 将此线程的名称更改为参数 name |
String getName() | 返回此线程的名称 |
Thread currentThread() | 返回对当前正在执行的线程对象的引用 |
- 代码演示
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"@@@"+i);
}
}
}
public class Demo {
//1.线程是有默认名字的,格式:Thread-编号
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.setName("小蔡");
t2.setName("小强");
t1.start();
t2.start();
}
}
- 运行结果
我们也可以通过 构造方法 传参的形式设置线程的名字
public class MyThread extends Thread {
public MyThread() {
}
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"@@@"+i);
}
}
}
public class Demo {
//1.线程是有默认名字的,格式:Thread-编号
public static void main(String[] args) {
MyThread t1 = new MyThread("小蔡");
MyThread t2 = new MyThread("小强");
t1.start();
t2.start();
}
}
public class Demo {
public static void main(String[] args) {
//返回当前正在执行的线程对象的引用
String name = Thread.currentThread().getName();
System.out.println(name);
}
}
运行结果:
1.9线程休眠
- 相关方法
方法名 | 说明 |
---|---|
static void sleep(long millis) | 使当前正在执行的线程停留(暂停执行)指定的毫秒数 |
- 代码演示
public class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
System.out.println("睡觉前");
Thread.sleep(3000);
System.out.println("睡醒了");
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
- 运行结果
注意:
睡觉前打印后不会立马打印睡醒了,会间隔三秒,run() 方法中的输出语句会间隔 100毫秒再打印。
1.10线程优先级
-
线程调度
- 两种调度方式
- 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片。
- 抢占式调度模型:优先让优先级高的的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些。
- Java使用的是抢占式调度模型
- 随机性
- 假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU 的时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到 CPU 的使用权是不一定的。
- 假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到 CPU 的时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到 CPU 的使用权是不一定的。
- 两种调度方式
-
举个例子来理解分时调度模型和抢占式调度模型
如上图,一个拳头准备打底下的几个人(ps:是不是一个比一个贱啊),分时调度模型就是 拳头会挨个打每一个人,而抢占式调度模型就是 当第一个人比其他人更贱时,他的优先级更高,可能当拳头打完第一个人和第二个人时,又返回来打第一个人(ps:显然他更欠打),这里的拳头就相当于 CPU,而每个人就相当于多个线程。 -
优先级相关方法
方法名 | 说明 |
---|---|
final int getPriority() | 返回此线程的优先级 |
final void setPriority(int newPriority) | 更改此线程的优先级线程默认优先级是5;线程优先级的范围是: 1-10 |
- 代码演示
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"----"+i);
}
return "线程执行完毕了";
}
}
public class Demo {
public static void main(String[] args) {
//优先级:1 - 10 默认值:5
MyCallable mc = new MyCallable();
FutureTask<String> ft=new FutureTask<>(mc);
Thread t1 = new Thread(ft);
t1.setName("飞机");
t1.setPriority(10);
// System.out.println(t1.getPriority());
t1.start();
MyCallable mc2 = new MyCallable();
FutureTask<String> ft2=new FutureTask<>(mc2);
Thread t2 = new Thread(ft2);
t2.setName("坦克");
t2.setPriority(1);
// System.out.println(t2.getPriority());
t2.start();
}
}
-
运行结果
注意:
线程的优先级越高,只能说明它抢到CPU的几率越高,虽然线程坦克优先级比线程飞机高,但是线程飞机先抢到的 CPU使用权。我们查看 Thread类的源码,可以看到线程优先级的范围。
1.11守护线程
-
相关方法
方法名 | 说明 |
---|---|
void setDaemon(boolean on) | 将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出 |
- 代码演示
public class MyThread1 extends Thread {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+"---"+ i);
}
}
}
public class MyThread2 extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName()+"---"+i);
}
}
}
public class Demo {
public static void main(String[] args) {
MyThread1 t1 = new MyThread1();
MyThread2 t2 = new MyThread2();
t1.setName("女神");
t2.setName("备胎");
//把第二个线程设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
}
}
- 运行结果
注意:
当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了,不会立马停止。
举个例子:
当我们退出qq 之前,有一个聊天界面正在桌面上,当我们退出qq时,聊天页面也会消失不见,此时聊天界面就相当于守护线程。
总结
本文讲述了并发和并行,进程和线程的相关概念,并举了形象的例子以便于读者理解,还讲述了实现多线程的三种方式,以及三种方式对比之下的优缺点,并且介绍了线程休眠、线程优先级、守护线程的相关知识点,最后,希望大家多多关注,你们的支持,是我更新的动力!