1. 线程简介
- 需要学习什么是任务 进程 线程 多线程
1. 多任务
现实中太多这样同时在做多件事情的例子了,看起来是多个任务都在做,其实本质上我们的大脑在同一时间依旧只做了一件事情。
2. 多线程
原来是只有一条路,慢慢因为车太多了,道路阻塞,效率极低。为了提高使用的效率,能够充分利用道路,于是加了多个车道。
生活实例:玩王者荣耀一样,多个人能同时玩游戏。
普通方法调用和多线程调用分析:
3. 进程
- 程序:程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
- 进程:是执行程序的一次执行过程。 一个进程中可以包含若干个 线程 ,一个进程中至少有一个线程。
是系统资源分配的单位
注意:
- 很多多线程都是模拟出来的,真正的多线程指有多个 cpu,即多核,如服务器。
- 如果是模拟出来的多线程,即在一个 cpu 的情况下,在同一个时间点,cpu 只能执行一个代码,因为切换的很快,所以就有同时执行的错局。
4. 总结
- 线程就是独立的执行路径。
- 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc 线程。
- main() 称之为主线程,为系统的入口,用于执行整个程序。
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序不能人为的干预。
- 对于同一份资源操作时,会存在资源掠夺问题,需要加入并发控制。
- 线程会带来额外的开销,如 cpu 调度时间,并发控制开销等。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
2. 线程的创建
1. 继承 Thread 类(重点)
- 开发步骤:
1. 自定义线程类继承 Thread 类
2. 重新 run() 方法,编写线程执行体
3. 创建线程对象,调用 start() 方法启动线程
代码实现:
package com.baiyi.createthread;
/**
* @author 白衣
* @Description: 第一种方式继承 Thread 类 创建线程
* @Date 2020/11/22
*/
public class ExtendThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("child thread" + i);
}
}
public static void main(String[] args) {
ExtendThread method1 = new ExtendThread();
method1.start();
for (int i = 0; i < 200; i++) {
System.out.println("parent thread" + i);
}
}
}
结果分析:
案例:网图下载
- 步骤分析
1. 导入 Apache commons-io 包
2. 使用 FileUtils.copyURLToFile(url, fileName) 工具类
3. 通过多线程进行下载
- 结果分析
- 下载图片的顺序是不确定的,理论上是 1.jpg --> 2.jpg --> 3.jpg
实际上是 不确定的顺序
package com.baiyi.exercise;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
/**
* @author 白衣
* @Description: 使用多线程进行网图下载练习
* @Date 2020/11/22
*/
public class PictureDownload extends Thread{
private String url;
private String name;
public PictureDownload(String url, String name){
this.url = url;
this.name = name;
}
@Override
public void run() {
WebDownLoader webDownLoader = new WebDownLoader();
webDownLoader.downloader(url, name);
System.out.println("下载了文件名为: " + name);
}
public static void main(String[] args) {
PictureDownload t1 = new PictureDownload("https://picsum.photos/id/1/200/300", "1.jpg");
PictureDownload t2 = new PictureDownload("https://picsum.photos/id/2/200/300", "2.jpg");
PictureDownload t3 = new PictureDownload("https://picsum.photos/id/3/200/300", "3.jpg");
t1.start();
t2.start();
t3.start();
}
}
class WebDownLoader{
/**
* @Author 白衣
* @Description 网图下载
* @Date 2020/11/22
* @param url 下载地址
* @param name 文件名称
* @return void
*/
public void downloader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO 异常, downloader 方法出现问题");
}
}
}
2. 实现 Runnable 接口(重点)
- 推荐使用 Runnable 接口
- 开发步骤:
1. 定义 MyRunnable 类实现 Runnable 接口
2. 重写 run() 方法,编写线程执行体
3. 创建线程对象,执行线程需要丢入 runnable 接口实现类
4. 调用 start() 方法启动线程
package com.baiyi.createthread;
/**
* @author 白衣
* @Description: 创建线程的第二种方式 实现 Runnable 接口
* @Date 2020/11/22
*/
public class RunnableImpl implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("child thread " + i);
}
}
public static void main(String[] args) {
RunnableImpl runnable = new RunnableImpl();
// 创建线程对象, 通过线程对象来开启我们的线程, 代理
new Thread(runnable).start();
for (int i = 0; i < 200; i++) {
System.out.println("parent thread " + i);
}
}
}
案例:龟兔赛跑
1. 首先来个赛道距离,然后要离终点越来越近
2. 判断比赛是否结束
3. 打印出胜利者
4. 龟兔赛跑开始
5. 故事中是乌龟赢的,兔子需要睡觉,所以我们来模拟兔子睡觉
6. 终于,乌龟赢得了比赛
package com.baiyi.exercise;
/**
* @author 白衣
* @Description: 模拟龟兔赛跑
* @Date 2020/11/22
*/
public class Race implements Runnable{
private static String winner;
private int destination = 100;
@Override
public void run() {
for (int i = 0; i <= destination; i++) {
// 模拟兔子睡觉
if (Thread.currentThread().getName().equals("兔子") && i%10 == 0){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (gameOver(i)){
break;
}
System.out.println(Thread.currentThread().getName() + "--->跑了" + i + "步");
}
}
/**
* @Author 白衣
* @Description 判断是否完成比赛
* @Date 2020/11/22
* @param step 当前距离终点距离
* @return boolean
*/
private boolean gameOver(int step){
if (winner != null){
return true;
}else {
if (step >= destination){
winner = Thread.currentThread().getName();
System.out.println("winner is " + winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Race race = new Race();
new Thread(race, "兔子").start();
new Thread(race, "乌龟").start();
}
}
3. 实现 Callable 接口
- 开发步骤:
1. 实现 Callable 接口,需要返回值类型
2. 重写 call() 方法,需要抛出异常
3. 创建目标对象
4. 创建执行服务: ExecutorService ser = Executors.newFixedThreadPool(1);
5. 提交执行: Future<Boolean> result1 = ser.submit(t1);
6. 获取结果:boolean r1 = result1.get();
7. 关闭服务:ser.shutdownNow();
- 好处:
1. 可以定义返回值
2. 可以抛出异常
package com.baiyi.createthread;
import com.baiyi.exercise.PictureDownload;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
/**
* @author 白衣
* @Description: 创建线程的第三种方法 实现 Callable 接口
* @Date 2020/11/22
*/
public class CallableImpl implements Callable<Boolean> {
private String url;
private String name;
public CallableImpl(String url, String name){
this.url = url;
this.name = name;
}
@Override
public Boolean call() {
WebDownLoader webDownLoader = new WebDownLoader();
webDownLoader.downloader(url, name);
System.out.println("下载了文件名为: " + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
PictureDownload t1 = new PictureDownload("https://picsum.photos/id/1/200/300", "1.jpg");
PictureDownload t2 = new PictureDownload("https://picsum.photos/id/2/200/300", "2.jpg");
PictureDownload t3 = new PictureDownload("https://picsum.photos/id/3/200/300", "3.jpg");
// 1. 创建执行服务
ExecutorService ser = Executors.newFixedThreadPool(3);
// 2. 提交执行
Future<Boolean> result1 = (Future<Boolean>) ser.submit(t1);
Future<Boolean> result2 = (Future<Boolean>) ser.submit(t2);
Future<Boolean> result3 = (Future<Boolean>) ser.submit(t3);
// 3. 获取结果
Boolean r1 = result1.get();
Boolean r2 = result2.get();
Boolean r3 = result3.get();
// 4. 关闭服务
ser.shutdownNow();
}
}
class WebDownLoader{
/**
* @Author 白衣
* @Description 网图下载
* @Date 2020/11/22
* @param url 下载地址
* @param name 文件名称
* @return void
*/
public void downloader(String url, String name){
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO 异常, downloader 方法出现问题");
}
}
}
4. 小结
-
继承 Thread 类
- 子类继承 Thread 类具备多线程能力
- 启动线程: 子类对象.start()
- 不建议使用:避免 oop 单继承局限性
-
实现 Runnable 接口
- 实现 Runnable 接口具备多线程能力
- 启动线程: 传入目标对象 + Thread对象.start()
- 推荐使用:避免单继承,灵活方便,方便同一个对象被多个线程使用