目录
- 多线程原理详解
- 01_线程简介
- 多任务
- 多线程
- 程序、进程、线程
- Process(进程)与 Thread (线程)
- 总结:
- 02_线程创建三种方式:
- 1、继承 Thread 类
- 1-1:通过继承Thread类实现多线程
- 演示代码
- 1-2:演示多线程同步下载图片:
- 演示代码
- 2、实现 Runnable 接口 (推荐)
- 2-1:通过实现Runnable接口来实现多线程
- 具体代码
- 2-2:通过实现Runnable接口来实现多线程同步下载图片
- 具体代码
- 2-3:演示多个线程同时操作同一个对象出现的并发问题
- 演示买火车票的例子
- 具体代码
- 通过 synchronized 关键字同步代码块解决并发问题
- 具体代码
- 2-4:演示龟兔赛跑案例
- 用多线程的方式模拟龟兔赛跑代码分析
- 具体代码
- 3、实现 Callable 接口
- 演示多线程同时下载图片
- 具体代码
- 4、总结:三种创建方式的区别:
多线程原理详解
01_线程简介
任务、进程、线程、多线程
多任务
就是在一个时间内做很多事情,比如边吃饭变玩手机,但是本质上,在那一秒内,其实只是在玩手机,或者在一秒内只是在吃饭。
看起来像是多个任务都在做,其实本质上我们的大脑在同一时间依旧只做了一件事情而已
多线程
比如原本一条道路看成一个线程,多辆车都在一个道路上形式,容易出问题。
但是如果给道路划分路线,相当于弄多条线程,让多辆车在各自的线程行驶,出问题的概率就变小。
平常项目中的普通的方法调用,基本是一条线程走到底,即使调用方法,也是在方法执行完后继续往下走,只用到主线程而已。
多线程就是多条线程各自执行,效率更高。
就像一个大超市,只有一个收银员(单线程)和多个收银员(多线程)的区别。
程序、进程、线程
在操作系统中运行的程序就是进程,比如微信、QQ、腾讯视频、游戏这些就是程序,也就是进程。
一个进程(腾讯视频)可以有多个线程(视频声音、图像、弹幕等)
Process(进程)与 Thread (线程)
程序(静态的)–>程序跑起来变成进程(动态的)–>进程里面包含多个线程,真正执行功能的就是这些线程
总结:
注意:
单核 CPU 上所谓的"多线程"那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。
多核 CPU 上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的
02_线程创建三种方式:
三种创建方式主要是:继承 Thread 类、实现 Runnable 接口,还有一个是 实现 Callable 接口
最好的是实现 Runnable 接口,因为实现接口的方式比继承类的方式更灵活,也能减少程序之间的耦合度。
这三种方式都能开启多个线程来执行业务逻辑。
1、继承 Thread 类
步骤:
1、自定义一个类,继承 Thread 类;
2、重写 run() 方法,编写线程执行体;
3、创建线程对象,调用 start() 方法启动线程;
解释:
自定义一个类去继承Thread 类,作为一个线程类对象,然后就可以重写Thread类的 run() 方法;
接着在main主线程里面,可以通过线程对象去调用 start() 方法,该方法的作用是让 java 虚拟机去启动一个新的线程去执行 run() 方法里面的代码。
然后主线程继续执行自己的业务代码。
这样主线程和新的线程就都同时执行,这个就是多线程操作。
1-1:通过继承Thread类实现多线程
当我们调用 start() 方法后,start() 方法会通知 Java 虚拟机启动一个新线程(也就是子线程),并在新线程中调用该线程对象(t1)的 run() 方法。
而主线程调用start() 方法后,也会继续执行下面的代码。
这个时候,主线程和新线程就会并发执行,这样就演示出了多线程并发执行的特性。
演示代码
//创建线程方式一:继承 Thread 类, 重写 run() 方法,调用 start() 方法开启线程
//注意:线程开启不一定立即执行,由 CPU 调度执行
public class TestThread01 extends Thread
{
//重写 run() 方法
@Override
public void run()
{
//run 方法线程体
for (int i = 0; i < 20; i++)
{
System.err.println("run方法线程:bbbbbb--->" + i);
}
}
//main 主线程
public static void main(String[] args)
{
//创建一个线程对象
TestThread01 t1 = new TestThread01();
//直接调用run方法,只是普通的调用,跟多线程没有关系
//t1.run();
//调用start()方法开启线程,主线程和run()方法的线程就会同时进行
t1.start();
for (int i = 0; i < 20; i++)
{
System.err.println("main主线程:aaaaaaaaaa--->" + i);
}
}
}
1-2:演示多线程同步下载图片:
先下载一个 Apache 的 Commons IO 的 jar 包,是一个文件下载的工具类库
下载之后解压,把这个 jar 包拷贝到项目里面去
点击右键,点击【Add library】添加进去即可。
有了这个工具类库,才可以使用比如 FileUtils 这样的工具类
代码实现:
代码中,可以看出如果是单线程下载这3张图片的话,下载顺序应该是1、2、3
可从结果看,下载顺序是3、2、1,说明是多线程在同步下载图片,哪条线程快就哪条先下载完。
如图,图片成功下载下来了,这图片是我其他 博客文章里面的图片
演示代码
package cn.ljh.threaddemo.demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
// 实现多线程同步下载图片
public class TestThread02 extends Thread
{
private String url; //网络图片地址
private String name;//保存的文件名
//构造器
public TestThread02(String url ,String name){
this.url = url;
this.name = name;
}
//重写run方法
@Override
public void run()
{
//run方法里面调用了文件下载的downloader方法
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url,name);
System.err.println("下载了文件名为:"+ name);
}
//主线程
public static void main(String[] args)
{
//创建三个线程对象
TestThread02 t1 = new TestThread02(
"https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
"1.jpg");
TestThread02 t2 = new TestThread02(
"https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
"2.jpg");
TestThread02 t3 = new TestThread02(
"https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
"3.jpg");
//启动3条线程
t1.start();
t2.start();
t3.start();
}
}
//图片下载器
class WebDownloader{
//下载方法
public void downloader(String url,String name){
try
{
//把网络图片地址url变成一个文件,参数1:网络图片地址 ;参数2:下载后保存的文件名
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e)
{
e.printStackTrace();
System.err.println("IO异常,downloader 方法出现问题");
}
}
}
2、实现 Runnable 接口 (推荐)
步骤:
1、自定义一个类,实现 Runnable 接口;
2、实现 run() 方法,编写线程执行体;
3、创建线程对象,调用 start() 方法启动线程;
2-1:通过实现Runnable接口来实现多线程
这个和上面继承 Thread 类的操作差别不大。
如图:这个实现了Runnable接口的自定义线程类TestThread03,作为参数传进到 Thread 这个线程对象里面,然后通过 Thread 这个线程对象调用 start() 方法启动新线程。
解释下这个自定义线程类TestThread03,为什么说它是一个代理对象。
详细解释:
代码中:
将 t 视为一个代理对象的原因是它充当了线程对象(thread)和具体任务逻辑之间的中介。
具体来说,t 自定义线程类对象 实现了 Runnable 接口,并在其 run() 方法中定义了线程要执行的任务逻辑。
当我们通过 Thread 类创建一个 thread 对象时,需要将一个实现了 Runnable接口的对象(t)作为参数传入,那么这个对象(t)就是线程对象(thread)的代理,它 (t) 负责管理线程的生命周期和执行任务的逻辑。
在调用 thread.start() 方法后,线程对象(thread)会自动调用代理对象(t)的 run() 方法,从而执行具体的任务逻辑。
t 作为一个代理对象,充当了线程对象(thread)和具体任务逻辑之间的桥梁,使得线程的管理和任务的执行能够分开进行,提高了代码的灵活性和可维护性。
简单来说:
实现了Runnable接口的t对象,它实现了run()方法,作用是在run()方法里面写具体的业务逻辑代码。
而创建一个 Thread 对象,作用是用来调用 start() 方法从而启动新线程。
而启动的新线程要执行的业务逻辑代码在t对象的run()方法里面,所以就把 t 作为代理对象传给 thread 对象。
这样就是通过代理的方式实现了线程的启动(thread.start())和任务(任务在run()方法里面)的执行。
和上面的继承 Thread 类差不多。
再简洁的说法:
继承 Thread 类的自定义线程类对象,是自身调用 start 方法启动新线程。
实现Runnable 接口的自定义线程类对象,是作为创建Thread线程对象的参数,由Thread线程对象来调用start方法启动新线程
具体代码
package cn.ljh.threaddemo.demo01;
//创建线程方式2:实现 Runnable 接口,重写 run 方法,执行线程需要丢入 runnable 接口实现类,调用 start 方法
public class TestThread03 implements Runnable
{
//实现 run() 方法
@Override
public void run()
{
//run 方法线程体
for (int i = 0; i < 20; i++)
{
System.err.println("run方法线程:bbbbbb--->" + i);
}
}
//main 主线程
public static void main(String[] args)
{
//创建 Runnable 接口的实现类对象
TestThread03 t = new TestThread03();
//创建线程对象,通过线程对象thread来开启我们的线程---代理对象t
Thread thread = new Thread(t);
thread.start();
for (int i = 0; i < 20; i++)
{
System.err.println("main主线程:aaaaaaaaaa--->" + i);
}
}
}
2-2:通过实现Runnable接口来实现多线程同步下载图片
小小区别:
具体代码
// 实现多线程同步下载图片
public class TestThread04 implements Runnable
{
private String url; //网络图片地址
private String name;//保存的文件名
//构造器
public TestThread04(String url ,String name){
this.url = url;
this.name = name;
}
//重写run方法
@Override
public void run()
{
//run方法里面调用了文件下载的downloader方法
WebDownloader04 webDownloader = new WebDownloader04();
webDownloader.downloader(url,name);
System.err.println("下载了文件名为:"+ name);
}
//主线程
public static void main(String[] args)
{
//创建三个线程对象
TestThread04 t1 = new TestThread04(
"https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
"1.jpg");
TestThread04 t2 = new TestThread04(
"https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
"2.jpg");
TestThread04 t3 = new TestThread04(
"https://img-blog.csdnimg.cn/direct/0aded64215424e0488d3da76357750b6.png",
"3.jpg");
//通过线程对象启动3条线程
Thread thread = new Thread(t1);
thread.start();
new Thread(t2).start();
new Thread(t3).start();
}
}
//图片下载器
class WebDownloader04{
//下载方法
public void downloader(String url,String name){
try
{
//把网络图片地址url变成一个文件,参数1:网络图片地址 ;参数2:下载后保存的文件名
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e)
{
e.printStackTrace();
System.err.println("IO异常,downloader 方法出现问题");
}
}
}
2-3:演示多个线程同时操作同一个对象出现的并发问题
演示买火车票的例子
演示多个线程同时操作同一个对象—以买火车票为例子
多个线程操作同一个资源的情况下,线程不安全,数据紊乱,
这个就是多线程情况下会出现的并发问题。
具体代码
//演示多个线程同时操作同一个对象---以买火车票为例子
//出现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class TestThread05 implements Runnable
{
//票数
private int ticketNums = 10;
@Override
public void run()
{
while (true){
if (ticketNums<=0){
//终止当前所在循环
break;
}
//模拟延迟
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
System.err.println(Thread.currentThread().getName()+"--->买到了第【 "+ticketNums-- + " 】张票");
}
}
public static void main(String[] args)
{
TestThread05 t = new TestThread05();
//启动新线程 参数1:t 就是代理对象 参数2:name 就是当前线程名字
new Thread(t,"小黄").start();
new Thread(t,"小白").start();
new Thread(t,"小绿").start();
}
}
通过 synchronized 关键字同步代码块解决并发问题
简单解决下上面买车票的并发问题:
使用 synchronized 保证同一时间只有一个线程能来操作这个代码块。
具体代码
package cn.ljh.threaddemo.demo01;
//演示多个线程同时操作同一个对象---以买火车票为例子
//出现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱
public class TestThread05 implements Runnable
{
//票数
private int ticketNums = 10;
@Override
public void run()
{
while (true)
{
synchronized (this)
{
if (ticketNums <= 0)
{
//终止当前所在循环
break;
}
System.err.println(Thread.currentThread().getName()
+ "--->买到了第【 " + ticketNums-- + " 】张票");
}
//模拟延迟
try
{
Thread.sleep(500);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)
{
TestThread05 t = new TestThread05();
//启动新线程 参数1:t 就是代理对象 参数2:name 就是当前线程名字
Thread t1 = new Thread(t, "小黄");
Thread t2 = new Thread(t, "小白");
Thread t3 = new Thread(t, "小绿");
t1.start();
t2.start();
t3.start();
}
}
2-4:演示龟兔赛跑案例
自定义一个线程类,实现 Runnable 接口,然后启动两个线程(一个兔子线程和一个乌龟线程),用for循环,看哪个线程先执行到100米。
用多线程的方式模拟龟兔赛跑代码分析
具体代码
// 模拟龟兔赛跑案例
public class Race implements Runnable
{
//胜利者
private static String winner;
//实现 run() 方法
@Override
public void run()
{
for (int i = 0; i <= 100; i++)
{
//模拟兔子休息--当前线程是"兔子" 并且 每跑10步,就休息一会
if (Thread.currentThread().getName().equals("兔子") && i%10==0){
try
{
Thread.sleep(5);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
//判断比赛是否结束
boolean flag = gameOver(i);
//如果flag为true,表示比赛结束,直接终止当前所在循环
if (flag){
break;
}
System.err.println(Thread.currentThread().getName()
+ " ---> 跑了【 " + i + " 】步");
}
}
//主线程
public static void main(String[] args)
{
Race race = new Race();
//启动名为兔子的新线程
new Thread(race, "兔子").start();
//启动名为乌龟的新线程
new Thread(race, "乌龟").start();
}
//判断是否完成比赛的方法
public boolean gameOver(int steps)
{
//判断是否有胜利者
if (winner != null)
{
//已经存在胜利者了,返回true
return true;
} else if (steps >= 100)
{
winner = Thread.currentThread().getName();
System.err.println("胜利者是:【 " + winner + " 】");
return true;
}
return false;
}
}
3、实现 Callable 接口
步骤:
演示多线程同时下载图片
演示的依然是这个多线程同时下载图片的功能,不过这里是实现 Callable 接口。
这里的多线程操作,是通过创建一个线程池,然后把实现了 Callable 接口的自定义线程类对象(t1、t2、t3)作为任务参数提交给线程池去执行,并且可以获取线程执行后的结果,最后关闭线程池。
t1、t2、t3各自代表一个任务,它们实现了Callable接口,并且在call()方法中定义了具体的任务逻辑。
提交t1、t2、t3任务给线程池后,线程池会负责安排线程来执行t1、t2、t3中定义的任务逻辑。
通过源码可以看出 ,Callable 接口里面只有一个 call() 方法需要重写
具体代码
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
//线程创建方式三:实现 Callable 接口
/**
* Callable 的好处
* 1、可以定义返回值
* 2、可以抛出异常
*/
// 实现多线程同步下载图片
public class TestCallable implements Callable<Boolean>
{
private String url; //网络图片地址
private String name;//保存的文件名
//构造器
public TestCallable(String url, String name)
{
this.url = url;
this.name = name;
}
//实现 Call 方法
@Override
public Boolean call() throws Exception
{
//run方法里面调用了文件下载的downloader方法
WebDownloader webDownloader = new WebDownloader();
webDownloader.downloader(url, name);
System.err.println("下载了文件名为:" + name);
//返回true表示任务执行成功
return true;
}
//主线程
public static void main(String[] args) throws Exception
{
//创建三个线程对象
TestCallable t1 = new TestCallable(
"https://img-blog.csdnimg.cn/direct/beb94daf07094fd5979693243c8ac60b.png",
"1.jpg");
TestCallable t2 = new TestCallable(
"https://img-blog.csdnimg.cn/direct/beb94daf07094fd5979693243c8ac60b.png",
"2.jpg");
TestCallable t3 = new TestCallable(
"https://img-blog.csdnimg.cn/direct/beb94daf07094fd5979693243c8ac60b.png",
"3.jpg");
//ExecutorService是Java中用于管理线程池的接口,通过ExecutorService,可以创建一个线程池,并使用该线程池执行任务
// 1、创建执行服务:创建了一个固定大小为3的线程池,这意味着该线程池最多同时运行3个线程 fixed:固定的
ExecutorService ser = Executors.newFixedThreadPool(3);
// 2、提交执行:使用ExecutorService的submit()方法提交了三个任务(t1、t2、t3)给线程池执行,并且通过Future对象(r1、r2、r3)接收返回结果
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);
// 3、获取返回值结果
Boolean result1 = r1.get();
Boolean result2 = r2.get();
Boolean result3 = r3.get();
System.err.println(result1);
System.err.println(result2);
System.err.println(result3);
// 4、关闭服务,也就是关闭线程池
ser.shutdown();
}
//图片下载器
class WebDownloader
{
//下载方法
public void downloader(String url, String name)
{
try
{
//把网络图片地址url变成一个文件,参数1:网络图片地址 ;参数2:下载后保存的文件名
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e)
{
e.printStackTrace();
System.err.println("IO异常,downloader 方法出现问题");
}
}
}
}
4、总结:三种创建方式的区别:
Thread、Runnable、Callable,都能用来开启多个线程来执行业务逻辑。
继承Thread 类 和 实现 Runnable接口的对比:
OOP 指的是面向对象编程(Object-Oriented Programming)
继承 Thread 类:(重写run()方法)
自定义线程子类A,继承 Thread 类,然后重写 run() 方法,把具体的业务逻辑写在run方法里面。
通过子类自身去调用 start() 方法,让 JVM 去开启一条新的线程去执行 run() 方法。
这样就可以启动多条线程。
实现 Runnable 接口:(实现 run() 方法)
自定义线程类B,实现 Runnable 接口,它启动多个线程,是通过 创建多个Thread 线程对象,然后把线程类B作为参数传给 Thread 线程对象,由Thread线程对象来调用 start 方法的。
实现 Callable 接口:(实现 call() 方法)
自定义线程类C,实现 Callable 接口,它启动多个线程,是通过创建一个线程池,然后把线程类对象C作为任务参数提交到线程池,由线程池负责提供新线程来执行线程类对象C中定义的任务逻辑。
线程类对象C中定义的任务逻辑,就是指重写 call() 方法里面的业务逻辑。